ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent框架生产落地全指南:多Agent协作、工具稳定与可观测性

Agent框架生产落地全指南:多Agent协作、工具稳定与可观测性

这个系列写到第9篇,Agent框架这部分总算要收尾了。上一篇我们把框架的基础讲完了:什么是Agent、模型接入和工具调用在框架里怎么配合、一个单Agent怎么跑起来。如果你只是想让Agent“能跑通一个demo”,上篇的内容其实已经够用;但如果你和我一样,是把Agent塞进真实业务流程里——订单处理、售后答疑、数据分析、知识库问答——那这一篇才是要反复翻的。

这篇不聊概念,全是工程上绕不开的硬骨头,核心有五块:多Agent协作怎么编排、记忆和状态怎么不“失忆”、工具调用怎么保证稳定、上线之后怎么观测排查、以及从demo到生产环境的最后一公里怎么走。适合谁看?已经跑通过至少一个Agent任务、准备往真实业务上推的人。第一次接触Agent的朋友,建议先把上一篇基础看完再来,否则很多坑你还没有概念。

1. 多Agent协作:从单兵作战到流水线调度

1.1 为什么真正上线的系统很少用“超级Agent”

先放一个结论:真正落地的Agent系统,绝大多数不是由一个超级Agent包办所有事,而是多个专职Agent分工协作。原因有三个。

第一,上下文撑不住。一个Agent既要做意图识别,又要查订单,又要生成售后话术,所有工具和业务背景都得塞进同一个系统提示词。我见过最夸张的项目,系统提示词写到一万多token,每次请求光提示词的消耗就比答案本身还贵。更麻烦的是,任务混在一起之后,模型注意力会被摊薄,经常出现查订单的时候还在琢磨话术语气的串味情况。

第二,职责耦合导致回归风险。你只是改一个售后环节的提示词,结果下单环节的行为也变了,因为它们在同一个上下文里相互影响。一旦出问题,你很难定位到底是哪段提示词导致的行为漂移。

第三,token白白浪费。每一轮都要把全部工具定义和业务背景带上,真正用到的只有两三个。拆成多个Agent之后,每个Agent提示词短、工具少、职责单一,模型行为可控性明显提升。这里可以打个比方:一个全能店员既要收银又要理货还要处理售后,高峰期一定出错;超市的做法是分岗位,每个岗位只背自己那一块的SOP。多Agent本质上就是给Agent分岗位,让每个岗位变得简单且专注。

1.2 三种编排架构,按业务形态选

多Agent怎么排?不需要那么多花活,常见的就三种:顺序流水线、中心化调度、辩论竞合。选型不看出身,看任务结构。

架构模式工作方式适用场景主要风险
顺序流水线上一步输出是下一步输入流程固定、环节清晰,比如意图识别到查单到话术生成错误会逐级传导
中心化调度一个Planner拆任务发给Worker,再汇总任务多变、需要动态规划Planner单点瓶颈,拆错任务全盘错
辩论竞合多个Agent独立作答,仲裁者选中答案高价值问题、答案质量优先成本高、耗时翻倍

顺序流水线最简单,适合稳定的流程,但每一级之间要有校验。上一级输出如果是结构化JSON还好,如果是自然语言,下一步的输入会很脏,所以要在这里加一道字段提取的小逻辑,而不是把文本直接往下传。

中心化调度是目前最常见的形态。一个Planner负责拆解任务、下发Worker、回收结果,再自己汇总。Planner的提示词质量基本决定了这个系统的天花板,它的风险在于拆出来的子任务可能根本做不了。我在实际项目里遇到过Planner把一个“查天气”的任务拆成“获取用户位置”和“查询天气接口”两步,看起来合理,但第二步的接口参数它自己都填不对,最后分支失败。所以中心化调度一定要设计兜底分支,某个子任务执行失败时,Planner要能重新拆解,而不是把错误原样甩给用户。

辩论竞合模式成本最高,一般2到3个Agent就够,不要搞五六个。我第一次做的时候让5个Agent各答一遍再由仲裁者选,多出来的两个Agent并没有带来质量提升,反而经常被仲裁者选中一个“看起来全面”但实际在编造细节的答案。后来给仲裁者加了评分规则,要求答案里必须引用工具返回的原始事实,这才把幻觉压下去。

选型一句话总结:流程固定的,先上顺序流水线;需要处理开放需求的,再加Planner;只有答案直接影响客户钱的,才考虑辩论竞合。

1.3 可落地的多Agent配置与数据流向

