ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Multi-Agent搞砸了别只会重试:失败处理完整工程化指南

Multi-Agent搞砸了别只会重试:失败处理完整工程化指南 凌晨两点手机连震三次。告警群里那张 Multi-Agent 任务执行失败的截图配着一行“请重试”。我揉着眼睛爬起来点了重试等了五分钟又失败。再重试还是失败。最后发现根本不是偶发网络抖动而是其中一个子 Agent 的鉴权 token 过期了——重试一百次也是同一个结果。这类事情我见过太多次了。“Multi-Agent 里某个 Agent 执行失败第一反应是重试”这个习惯几乎刻在所有做 Agent 编排的人骨子里。但说句得罪人的话只会重试说明还没把失败处理当成一个完整的工程问题来对待。Multi-Agent 的失败处理本质上是一套“分类、决策、降级、补偿、预防”的闭环体系。重试只是其中最基础的一个动作甚至连“最基础”都算不上——它是最后的手段不是默认的动作。这篇文章我不会跟你讲那些花哨的 Agent 框架有什么炫酷功能只聊一件事当一个 Agent 在编排链路里倒下的时候成熟的做法到底是什么。我会从失败分类、可重试性判定、工程化重试策略、降级补偿、乃至架构层面的容错设计把这条完整链路拆开讲透顺便附上我在实际项目中踩过的坑和调参经验。代码和思路都可以直接抄适用场景不限框架——LangGraph、AutoGen、自研调度器都能用同一套逻辑。1. 先搞清楚一件事Multi-Agent 为什么总在执行失败很多人把 Multi-Agent 执行失败简单归类为“网络不好”或者“模型抽风”然后直接重试。这是最偷懒的做法。实际排障时失败原因至少可以拆成五类每一类的处理方式完全不同。第一类瞬时错误。网络闪断、HTTP 5xx、限流rate limit、上游服务短暂的不可用。这类错误有个共同点只要等一会儿系统自己就会恢复。重试对它们有效。第二类配置与凭证错误。模型 API key 失效、token 过期、调用的模型名不存在、某个 Agent 依赖的外部服务地址配错。这类错误有个典型特征你重试一万次结果都一样。它们需要的是“刷新凭证”“修改配置”“换一个服务地址”而不是重试。第三类上下文与资源超限。最常见的表现是“上下文窗口溢出”——类似“启用 1m 上下文后重试”这种提示本质上是当前任务的输入长度已经超过了模型能处理的窗口。还有显存不足、磁盘写满、API 配额耗尽。这类错误的解法是“重新分配资源”或“处理输入”而不是简单重试。第四类逻辑与输出格式错误。Agent 的推理结果不符合下游要求的 schema或者模型只输出了思考过程、没输出正文。这类错误源于模型行为的不确定性简单重试可能成功但成本极高——你等于让模型把同样的思考再跑一遍。第五类级联传播错误。上游 Agent 给出了错误的中间结果比如把日期格式解析错下游 Agent 基于这个错误输入继续处理最终导致整个链路的输出是垃圾。这种情况最讽刺重试的不是出错的那个环节出错的是上游。你盯着下游失败点反复重试永远修不好。这五类错误混在一起就是很多人在 Multi-Agent 项目里“天天救火”的根源。你没有对失败分类就没有决策依据没有决策依据重试就是抓阄。我见过一个真实案例某团队做了一个“信息抽取→去重→入库”的三 Agent 流水线每天凌晨跑批总有几个任务失败。负责的小哥非常勤奋每次失败都手动触发重试一天能重试几十次。后来我帮他看了日志发现 80% 的失败都来自同一个根因——上游抽取 Agent 偶尔会在结果末尾多打印一行调试信息导致下游 schema 校验不过。这种问题你重试一百遍唯一的区别就是多花一百遍的 token 钱。把解析逻辑改成“取有效 JSON 片段”之后一晚上再也没炸过。2. 重试前先回答三个问题可重试性判定才是核心既然失败分了类型下一步就是建立一套“可重试性判定”的规则。这套规则解决的核心问题是拿到一个异常之后先决策再行动。2.1 先给错误分门别类定义统一的异常结构第一步很朴素把 Multi-Agent 链路里所有可能出现的异常统一收敛到一个结构化的错误对象里。别管是框架抛出的、HTTP 层返回的、还是模型输出解析失败的全部转成同一种表达方式。我用 Python 举个最小的示例from enum import Enum class ErrorCategory(str, Enum): TRANSIENT transient # 瞬时错误可重试 CONFIG config # 配置/凭证错误不可重试 RESOURCE resource # 资源超限调整后可重试 OUTPUT_FORMAT output_format # 输出格式错误需要修正后重试 CASCADE cascade # 级联错误需追溯上游 class AgentError(Exception): def __init__(self, message: str, category: ErrorCategory, retryable: bool): super().__init__(message) self.category category self.retryable retryable这个结构看起来简单但它解决了一个实际问题把“错误信息”和“错误语义”解耦了。日志里打印“failed to refresh token: invalid”只是信息但只有当它被标记为CONFIG类别时系统才能正确地选择“去重新登录刷新 token”而不是“无脑重试”。对应的分类映射逻辑大概是这个样子def classify_error(err: Exception, context: dict) - ErrorCategory: if isinstance(err, TimeoutError): return ErrorCategory.TRANSIENT if token in str(err).lower() or auth in str(err).lower(): return ErrorCategory.CONFIG if context length in str(err).lower() or window in str(err).lower(): return ErrorCategory.RESOURCE if schema in str(err).lower() or json in str(err).lower(): return ErrorCategory.OUTPUT_FORMAT if context.get(upstream_failed): return ErrorCategory.CASCADE return ErrorCategory.TRANSIENT # 默认可重试但记日志注意最后一行的默认值拿不准时默认按可重试处理但必须留下结构化日志。这背后的策略是“宁可错杀也不能卡死”——很多分布式系统里偶发故障的比例远高于永久故障所以默认可重试是合理的。但如果你连分类都没有就谈不上“默认”二字。2.2 三种明确禁止重试的情况分类之后我一般会直接列一个“禁止重试清单”。以下三种情况见了直接走其他分支别碰重试按钮第一种鉴权与配置错误。凡是包含invalid token、auth failed、permission denied、model not found、invalid request字样的异常一律不重试。重试只会增加无意义的 API 调用。正确做法是触发凭证刷新流程、加载新的配置、或者直接把任务标记为“需人工介入”。你有没有想过为什么很多系统提示“failed to refresh token: invalid”之后你手动重试一百次都没用因为问题出在“刷新 token 这个动作本身失败了”要先修刷新逻辑不是修业务重试。第二种上下文与资源溢出。类似“上下文长度超过限制”的错误重试多少次都不可能把输入变短。除非你在重试前做了输入裁剪、摘要压缩、或者切换到一个更大上下文的模型否则重试无意义。这里顺便提一句现在很多平台宣传“1m 上下文全量可用”但“可用”不等于“任何任务都能塞进去”。1m 上下文的实际效果跟你用的模型、检索方式、任务的 token 消耗规律强相关。当报错说“请启用 1m 上下文后重试”时你应该意识到这已经是一个资源调配问题不是重试能解决的。第三种级联失败。当错误上下文里带有“上游 Agent 已失败”标记时当前 Agent 的失败只是一个“果”不是“因”。此时你要做的是回溯上游处理链路而不是在这个节点上重试。举个例子A Agent 负责从网页里抽取结构化字段B Agent 负责把字段填入数据库。A 抽错了字段类型B 反复重试“填写”动作数据库里收获一堆错误记录。这不叫解决问题这叫放大错误。3. 工程化重试把“再跑一次”变成指数退避与幂等控制好现在到了重试真正该上场的地方。对瞬时错误重试是非常有效的策略但“有效”的前提是重试本身被工程化而不是简单地在 catch 块里写个 for 循环。3.1 别再固定间隔重试了指数退避与抖动固定间隔重试有个致命问题如果下游服务因为过载而返回 5xx你每隔 2 秒打一次请求等于持续加重下游负担让恢复变得遥遥无期。这就是“修复失败时把自己也拖进去”的经典案例。指数退避的思路很简单每次重试的等待时间按指数增长并加入随机抖动jitter防止多个请求同时重试造成“惊群效应”。下面这个伪代码我直接用了很多次import random import time def run_with_retry(func, max_retries3, base_delay1.0, max_delay30.0): for attempt in range(max_retries 1): try: return func() except RetryableError as e: if attempt max_retries: raise delay min(max_delay, base_delay * (2 ** attempt)) delay delay * (0.5 random.random()) # 加入 50%~100% 的抖动 time.sleep(delay) raise RuntimeError(unreachable)几个我实测出来的参数经验max_retries不要超过 3~5 次。超过 5 次等待时间已经拉到十几秒以上对用户侧感知是“卡死”而且大概率下游已经彻底挂掉了。base_delay从 1 秒起步比较温和如果你调用的是非常耗时的模型推理base_delay建议直接翻倍到 2~3 秒因为模型推理的耗时本身就有几秒你太快重试只会相互叠加。max_delay一定要设上限。没有上限的指数退避会让任务在后台默默等待十几分钟用户早就跑了调度系统还以为任务在推进。3.2 幂等性陷阱重试两次不等于跑了一次这是 Multi-Agent 重试里最隐蔽的坑。你以为“重试是把没做完的事情再做一遍”但很多时候第一次执行可能已经部分完成了。举个非常典型的场景一个 Agent 做“订单创建 扣款 发送通知”三步操作。如果扣款成功、但发送通知时网络超时你无脑重试整个 Agent 任务会发生什么第二次执行会再创建一单、再扣一次款。重试不是“修正”是“重复”。破解方法是给每个任务绑定全局唯一的trace_id并在任务开始时执行幂等检查task_id task_20250607_001 if redis.exists(fdone:{task_id}): print(任务已完成跳过重试) return load_result(task_id)这里的关键点在于执行前先检查结果执行成功后写入结果。“先查再做”能挡住大多数重复执行问题。尤其在 Multi-Agent 场景下多个子任务并行跑重试只应该发生在“确认没有产生副作用”的环节。副作用是否发生判断依据是状态存储里的任务状态重试的入口也应该是状态机驱动的而不是异常驱动。还有一点重试时要区分“整个链路重试”还是“失败节点重试”。推荐只重试失败节点而不是把整个 Multi-Agent 链路从头跑一遍。因为链路里其他节点可能已经成功重跑整个链路等于把所有成功节点又变成不确定因素——纯属给自己找事。3.3 聪明的重试会逐级提升预算这个细节很多人完全没注意到。现状是有些系统确实会“自动重试”但每次重试都用一模一样的参数那基本等于“把命运交给随机”。实际上模型类 Agent 的失败有一个显著特征某些失败是因为“这次推理的预算不够”。比如模型“只输出了思考过程、没有产出正文”——很多平台的自动重试机制会“逐级提升输出预算”再试 2 次这比我见过的大多数手动重试都聪明。因为思考过程已经消耗了大量 token剩余配额不够生成正文了如果你第二次重试还是给同样的max_tokens大概率还是同样的结局。把这个思路通用化重试参数应该是动态的重试轮次温度max_tokens模型等级失败应对第 1 次0.2默认值默认模型瞬时故障第 2 次0.3默认值 × 1.5默认模型输出截断/思考过长第 3 次0.5默认值 × 2更强模型输出格式不稳第一轮解决“偶发抖动”第二轮解决“预算不够”第三轮解决“模型力不从心”。每一轮重试的背后都有一个自己的假设而不是简单地说“刚才失败了所以再来一次”。这就是“工程化重试”和“傻瓜式重试”的分水岭。在工程实现上你可以在异常对象里带上attempt计数重试调度器根据计数调整参数def build_payload(attempt: int, base_payload: dict) - dict: payload base_payload.copy() if attempt 1: payload[max_tokens] int(payload.get(max_tokens, 1024) * 1.5) if attempt 2: payload[temperature] min(0.6, payload.get(temperature, 0.2) 0.2) payload[model] stronger-model-name return payload别小看这个细节很多线上 Multi-Agent 任务的失败率就是靠这种“逐级加码”的方式压下去的。4. 降级与补偿失败之后任务还要继续走下去重试终究是有上限的。当重试耗尽、错误又是不可重试类型时你要面对的问题是这个失败的任务怎么收场两种可落地的思路降级处理或者补偿回滚。4.1 降级链主方案失败备选方案顶上降级的本质是“目标不变路径更换”。Multi-Agent 场景里最常见的降级做法是给每个关键 Agent 准备一个备选 Agent或者准备一套非模型逻辑的兜底方案。举个例子主 Agent 负责从一封邮件中提取“会议时间”它是个大模型能力最强但偶尔不稳定。一旦它连续失败 3 次我并不会无限重试而是降级到备用 Agent——一个专门微调过的小模型只做时间抽取这一件事速度快、稳定性好。如果备用 Agent 还是失败再降级到正则表达式规则引擎从常见日期格式里硬匹配。虽然覆盖场景窄但它绝不会“只输出思考过程不输出正文”。在设计降级链时有一个判断清单非常实用场景能否降级降级方案示例摘要生成能换成抽取式摘要提取关键句而非生成式总结意图分类能用规则匹配兜底结构化信息抽取部分能先用规则模板失败再上模型代码生成一般不建议宁可标记失败也不要乱生成支付/删除等有副作用操作绝不能降级必须走人工审核降级最忌讳的是一锅端。比如“支付没成功就自动换个支付渠道再扣一次”——这种行为已经不是降级了是事故策划。有副作用、涉及资金、涉及不可逆操作的任务失败后合适的归宿是“挂起 通知人工”而不是自动换路。4.2 补偿部分成功的任务怎么收尾Multi-Agent 链路里最尴尬的不是“全部失败”而是“部分成功”。A Agent 完成了数据写入B Agent 在生成报告时挂掉了。此时如果你直接把整个任务标记为“失败”你还要考虑 A 已经写入的数据怎么办如果你把它标记为“成功”那 B 缺失的部分就变成了线上盲区。补偿机制解决的就是这个问题。补偿动作是“反向操作”A Agent 写入了一条数据那么补偿任务就是删除这条数据但如果 A 和 B 的操作逻辑上是先后依赖的补偿也可以是“手动补跑 B”。具体选择哪种取决于业务需求的最终一致性。我见过很多团队在这里犯一个低级错误补偿逻辑不是幂等的。补偿任务本身也可能失败因为它运行在同样的不稳定环境里如果补偿动作没有幂等性补偿重试就会产生二次破坏。所以补偿操作也得有trace_id也得更谨慎还是那句话先查再做。另一点值得强调补偿不等于“把状态改回失败”。补偿的目标是让系统达到“逻辑上一致”的状态而不是“时间上回到过去”。比如订单 Agent 先创建了订单号后面库存 Agent 扣减失败这时候补偿不一定是删除订单也可能是“把订单标记为待支付”并通知用户。灵活设计补偿动作比简单地 rollback 有用得多。4.3 缓存的妙用失败时的最后一根稻草很多从零搭建 Multi-Agent 的人会忽略一个现成的降级资源缓存。这里说的缓存不只是“之前算过的结果”还包括“外部数据的最新快照”。举个例子某个 Agent 的任务是“根据当前库存数据生成补货建议”而库存数据来自一个第三方 ERP 接口。如果 ERP 接口超时与其盲目重试或者直接失败不如退而使用“两小时前缓存的最新库存数据”生成一个附带“数据可能不是最新”标签的建议。这比直接失败要好得多——因为很多场景下“有延迟的数据”比“没有数据”对业务更有价值。在设计缓存兜底时注意给结果打上“stale”标记。下游 Agent 看到这个标记后会调整自己的置信度不会把基于旧数据的建议当成最新事实来用。这种“带置信级别联传播”的做法是进阶的 Multi-Agent 设计思维。5. 在设计阶段就把失败率降下来拓扑、超时与观测性前面讲的是“事后处理”但一个成熟的系统事前的设计对失败率的影响远超你的想象。很多失败其实是架构设计阶段埋下的雷。5.1 拓扑结构决定了失败传播的范围Multi-Agent 的拓扑和通信机制不是什么纸上谈兵的概念它直接决定了“一个失败会死多大面积”。链式拓扑A→B→CB 挂了C 拿不到输入失败沿着链路一路传导。优点是结构简单缺点是“一死一大片”。星型拓扑一个编排器Orchestrator统一调度所有子 Agent任何子 Agent 失败编排器都能拦截并把任务重路由到其他节点。这是我最推荐你优先采用的拓扑因为失败的传播被隔离在“子节点内部”编排器是天然的“熔断器”。网状拓扑Agent 之间任意通信灵活性最强但失败传播路径不可控一个 Agent 的异常可能像病毒一样在整个网络里游走排查起来极度痛苦。除非你有很强的可观测性基建否则别轻易上这个拓扑。另外通信机制也很关键。我建议Agent 之间的通信尽量用异步消息队列而不是同步 HTTP 调用。同步调用的问题是一个超时调用方的线程/协程全被拖住重试又多整个系统的并发资源很快就耗尽了。异步队列天然提供“缓冲 解耦 消息重投”的能力。如果非要同步调用至少要给每个调用设置独立的超时上限并且不要无脑嵌套重试。这里必须点名一个经典问题通信通道故障常被误判成“子 Agent 失败”。热词里“设备已离线请确认某个桥接层已连接后重试”就是典型的通道错误。Agent 本身可能活得好好的问题是编排器和它之间的消息通道断了。对这种错误重试前必须先执行“探活”动作——确认通信通道恢复再谈重试业务。5.2 超时预算把“无限等待”从根上消灭Multi-Agent 里最贵的资源是什么不是 token不是 API 调用次数是人类的注意力。一个任务卡在“等待某个 Agent 响应”的状态用户盯着进度条等了两分钟这比失败更让人崩溃。策略是给每个 Agent 设“单个调用超时”还要给整个链路设“全局超时预算”。全局预算要分成几段每个子 Agent 预留多少时间Agent 之间通信预留多少最后留多少缓冲。如果全局预算快用完了优先保障“已成功的子任务落库”而不是继续等最后一个失败节点。GLOBAL_BUDGET_SECONDS 60.0 start time.monotonic() for agent in agents: remaining GLOBAL_BUDGET_SECONDS - (time.monotonic() - start) if remaining 5: raise BudgetExceeded(剩余预算不足提前终止链路) result await asyncio.wait_for(agent.run(), timeoutremaining * 0.6)这段代码的思路是每个 Agent 最多使用当前剩余预算的 60%剩下的 40% 留给后续处理。宁可最后让任务“超时失败”也不能让它在某个 Agent 上无限阻塞。5.3 不总结失败教训的重试体系等于白搭最后一个建议可能听上去不够“技术”但它是我这么多年最深刻的感受把每次失败都当成数据记录好它就是你系统里最值钱的信息资产。你可以为每个失败记录这样一条结构化日志task_id: xxx agent_name: extractor error_category: resource retry_count: 2 final_action: downgrade_to_rule_engine latency_ms: 4200 input_preview: ... upstream_status: ok把这些数据汇总起来你能得到一张“失败热力图”——哪个 Agent 最容易挂、哪个错误类型占比最高、哪个时间段失败率最高。热力图指向哪里优化资源就投向哪里。没有数据支撑的容错设计说到底都是瞎猜。6. 常见问题速查那些提示“重试”背后的真问题最后放一张我在实战中积累的速查表。它对应了那些特别容易迷惑人的报错场景也是很多人在群里反复问的“经典问题”。先看现象再找根因别急着点重试。现象根因类型真正的处理方式网络连接失败请检查网络后重试3002瞬时/通道故障先探活再指数退避重试不要连续点重试当前设备已离线请确认某桥接层已连接后重试通信通道故障检查桥接层进程与 socket 连接恢复通道后任务无需重试failed to refresh token: invalid凭证过期修“刷新 token 的逻辑”而不是重试业务任务上下文长度超限请启用更大上下文后重试资源超限对输入做摘要/裁剪或切换更大上下文模型后再跑模型只输出了思考过程、没有产出正文输出预算/模型状态问题动态提升 max_tokens或换更强的模型而不是同参数重试无法显示目录下文件请刷新重试资源索引过期重新拉取文件索引业务逻辑本身没有失败安装更新时出现一些问题稍后重试瞬时/本地锁冲突检查是否有并发进程占用了文件锁解冲突后再重试这几条里我最想展开聊的是“模型只输出了思考过程、没有产出正文”这个现象。很多平台的自动重试已经做得很好了——“系统已自动重试 2 次逐级提升输出预算”这个描述背后其实就是我在第三章里提到的“重试参数动态化”。但如果你自己搭的 Multi-Agent 遇到了这个问题请务必检查两个地方第一max_tokens是否足够正文输出第二是否在 Agent 的输出解析层把“思考过程”误当成了“最终结果”。后者是逻辑 bug跟重试八竿子打不着。还有那个 3002 错误我遇到过很多次。它的诡异之处在于报错信息只说“网络连接失败”但实际原因五花八门——可能是防火墙切断了长连接、可能是 DNS 解析间歇性失败、也可能是目标服务主动断连。这种错误的正确解法是先做分层探活进程活着吗端口通吗握手成功吗哪一层断了修哪一层修完直接跑根本不需要浪费一次重试。最后聊几句实话我自己的经验是凡是需要靠“手动重试”来维持稳定的 Multi-Agent 系统方案设计一定有问题。有个对比我记得特别清楚刚开始搭建自己的 Agent 编排服务时我有一张 Table记录着线上任务的重试次数——平均每个任务要重试 1.8 次才成功。当时我还觉得“做了重试挺成熟的”。后来我花了两周时间把错误分类、幂等控制、降级链、超时预算全部补齐重试次数直接降到了平均 0.2 次。注意0.2 次不是“重试机制变聪明了”而是“失败发生率变低了”。好的容错设计效果不是让你更会救火而是让你基本不用救火。如果你现在正被某个“只提示重试”的错误折磨不妨先停下自动重试的手花十分钟做一件小事把最近一周的失败日志导出来按错误信息分组统计每一类的占比。你大概率会发现80% 的失败集中在两三种固定原因上。把这几个根因修掉你的 Multi-Agent 系统自然就稳定了。这个动作比我上面写的所有策略都更值得你先做。
返回列表