你一定见过那张图。
一个同心圆,最里面是"LLM",外面一圈是"Tools",再外面是"Memory",最外层是"Planning"。或者换个版本:一个循环箭头图,"Observe → Think → Act → Observe",周而复始。
这些图不是错的。它们只是没用。
当你真正要设计一个能跑在生产环境的Agent系统时,你会发现这些图告诉你的东西,和你要解决的问题之间,隔着一道鸿沟。它们告诉你Agent"概念上怎么运作",但没告诉你"工程上怎么实现"。
这篇文章要画另一张图。一张能指导你写代码、做技术选型、排查问题的图。
同心圆图的问题在哪
先说清楚,同心圆图和ReAct循环图在教学层面是有价值的。它们帮初学者建立了一个心智模型:Agent不是魔法,是一个有输入、有处理、有输出的系统。
但当你从"理解概念"进入"设计系统"时,这些图就开始误导了。
问题一:掩盖了复杂度的真实分布
同心圆图暗示每一层是等价的、对称的。但真实系统中:
- LLM调用是核心,但只占代码量的5%
- 工具调度是重头,占代码量的30%,也是bug高发区
- 记忆系统不是一层,是三层(工作记忆、短期记忆、长期记忆),每层的存储介质、检索策略、失效策略都不同
- 规划不是独立模块,是渗透在每一层的决策逻辑
同心圆图让你以为"加个记忆层"就像给洋葱加一层皮。实际上,记忆系统涉及向量数据库选型、embedding模型选择、检索策略设计、记忆压缩、遗忘机制……每一个都是独立的技术决策点。
(见配图1:同心圆图 vs 6层架构对比)
问题二:没有数据流
ReAct循环图告诉你Agent会"循环",但没告诉你:
- 循环的触发条件是什么
- 循环的终止条件是什么
- 循环中的状态如何传递
- 循环失败时如何回退
没有数据流,你就无法推理系统的行为。当Agent在生产环境中出现"死循环"或"过早退出"时,你拿着ReAct图根本无法定位问题。
问题三:没有边界
同心圆图没有告诉你在哪里划线。哪些是Agent系统的边界?哪些是外部系统?哪些是基础设施?
结果就是,很多Agent系统设计成了一锅粥:LLM调用、工具执行、状态存储、日志记录、监控告警全部耦合在一起。改一个工具的返回格式,可能牵连到记忆系统的解析逻辑。
生产级Agent的6层架构
现在我们来画一张有用的图。
这张图的核心思想是:Agent系统是一个分层架构,每一层有明确的职责和接口。你可以在每一层做技术选型、做单元测试、做性能优化,而不影响其他层。
┌─────────────────────────────────────────────────────────────────┐
│ 监控与可观测层 │
│ Tracing / Logging / Metrics / Alerting │
└─────────────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────────────┐
│ 编排层 │
│ 任务调度 / 流程控制 / 错误处理 / 重试策略 │
└─────────────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────────────┐
│ 记忆系统 │
│ 工作记忆(Context) / 短期记忆(Session) / 长期记忆(Vector/KG) │
└─────────────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────────────┐
│ 工具调度层 │
│ 工具注册 / 工具选择 / 参数绑定 / 执行 / 结果解析 │
└─────────────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────────────┐
│ LLM调用层 │
│ Prompt构建 / 请求发送 / 响应解析 / Token统计 │
└─────────────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────────────┐
│ 输入输出层 │
│ 用户输入解析 / 输出格式化 / 流式输出 / 多模态处理 │
└─────────────────────────────────────────────────────────────────┘
第1层:输入输出层
职责:处理用户交互的"最后一公里"
这层看起来简单,实际上坑很多:
- 输入解析:用户输入是纯文本?结构化指令?多模态(图片+文本)?需要做意图分类吗?
- 输出格式化:返回纯文本?Markdown?JSON?需要做格式校验吗?
- 流式输出:LLM的streaming输出如何传递给前端?SSE?WebSocket?
- 多模态处理:如果Agent要返回图片/文件,如何处理?
技术选型示例:
- 简单场景:直接字符串处理
- 复杂场景:Pydantic模型做输入校验,FastAPI的StreamingResponse做流式输出
第2层:LLM调用层
职责:封装LLM API调用,提供统一接口
这层的核心是抽象。你不想让业务代码直接依赖OpenAI的SDK,因为:
- 模型会换(GPT-4 → Claude → DeepSeek)
- API会变(OpenAI的接口改过好几次)
- 调用策略会调整(重试、超时、降级)
关键设计:
class LLMClient(ABC):
@abstractmethod
async def complete(self, messages: list[Message], tools: list[Tool] | None = None) -> Response:
pass
class OpenAIClient(LLMClient):
async def complete(self, messages, tools=None):
# 实际调用OpenAI API
...
class MockClient(LLMClient):
async def complete(self, messages, tools=None):
# 测试用的mock
...
技术选型示例:
- 轻量级:自己封装(200行代码足够)
- 重量级:LiteLLM(统一多家API,但引入额外依赖)
第3层:工具调度层
职责:管理工具的生命周期,执行工具调用
这是Agent系统的核心复杂度所在。工具调度不只是"调用API",还包括:
- 工具注册:如何描述工具?OpenAI的function calling格式?JSON Schema?
- 工具选择:LLM返回的工具调用请求,如何路由到实际实现?
- 参数绑定:LLM返回的参数是JSON字符串,如何解析并绑定到函数参数?
- 执行:同步还是异步?超时怎么处理?并发控制?
- 结果解析:工具返回的结果如何格式化回传给LLM?
一个真实的bug案例:
某Agent在调用天气API时,LLM返回了 {"city": "北京"},但API实际需要 {"location": "Beijing"}。问题出在哪?
- 不是LLM的错(它按工具描述返回了)
- 不是API的错(它按文档要求参数)
- 是工具描述和API实现不一致
这个bug在"同心圆图"里根本无法定位。在6层架构里,你知道这是"工具调度层"的问题——工具描述(schema)和实际实现(handler)的契约不匹配。
第4层:记忆系统
职责:管理Agent的"状态"和"经验"
记忆不是一层,是三层:
(见配图2:记忆系统3层架构)
| 层级 | 存储介质 | 生命周期 | 典型实现 |
|---|---|---|---|
| 工作记忆 | LLM上下文窗口 | 单次推理 | 消息列表 |
| 短期记忆 | 内存/Redis | 单次会话 | 会话状态字典 |
| 长期记忆 | 向量数据库 | 跨会话 | Pinecone/Milvus/PGVector |
关键问题:
- 记忆写入:什么信息值得记住?全部记住会撑爆上下文窗口
- 记忆检索:如何检索相关记忆?纯向量相似度?混合检索?
- 记忆更新:用户说"我之前说的那个城市改成上海",如何更新已有记忆?
- 记忆遗忘:过时信息如何清理?错误推理链如何删除?
一个反直觉的设计:
很多Agent系统的问题不是"记不住",而是"记得太多"。上下文窗口塞满了无关信息,导致LLM推理质量下降。
好的记忆系统,遗忘机制比记忆机制更重要。
第5层:编排层
职责:控制Agent的执行流程
这是"规划"真正发生的地方。但不是让LLM"自己规划自己执行",而是:
- 任务分解:复杂任务如何拆成子任务?
- 流程控制:串行执行?并行执行?条件分支?
- 错误处理:某一步失败了,重试?跳过?回滚?
- 终止条件:什么时候算"完成"?什么时候算"失败"?
编排模式对比:
(见配图3:编排模式对比)
| 模式 | 适用场景 | 复杂度 |
|---|---|---|
| 单次调用 | 简单查询 | 低 |
| ReAct循环 | 多步推理 | 中 |
| Plan-and-Execute | 复杂任务 | 高 |
| 状态机 | 固定流程 | 中 |
| DAG编排 | 并行任务 | 高 |
一个踩坑案例:
某Agent用ReAct循环处理用户请求,但LLM在"Think"步骤里反复纠结,形成了死循环。问题出在哪?
- 不是LLM的错(它只是在"思考")
- 是编排层缺少终止条件
解决方案:在编排层加一个"最大步数限制"和"重复检测"——连续3次相同的思考就强制终止。
这个设计在"ReAct循环图"里是看不到的。它属于编排层,不属于"推理"。
第6层:监控与可观测层
职责:让Agent系统"可调试"
Agent系统是黑盒吗?不应该是。
但很多Agent系统确实是黑盒——你只知道"输入"和"输出",中间发生了什么,完全不知道。LLM调了几次?选了什么工具?工具返回了什么?为什么选这个工具而不是那个?记忆检索到了什么?
需要记录什么:
- 推理链:每一步的思考过程
- 工具调用:调用参数、返回结果、耗时
- 状态变迁:记忆的读写、会话状态的变化
- 性能指标:每一步的耗时、token消耗
技术选型示例:
- LangSmith:LangChain生态,开箱即用
- Arize Phoenix:开源,支持多种框架
- 自建:OpenTelemetry + Grafana
从"单次对话"到"长期运行系统"
理解了6层架构,你就能理解一个关键转变:
Agent系统不是"对话系统",是"长期运行的自动化系统"
(见配图5:Demo到生产的关键转变)
对话系统:用户发一句话,系统回一句话,结束。状态可以不保存。
Agent系统:用户发一个任务,系统可能要执行几分钟、几小时、甚至几天。中间可能调用几十次工具、读写几百条记忆、产生几千条日志。系统崩溃后要能恢复,执行失败后要能重试,用户中途要能干预。
这个转变意味着:
- 状态管理变得至关重要(不能全放内存)
- 错误处理要系统化设计(不能只靠try-catch)
- 可观测性是必需品(不是锦上添花)
- 成本控制要纳入架构设计(token消耗、API调用次数)
一张图胜过千言万语
现在,我们来看一张真实的Agent架构图。
(见配图:生产级Agent架构图)
这张图和同心圆图的区别:
- 有层次:每一层职责清晰,可以独立设计、测试、优化
- 有数据流:箭头表示数据流向,可以推理系统行为
- 有边界:虚线框表示系统边界,明确哪些是内部、哪些是外部
- 有基础设施:监控、日志、存储不是"外挂",是架构的一部分
这张图怎么用
当你设计一个Agent系统时,按这个流程:
- 从上到下梳理:用户输入是什么?输出是什么?需要哪些工具?需要记忆吗?执行流程是什么?如何监控?
- 逐层做技术选型:每一层有哪些方案?各有什么trade-off?
- 逐层写代码:先写LLM调用层,再写工具调度层,再写记忆系统……不要一上来就写"Agent"
- 逐层测试:每一层可以独立单元测试,不需要端到端跑起来才能测
当你排查一个Agent系统的bug时,按这个流程:
(见配图4:Agent故障排查路径图)
- 定位层级:问题出在哪一层?
- 检查接口:这一层的输入输出是否符合预期?
- 检查实现:这一层的内部逻辑是否有问题?
- 检查依赖:这一层依赖的下层是否正常?
小结
同心圆图和ReAct循环图是教学工具,不是设计工具。
当你从"理解Agent是什么"进入"设计Agent系统"时,你需要的是一张能指导工程实践的架构图。
6层架构不是唯一正确的架构,但它是一个可操作的起点:
- 每一层有明确的职责
- 每一层有成熟的技术选型
- 每一层可以独立测试和优化
下一篇文章,我们讨论一个更实际的问题:什么场景该用Agent,什么场景不该用。不是所有问题都需要Agent,过度Agent化是当前最大的坑。