干这一行久了,你会发现Multi-Agent系统跑挂是常态,不挂才是意外。但我看很多人处理执行失败的方式特别单调——点重试,再失败再重试,最多加个延时,然后继续重试。以前我也这么干,直到一次线上事故让我彻底改了想法:那次是某个子Agent调用外部支付接口超时,重试逻辑傻乎乎地又触发了一次扣款请求,用户被重复扣了钱。所以今天我想认真聊聊这个事儿:Multi-Agent里一个执行失败,你只会重试就太初级了。
这篇文章不是讲什么高深理论,就是把我这些年踩过的坑、验证过的方法整理出来。我会从失败分类讲起,聊到怎么设计一个带退避的重试策略、什么时候该熔断降级、典型报错的排查实录,以及多智能体拓扑和通信机制下失败会怎么传染。不管你是在用Coze搭工作流,还是自己写Agent框架,这些思路应该都能用上。
1. 先把话说透:Multi-Agent为什么老执行失败
1.1 失败不是偶发,是分布式系统的默认状态
Multi-Agent系统的本质是多个决策单元协作完成一个复杂任务。这就意味着它天然具备分布式系统的所有毛病:网络抖动、依赖服务超时、消息丢失、节点状态不一致。你想想,一个任务拆给三四个Agent,每个Agent又要调用不同的工具和模型接口,任何一个环节出问题,整个流程就卡住了。
这不是代码写得差,而是系统复杂度决定的。你做单Agent的时候,一个模型吃一次输入吐一次输出,失败面很窄。但Multi-Agent把任务拆解、工具调用、结果汇总、子任务再分配这些逻辑全都串起来了,出错概率是指数级上升的。我见过有的团队上线一个三Agent协作的客服机器人,一周内收到的失败告警比之前单Agent服务一个月还多。说白了,只要你选择了Multi-Agent,就得接受"失败是默认状态"这个前提。
1.2 常见失败类型:工具调用、上下文超限、模型输出异常、节点失联
实践中我习惯把失败分成这么几类,每一类的处理方式完全不同。
第一类是工具调用失败。Agent要查订单、发消息、调数据库,结果工具接口超时、返回500、参数校验不过。这一类最像传统后端故障,但难点在于Agent是"自动驾驶"的,它可能用错误的方式重试,比如换个参数再试几次反而把数据弄脏了。
第二类是上下文管理失败。上下文超限是最典型的,尤其当你用1m上下文这类超大窗口时,配置没跟上,模型直接报错。还有一种是上下文被截断,导致前面的关键信息丢了,Agent后面的决策全是无源之水。
第三类是模型输出异常。比如系统提示"模型本轮只输出了思考过程、没有产出正文",模型想了一堆,就是不给你最终答案。还有输出截断、格式不符合约定、出现幻觉内容。
第四类是节点失联。某个Agent所在的进程挂了、设备离线了、端口不通了。比如Coze场景里,你本地起了个coze-bridge,结果它断开了,Agent调用本地能力时就会报"请确认coze-bridge已连接后重试"。这类问题重试一万次都没用,因为环境本身坏了。
1.3 为什么单纯重试解决不了80%的问题
一句话:重试只对瞬时故障有效,而Multi-Agent里的失败大部分不是瞬时故障。
什么叫瞬时故障?网络数据包偶尔丢了一下、服务端偶发超时、某个瞬间连接池满了。这种故障的特点是你重试一下往往就好了,因为故障源自己恢复了。
但配置错误、上下文超限、模型输出格式不对、节点离线,这些不会因为你多试几次就自己好起来。你重试只会让系统不断消耗token、不断产生告警、甚至不断放大副作用。就好比你给一个人打电话打不通,重拨几次可能有用;但要是对方手机停机了,你重拨一万次也没用,你得先去缴费。很多人没搞明白这个逻辑,把所有失败都当成了"手机没接",难怪被线上问题反复折磨。
2. 从"重试"升级到"分级响应":先给失败分个类
2.1 可重试失败 vs 不可重试失败
我说烂了的经验是:任何执行失败的异常,都要先给它打标签。可重试的,才走重试逻辑;不可重试的,直接走降级或者报错。
可重试失败通常有几个特征:错误是临时的、不改变系统状态、重试成本可控。比如HTTP 503、网络超时、连接池满了。不可重试失败的特征也很明显:错误由配置或代码导致、重试只会重复副作用、错误原因不会随时间变化。比如400参数错误、401鉴权失败、上下文超限、设备离线、token失效。
我整理了一个速查表,团队内部一直用这个做参考:
| 失败类型 | 典型标志 | 是否可重试 | 建议处理 |
|---|---|---|---|
| 瞬时网络故障 | 超时、503、连接重置 | 可重试 | 指数退避重试 |
| 配置/环境错误 | 设备离线、token失效、未启用1m上下文 | 不可重试 | 修复配置或人工介入 |
| 模型输出异常 | 只输出思考过程、格式不符 | 不可重试 | 调整prompt或重生成单次 |
| 工具业务错误 | 参数无效、业务校验失败 | 不可重试 | 让Agent重新规划任务 |
| 依赖服务过载 | 429限流、服务降级 | 可重试需谨慎 | 重试+熔断 |
注意,可重试和不可重试不是一成不变的。同一个错误在不同阶段可能转换。比如一开始是瞬时超时,但因为你重试太多次,把服务打出了限流,瞬时故障就变成了过载故障。所以重试一定要克制。
2.2 幂等性判断:能不能重试,先看副作用
在重试之前,比"是否可重试"更优先的问题是"重试是否安全"。这就要聊到幂等性。
幂等的意思是:同一个操作执行一次和执行多次,结果是一样的。比如查询订单状态,查一百次也没事;但创建订单、扣款、发送消息就不一样了,执行两次就会重复消费、重复扣款、重复推送。
我之前遇到的那个扣款事故就是这么来的:子Agent调支付接口时超时了,异常捕获后直接触发重试,结果第一次请求其实已经到达了服务端,只是响应丢了,第二次重试等于又下了一单。后来我们的处理方式是:给每次外部调用生成一个全局唯一的request_id,重试时带上同一个id,服务端做幂等键去重。这样就算重试十次,服务端也能识别出是同一笔请求,只处理一次。
所以在设计Agent执行引擎时,我强烈建议把"幂等检查"内建到工具调用层。每个需要写操作的工具调用,都要支持幂等键。如果没有这个保障,宁可不要自动重试,直接报错走人工。
2.3 用状态机管理执行状态,而不是写死循环
我看到很多人处理重试就是写个for循环,重试三次,失败了抛异常。这看起来简单,但对Multi-Agent来说远远不够。
一个Agent任务从开始到结束,会经历排队、执行、等待工具返回、解析结果、成功或失败等好几个阶段。不同阶段失败,处理策略完全不一样。比如执行阶段失败可以重试,但结果解析阶段失败往往要重新生成输出;工具等待超时是网络问题,但Agent状态异常可能就是代码bug。
我们团队的做法是用一个明确的状态机来管理:pending、running、waiting、succeeded、failed、retrying、degraded。每个状态都有明确的进入条件和退出条件。比如retrying状态只能在"失败原因可重试且未超过预算"时才允许进入;degraded状态是重试预算耗尽后进入的兜底状态,此时不再自动重试,而是降级到备用方案或者通知人工。
这一步做完,你会发现重试从一个"操作"变成了"状态转换",整个流程可观测、可控制,再也不会出现那种重试到天荒地老的情况。
3. 实操:给Multi-Agent加上智能重试与降级机制
3.1 带退避的重试策略:指数退避加抖动
如果你确定某个失败是可重试的,那也不能傻乎乎地固定间隔重试。我们需要指数退避,也就是每次重试的间隔按指数增长:第一次等1秒,第二次等2秒,第三次等4秒。这能有效避免在服务还没来得及恢复时疯狂撞门。
为了进一步避免多个Agent同时重试导致的重试风暴,还要加抖动。所谓抖动就是在退避间隔基础上加一个随机偏移量。比如基础等待2秒,实际等2秒加0到1秒之间的随机值。别小看这一步,不加抖动时,十个Agent同时失败同时重试,本来服务快恢复了,硬是被这一波重试又打挂了。
我给一个我们内部用的重试参数模板:
- 单个任务最大重试次数:3次
- 基础退避时间:1秒
- 退避倍数:2
- 最大退避时间:60秒
- 抖动范围:0到退避时间的20%
对应代码逻辑大概是这个样子:
import random import time def retry_with_backoff(func, max_retries=3, base_delay=1.0, factor=2.0, max_delay=60.0): retries = 0 while True: try: return func() except RetryableError as e: if retries >= max_retries: raise delay = min(max_delay, base_delay * (factor ** retries)) # 加抖动,防止重试风暴 delay = delay + random.uniform(0, delay * 0.2) time.sleep(delay) retries += 1这里的RetryableError就是前面说的可重试异常。只有这类异常才会进入退避重试,其他的直接抛出去,交给上层做降级处理。
3.2 重试次数和预算控制:别让一次失败烧光所有成本
重试是有成本的,不仅是时间成本,还有真金白银的成本。在Multi-Agent场景里,每一次重试都可能消耗模型token、外部API调用费、计算资源。你要是放开重试,一个本来2块钱能跑完的任务,可能在重试下烧掉20块还没结果。
所以我们给每个任务设置了三个预算维度。第一是重试次数上限,这个最简单,一般3到5次封顶。第二是总耗时预算,一个任务从开始到最终成功或放弃,最多不能超过配置的时限,比如5分钟。第三是成本预算,对模型调用和工具调用做累计计量,超过阈值直接放弃。
还有个容易被忽略的维度:逐级提升输出预算。如果你用的模型支持reasoning,某些比较难的推理任务第一次跑可能只输出了思考过程,没有生成正文。这时候系统可以自动重试,并且逐级提升输出预算,让模型有更多token去产出最终答案。热词里那句"模型本轮只输出了思考过程、没有产出正文。系统已自动重试2次,逐级提升输出预算"说的就是这种场景。但要注意,这种自动重试不代表可以无限提升,设置好上限,否则模型会把预算全部花在思考上。
3.3 失败后的降级路径:换模型、换工具、换Agent
重试耗尽之后,别急着报错,先看有没有降级路径。
降级的第一层是换模型。主模型超时或质量不达标时,切换到备用模型。比如主模型是满血版,处理不了的时候就切到快版模型,业务响应优先。这个切换需要提前在配置中心配好模型映射表,并且要确认备用模型的上下文能力也够用。
降级的第二层是换工具。一个工具接口挂了,能不能用另一个工具替代?比如主用的第三方天气API超时,可以切到备用的聚合数据源。这个逻辑在Agent设计阶段就要想清楚,哪些能力有冗余,哪些没有。
降级的第三层是换Agent。有的Agent可以专职干一件事情,但也可以由另一个通用Agent临时接管。比如负责写文案的Agent不可用,可以让负责内容策划的Agent先顶上来,保证任务不中断。
降级之后还有最后一道兜底:人工介入。我在设计任何一个关键业务流程时,都会留一个"转入人工"的出口。降级路径全部走不通时,系统把上下文、失败原因、已经试过的方案整理成工单,推给运营或技术人员处理。这样即使用户体验不到完全自动化,至少不会卡死在那里。
3.4 断路器模式:连续失败要熔断,而不是硬刚
单独看一次重试是没问题的,但如果你把视野放到长时间维度,会发现同一个依赖服务反复失败,这时候无条件重试就是在火上浇油。我们需要断路器。
断路器的思路很朴素:记录依赖服务的失败率,当连续失败次数超过阈值(比如5次)或失败率超过比例(比如50%),就打开熔断开关,在一段时间内(比如30秒)不再发起任何请求,直接进入降级逻辑。熔断结束后进入半开状态,放一小部分流量试探,如果成功了就关闭熔断,恢复正常。
这个模式在Multi-Agent里格外重要。因为Agent会自主决定调用什么工具,一个坏掉的工具可能被多个Agent同时调用,不熔断的话,整条链路都会被它拖垮。我们实现断路器时还会把"熔断状态"回传给Agent,这样Agent在规划下一步时就不会再选这个工具了,而是主动找替代路径,效果比硬等超时好得多。
4. 排查实录:几个典型失败场景的处理过程
4.1 场景一:"当前设备已离线,请确认coze-bridge已连接后重试"
这个报错我在本地调试Coze工作流时遇到太多次了。第一次看到时会以为重启一下服务就行,但重启几次都没用,后来才搞明白问题出在哪。
coze-bridge是把Coze工作流和本地设备能力连起来的桥接工具。它的工作机制是本地起一个进程,和Coze云端建立长连接,然后Agent才能调用本地文件、摄像头、脚本之类的能力。当报"当前设备已离线"时,代表这个长连接已经断了,云端找不到你本地的东西。
这种问题重试是完全无效的,因为它不是瞬时故障,是连接状态已经失效。我的排查顺序一般是这样的:
- 先看coze-bridge的进程还在不在,很多时候是进程崩了但没有自动拉起;
- 再看本地日志里连接断开的原因,常见的有关闭超时、心跳超时、token过期;
- 确认设备网络是否正常,本地网络切了Wi-Fi或者断了几分钟,长连接就会断开;
- 最后重新建立连接,让coze-bridge重新回连。
排查完我还会给coze-bridge加一个自动重连机制,断线后自动尝试重新建立连接,而不是只给用户一个冷冰冰的提示。
4.2 场景二:"1m上下文已经全量可用,请启用1m上下文后重试"
这个报错很有意思,它看起来像配置问题,但实际上是需要主动开启。"1m上下文全量可用"是说你的模型已经支持100万token的上下文窗口,但你的工作区配置还没启用这个能力。系统检测到你想用大上下文,但你没开开关,就直接给你拦住了。
遇到这种情况,你不知道原理的话真的会傻傻重试,因为错误信息很长,最后落在"重试"两个字上,很容易让人误以为是网络问题。实际上,点一百次重试也没用,正确做法是去配置中心把上下文窗口从128k调到1m,然后重新发布工作流。
这里我也想说一句,上下文不是越大越好。1m上下文确实很诱人,但上下文大了,模型推理的延迟和成本都会涨。我一般只在需要一次性处理超长文档或多轮长对话时才开启1m上下文,普通的业务场景用128k就够跑了。所以当系统提示你"启用1m上下文后重试"时,先反问自己:我真的需要这么大的上下文吗?如果只是偶尔要用,建议用单独的Agent来处理大上下文任务,而不是让所有Agent都扛着一个巨无霸上下文跑来跑去。
4.3 场景三:模型只输出了思考过程,没有产出正文
这个故障很隐蔽,它不是崩溃,而是模型"跑神了"。有的模型在开启reasoning能力后,会先输出一大段思考内容,然后才输出正稿。但在某些情况下,模型可能只输出了思考,正文没有生成,或者正文生成了但被截断。
错误信息里面会写"系统已自动重试2次,逐级提升输出预算"。这说明框架层已经帮你做了重试,而且是通过增加输出预算的方式重试的。但如果你自己写的Agent遇到这种情况,光靠增加max_tokens是不够的,你得从几个方向一起排查:
- 检查prompt里是不是没有明确要求"在思考结束后必须给出正文",加上一句"请务必输出最终结论"能减少这种情况;
- 检查输出解析层,是不是把思考过程当成了正文,需要把reasoning_content和content分开解析;
- 检查输出token上限,如果你的max_tokens设得太小,正文写一半就停了,系统自然只能看到思考过程;
- 检查流式输出是不是提前断流了,用户端只接收了前面的thinking部分就判定结束了。
4.4 场景四:网络连接失败,请检查网络后重试 3002
看到3002这个错误码,我第一反应是先确认它是哪个层的错误。如果只是在Coze工作流里出现,通常和云端调用链路的网络环境有关;如果出现在本地调试环境,那还要检查本地网络、代理配置、DNS解析这些。
这里我想分享一个排查原则:错误码只能帮你缩小范围,不能直接定罪。3002这种网络类错误,背后可能有五六种原因,你直接按"网络连接失败"去检查,方向没错但不够细。我的做法是这样:
- 第一步,看报错的时间点,是不是固定出现在某一个Agent调用某个工具之后,如果是,可以判断是那个工具的网络依赖出问题了;
- 第二步,在对应的环境里单独测一下工具接口的连通性,用curl或者调试面板直接调一次,看能不能通;
- 第三步,检查Agent执行日志里这个节点的超时配置,网络重试不是不行,但要和超时时间匹配,很多问题就是超时设太短,正常响应还没回来就被判定失败了。
如果确认是网络层的瞬时故障,那倒是可以重试,但要按第3节说的退避策略来,不要一秒钟试五次。
5. 多智能体拓扑与通信机制:失败会传染
5.1 链式、并行、协商拓扑的失败传播差异
聊完单点失败的处理,必须得聊拓扑。Multi-Agent的失败不是孤立的,它会沿着Agent之间的依赖关系传播。我把常见的拓扑分为三种,它们的失败传播路径完全不同。
链式拓扑是最脆弱的,A处理完传给B,B再传给C,整个链路像串珠子一样,任何一颗断了,后面全部停摆。这种拓扑下,你要么给每个环节都做重试和降级,要么在编排层加"跳过"逻辑,让B失败时可以绕到备选路径。
并行拓扑抗失败能力稍强。多个Agent同时处理各自的部分最后汇总,某个Agent失败不会立刻拖垮其他Agent。但这里有个坑:汇总节点会等待所有并行结果,如果其中一个Agent一直卡着,整个汇总也会卡住。所以并行拓扑一定要给每个分支设独立的超时和失败兜底,比如"其中一个失败就用默认值参与汇总"。
协商拓扑是最灵活的,也最难控制。Agent之间动态传递消息、互相协商谁能处理某个任务。这种拓扑下失败不容易被感知,因为失败信号可能在多轮会话中被稀释了。我的经验是给协商过程加一个"最终期限",超过期限强制出结果,否则任务永远在互相踢皮球。
5.2 通信机制层面的常见坑:消息确认、超时、死信队列
拓扑是骨架,通信是血脉。通信机制设计不好,失败数量会成倍增加。
首先是消息确认机制。Agent之间通过消息队列或RPC传递任务,发送方必须能确认接收方确实拿到了消息。没有确认机制的话,消息丢了发送方还不知道,还在那里傻等,等超时了报个错,这个错误表面上看是超时,实际是消息丢失。
其次是超时设置。我见过一个系统,所有Agent的超时时间都写死成30秒。结果A任务简单,1秒就完成了;B任务复杂,因为依赖一个慢接口,每次都要跑50秒。B永远超时,超时后还触发重试,重试又超时,整个系统天天告警。后来把超时改成按任务类型动态下发,问题一下子就没了。
第三是死信队列。凡是重试耗尽、降级也失败的消息,不能直接丢进黑洞,要放进死信队列,等待人工或者定时任务来处理。这是可观测性的最后一块拼图。有了死信队列,你可以统计一段时间内哪些任务频繁失败,反过来优化重试策略或拓扑结构。
5.3 如何设计"故障隔离":让一次失败不要变成一场灾难
故障隔离的核心理念是:任何一个Agent或工具都可能是不可靠的,你要假设它随时会挂,并且让它挂了以后影响范围可控。
一个实用做法是给每个Agent设置独立的运行空间。比如把关键Agent和次要Agent放在不同的进程或容器里,不要让它们在同一个环境里争抢资源。否则一个Agent内存泄漏,把整个进程拖垮,其他Agent全都跟着遭殃。
另一个做法是限制级联调用深度。Agent调用Agent,不要无限嵌套下去。我们规定调用链最多三层,超过三层必须由编排层接管。因为嵌套越深,排查越难,失败传播的范围也越大。
最后,任何跨Agent的调用都要有独立的错误边界。A调用B,A不应该知道B内部怎么失败的,它只需要知道B给了三种结果:成功、可重试失败、不可重试失败。这样调用方的逻辑会非常干净,不会被底层的复杂异常打乱。
6. 效果观察与踩坑总结
6.1 上线分级响应机制后的数据变化
这套机制我用了大半年,最明显的感受是:失败告警数量没有少多少,但"需要人盯着处理"的失败少了非常多。
我们做了一次前后对比。改之前,线上Agent任务的成功率大概在87%,看起来还行,但剩下的13%里,有一大半是无意义重试造成的重复请求和卡顿,真正被归类为不可恢复失败的只有一小部分。改完之后,成功率大概稳定在94%左右,更重要的是,失败的处理从"靠人肉看日志判断要不要重试"变成了"系统自动分类并执行策略",值班的同事终于不用半夜爬起来点重试按钮了。
还有一个变化是成本。加了退避和熔断之后,外部接口调用量下降了大约35%,因为不再有那种疯狂重试的局面了。论token消耗,也健康了很多。
6.2 几个我自己都踩过的坑
第一坑:重试之前忘了做幂等设计。这个我开篇就讲过,付钱的血泪教训换来的经验。我的建议是,所有写操作的工具,第一版就要带上幂等键,不然后面每个需求方都会让你加,加一次改一次,还不如一开始就做好。
第二坑:把断路器阈值调得太灵敏。刚开始我设了连续两次失败就熔断,结果一个服务偶发抖动直接熔断30秒,流量全被导到备用服务,备用服务被冲垮了。后来我把阈值放宽到连续5次或1分钟失败率超过50%,同时让熔断恢复时间可以从最短5秒逐渐拉长,系统就稳了。
第三坑:上下文窗口"能开多大就开多大"。有一段时间为了减少上下文超限的报错,我直接把所有工作流都开了1m上下文。结果模型推理延迟从两秒涨到四五秒,成本也涨了不少。后来我改成按场景分档,短对话128k,长文档任务才开1m,报错少,性能也好。
第四坑:只做了降级,没有做降级后的体验设计。有一次某个Agent降级到备用模型,备用模型的口径和主模型不一样,回答质量明显下降,用户直接投诉了。后来我们强制规定,一旦发生降级,系统要在结果里打一个标记,让下游知道这条结果是降级产出的,评级或者可信度要单独计算。
6.3 这个方向还可以怎么继续演进
我现在正在做的尝试是给重试策略加入自适应能力。以前退避参数是配置死的,现在是让系统记录每个依赖方的历史失败率和恢复时间,动态调整退避倍数和熔断阈值。这样,一个平时很稳定的服务偶尔抖一下时,重试会更温柔;一个近期频繁出错的服务,会更快被熔断,避免拖垮链路。
另一个方向是把失败处理经验和知识沉淀成Agent自身的能力。比如把常见错误的排查步骤写进一个"故障诊断Agent"的上下文库,当业务Agent遇到未知错误时,先咨询诊断Agent,而不是直接抛给重试逻辑。这听起来有点套娃,但实际效果很好,相当于给系统配了一个运维专家。
我个人的体会是,Multi-Agent的稳定性不是靠消除失败实现的,而是靠把失败变成可控事件实现的。学会了和失败共处,比试图让系统永不失败要靠谱得多。如果你现在还在无脑重试,我建议你从今天开始,先把失败分类做起来,然后动手给系统加一个断路器。这个改动不复杂,但你会在下一场事故里感谢自己。