AI Agent 工程实践(21):为什么 Demo 永远变不成生产系统
系列:AI Agent 工程实践
上一篇:第 20 篇《组件都学完了,为什么还是写不出企业级 Agent》
下一篇:第 22 篇《AI Agent 项目应该如何分层》
一、开场:一次"完美 Demo,灾难上线"
去年帮一个朋友看他的客服 Agent。他在笔记本里跑得飞起:回答问题又快又准,调工具也稳,Demo 当天老板拍板立项,当场加人。
上线第一周,三个真实用户把系统打挂了:
- 一个用户上来问了句"在吗",Agent 触发了奇怪的工具调用循环,在"查订单 / 查物流 / 查订单"之间空转了四十多秒,把上下文和额度一起烧光;
- 一个用户上传了 200 页的 PDF 合同,进程直接 OOM 崩了,所有正在进行的会话一起丢失;
- 还有一个用户骂了句脏话,Agent 居然按"情绪负面→安抚"的提示,回去调了退款工具,把一笔没核实的订单退了。
朋友很委屈:"本地明明好好的。"
这就是 Demo 和 Production 的距离——不是代码能不能跑,而是"跑起来"和"被真实世界打"之间差着一整套假设。这一篇就拆开讲:为什么你把 chat.py 写得再漂亮,它也永远变不成生产系统。
二、问题背景:差的不是代码量,是假设
先看两种代码长什么样。
Demo 版,往往是一个文件:
chat.py # 几百行,什么都在这
生产版,是一棵目录树:
gateway/ # 入口:鉴权、限流、协议转换
runtime/ # 运行时:编排 Agent 的一次完整执行
memory/ # 记忆:跨会话的长期存储
planner/ # 规划:决定下一步做什么
tool/ # 工具:被调用的具体能力
provider/ # 模型:屏蔽各家 LLM 差异
api/ # 对外的接口契约
web/ # 前端或控制台
monitor/ # 监控:日志、指标、告警
很多人第一反应是:"这不就是代码多了吗?把 chat.py 拆开不就行了?"
不是。 差的从来不是行数,是这两套代码背后的"默认假设"完全相反:
| 维度 | Demo 的假设 | Production 的现实 |
|---|---|---|
| 用户 | 只有我一个人测 | 成千上万陌生人,输入完全不可控 |
| 输入 | 都是善意、规范的问题 | 有脏话、有乱码、有 200 页 PDF、有注入攻击 |
| 路径 | 永远走 happy path | 工具报错、模型抽风、网络超时是常态 |
| 稳定性 | 崩了重启就行 | 崩一次=丢一批会话=丢钱丢信任 |
| 审计 | 不需要解释为什么 | 出了问题要能复盘"当时为什么调了退款" |
| 协作 | 我一个人改 | 三个人同时动,要互不踩脚 |
一句话:Demo 假设"一切顺利",Production 假设"一切都会坏"。 后面的所有分层,本质都是把"一切都会坏"这件事,从祈祷变成工程。
三、错误尝试:三种最常见的翻车
错误 1:把 Demo 套层皮就上线
朋友最初的真实做法,是在 chat.py 外面套了个 FastAPI:
# 看似"上线了",其实只是把 Demo 暴露到公网
from fastapi import FastAPI
app = FastAPI()
# ... 直接 import 了 chat.py 里的所有逻辑
@app.post("/chat")
def chat(req):
return chat_with_agent(req.text) # 一行没改
结果:一个没被捕获的异常,整个进程挂掉,所有会话丢失。因为所有状态都活在进程内存里,没有一层"即使崩了也能恢复"的东西。
错误 2:分层但分错层
他后来听人说"要分层",于是按技术类型分:
models/ # 所有 .py 模型
utils/ # 所有工具函数
services/ # 所有业务逻辑
改一个"退款工具"要同时在三个文件夹里跳。分层是为了关注点分离,不是为了让文件夹变多。按技术类型分层,等于把"同一件事"拆进了三个柜子。
错误 3:为了"显得专业"照搬大厂目录
加了 Event Bus、CQRS、六边形架构、领域事件……团队三个人,一半时间在内耗目录本身。这是反过度工程的反面教材——用生产级复杂度去解决 Demo 级问题。
四、关键观察:分水岭是"假设反转"
把前面三种翻车归纳一下,真正的分水岭不是"功能更多",而是三个工程支柱,让系统从"会跑"变成"可运营":
1. 可替换(Swappable)
模型、工具、存储,任何一层变了,业务代码不动。Demo 里 DeepSeek() 写死在函数里;生产里是 provider.get("default"),今天用 DeepSeek 明天换 Claude,零改动。
2. 可观测(Observable)
每一次请求,都能回答:它调了哪些工具?花了多少 token?卡在哪一步?Demo 里靠 print;生产里靠日志 + Trace,出问题能复盘(还记得那个误退款吗?没有日志,你永远不知道它为什么调了退款工具)。
3. 可演进(Evolvable)
来一个新需求(比如"加一个人工审核环节"),是在既有边界里插一块,而不是推倒重写。Demo 改一点就崩;生产加一块还能跑。
目录结构只是这三个假设的可见形态。 你看到 provider/、memory/、monitor/,背后其实是"可替换 / 可替换 / 可观测"的工程决策。所以第四阶段(21–35)不造新零件——前三阶段(01–19)的零件你都有了——它只干一件事:把零件装配成一辆能上路的车。
五、最终方案:企业级分层长什么样
回到第二节那棵目录树,把每层职责讲清:
| 层 | 职责 | 它解决的"坏假设" |
|---|---|---|
gateway/ |
鉴权、限流、请求校验、协议转换 | 陌生人 + 不可控输入 |
runtime/ |
编排一次 Agent 执行的完整生命周期 | 崩了全丢、状态无处放 |
planner/ |
决定下一步做什么(调用哪类工具) | 路径失控、空转循环 |
tool/ |
具体能力实现 + 权限 + Schema | 工具乱调、误退款 |
provider/ |
屏蔽各家 LLM 差异 | 换模型要改业务代码 |
memory/ |
跨会话长期存储 | 每次都从零开始、记不住 |
api/ |
对外接口契约(请求/响应结构) | 前后端互相踩 |
web/ |
控制台 / 前端 | 运维人员无法干预 |
monitor/ |
日志、指标、告警、Trace | 出问题无法复盘 |
Demo vs Production 六维对照表:
| 维度 | Demo(chat.py) |
Production(分层) |
|---|---|---|
| 故障域 | 一个异常带走全部 | 一层挂,其余隔离 |
| 换模型 | 改代码重发 | 改配置即生效 |
| 出问题 | 靠回忆 | 靠 Trace + 日志 |
| 并发 | 单用户 | 限流 + 队列 |
| 输入 | 信任 | 校验 + 沙箱 |
| 演进 | 重写 | 插模块 |
六、一张图看请求怎么流(Mermaid)