给一个简化的编排配置示例。这个示例不依赖特定框架,你可以在自己用的框架里找到对应的抽象。

# 以类配置方式定义一个简化的编排:router -> workers -> summary from agent_framework import Agent, Pipeline router = Agent( name="intent_router", instruction="判断用户意图,输出严格JSON:{\"intent\": \"order_query|policy_query|chat\"}", tools=[], ) order_worker = Agent( name="order_worker", instruction="根据用户问题查询订单状态,只使用order_query工具,禁止回答其他问题。", tools=[query_order], ) policy_worker = Agent( name="policy_worker", instruction="根据用户问题检索售后政策,只使用policy_search工具回答,不要夹带业务判断。", tools=[policy_search], ) summary_agent = Agent( name="summary_agent", instruction="把各worker返回的结果合并成一段自然语言给用户,不要添加任何额外信息。", tools=[], ) pipe = Pipeline() pipe.add_router(router, key="intent", routing={ "order_query": order_worker, "policy_query": policy_worker, "chat": None, # 直接结束,不调用工具 }) pipe.add_last(summary_agent)

这个示例的要点不在代码,而在数据流。router只做分类,不携带业务上下文;worker只接收“任务描述加必要的业务参数”,不接收完整对话历史;summary只合并结果。整个链路做成单向流动,排查问题的时候从开头看到结尾,走到哪一步出错一眼就能定位。

我踩过的坑:第一次做多Agent的时候,顺手把整个对话历史塞给每个worker,结果三个Agent的回答都在重复用户开头那句话,上下文全部污染。后来就强制规定:下发任务时除了任务描述,最多再带三个必要字段,不能再多。

给新人的建议是:先用一个Agent把核心流程跑通,一定要用多Agent就先从这种顺序加路由的模式开始。一上来就上Planner加辩论,大概率两个月后连自己都看不懂日志。

2. 记忆与状态:Agent的“半条命”

2.1 四层记忆分开设计

先说一个判断:Agent丢记忆,大部分不是模型不行,而是架构上没把记忆分层。推荐至少分四层处理。

  • 短期对话记忆:当前会话的上下文,相当于白板,写完就能擦。
  • 工作记忆:当前任务执行到哪一步、已经拿到了什么数据,相当于挂在工位上的工单。
  • 长期记忆:跨会话沉淀的事实、用户偏好、业务规则,相当于档案柜。
  • 外部记忆:知识库、数据库,相当于图书馆。

很多框架把这四层混在一个history对象里,短期任务没问题,业务一复杂就乱。最需要区分的其实是短期记忆和工作记忆:短期记忆存的是“用户说了什么”,工作记忆存的是“任务做到哪一步”。如果把中间步骤全塞回对话上下文,上下文很快就会爆,而且模型容易被中间数据带偏。

常见的一个失败场景是:Agent要生成一份数据分析报告,中间步骤算了二十几个指标,这些指标全被写进上下文,结果模型在最后总结时开始惦记中间某个数字怎么算出来的,反而忽略用户最初要的结论。正确做法是,中间计算结果放在工作记忆里,最后需要时再取关键字段拼进上下文。

2.2 超长对话的淘汰与摘要策略

上下文窗口不够用怎么办?很多新手直接截断,结果用户第一句话里提到的核心诉求被干掉,模型后面对着一堆没头没尾的对话强行回答,质量可想而知。

标准做法是三级策略:滑动窗口保留最近N轮原样数据,超过阈值之后把更早的部分做摘要压缩,关键的长期事实写入向量库按需取回。一句话概括:最近的对话原文,早期的对话留摘要,跨越会话的事实进记忆库。

max_keep_turns = 20 # 最近20轮原始保留 max_context_tokens = 8000 def build_context(session_history): recent = session_history[-max_keep_turns:] older = session_history[:-max_keep_turns] if estimate_tokens(older) < 2000: return render(older + recent) summary = summarize(older) # 调用摘要Agent return render({"_summary_": summary, "history": recent})

摘要动作本身也消耗token,不能每次都做,要设置触发阈值,比如累计超过max_context_tokens的70%才执行一次。摘要提示词要单独写,明确给出三个要素:用户诉求、关键业务参数、未完成事项。不要拿系统提示词凑数,否则摘出来的全是正确的废话。

另外一个容易忽略的点:摘要生成之后,旧的原始记录不要立刻删,至少要归档保留一段时间。摘要再怎么准也会丢细节,排查线上问题的时候,经常需要回头翻原始对话看用户到底说了什么,归档是给自己留后路。

