首页 / 资讯中心 / 文章详情

Agent 五层能力架构:从 ReAct 到 LangGraph 的渐进式落地

Agent 五层能力架构:从 ReAct 到 LangGraph 的渐进式落地 ★ FEATURED ARTICLE
Agent 这个词在过去一年里被用得太泛了。打开任何一个技术社区满屏都是我用 XX 框架三天搭了一个 Agent但真把代码拉下来跑一遍你会发现大部分所谓 Agent 不过是一个套了 while 循环的 prompt 调用器——能聊天能查天气但一旦任务稍微复杂一点比如帮我把这份季度报表里的异常数据找出来生成图表再写一段分析发到群里它就开始胡言乱语要么死循环要么把工具调用参数传错。问题出在哪不是模型不够强而是架构缺层。一个能真正干活的 Agent它的能力不是靠一个 prompt 堆出来的而是像盖楼一样一层一层长出来的。我从去年开始陆续做了几个落地项目从最简单的 ReAct 循环到后来用 LangGraph 编排多节点工作流中间踩的坑足够写一本小册子。这篇文章就把这套五层能力架构完整拆开讲清楚每一层解决什么问题、为什么不能跳过、实际写代码时哪些地方最容易翻车都会讲到。不管你是刚接触 Agent 开发的新手还是已经用 LangGraph 搭过几个 demo 但总觉得不够稳的老手应该都能从里面找到对自己有用的东西。1. 为什么大多数 Agent 项目死在了第二层1.1 一个真实项目的崩溃现场先讲个我亲身经历的事。去年下半年我接了一个内部工具的需求目标是做一个能自动处理客服工单的 Agent读取工单内容判断类型查知识库生成回复草稿必要时转人工。听起来不复杂对吧我当时的做法很典型——选了一个支持 function calling 的模型写了一个 ReAct 风格的循环把知识库查询、工单分类、回复生成三个工具注册进去跑了个 demo效果还不错。然后上线测试第一天就崩了。崩的方式很具体当工单内容里同时包含退款和物流两个意图时Agent 会在两个工具之间反复横跳调完知识库查退款政策又觉得应该先查物流状态查完物流又回来查退款循环了十几次之后 token 耗尽返回一个半截的回复。更离谱的是有时候它会把工单 ID 当成用户 ID 传给查询接口因为 prompt 里这两个字段的描述太像了。我当时的第一反应是模型不行换了个更强的模型好了一阵但换了个工单类型又出问题。后来我复盘才想明白这不是模型的问题是我的架构只有两层——模型层和工具层中间缺了状态管理层和决策控制层。ReAct 循环本质上是一个无状态的 while 循环每一轮它只看到当前的对话历史没有对我已经查过什么当前任务进行到哪一步哪些工具不该再调了的显式记录。模型只能靠上下文里残留的信息去猜猜错了就循环猜对了是运气。1.2 五层架构的全景图踩过这些坑之后我逐渐总结出一套分层的思路。一个实用的 Agent它的能力可以拆成五层从下往上依次是层级名称核心职责缺失后的典型症状L1模型接入层统一不同 LLM 的调用接口、流式输出、重试换模型要改遍全代码L2工具与协议层工具注册、参数校验、MCP 协议对接工具调用参数错乱、无法复用L3状态与记忆层会话状态、任务进度、长期记忆多轮任务丢失上下文、重复劳动L4决策与编排层任务规划、条件分支、循环控制死循环、任务跑偏、无法中断L5观测与安全层日志追踪、成本控制、权限边界出问题无法定位、token 烧钱这五层不是必须一次性全建好但你要清楚每一层在解决什么问题。很多项目死在第二层就是因为开发者只做了 L1 和 L2以为 ReAct 循环就是全部结果一遇到多步骤、多意图的任务就露馅。下面我逐层拆解重点讲每一层为什么必须存在以及实际怎么落地。2. 模型接入层别小看这层薄封装2.1 为什么不能直接调 SDK很多人觉得模型接入层没什么好讲的不就是调个 API 吗我一开始也这么想直到我需要在同一个项目里同时用三家不同厂商的模型——主力用一家做复杂推理简单分类用另一家便宜的还有一个本地部署的小模型做敏感数据脱敏。如果每处调用都直接写对应厂商的 SDK代码里会散落几十处if provider a ... elif provider b改一个参数要改遍全项目。模型接入层的价值就在于把调用模型这件事抽象成一个统一接口。它要屏蔽的差异包括请求参数格式有的用max_tokens有的用max_output_tokens、返回结构有的把内容放在choices[0].message.content有的放在content[0].text、流式输出的 chunk 格式、错误码和重试策略、token 计费口径。这些差异看起来琐碎但当你需要做 A/B 测试、成本优化、故障切换时没有这层封装会非常痛苦。2.2 一个够用的接入层该长什么样我的做法是定义一个LLMClient抽象核心方法就三个chat()同步调用、stream()流式调用、count_tokens()预估 token。每个厂商写一个适配器实现这个接口。关键设计点有几个统一的消息格式内部统一用[{role, content, tool_calls}]这种结构适配器负责转换成各厂商的格式。这样上层编排逻辑完全不用关心底层是谁。重试与降级网络抖动、限流是常态。接入层要内置指数退避重试并且支持配置降级链——主模型连续失败 N 次后自动切到备用模型。流式输出的统一不同厂商的流式 chunk 结构差异很大接入层要把它归一化成统一的增量事件上层才能用同一套逻辑处理。class LLMClient: def chat(self, messages, toolsNone, **kwargs): raise NotImplementedError def stream(self, messages, toolsNone, **kwargs): raise NotImplementedError def count_tokens(self, messages): raise NotImplementedError class ProviderAAdapter(LLMClient): def chat(self, messages, toolsNone, **kwargs): # 转换消息格式 - 调用 - 归一化返回 ...提示接入层不要做业务逻辑不要在这里判断该不该调工具那是上层的事。保持这层足够薄、足够纯它才能稳定。2.3 流式输出踩过的坑流式输出这块我踩过一个很隐蔽的坑。当时为了让前端有打字机效果我用了流式接口但工具调用tool_calls在流式模式下是分片返回的——一个工具调用的名字和参数可能分散在好几个 chunk 里需要自己拼接。我一开始没处理这个导致工具参数经常是半截 JSON解析直接报错。正确做法是在接入层维护一个缓冲区按工具调用的 index 累积拼接等流结束时再统一解析。这个细节很多教程不会讲但不处理的话流式 工具调用这个组合基本没法用。3. 工具与协议层MCP 到底解决了什么3.1 工具注册的混乱现状工具层是 Agent 的手脚。没有工具Agent 只能聊天有了工具它才能查数据库、发请求、读写文件。但工具多了之后管理就成了问题。我见过一个项目工具定义散落在五六个文件里每个工具的 schema 写法都不一样有的用 JSON Schema有的用 Pydantic有的干脆用自然语言描述参数。结果就是模型经常传错参数因为工具描述本身就不一致。工具层要解决的核心问题是标准化统一的工具定义格式、统一的参数校验、统一的错误返回。我习惯用 Pydantic 定义每个工具的输入 schema这样既能自动生成 JSON Schema 给模型看又能在调用前做参数校验参数不对直接返回结构化错误让模型重试而不是让错误穿透到业务代码里。3.2 MCP工具层的USB 接口MCPModel Context Protocol这两年被讨论得很多热词里也频繁出现。它的本质是什么用一句话说它把工具从跟某个 Agent 绑死变成了可以被任何 Agent 复用。在没有 MCP 之前你给 Agent A 写的数据库查询工具想给 Agent B 用得把代码复制过去或者抽成一个内部库。MCP 定义了一套标准的通信协议工具提供方实现一个 MCP Server任何支持 MCP 的客户端都能连上来用这些工具。这带来的变化是结构性的。以前工具和 Agent 是耦合的现在工具变成了独立的服务。你可以有一个专门管文件的 MCP Server一个专门管数据库的 MCP Server一个专门管内部 API 的 MCP Server不同的 Agent 按需连接。这跟微服务架构的思路是一脉相承的——把能力拆成独立部署、独立演进的单元。不过我要泼一盆冷水MCP 不是银弹。它解决的是工具复用和标准化的问题但引入了额外的通信开销和部署复杂度。如果你的项目就一个 Agent、三五个工具直接内置工具函数完全够用上 MCP 反而是过度设计。什么时候该上 MCP我的判断标准是当你的工具需要被两个以上 Agent 共享或者工具本身需要独立部署、独立权限管理时才值得引入。3.3 工具描述的写法直接决定调用准确率这是我最想强调的一点也是很多文档不会告诉你的工具能不能被正确调用八成取决于描述怎么写。模型看不到你的代码它只能看到工具的 name、description 和参数 schema。我做过对比测试同一个查询工具描述写得含糊和写得精确调用准确率能差出三四十个百分点。几个实操经验description 要写什么时候用和什么时候不用。比如查询订单状态这个工具描述里要明确当用户询问订单物流、配送进度时使用当用户询问退款进度时不要用这个工具用退款查询工具。把边界写清楚模型才不会乱调。参数描述要带示例。order_id这个参数光写订单 ID不够要写订单 ID格式为 ORD 开头的 16 位字符串例如 ORD202401150001。模型看到示例传参准确率明显提升。参数名不要用缩写。uid、oid这种缩写模型容易混淆老老实实写user_id、order_id。4. 状态与记忆层Agent 的工作台4.1 无状态循环为什么撑不住复杂任务回到开头那个客服工单的例子。为什么 Agent 会反复横跳因为 ReAct 循环是无状态的每一轮它只能从对话历史里推断我做过什么。但对话历史是线性的文本模型要从一堆工具调用记录里重建出当前任务状态这本身就是个高难度任务尤其是历史长了之后早期的信息会被稀释。状态层的核心思想是把任务进度从隐含在对话历史里变成显式存储的结构化数据。比如用一个 state 对象记录当前任务类型是什么、已经完成了哪几步、哪些工具已经调用过、拿到了哪些中间结果、还差什么。每一轮决策时把这个 state 明确地喂给模型而不是让它自己去历史里翻。4.2 短期状态与长期记忆要分开设计状态和记忆经常被混为一谈但它们是两回事。短期状态是当前任务的工作台任务结束就清空长期记忆是跨会话的知识沉淀比如用户偏好、历史交互摘要。这两者的存储方式、生命周期、检索方式都不一样混在一起设计会很难维护。短期状态我一般直接放在内存里跟着任务生命周期走。长期记忆则需要持久化常见做法是向量库 结构化存储的组合语义相关的信息存向量库做相似检索确定性的信息如用户 ID、偏好设置存关系库或 KV 库。检索的时候两者结合先按用户 ID 过滤再做语义检索。4.3 状态设计的一个反直觉经验我一开始设计状态时总想把所有信息都塞进去觉得信息越全模型判断越准。结果发现状态太臃肿反而拖累效果——模型面对一大堆字段注意力被分散关键信息反而被淹没。后来我改成只存决策必需的最小状态当前步骤、已完成步骤列表、关键中间结果。其他细节需要时再通过工具去查。这个思路跟人干活是一样的工作台上只放当前要用的东西资料放抽屉里需要再拿。5. 决策与编排层从 ReAct 到 LangGraph5.1 ReAct 的适用边界ReActReasoning Acting是最经典的 Agent 范式模型先思考Thought再决定行动Action执行后观察结果Observation循环往复。它的优点是简单、灵活适合那种步骤不固定、需要模型临场判断的任务。但它的边界也很明显当任务流程相对固定、有明确的分支和循环时让模型每一步都自由决策既浪费 token 又不稳定。我现在的判断标准是如果任务的步骤可以画成一张流程图那就不要用纯 ReAct用编排框架把流程固化下来只在需要判断的节点让模型做决策。这样既保留了灵活性又保证了主流程的稳定。5.2 LangGraph 的核心概念拆解LangGraph 是这两年编排层最受关注的工具之一。它的核心抽象其实就三个State状态、Node节点、Edge边。State 是一个共享的数据结构所有节点读写它Node 是一个个处理单元可以是调模型、调工具、做判断Edge 定义节点之间的流转关系包括条件边根据 state 决定走哪条路。用 LangGraph 重写客服工单那个例子结构会清晰很多入口节点做意图分类然后条件边根据意图分流到不同的处理子图每个子图内部是固定的处理步骤最后汇聚到回复生成节点。整个流程是显式的图结构哪里会循环、哪里会分支一目了然不会出现 ReAct 那种不可预测的横跳。from langgraph.graph import StateGraph, END def classify_intent(state): # 调用模型做意图分类 return {intent: refund} def handle_refund(state): # 退款处理逻辑 ... graph StateGraph(AgentState) graph.add_node(classify, classify_intent) graph.add_node(refund, handle_refund) graph.add_conditional_edges(classify, route_by_intent) graph.set_entry_point(classify) app graph.compile()5.3 循环控制给 Agent 装上刹车不管用 ReAct 还是 LangGraph循环控制都是必须的。我见过太多 Agent 因为缺少循环上限而烧光 token。基本的防护有三道最大迭代次数硬上限比如 15 轮、无进展检测连续两轮 state 没有实质变化就中断、成本上限累计 token 超过阈值就停。这三道防线缺一不可尤其是无进展检测能拦住大部分死循环。LangGraph 里可以用条件边实现这些控制每轮结束检查迭代次数和 state 变化满足中断条件就路由到结束节点。ReAct 循环里则要在 while 条件里加上计数器和状态对比。5.4 人在回路什么时候必须让人类介入有些决策 Agent 不该自己做比如涉及资金、权限变更、对外发送的操作。这时候需要人在回路Human-in-the-loop机制Agent 执行到敏感节点时暂停把决策权交给人类人类确认后再继续。LangGraph 支持中断和恢复可以在指定节点前设置断点把当前 state 持久化等人类输入后从断点恢复。这个机制在落地项目里非常重要它决定了 Agent 能不能被放心地用在真实业务里。6. 观测与安全层让 Agent 可被信任6.1 没有追踪的 Agent 就是个黑盒Agent 出问题时最难的不是修而是定位。它可能调了十几次工具、经过好几个节点最后输出一个错误结果你根本不知道是哪一步出的问题。所以观测层不是可选项是必选项。最基本的要记录每一轮的输入输出、工具调用的参数和结果、每个节点的耗时、累计 token 消耗。有了这些出问题才能回溯。我习惯用结构化的方式记录 trace每个任务一个 trace_id每个步骤一个 span这样可以在追踪系统里看到完整的调用链。LangSmith 这类工具就是干这个的当然自己用日志系统搭一套也完全可行。6.2 成本控制token 是怎么悄悄烧掉的Agent 的 token 消耗比普通对话高一个数量级因为它每一轮都要把完整的历史和工具定义重新发一遍。一个跑了十轮的任务token 消耗可能是单轮对话的十几倍。控制成本的手段有几个精简工具定义只挂当前任务需要的工具、压缩历史早期轮次做摘要、用便宜模型做简单判断分类、路由用便宜模型复杂推理才用贵模型、设置成本上限。6.3 权限边界Agent 能碰什么不能碰什么安全层最后一块是权限。Agent 拿着工具理论上能碰它连接的所有资源这很危险。必须做最小权限每个工具只给完成任务必需的权限敏感操作要二次确认对外发送类操作要有速率限制和内容审核。我一般会把工具按风险分级高风险工具删除、发送、支付强制走人在回路低风险工具查询、读取才允许自动执行。7. 五层怎么长出来一个渐进式的落地路径讲了这么多层你可能会问难道要一次性全建好当然不是。我的建议是渐进式演进跟着需求长。第一阶段先做 L1 L2用 ReAct 跑通单步任务验证工具调用链路。第二阶段任务变复杂了加 L3 状态层把任务进度显式化。第三阶段流程固定了引入 L4 编排层用 LangGraph 把主流程固化。第四阶段要上线了补 L5 观测和安全。每一层都是被真实需求逼出来的而不是一开始就设计好的。这个顺序很重要。我见过有人一上来就搭全套架构结果发现最简单的任务都跑不通因为底层工具调用还没调稳。架构是长出来的不是设计出来的。先把最小闭环跑通遇到瓶颈再加层每一层都解决一个具体的、你已经亲身遇到的问题这样长出来的架构才是扎实的。最后分享一个我自己的体会判断一个 Agent 架构好不好不看它用了多新的框架而看它出问题的时候你能不能快速定位、快速修复。能扛住真实业务里那些脏数据、边界情况、异常流程的 Agent才是好 Agent。框架只是工具对每一层在解决什么问题的理解才是真正值钱的东西。
阅读完成 · 觉得有帮助?
咨询建站