注意 monitor 不在主链路分支上,而是旁路贯穿——这正是"可观测"的工程形态:观测不阻塞业务,但业务发生的每件事都被它看见。
七、代码对比:同一个功能,两种写法
假设要实现一个"用户问天气,Agent 调天气工具"的能力。
Demo 写法(问题版):
# chat.py —— 能跑,但一打就碎
import os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def chat(text):
tools = [{"type": "function", "function": {
"name": "get_weather",
"description": "获取天气",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}}]
# 没有超时、没有重试、没有错误分支
resp = client.chat.completions.create(
model="gpt-4o", messages=[{"role": "user", "content": text}], tools=tools
)
# 如果模型抽风没返回 tool_call,这里直接 IndexError
name = resp.choices[0].message.tool_calls[0].function.name
return f"调用了 {name}"
问题清单:api_key 硬编码来源不明、无超时、模型名写死(不可替换)、无错误处理(不可运营)、无任何日志(不可观测)。
生产写法(分层示意):
# runtime/orchestrator.py
from provider import get_provider # 可替换:换模型改配置
from tool.registry import ToolRegistry # 统一工具管理
from monitor.trace import trace # 可观测:全程埋点
def run_agent(user_input: str) -> str:
provider = get_provider("default") # 不关心背后是 DeepSeek 还是 Claude
registry = ToolRegistry() # 工具从注册表取,带权限校验
with trace("agent_run"): # 一次请求一条 Trace
try:
return provider.generate(user_input, tools=registry.list())
except ProviderTimeout: # 坏假设被显式接住
return registry.get("fallback_reply").call(user_input)
关键差异:模型从 get_provider("default") 取(配置驱动、可替换);工具从 ToolRegistry 取(统一、有权限);整个调用包在 trace 里(可观测);异常被显式接住(可运营)。代码没多写功能,多写的是"对坏的假设"。
八、设计权衡:什么时候不必分层
反过度工程必须补一句:不是所有项目都该上这套。
| 场景 | 建议 | 理由 |
|---|---|---|
| 个人玩具 / 一次性脚本 | 一个 chat.py 足够 |
没有"真实世界来打你",分层是负债 |
| 内部小工具,1–2 人用 | chat.py + 一个 .env + 简单日志 |
可控,崩了影响面小 |
| 对外服务 / 多人协作 / 长期维护 | 必须分层 | 故障域、可替换、可观测都是刚需 |
判断标准就一条:有没有"不可控的外界"会来打你。 有,就得分;没有,就别分。这一篇的价值不是"让你从此写大项目",而是让你在该分的时候,知道分什么、为什么分。
九、总结
- ✅ Demo 和 Production 差的不是代码量,是假设相反:Demo 假设一切顺利,Production 假设一切会坏。
- ✅ 三种常见翻车:套皮上线、按技术类型错层、为显得专业照搬大厂复杂度。
- ✅ 分水岭是三个支柱——可替换、可观测、可演进;目录结构只是它们的可见形态。
- ✅ 企业分层不是把
chat.py拆开,而是给"坏假设"每一类都安排一个去处:故障域隔离、换模型改配置、出问题靠 Trace。 - ✅ 反过度工程:没有不可控外界时,一个
chat.py就是最优解;分层是为了扛真实世界,不是为显得专业。
下一篇,我们不讲"为什么",直接画那棵目录树每一层的边界与接口——AI Agent 项目应该如何分层。
参考资料(带用途说明)
- 本系列(19)生产 Checklist:本文"问题背景"里"零件齐全但系统空白"的对照基线——十项全过只是 Demo 关门,系统开门另算。
- 本系列(20)过渡篇:本文是(20)指出的"把零件翻译成目录结构"能力的具体第一讲。
- 《12-Factor Agents》(Chroma 团队,公开技术文章):理解"把 Agent 拆成可替换步骤"的关注点分离思想,对应本文"关键观察"三支柱。
- OpenTelemetry 官方文档(opentelemetry.io):本文架构图里
trace / span的标准术语来源,第四阶段(28)可观测性会展开。 - DeepSeek 开放平台文档(platform.deepseek.com):本文代码示例中
get_provider抽象背后真实对接的模型方之一,说明"可替换"在企业里如何落地。
本文是 AI Agent 工程实践系列的第 21 篇(第四阶段首篇)。
系列导航
上一篇:第 20 篇《组件都学完了,为什么还是写不出企业级 Agent》
下一篇:第 22 篇《AI Agent 项目应该如何分层》
更多推荐


所有评论(0)