2.3 状态持久化:崩溃后从断点继续

Agent任务不是一次请求就结束的。比如一个批量报表生成任务,要跑十几个步骤,跑到第七步模型接口超时了。如果没有状态持久化,一切从头开始,用户等不到结果,体验极差。

框架层应该支持把Agent的当前状态序列化保存,至少包含session_id、current_step、collected结构化结果、retry_count和上下文摘要。恢复的时候从current_step继续,而不是重新跑全过程。

agent_state = { "session_id": "sess_20240307_001", "current_step": "data_fetch", "collected": {"user_query": "...", "order_id": "M20240301"}, "retry_count": 2, }

个人经验:保存状态时,一定要把已经获取的结构化数据原样保存,不要只存一句“查询订单成功”这种自然语言描述。否则恢复时模型还得重新理解之前干了什么,错误率会明显提高。最好把状态存储本身当作一个小数据库,每次状态变更是一次update,带上版本号,防止并发覆盖。

3. 工具调用的稳定性:从“会调用”到“调不坏”

3.1 工具定义质量决定模型不犯错

Agent能不能正确使用工具,首先取决于工具说明书的质量。在框架里挂一个工具不是堆上去就完事,要把名字、参数Schema、错误返回三件事做扎实。

名字要用动词加宾语,比如query_order、send_reminder,不要起名叫get_data或者foo这种含糊的名字。参数Schema要设好类型、必填、枚举值,描述里写清楚参数语义。错误返回必须是固定结构,比如字典形式的code加message,不要一会儿返回字符串一会儿返回对象。

@tool def query_order(order_id: str) -> dict: """查询订单状态。 Args: order_id: 订单编号,必填。示例:M20240307001。 Returns: 包含 order_status、recipient、purchase_time、tracking 的字典; 查询不到时返回 {"code": 404, "message": "订单不存在"}。 """

模型把参数填错,绝大多数情况不是它笨,而是你没告诉它参数该怎么填。我做过一个对比实验:同一个工具,在参数描述里加了“订单编号以M开头,例如M20240307001”之后,模型传参错误的次数下降大约一半。所以多花五分钟写清楚描述,比事后加一堆重试逻辑划算得多。

3.2 超时、重试与熔断缺一不可

工具背后往往是真实业务接口,接口一抖动,Agent的连环调用会把问题放大。因为Agent可能在十秒内连续调用同一个接口好几次,直接把弱接口打挂。

三个机制一定要配齐:单次调用超时,最长等十秒,超时算失败;失败后最多重试两次,并且要退避,不要立即重试;同一个工具在一分钟窗口内连续失败五次就临时熔断,返回一个明确信号给模型。

熔断信号的设计很关键。模型收到一个模糊错误,比如直接的“500”,它往往会换个参数再试几次,造成死循环。更有效的做法是返回:

{"code": 503, "message": "工具暂不可用,请勿重试,改用其他方式或直接回复用户无法完成"}

这种明确的“不要重试”信号,能让模型快速换路径。框架层同时设置连续失败计数器,我常用的配置是单次超时10秒、最多重试2次、1分钟内连续失败5次熔断5分钟,你可以根据自己接口的P95时延调整。

3.3 模型执着于同一个工具怎么办

模型调工具失败后,非要用同一个工具换个参数继续试,这是Agent落地中最常见的问题之一。控制手段有三层。

一是全局最大工具调用轮数,在框架层面硬限制,比如一个任务最多调5次工具,超了强制进入收尾。二是同一工具连续失败两次后,由框架在下一次模型输入里附加提示:“该工具连续失败,不要再次调用”。三是在系统提示词里写明:某个工具失败两次之后,要么换工具,要么如实告诉用户做不了,不要反复尝试。

提醒一句,第三点只是辅助,不要指望模型一定遵守,框架层的硬拦截才是兜底。我见过一个真实案例:Agent查询某第三方接口,连续失败8次,每次还把失败的返回拼进上下文,费用烧掉正常会话20倍,用户只看到一直转圈。加了最大轮数限制和熔断之后,最多5轮就停了,这个问题再没出现过。

4. 可观测性:没有追踪就别谈上线

4.1 追踪一次Agent调用的完整轨迹

Agent不像传统接口,一次请求对应一次SQL。它内部有多次模型调用、多次工具调用、多次分支,任何一个环节出错都要能被还原。

