先问个问题:你跑过多Agent编排吗?就是那种把大活拆成几个角色,各自调用模型、工具、API,最后拼出一个结果的工作流。如果你跑过,大概率经历过这个场面——5个Agent并行跑,其他4个都快出结果了,突然有一个节点报了个超时错误,然后整个调度器像被掐了电一样,所有任务全部终止,白跑的进度瞬间清零。
这篇文章想聊的,就是“一个节点失败,其他节点怎么继续”这件事。抛开理论,我会把多Agent调度里最常见的那几种失败场景拆开讲,分享我实测过的三种容错架构——失败降级、分支隔离、异步解耦——以及完整的实测记录、常见问题排查。不论你是刚开始用LangGraph、AutoGen、Dify这类编排工具,还是自己在写调度器,这都能帮你少踩几个坑。
为了把话说清楚,先统一一下“节点”这个词的含义。在下面的语境里,节点可以是一个Agent实例、一个任务执行单元,也可以是一个工作流里的具体步骤。核心问题是同一个:当某一块执行单元挂了,调度系统能不能让其他部分接着跑。
1. 多Agent调度到底在调什么
1.1 从单Agent到多Agent,调度器管了哪些事
单Agent的场景很单纯:一个LLM,一条prompt,一次调用,一个结果。但真实业务一旦复杂起来,你会发现单Agent什么都干不了——你要它既做市场调研、又写文案、又排版本,一个模型上下文塞不下,职责也分不清。于是多Agent出现了:资料收集Agent、大纲Agent、写作Agent、配图Agent、审核Agent,各干各的,最后合成一个完整交付物。
调度器就是连接这些Agent的“项目经理”。它至少要做三件事:
- 任务分发:决定哪个Agent在什么时机执行,拿到谁的输出作为输入。
- 状态管理:保存每个节点的中间产物,让下游节点拿得到上游结果。
- 生命周期管理:启动、监控、终止、重试节点,处理节点之间的依赖关系。
我见过不少团队把多Agent用成了“多线程调用”,以为把好几个模型调用扔在一起就是多Agent了,结果一遇到失败就全线崩溃。真正的问题往往不在Agent本身,而在调度器对“失败”这件事的处理。
1.2 四种编排模型,容错难度不一样
先理清多Agent的编排模型,因为不同的模型,失败传播的方式完全不同。
- 顺序链(Sequential Chain):A → B → C → D,一个接一个。任何一个挂了,后面全部无法开始。
- 并行分支(Parallel Branch):几个Agent同时跑,各自独立,最后汇总。某一个挂了,其他分支理论上可以继续,但汇总节点要处理“部分结果缺失”。
- 条件路由(Conditional Routing):根据某个节点的输出,决定下一个走哪个分支。如果路由判断节点挂了,整个流程直接停摆。
- 动态编排(Dynamic Workflow):先跑规划Agent,动态生成后续任务图,再执行。这种最灵活,但也最难做容错——因为任务图本身是“运行时才生成的”,你没法提前设计每个失败路径。
从我的实测经验来看,最容易出问题的是顺序链,因为节点之间是强耦合;最难搞好的是动态编排,因为失败路径不可预知。但不管哪种模型,“一个节点失败”这件事本身并不可怕,关键看调度器设计了几条后路。
1.3 “一挂全崩”的四个根因
把根因归纳分析一下,你会发现绝大多数系统里,不是节点太脆弱,而是调度器没有隔离故障。
- 同步阻塞调用:调度器串行等待每个节点返回。只要一个节点卡住,队列里所有节点一起等,表面看起来就是“全崩了”。
- 异常直接冒泡:子节点的异常没有在边界处捕获,一路抛到顶层,导致整个工作流中断。
- 状态只存在内存里:节点执行了一半,进程崩溃或任务重启,中间状态全没了,下游无从继续。
- 没有超时与重试策略:外部API一抖动,节点无限等待或者一失败就放弃,两者都不可接受。
这四个根因我都在实测中踩过。最典型的一种事故是:一个Agent调用第三方数据库工具,对方服务临时不可用,Agent抛了个ConnectionError,调度器没兜住,整个Pipeline直接失败。下游Agent其实根本不需要那个数据库的结果——它可以从缓存的另一份数据继续——但因为没有“降级路径”,只能陪葬。
2. 节点失败的真实类型与判定
2.1 失败可以分成这么几类
在写容错逻辑之前,你一定要先清楚一点:不是所有失败都需要同样的处理。我把多Agent调度中遇到的失败分为四类:
| 失败类型 | 典型表现 | 例子 | 可恢复性 |
|---|---|---|---|
| 超时型 | 调用长时间无返回 | LLM API 响应慢、外部工具挂起 | 可恢复,重试有效 |
| 资源型 | 请求被封、算力不够 | 模型限流、GPU OOM、并发配额超限 | 部分可恢复,需等待或降级 |
| 逻辑型 | 输出不符合预期 | Agent返回非法JSON、工具返回null | 不可恢复,需要换路径 |
| 依赖型 | 上游失败导致下游缺料 | 前置节点失败,后续节点拿不到输入 | 视情况决定是否替代 |
这四类失败混在一起,如果统一处理,往往适得其反。我见过有人对所有异常都重试10次,结果遇到逻辑型失败白白浪费10次token;也有人对所有失败直接放弃,结果限流这种明明等一下就好的问题也直接断送整个任务。
2.2 先判断“致命失败”还是“可恢复失败”
我的设计原则很简单:重试只用于可恢复失败,不可恢复失败必须立刻走降级或终止。
怎么判断?看错误类型和重试后的表现:
- 如果是限流、网络超时、短时间服务不可用,属于可恢复失败,重试+退避就有戏。
- 如果是输入数据不合法、Agent 输出的格式反复不对、配置错误,属于致命失败,重试多少次结果都一样。
判断时机最好是“抛出异常的那一刻”。在调度器里,我给每个节点一个错误分类器,先把异常归一化成三类:RetriableError、FatalError、FallbackNeeded。别让Agent自己乱抛原始异常,那样调度器根本没法统一决策。
2.3 超时、重试、健康检查:三个核心参数
- 超时时间:不要设成“无限”。我通常给LLM调用设30~60秒,给外部工具设10~15秒。如果超过,立刻视为超时失败。超时时间太小容易误杀慢任务,太大则会拖垮整体节奏,建议先压测再定。
- 重试次数:3次是个不错的默认值,具体要看失败类型。限流可以多试两次,逻辑型一次都不要试。重试之间加指数退避:第1次等1秒,第2次等2秒,第3次等4秒,避免风暴。
- 健康检查:对外部依赖最好做主动健康检查,比如每30秒探测一次下游API是否可用。但注意,健康检查不能代替超时重试——探测通过了,实际调用时还是可能失败。
另外,千万别忘了记录失败上下文。失败时要把“哪个节点、哪个输入、哪次重试、错误堆栈、当时的全局状态”写进日志。没有这个,出问题你根本无从排查。
3. 其他节点怎么继续:三种容错架构实测
3.1 方案一:失败降级——换个方式完成任务
第一种思路是“降级”。当某个节点失败时,不中止整个流程,而是用一个后备方案顶上。
最常见的两种降级:
- 模型降级:主模型不可用,自动换备用模型。我实测过,LLM供应商限流时,把调用从大模型降级到小模型,任务照样能跑完,只是质量略降,但总比全盘失败强。
- Agent降级:某个专家Agent失败,自动让另一个通用Agent接管它的职责。比如配图Agent调用图像生成服务失败,就让文案Agent生成一段图片描述兜底。
降级的核心是“提前定义后备路径”,不是在失败那一刻才临时想。我在设计调度图时,会给每个高风险节点配置一个Fallback节点。一旦检测到重试耗尽,就直接走Fallback边。
3.2 方案二:分支隔离——失败不扩散
第二种思路是“隔离”。让并行分支之间互不干扰,一个分支挂了,不影响其他分支继续跑完。
关键点有两个:
- 状态隔离:每个分支维护自己的独立状态空间,不要共享一个全局写变量。我见过最惨的事故就是两个分支同时往同一个变量里写,一个写坏了,另一个读出来也是脏数据。
- 事务性:每个分支的执行可以看作一个小型事务,成功提交状态,失败回滚但只影响自己。
我在实测中做过一组对照:5个并行节点同时跑,其中一个节点调外部接口超时。在“无隔离”模式下,调度器一发现失败就终止全部;在“隔离模式”下,另外4个分支正常跑完,最后汇总节点看到缺失了一个分支的数据,打了一条警告,然后用剩余数据产出了结果。
这个方案适合“部分结果可接受”的业务场景。比如生成了5个章节,丢了1章,你可以接受这4章,也可以再单独补一章。
3.3 方案三:异步队列解耦——让节点之间不互相等待
第三种思路是“解耦”。节点与节点之间不再同步调用,而是通过消息队列异步传递任务。
结构大概是:前一个节点把任务消息发到队列,然后立刻返回;后一个节点作为消费者,从队列里取消息执行。节点失败后,消息不会消失,而是进入重试队列或死信队列,不影响其他节点消费自己的消息。
实测下来这个方案对“高并发、长耗时、多节点”的场景特别友好。比如在内容生产流水线里,收集资料节点很慢,但写作节点只要等到资料消息到达就可以开始,不需要整个流程同步等它。
不过有个代价:异步化之后,你很难再保持一个“同步的最终结果”。想要拿到最终结果,需要自己实现结果汇聚,或者做一个异步回调通知。这个方案不适合对延迟要求极高的场景,但作为调度容错底子是足够扎实的。
3.4 三种方案怎么选
| 方案 | 核心思想 | 适合场景 | 缺点 |
|---|---|---|---|
| 失败降级 | 换后备方案顶上 | 有替代路径、质量可降 | 降级结果可能不完整 |
| 分支隔离 | 故障隔离、不扩散 | 并行分支、可接受部分结果 | 汇总节点需处理缺失 |
| 异步队列 | 消息解耦、失败重投 | 高并发、长流程 | 结果汇聚复杂、延迟高 |
如果是生产级系统,我建议组合使用:外部依赖类失败用降级,并行子任务用隔离,全链路长流程用异步队列。
4. 实操记录:一个演示环境下的完整流程
4.1 场景设定:内容生产流水线
为了验证“一个节点失败,其他节点怎么继续”,我搭了一个5节点内容生产流水线:
- collect资料收集Agent:从测试库拉取资料。
- outline大纲Agent:生成文章大纲。
- write写作Agent:按大纲写正文。
- image配图Agent:为文章生成图片描述。
- review审核Agent:把关文字质量。
设计上,collect节点采用分支隔离模式,outline节点配置降级路径,write和image是并行分支,review最后汇总。我刻意模拟了“outline节点调用LLM API超时”的故障,看整个调度如何处理。
4.2 代码与配置实现
调度器用Python写了一个简化版,核心是定义节点、边和容错策略。下面贴出核心代码(伪代码风格,忽略生产级细节):
import time from typing import Dict, Any class RetriableError(Exception): """可恢复失败,重试有效""" pass class FatalError(Exception): """致命失败,重试无效""" pass def collect_node(state: Dict[str, Any]) -> Dict[str, Any]: print(">> collect 开始") time.sleep(1) state["docs"] = ["行业报告.pdf", "用户访谈.md", "竞品分析.xlsx"] print(">> collect 完成,收集到 3 份资料") return state def outline_node(state: Dict[str, Any]) -> Dict[str, Any]: print(">> outline 开始,调用 LLM API...") time.sleep(2) # 模拟超时失败:第一次抛异常,后续重试仍失败 raise RetriableError("LLM API timeout after 2s") def fallback_outline_node(state: Dict[str, Any]) -> Dict[str, Any]: print(">> fallback_outline 开始,使用本地规则模板兜底") state["outline"] = { "title": "多Agent容错实践指南", "sections": ["背景", "失败类型", "容错架构", "实测记录", "经验总结"] } print(">> fallback_outline 完成,生成兜底大纲") return state def write_node(state: Dict[str, Any]) -> Dict[str, Any]: print(">> write 开始,基于大纲写正文...") time.sleep(3) state["article"] = f"基于大纲:{state['outline']['title']} 的正文内容" print(">> write 完成") return state def image_node(state: Dict[str, Any]) -> Dict[str, Any]: print(">> image 开始,生成配图描述...") time.sleep(2) state["image"] = "一张包含调度架构图的配图" print(">> image 完成") return state def review_node(state: Dict[str, Any]) -> Dict[str, Any]: print(">> review 开始,审核全文质量...") time.sleep(1) print(">> review 完成,输出最终检查结果") return state调度主逻辑里,我用边连接各个节点,并给outline设置重试与降级:
def run_with_retry_and_fallback(node, state, retries=3, fallback=None, timeout=5): for attempt in range(1, retries + 1): try: result = node(state) return result except RetriableError as e: print(f" ! {node.__name__} 第 {attempt} 次失败:{e}") if attempt == retries and fallback: print(f" ! 重试耗尽,切换到 {fallback.__name__}") return fallback(state) duration = 2 ** attempt # 指数退避 print(f" ! 等待 {duration} 秒后重试...") time.sleep(duration) except FatalError as e: print(f" ! {node.__name__} 遇到致命错误,不再重试:{e}") raise raise FatalError(f"{node.__name__} 重试耗尽且无降级路径")在这个简化实现里,outline节点执行失败会触发3次重试,重试期间使用指数退避。重试耗尽后,自动切换到fallback_outline节点。
4.3 模拟节点失败,观察其他节点行为
为了模拟一个真实故障,我故意让outline一直抛RetriableError。跑起来后日志输出如下(节选关键步骤):
>> collect 开始 >> collect 完成,收集到 3 份资料 >> outline 开始,调用 LLM API... ! outline 第 1 次失败:LLM API timeout after 2s ! 等待 2 秒后重试... >> outline 第 2 次失败:LLM API timeout after 2s ! 等待 4 秒后重试... >> outline 第 3 次失败:LLM API timeout after 2s ! 重试耗尽,切换到 fallback_outline >> fallback_outline 开始,使用本地规则模板兜底 >> fallback_outline 完成,生成兜底大纲 >> write 开始,基于大纲写正文... >> image 开始,生成配图描述... >> write 完成 >> image 完成 >> review 开始,审核全文质量... >> review 完成,输出最终检查结果关键观察点:
- 失败没有向上冒泡:outline失败后,调度器只对outline进行重试和降级,没有中断流程。
- write和image并行执行:在outline重试期间,write和image并未等待。当然在这个简化日志里它们是从同一调度循环发起的,实际生产中可以做成真正的并行执行。
- review最终拿到了兜底大纲:fallback_outline输出的结构化大纲让write正常执行,整个流水线没有因为outline失败而全盘终止。
4.4 结果数据
我记录了一个运行批次的节点状态汇总:
| 节点 | 预期行为 | 实际结果 | 耗时 |
|---|---|---|---|
| collect | 成功 | 成功,3份资料 | 约1秒 |
| outline | 调用LLM | 3次重试失败,切换fallback | 约12秒 |
| fallback_outline | 兜底大纲 | 成功,结构完整 | 约0.1秒 |
| write | 成功 | 成功,正文生成 | 约3秒 |
| image | 成功 | 成功,配图描述生成 | 约2秒 |
| review | 成功 | 成功,最终审核完成 | 约1秒 |
如果采用“无容错同步调度”,outline失败那一刻整个流程就会终止,后面write、image、review根本不会执行。加了重试、降级和隔离之后,这条流水线的最终交付物如期产出,只是内容大纲换成了兜底方案。
5. 常见问题与排查实录
5.1 问题速查表
我把实测中遇到的典型问题整理成一个速查表:
| 现象 | 可能原因 | 排查思路 | 解决建议 |
|---|---|---|---|
| 一个节点失败,整个流程全停 | 异常未在调度边界捕获,或同步阻塞 | 看日志中第一个异常抛出的位置 | 在调度器入口统一捕获异常,配置Fallback |
| 重试多次仍然失败 | 把逻辑型失败当成可恢复失败 | 检查错误类型分类器 | 对FatalError禁止重试,直接降级或终止 |
| 并行节点互相影响,数据变脏 | 共享了全局状态,没有隔离 | 检查状态读写位置 | 每个分支用独立状态副本 |
| 任务卡死,永不超时 | 没有设置超时时间 | 看耗时最长的调用 | 对LLM和工具调用设置明确超时 |
| fallback节点没生效 | Fallback边配置错误,或没有触发条件 | 检查调度图定义 | 把Fallback作为显式边,加日志确认触发 |
| 失败信息丢了,很难排查 | 没记录失败上下文 | 查看日志是否包含错误堆栈 | 失败时记录节点、输入、重试次数、堆栈 |
5.2 排查思路
排查多Agent调度故障,我习惯按这个顺序走:
- 看失败发生在哪个节点:通过日志里的节点名和错误信息定位。
- 看失败类型:是可恢复失败还是致命失败,这决定了下一步的方向。
- 看重试路径:重试了几次、有没有退避、有没有走降级。
- 看事后状态:从状态快照或日志里恢复“失败瞬间”的上下文。
- 看下游影响:失败节点的下游节点是否拿到替代数据或缺失标记。
多Agent系统里最难的不是定位,而是“复现”。因为LLM调用本身有随机性,同一个输入可能这次成功下次失败。所以日志里一定要带上全局状态快照,不然复现成本极高。
5.3 避坑经验
最后分享几条我在实测中踩过之后总结的经验:
- 不要在Agent内部吞掉异常。如果你在Agent内部把异常catch掉然后返回一个空字符串,调度器会以为“这个节点成功了”,然后下游拿空数据进行加工,最后产出一篇鬼话连篇的文章,你还不知道问题出在哪。正确做法是让异常往上抛,在调度器边界统一决策。
- 不要无脑重试。重试是要花时间和token的。我见过一个团队对限流和逻辑错误都用同一个重试逻辑,结果逻辑错误的Agent重试了5次,浪费了大量LLM调用成本。重试次数和退避策略一定要按错误类型区分。
- 降级路径要提前定义,不要临时拍脑袋。Fallback节点必须在调度图里预先画好,并经过测试。临时在异常处理里写“那就换个prompt再试一次”这种做法,生产环境根本来不及,也不可靠。
- 监控要覆盖到“失败但继续”的场景。这种场景比直接失败更隐蔽——任务最后成功了,但某节点其实是靠降级顶过来的,如果没人注意,你会误以为系统是健康稳定的。要专门为“降级执行”打上指标和告警,比如“节点outline降级次数 > 0”就告警。
- 异步队列方案中,千万要做好死信队列监控。任务反复重试最终进死信队列时,如果没人看,那这个任务就永远消失了。死信数量要进告警。
最后再分享一个我个人的体会。做多Agent调度,最难的不是把节点串起来,而是设计“失败时怎么办”的路径。我实测下来最大的变化是心态:以前我写调度器,默认节点都会成功;现在我默认每个节点都有可能失败,所有设计都从“如果这里挂了,系统会怎样”出发。这个思路转变之后,系统的稳定性提升了一个档次。你可以在自己的项目里先挑一个最核心的节点,给它配置重试、降级和隔离,然后故意把它杀死看看整个流程的反应——这个实验成本很低,但收获会非常大。