Multi-Agent 的项目一跑起来,真正让人焦头烂额的不是 Agent 不够聪明,而是它在半夜两点准时告诉你:某个节点执行失败。遇到这种消息,很多人的第一反应就是把max_retries从 2 改成 5,或者在外面套一层while True。我在几个线上多智能体项目里都见过这种写法,看起来系统"稳"了,实际上只是把同一个错误反复吞掉,日志里躺着一排 SKIP,业务侧该漏的单还是漏了。
为什么会这样?因为 Multi-Agent 里的"失败"根本不是一种病,重试只是最低级的药,吃多了还容易掩盖真正的病灶。这篇文章不聊玄学,只讲工程:一个 Agent 执行失败后,到底该怎么判断、怎么隔离、怎么补位、怎么让整条链路恢复,而不是无脑 retry。
1. 失败不是同一种病,先给 Agent 挂个"病历本"
1.1 先分清:临时故障还是永久性故障
很多团队把重试写成一个全局装饰器,任何异常都套三层重试。这种策略最大的问题,是把"过一会儿就能好"的临时故障,和"再试一百次也好不了"的永久故障混在一起。
临时故障很典型:
- 网络短暂抖动,TCP 连接超时;
- 下游服务返回 502/503,对方正在重启;
- 依赖的接口触发限流,返回 429;
- 数据库连接池瞬时耗尽。
这类故障的特点是根因可能几秒到几分钟内消失,重试确实有效。但永久性故障完全不是一回事:
- 凭据过期,比如
failed to refresh token: invalid; - 业务状态已经变更,订单早被取消了;
- 模型输出缺少必填字段,校验根本过不去;
- 上游接口的契约条件对本次请求不适用。
这些故障重试多少次都是同样的结果,白白浪费一次模型调用或外部接口配额,还会把真实问题拖到凌晨告警才浮出来。
所以第一步不是写重试函数,而是给异常分类。我的做法是给异常定义retryable属性,或者在异常类型上做白名单。只有命中明确的可重试类型才允许进入重试流程,其他一律走失败处理流程。这么一改,重试次数通常还会变少,系统表现反而更稳定。
1.2 从"失败表象"追到"根因层"
代码里的except Exception只是看到表象。同一个"执行失败"在不同原因下,处理策略完全不同。我评审容错方案时,一定会拿下面这张表逐行过一遍:
| 失败表象 | 常见根因 | 自动重试有没有用 | 更应该做的事 |
|---|---|---|---|
| 连接超时 | 网络闪断、防火墙抖动 | 有用,但要配合退避 | 确认对端健康,再看是否切换机房通道 |
| 下游 502/503 | 服务重启或过载 | 短时有用 | 开启熔断,限制重试并发 |
| 429 RateLimit | 配额不足 | 有用,但要退避 | 查剩余配额,考虑降级到另一个供应商 |
| 401/403 / token invalid | 凭据过期 | 无用,重复一样失败 | 刷新凭据后只回放一次 |
| 模型输出 JSON 缺字段 | 输出格式不对 | 低效,经常连续失败 | 追加校验错误再重试,或转人工 |
| 业务状态冲突 | 数据已经被其他流程改了 | 禁止自动重试 | 走补偿流程或人工确认 |
| 设备离线 / bridge 未连接 | 底层通道断开 | 无用,通道没恢复 | 先心跳探测、重新建连,成功后重放 |
最后一条特别值得注意。很多桥接组件会提示"当前设备已离线,请确认 coze-bridge 已连接后重试",它其实不是 Agent 处理业务失败,而是底层的通信链路断了。如果不对这种错误做心跳探测就去重试,会一直打到超时,直到人工把连接恢复。合理的做法是:触发重连、等待注册成功、然后只重放这一次任务。
1.3 失败不是终点,是一个可转移状态
在 Multi-Agent 的运行时里,每个 Agent 实例应该有自己的状态机,而不是一个简单的failed布尔值。我常用的状态至少包括:
pending:排队未开始;running:正在执行;succeeded:成功,结果已持久化;retryable_failure:可重试错误,等待进入重试队列;permanent_failure:不可重试错误,等待补偿;awaiting_human:需要人工确认;compensated:已经通过其他路径完成。
有了这些状态,重试循环才会懂事。比如状态是awaiting_human时,任何自动重试都应该忽略;状态是permanent_failure时,要么触发降级,要么发工单,而不是在一个死循环里反复撞墙。
2. 重试之外:幂等、降级、人工接管三板斧
2.1 幂等键:比重试次数高一整个台阶
重试最大的隐患不是重试本身,而是"重试造成的副作用"。
举个例子:一个比价 Agent 调用供应商接口,正常请求会生成一张报价单。如果没有幂等键,第一次超时后你不知道服务端到底有没有处理成功,于是重试;第二次调用又生成了一张报价单。客户看到两张重复报价,业务直接就蒙了。
幂等键的做法很简单:每次 Agent 任务生成一个唯一业务键,在重试过程中保持不变,外部系统根据这个键去重。可以按工作流维度生成:
import uuid def build_idempotency_key(workflow_id, node_name, business_no, round_no): raw = f"{workflow_id}:{node_name}:{business_no}:{round_no}" return str(uuid.uuid5(uuid.NAMESPACE_URL, raw))把这个键放进请求头或请求体,比如Idempotency-Key,重试时永远用同一个键。如果外部接口不提供幂等能力,可以在本地执行记录表里加唯一约束,字段就是node_name + business_no + round_no,重试前先查这条记录是否已经成功。一句话:重试前不保证幂等,等于给线上埋雷。
2.2 降级到备用执行路径
有些失败重试没有意义,但整条链路又不能卡死。这时候要启用备用执行路径。
备用路径不是简单地在代码里catch后换个 Agent 再跑一遍,而是换一种执行策略。比如:
- 主 Agent 是"从供应商 A 和 B 实时查询报价",失败后降级为"读取上一次成功缓存的供应商快照",并标记结果
low_confidence; - 主 Agent 是"AI 自动生成合同条款",失败后降级为"使用模板合同并打上待法务审核标签";
- 主 Agent 是"视觉识别货物图片并入库",失败后降级为"把图片转入人工复核队列"。
降级不是放弃,而是用较低质量的结果维持主链路进展,同时把偏差通知给相关方。这比让整个工作流卡在第 4 个节点上强得多。
另外一个关键点:备用路径不要和主路径共享同一个可能故障的资源。如果主 Agent 因为某台机器负载高而失败,备用路径还调度到同一台机器上,那这个"降级"只是心理安慰。至少要做到不同网络出口、不同的模型服务、不同的供应商。
2.3 人工接管不是"退步",而是"兜底"
有些失败业务上不允许自动决策。比如付款指令已发出、库存已锁定、合同已经发送给对方,这种情况下自动重试或自动降级都可能造成无法挽回的后果。
正确的姿势是把失败转成"待确认事件":
- 把当前 Agent 的输入、输出、异常信息、上下文摘要、trace_id 打包;
- 写入一个
manual_review表; - 通过企业微信机器人推送给值班人;
- 值班人审阅后选择:继续重试 / 跳过 / 走补偿流程 / 终止整个工作流。
我见过一个很稳的团队,他们的 Multi-Agent 里人工处理率维持在 3% 左右,问题不是系统经常失败,而是所有"不该自动决策"的失败都有人接住。人工接管不是退步,恰恰是系统成熟的标志。
3. 调度层该干的事:状态机、Checkpoint 与失败分支
3.1 让 DAG 或状态机记住已经干完的活
Multi-Agent 不是单机脚本,是一个工作流。多个 Agent 之间存在依赖关系,如果其中一个执行失败,最蠢的做法是把整条链路从零开始重跑。前面已经成功、已经写入结果、已经通知过的节点,统统再执行一遍,副作用不可控。
调度层应该具备工作流状态管理能力。像 DolphinScheduler 这样的平台在做定时调度时,为什么大家都爱用?核心不是"能定时",而是每个任务节点的状态、依赖、失败策略都清晰可见:一个节点失败后,它前面的成功节点不用重跑,后面的节点可以被阻断或跳过。
自研系统至少要做到两点:
- 每个 Agent 节点执行结果持久化,并记录
node_status; - 失败恢复时,从失败节点继续,而不是从头开始。
你可以把已经完成的节点结果缓存到 Redis 或数据库。重跑失败节点时,上游 Agent 读取缓存结果就能继续,不用真的重新调用一遍。
3.2 失败分支条件化:重试、跳过、补偿三选一
失败策略不要写死在try-except里,而是定义在工作流级。每个节点应该有一个on_failure配置,明确告诉调度器:这个节点失败后走哪条路。
我常用的配置长这样:
| 节点类型 | 失败后默认行为 | 理由 |
|---|---|---|
| 数据采集类 | 先重试,再跳过并标记缺失 | 采集结果缺失可以后续补数据 |
| 消息通知类 | 重试一次后降级为静默 | 通知重复发送比漏发还讨厌 |
| 资金交易类 | 禁止自动重试,直接人工 | 幂等无法覆盖所有边界 |
| 模型生成类 | 带校验错误重试一次 | 第二次通常能纠正格式 |
| 外部审批类 | 转等待状态,不阻塞其他分支 | 审批结果需要异步回调 |
重试、跳过、补偿本质上是三条不同的路。调度器应该在失败那一刻就看懂配置,而不是靠业务代码里堆if/else。
3.3 超时预算与上下文管理
还有一个容易被忽略的点:一次失败重试不是"免费"的。它在占用模型上下文、API 配额、时间窗口。
哪怕底层的上下文窗口已经能做到 1M 全量可用,也不要错误地以为可以无限把失败现场堆回去。一次失败的 Agent 可能往上下文里塞了一大段错误堆栈、无效 JSON、重复的思考过程。如果重试时不做上下文清理,模型会被上一轮失败的残骸污染,越重试越糊涂。
我的做法是:
- 每个节点定义一个"上下文快照";
- 节点失败后,重试任务使用进入该节点时的快照,而不是"模型已生成的末尾内容";
- 把失败信息压缩成一条结构化错误摘要,作为下一次尝试的系统提示;
- 如果连续失败超过阈值,不再追加上下文,直接转人工。
另外,每个失败尝试都要计入整体执行预算。一个工作流如果已经执行了 12 分钟,其中 9 分钟都在重试同一个节点,就算最终成功了,用户体验也是失败的。设置每节点超时和全链路超时,超时就熔断。
4. 那些不该重试却总被重试的场景:典型反例拆解
4.1 模型"只输出思考过程,没产出正文"
有朋友给我看过一屏这样的错误:"模型本轮只输出了思考过程、没有产出正文。系统已自动重试 2 次。"这就是典型的"重试解决不了问题"。
模型偶尔不产出正文,原因可能是输出预算被思考过程耗尽,也可能是提示词里没有给出明确的输出格式。此时单纯重试两次,大概率还是同样的结果。更有效的做法是:
- 用结构化输出约束,比如 Json Schema,让模型必须产出符合 schema 的结果;
- 检测到"只有思考过程"时,把这段思考过程压缩为一句提示,再追加"直接输出结果,不要解释";
- 重试后依然失败,就转人工,而不是让系统一直自动试。
换句话说,不是不能重试,而是重试前要对失败原因做一次"语义识别",带着纠正信息重试,重试率会显著下降。
4.2 "刷新 token 失败"却当成临时网络错误
failed to refresh token: invalid这行日志我见过很多次。它明确告诉你:刷新 token 操作失败了,原因可能是 refresh token 过期、被撤回,或凭据信息不对。这是永久性故障。
但不少代码会把所有网络异常都包进重试器里,于是一个失效的 token 被反复拿去刷新,每次都失败,每次都空等几秒,连续重试五分钟后告警。正确流程应该是:
- 识别为认证错误;
- 停掉自动重试;
- 重新获取新的凭据或触发 OAuth 流程;
- 用新凭据把原请求回放一次;
- 如果还是失败,才转人工。
4.3 对端设备离线、桥接未连接
这类错误通常不在 Agent 自己的工作范围里,而在通信层。提示"当前设备已离线,请确认 coze-bridge 已连接后重试",本质是"通道没建立",不是"业务执行失败"。
对通信层故障,盲目重试等于敲门没人应还一直敲。正确做法是把重连和重试分开:
- 先探测设备状态、bridge 的健康检查接口;
- 触发重新注册、重新建立连接;
- 等待状态变为
online后,再放行任务; - 如果 30 秒内无法恢复,直接告警,通知维护人员。
重试队列里的任务应该有"到期时间"和"最大滞留时间",否则桥接恢复后,几十个积压任务同时重放,又可能把恢复的通道打崩。
4.4 业务状态不可逆时,不允许自动重放
我一直强调:凡是会产生真实世界副作用的动作,都不允许无脑自动重试。
比如扣款、发券、锁定库存、发送合同、调用审批接口。这类操作的危险在于:第一次调用可能已经成功,只是响应超时;如果重试,就造成重复扣款、重复发券。就算有幂等键,也只能覆盖实现了幂等的系统,不能假设所有第三方都遵守。
所以对这类节点,我的策略是:
- 失败后进入"结果查询"模式,先查这笔操作在目标系统里的最终状态;
- 如果状态是成功,则本节点标记为成功;
- 如果状态是未处理,才允许重试一次;
- 如果状态未知,人工确认。
5. 可观测性与告警:别让失败在系统里无声蒸发
5.1 给每次执行一个 trace_id,失败时可回溯整条链路
没有可观测性的 Multi-Agent 就像一个黑盒:你只知道失败了,但不知道是哪个环节、哪次调用、哪条数据导致的。
我给每个工作流实例生成一个trace_id,每次 Agent 调用、每次外部 API 请求、每次重试都带上这个 ID。失败日志里必须包含:
- 当前节点名称;
- 当前工作流 ID;
- 本次尝试是第几次(attempt);
- 失败异常类型和摘要;
- 上游结果快照的引用;
- 上下文条数和 token 使用量。
有了这些信息,告警里就不会只有一句冷冰冰的"Agent failed"。值班人看到告警时,能立刻判断是重试、跳过还是人工接管。
5.2 任务执行失败时,用企微机器人主动推送
失败不能只写在日志里。如果业务侧关心结果,你就得用即时通知把人拉进来。
我常用的方式是企业微信群机器人,失败时推送一张结构化的卡片,内容包含节点、原因、影响范围、trace_id 和操作按钮。示例请求体大概长这样:
{ "msgtype": "markdown", "markdown": { "content": "### Agent 失败告警\n> 节点:`execute_pricing`\n> 原因:`rate_limit_exceeded`\n> 尝试次数:3\n> trace_id:`trace-xxx-123`\n> 影响订单:`PO-202506001`\n> 待办:[点击确认处理](http://your-console/tasks/manual-review)" } }推送之后要做"告警去重"。同一个原因在五分钟内连续推送,只会让值班人麻木。我的规则是:同一 trace_id 下的同节点失败,只在第一次、第三次、第五次和最终转人工时各推一次,防止刷屏。
5.3 关注失败原因分布,而不是失败总量
光看"成功率 99.9%"是不够的,还要看失败原因分布。我一般在 Prometheus 里记录四个维度的指标:
agent_failure_total{agent, reason}:不同原因的数量;agent_retry_total{agent, attempt}:重试次数分布;agent_failure_cardinality:失败原因种类数;agent_manual_review_total:需要人工介入的数量。
如果某个 Agent 的失败原因种类一直在增加,说明它的输入来源越来越复杂,需要补校验;如果重试成功率很高,说明网络问题多,可以考虑加大超时而不是重试次数;如果重试成功率接近于零,说明这个失败根本不该重试,要改分类。
6. 拓扑与通信机制:容错能力从架构里长出来
6.1 星型拓扑 vs 网状拓扑:调度器本身的容错
Multi-Agent 的拓扑和通信机制直接影响失败处理。星型拓扑里所有 Agent 都通过中央调度器通信,好处是状态集中、失败分支清晰,坏处是调度器一挂全链路瘫痪。这时候你的重试策略再精美也没用,必须先保证调度器高可用。
网状拓扑让 Agent 之间直接通信,故障点分散了,但"谁对失败负责"变得模糊:A 在等 B 的结果,B 其实已经失败但消息还在路上,A 会一直等到超时。所以网状拓扑里,每个 Agent 必须有独立的超时、重试预算和失败上报机制,不能把失败收敛任务推给一个不存在的中心节点。
我的建议是:默认用中心化调度 + 状态持久化,等业务规模大到需要低延迟直连时,再让部分节点走网状通信,同时保留全局 trace。
6.2 心跳、租约与注册中心:失联不能只靠错误提示
有时候 Agent 没有抛异常,但它已经失联了。比如某个执行节点所在的容器被杀掉、网络分区,或者底层 bridge 断开。这种情况如果只靠"任务执行失败"来判断,反应太慢。
更合理的设计是引入心跳和租约机制:
- 每个 Agent 定时向注册中心上报心跳;
- 注册中心给 Agent 发一个带 TTL 的租约;
- 超过 TTL 没有续约,就把该 Agent 标记为
offline; - 调度器对
offline节点不再派发新任务; - 已派发但未完成的任务进入待迁移队列,重新分配给其他可用节点。
这样很多失败在发生前就被拦截了,而不是等任务超时后再补救。
6.3 通信失败和处理失败:两个层次都得管
很多工程师只处理"接口调用失败",忽略了"消息已经发出但结果未知"的情况。网络问题最容易卡在这个模糊地带:请求到底有没有到达对端,谁都不知道。
通信失败和处理失败的策略要分开:
- 发送前失败:连接不上、握手失败,可以安全重试;
- 发送后超时:结果未知,不能原地重放,先去查任务状态;
- 对端返回业务错误:说明请求已到达,按业务规则处理;
- 对端成功但返回格式无法解析:把原始响应存下来,标记为需人工解析。
只有把通信层和处理层分开治理,才能避免那种"明明失败了,但重试之后造成两个不同结果"的诡异问题。
7. 落地清单:把"初级重试"升级成"容错编排"
最后给一份可以直接拿去复盘检查的清单。我每次给 Multi-Agent 项目做容错设计,都会从这七个维度过一遍:
| 检查项 | 合格标准 |
|---|---|
| 异常分类 | 代码里能区分可重试和不可重试异常 |
| 幂等保护 | 所有可能产生副作用的调用都带幂等键 |
| 降级路径 | 关键失败节点都能找到备用执行路径 |
| 人工接管 | 有manual_review队列和即时通知机制 |
| 状态持久化 | 失败恢复后从断点续跑,不重跑成功节点 |
| 超时预算 | 单节点、全链路都有超时上限 |
| 失败指标 | 能看到失败原因分布,而不是只看总数 |
我个人在实际项目里的体会是:重试参数越改越大,往往说明问题没有被正视。有一次我把某个 Agent 的重试次数从 5 次降到 2 次,线上整体故障率反而下降了——因为不可重试的错误不再被掩盖,坏节点很快被标记、替换、重建,而不是在角落里反复挣扎。
另一个小技巧是把"失败处理"当成功能需求写进每个 Agent 的验收标准,而不是让它沦为异常路径。上线前多做几次故障注入演练:断网、关闭下游服务、清空上下文、让模型吐一次非法 JSON。哪一种失败方式能让系统进入僵局,就在那一次把流程补齐。等这些演练都跑通了,你再回头看,会发现"重试"只是整个容错体系里最小的一块砖。