
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_idtrace-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。哪一种失败方式能让系统进入僵局就在那一次把流程补齐。等这些演练都跑通了你再回头看会发现重试只是整个容错体系里最小的一块砖。