每一条轨迹至少要记录:trace_id、session_id、每个节点的输入输出摘要、模型名称、token数、耗时、工具名称、工具参数、工具结果、工具状态和单次调用成本。日志结构可以像这样:

{ "trace_id": "tr_20240307001", "session_id": "sess_20240307_001", "nodes": [ {"type": "model_call", "model": "default_model", "input_tokens": 1200, "output_tokens": 300, "latency_ms": 900}, {"type": "tool_call", "tool": "query_order", "params": {"order_id": "M20240307001"}, "status": "success", "latency_ms": 200} ] }

有一个省钱的方案:工具返回结果可能很大,全部写进日志成本高,可以存前几百个字符加完整内容的哈希。排查需要原文时,按哈希到离线日志里捞。另一个细节是,所有日志都要有关联ID,从用户请求进来的那一刻开始生成,按这个ID串起所有Agent和工具记录。没有关联ID,可观测性就是空谈。

4.2 成本可视化:搞清楚钱花在哪了

模型调用成本大头往往不在最后生成回答那一下,而在中间反复的工具调用和越来越长的上下文。建议按三个维度统计:会话维度、Agent维度、工具维度。

统计维度核心指标用于发现问题
会话维度总token、总费用、调用轮数找到“烧钱会话”的共同特征
Agent维度各Agent调用次数与token占比定位提示词过长还是循环调用
工具维度工具调用次数、失败率发现工具缺陷或模型误导

有一个真实案例我印象很深:某个会话的费用是正常会话的20倍,排查后发现Agent在循环调用一个查询接口,每次把返回结果拼进上下文,上下文越长,后续输入token越高,形成正反馈循环。这个问题不是靠换模型解决的,而是靠可观测性先暴露出来,加上轮数限制和上下文衰减策略才修掉。

4.3 评估集:给Agent建一个自动化测试基线

提示词手调一次,看着一两个例子感觉不错,这不叫验收。线上行为可能随后台改动漂移,没有回归测试根本发现不了。

评估建议覆盖四个维度:答案正确率、工具调用准确率、格式合规率、成本与延迟。每个业务场景准备至少50条有代表性的问题作为基线,覆盖正常场景、边界场景、干扰场景。每次改提示词、换模型、改工具配置,先跑一遍基线的通过率,通过率下降超过5%就要把改动拉回来再审。

这个习惯一开始很烦,但坚持下来价值巨大。我把Agent交付给业务方的时候,手里有基线数据,业务方也会踏实很多,至少能证明改动不是拍脑袋。

4.4 日志里最值得盯的四个信号

线上日志里,这四种信号出现时不要放过:工具连续失败,多半是外部接口有问题,也可能模型在反复尝试同一个工具;空回复或敷衍回复,用户明明有明确诉求,Agent却回了几句“嗯嗯”就结束,多半是意图识别把任务归到了聊天;单会话token异常,远超平均值,要检查上下文是否没裁剪、是否有循环调用;上下文接近上限的会话,说明记忆管理策略没起作用,得检查摘要触发条件。

这四个信号背后,对应的是我们在前面章节讲过的四类问题:工具熔断、路径兜底、上下文管理、记忆策略。出现任何一个,都要回trace找根因,而不是只把当前会话清了继续跑。

5. 生产落地:把Agent塞进业务流程才是开始

5.1 并发与限流:队列比想象中重要

Agent任务对后端的压力不是线性翻倍的。用户发一个请求进来,它可能内部发起五六次模型调用加两次工具调用,一个用户的请求相当于传统接口的五六倍压力。

生产环境要做三件事:入口排队化,用户请求先进队列,不直接压到模型服务;单用户并发限制,同一个用户同时最多跑两个Agent任务,防止有人恶意刷接口;模型调用做令牌桶限流,避免高峰期集体冲破模型服务商配额。

给不同任务设优先级也很实用。用户在线等待的查询类任务优先处理,批量生成报表和离线分析类任务放到深夜低峰跑。这样既保证用户体验,又错开成本集中时段。

5.2 人工审批:不要让模型独自承担高危动作

退款、发短信、删数据、批量导入导出,这些动作不能让模型全自动执行,即使它的意图是善意的。原因很简单:模型判断不了合规责任,出了事也不能拿“它自己决定的”去解释。

设计上,Agent只负责生成“待审批动作”,流程进入pending_review状态,由有权限的人确认后再放行:

pending_review -> approved / rejected

