系列: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 项目应该如何分层》

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