审批不能沦为形式。我参与过一个批量邮件项目,让Agent先生成全部邮件草稿,业务人员在只读界面预览审核,抽查没问题后再发送。这比“模型直接发出去,出了事再补救”安全太多。工具定义里也要把危险操作拆开:一个工具负责生成草稿,另一个工具负责执行发送。执行发送的工具绑定审批状态,没审批不执行。不要给模型开放一次性执行全部动作的能力。

5.3 多环境分离与提示词版本化

开发环境、测试环境、生产环境要严格隔离,工具配置、提示词、Agent编排图都纳入版本管理。发版时按版本号拉取配置,而不是直接在线上环境改。

我有一次直接在线上环境改提示词调Bug,调完没记录,第二天线上行为完全不可解释,最后花了两小时翻对话日志才找到原因。从那之后,所有改动都走配置仓库,留痕可回滚。

提示词版本化不是把文本塞进Git就完事,最好连触发条件一起版本化:什么版本的系统提示词、什么版本的工具定义、什么版本的模型,组成一个不可变快照。排查问题时直接看快照信息,不用再问“之前线上跑的是什么提示词”这种悬案。

5.4 缓存与复用:不该烧的钱一分也不烧

用户问重复问题很常见,前面已经回答过类似问题,下次完全不必再走一次完整的Agent链路。入口做相似度缓存:把用户问题向量化,和最近一段时间的高频问题做相似度匹配,超过阈值直接复用历史答案。

工具结果也可以做短时间缓存,同一个订单号五分钟内重复查,直接返回缓存结果,不调外部接口。有两个禁区要牢记:涉及实时价格、库存、用户个性化权限的数据不能乱缓存;缓存一定要设置有效期,不然会长期把旧数据当新数据用。

把缓存当成一个带TTL的组件来设计,不要无限缓存。省下来的成本很可观,但引入的数据一致性问题也要有预案。某个环节数据更新了,该失效的缓存要立即失效,不能等TTL自然过期。

6. 常见问题与排查技巧实录

6.1 典型症状速查表

把我在真实项目里遇到的五类高频问题整理成一张表,遇到类似症状可以直接对号入座。

症状排查思路常见根因推荐解法
Agent陷入循环看trace里工具调用是否同一工具重复工具错误信息模糊,模型认为还有机会最大轮数+熔断+明确错误提示
多个Agent串话看状态存储是否共享了同一份上下文把完整对话历史塞给每个worker按session隔离上下文,只传必要字段
用户说“你好”也触发一堆工具看路由Agent的分类结果没有拒识路径,所有输入都被当作任务加chat兜底分类,直接回复不调工具
记忆检索结果不相关看检索相似度分数阈值设太低,低相关文本也注入上下文提高阈值,加时间衰减权重
并发返回别人会话的数据检查状态对象是否全局共用状态用了单例每次请求新建状态上下文

随便展开一个案例:订单查询Agent回答里出现了另一个用户的名字。第一反应是模型幻觉,后来加了trace才发现,是路由Agent把上一个会话的上下文一起传给了worker,状态对象用了全局变量。这个教训说明,Agent系统里的Bug往往不是模型问题,而是数据流问题。排查方向一定要先看数据是怎么流的,再看模型有没有犯傻。

6.2 排查方法论:三条铁律

排查Agent问题,我有三条经验。

第一条,第一步永远看trace,不要凭感觉猜。Agent的行为是概率性的,但日志里的每一步都是确定的,看trace能快速锁定是该看数据流、工具调用还是模型输出。

第二条,第二步要复现。用线上同一条输入在测试环境里跑,逐节点加日志,通常几轮就能定位。不要直接在生产环境改东西试,风险太大。

第三条,根因尽量修在框架层,不要只靠改提示词。提示词容易漂移,换个模型可能就失效;而框架层的轮数限制、熔断、状态隔离,与具体模型无关,稳定可靠。

最后聊点个人体会。到我写这篇为止,Agent框架这部分总算闭环了。我做Agent项目的最大感受是:框架只是脚手架,决定系统能不能活下来的,是工程细节——状态有没有持久化、工具调错了会不会兜底、上线后能不能说清楚每一笔token花在哪。demo跑得漂亮和线上稳定运行之间,隔着的就是这五类工程问题。

如果你正准备往生产推,我只有一个建议:从最小可用链路开始。先一个Agent跑通核心流程,再加工具、加记忆、加第二个Agent,每加一块就回归一遍评估集。不要一上来就组五六个Agent开会,那种架构画图很好看,排查问题起来是真的会让人头大。这篇把我在真实项目里踩过的坑提前标出来了,希望你们能绕过去。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进