
1. 一个节点挂了整条流水线就停摆这事得从调度器的容错设计说起多 Agent 系统跑起来之后最让人睡不着觉的不是单个 Agent 不够聪明而是某个节点突然失败之后整个任务链跟着一起崩。我见过太多团队在 Demo 阶段把多 Agent 协作跑得行云流水一上生产环境就原形毕露——一个负责检索的 Agent 超时了后面负责总结的 Agent 干等着负责写报告的 Agent 永远拿不到输入最后整个任务卡死在运行中状态既不出结果也不报错。这个问题的本质其实不是某个 Agent 为什么会失败而是调度层有没有把失败当成一等公民来设计。绝大多数多 Agent 框架的默认行为是串行等待 异常上抛也就是说A 调 B、B 调 C只要 B 抛异常A 的调用栈就直接炸了C 根本没机会被触发。这种设计在单机脚本里没问题但在多 Agent 调度场景下就是灾难。我这次实测的目标很明确构造一个包含 5 个节点的多 Agent 任务链人为让其中 2 个节点以不同方式失败一个超时、一个返回异常观察调度器在不同容错策略下的表现重点回答三个问题——失败节点的下游怎么继续失败节点的上游要不要重试整个任务的最终状态怎么判定如果你正在做多 Agent 编排、工作流引擎、或者任何需要部分失败但整体可继续的系统这篇实测记录应该能帮你少走不少弯路。2. 先搞清楚继续到底有几种含义不然调度策略根本没法选2.1 节点失败后的三种继续模式很多人一上来就问节点失败了怎么继续但这个问题本身是模糊的。在实际调度系统里继续至少分三种选错了模式后面所有设计都是白搭。第一种是跳过继续Skip and Continue。失败节点的下游如果对它的输出不是强依赖那就直接跳过这个节点把下游的输入标记为缺失或者用默认值填充让流程往下走。这种模式适合那种锦上添花型的节点比如一个负责补充背景信息的 Agent 挂了主流程照样能出结果只是质量差一点。第二种是降级继续Fallback and Continue。失败节点有一个备用实现主节点挂了就切到备用节点。比如主检索 Agent 用的是某个外部接口超时之后自动切到本地缓存检索。这种模式的关键是备用路径要提前注册好不能等挂了才临时找。第三种是补偿继续Compensate and Continue。失败节点的下游不仅继续还要额外触发一个补偿节点来修复影响。比如写操作失败了下游继续走但同时触发一个回滚 Agent 去清理脏数据。这种模式最复杂但也是生产环境最需要的。我实测下来大部分团队卡在第一步——他们根本没意识到自己在用哪种模式代码里写的是try-catch 之后 pass实际上既没跳过也没降级只是把异常吞了下游拿到一个 None 继续跑最后报一个莫名其妙的错。2.2 为什么继续比重试更难做重试的逻辑很直观失败了就再试一次试够次数还不行就放弃。但继续不一样它要求调度器在失败发生的瞬间做出判断——这个失败是致命的还是可容忍的下游能不能在没有这个输出的情况下工作整个任务的最终一致性怎么保证这里有个反直觉的结论重试解决的是暂时性失败继续解决的是永久性失败。你把重试次数调到 10 次遇到一个必然失败的节点比如参数配错了照样卡死。而继续机制要处理的是这个节点就是不行了但任务还得往下走的场景。我在实测里特意设计了一个必然失败的节点——一个故意传错参数的 Agent它无论重试多少次都会失败。这时候重试策略完全失效只有继续策略能救场。这个测试帮我理清了一个关键认知重试和继续不是二选一而是两个不同层次的容错必须同时存在。2.3 调度器需要维护的状态机要让继续真正落地调度器内部必须维护一个比成功/失败更细的状态机。我实测用的状态定义是这样的状态含义下游行为PENDING等待执行阻塞RUNNING执行中阻塞SUCCESS成功完成正常继续FAILED_RETRYABLE可重试失败触发重试FAILED_FINAL最终失败触发继续策略SKIPPED被跳过下游用默认值DEGRADED降级完成下游用降级输出COMPENSATED已补偿下游正常继续这张表是我踩了坑之后才补全的。一开始我只用了 SUCCESS 和 FAILED 两个状态结果下游根本分不清这个失败要不要等重试和这个失败是不是已经没救了导致要么过早继续拿到空数据要么死等一个永远不会成功的节点。提示状态机的粒度决定了容错的上限。如果你的调度器只有成功和失败两个状态那继续这件事你根本做不了因为下游没有足够的信息来判断该怎么继续。3. 实测环境搭建5 个节点、2 种失败、3 套策略3.1 任务链的设计思路我构造的任务链是一个典型的检索-分析-生成流水线5 个节点分别是Node A检索 Agent负责从数据源拉取原始素材模拟外部接口调用。Node B清洗 Agent对 A 的输出做格式化和去重。Node C分析 Agent对清洗后的数据做统计和归纳。Node D生成 Agent根据分析结果生成最终报告。Node E校验 Agent对报告做质量检查输出校验结果。依赖关系是 A → B → C → D → E 的串行链但我在 B 和 D 上做了手脚B 被配置成会超时失败D 被配置成会返回异常。这样我就能观察中间节点失败和末端节点失败两种场景下继续策略的差异。为什么选串行链而不是 DAG因为串行链是最容易暴露调度问题的结构。DAG 里某个分支失败其他分支还能并行跑问题被掩盖了串行链里任何一个节点失败整条链的命运就取决于调度器的容错设计问题无处可藏。3.2 三套容错策略的配置我准备了三套策略分别对应前面说的三种继续模式策略一纯跳过Skip-Only。任何节点最终失败后下游直接跳过用 None 作为输入继续。这套策略最激进能保证流程一定走完但输出质量最差。策略二降级优先Fallback-First。节点失败后先尝试降级路径降级也失败才跳过。降级路径我提前注册了本地缓存和简化算法两个备选。策略三补偿兜底Compensate-Last。在降级的基础上额外注册补偿节点失败发生后异步触发补偿主流程不等补偿结果继续走。三套策略共用同一套节点代码只改调度器的配置。这样对比才公平能排除节点实现差异带来的干扰。3.3 失败注入的具体方式失败注入这块我做得比较细因为失败本身有很多种形态不同形态对调度器的考验完全不同。Node B 的超时失败我设置的是 3 秒硬超时。这里有个细节超时时间必须小于调度器的节点级超时否则调度器会先判定失败节点自己的超时逻辑根本没机会执行。我把调度器超时设成 5 秒节点内部超时设成 3 秒这样节点能优雅地返回一个超时标记而不是被调度器强杀。Node D 的异常失败我让它抛一个自定义的DataValidationError。这里的关键是异常类型要可区分——调度器需要知道这是数据问题还是系统问题因为数据问题适合跳过系统问题适合重试。我见过太多代码把所有异常都 catch 成 Exception结果调度器完全无法做差异化处理。class DataValidationError(Exception): 数据校验失败属于不可重试的业务异常 pass class TransientError(Exception): 临时性错误属于可重试的系统异常 pass这个异常分类是我实测里最重要的一个设计决策。没有它调度器只能一刀切地处理所有失败容错策略根本没法精细化。4. 实测过程失败发生的那几秒调度器到底在干什么4.1 Node B 超时跳过策略下的连锁反应第一次跑策略一Node B 在 3 秒后返回超时标记。调度器收到标记把 B 的状态置为 FAILED_FINAL然后触发跳过逻辑。Node C 被唤醒但它的输入是 None。这里出现了第一个意外Node C 的代码没有处理 None 输入直接抛了TypeError。调度器把 C 也标记为失败然后继续跳过Node D 拿到 None又抛异常……整条链在 2 秒内全部失败最终任务状态是完成但全链失败。这个结果暴露了一个关键问题跳过策略要求所有下游节点都能处理缺失输入。如果你的节点代码默认输入一定存在那跳过策略只会引发雪崩。我后来给每个节点加了输入校验缺失输入时返回一个降级结果而不是抛异常这才让跳过策略真正可用。实测数据策略一下任务总耗时 8.2 秒5 个节点里 4 个失败最终输出是一个空报告。流程确实继续了但继续得毫无意义。4.2 Node B 超时降级策略如何救场换策略二重跑。Node B 超时后调度器先查降级注册表发现 B 注册了本地缓存检索作为降级路径。调度器把 B 的状态置为 DEGRADED然后调用降级节点。降级节点在 0.8 秒内返回了缓存数据虽然数据新鲜度差一些但结构完整。Node C 拿到正常输入顺利跑完后面的 D、E 也正常执行。最终任务状态是完成含 1 个降级节点。这里有个细节值得说降级节点的输出必须和主节点保持结构一致只是质量降级。我一开始图省事让降级节点返回了一个简化版数据结构结果 Node C 解析失败。后来我强制要求降级输出必须通过和主节点相同的 schema 校验这个问题才解决。实测数据策略二下任务总耗时 6.5 秒5 个节点里 4 个成功、1 个降级最终输出是一份完整报告只是检索部分标注了缓存数据。4.3 Node D 异常补偿策略的异步兜底策略三的测试重点在 Node D。D 抛出DataValidationError后调度器判定为不可重试失败先尝试降级——但 D 没有注册降级路径于是触发补偿逻辑。补偿节点是一个独立的报告修复 Agent它异步启动不阻塞主流程。主流程这边D 被标记为 COMPENSATEDNode E 拿到 D 的部分输出继续校验。补偿节点在 4 秒后完成把修复后的报告写回结果存储覆盖了 D 的残缺输出。最终任务状态是完成含 1 个补偿节点。这里的关键是补偿是异步的主流程不等它。如果补偿也同步等待那和直接重试没区别还多了一层复杂度。实测数据策略三下主流程耗时 5.1 秒补偿流程额外耗时 4 秒异步最终输出是一份经过修复的完整报告。4.4 三套策略的横向对比维度策略一跳过策略二降级策略三补偿主流程耗时8.2s6.5s5.1s最终输出完整性空报告完整含降级标注完整已修复实现复杂度低中高对下游节点的要求必须能处理 None必须能处理降级结构必须支持异步写回适用场景非关键节点有备用路径的节点写操作、有副作用的节点这张表是我实测后最有价值的产出。它说明了一个道理没有最好的策略只有匹配场景的策略。跳过策略适合日志、埋点这类非关键节点降级策略适合检索、计算这类有替代方案的节点补偿策略适合写库、发消息这类有副作用、必须修复的节点。5. 踩过的坑那些文档里不会写的调度陷阱5.1 超时时间的层级关系搞反了我第一个坑就是超时时间设反了。一开始我把调度器超时设成 3 秒节点内部超时设成 5 秒结果节点还在正常执行调度器已经判定它失败并触发了降级。降级节点和主节点同时跑两份输出互相覆盖数据直接乱了。正确的层级关系应该是节点内部超时 调度器节点级超时 任务级总超时。节点内部超时负责优雅退出调度器超时负责兜底强杀任务级超时负责整体止损。三层超时各司其职缺一不可。我后来把配置改成节点内部 3 秒、调度器 5 秒、任务级 30 秒再没出现过超时打架的问题。5.2 降级节点的输出没做 schema 校验前面提过降级节点返回简化结构导致下游解析失败。这个坑的根因是降级路径没有纳入统一的输出契约。主节点和降级节点如果各自定义输出格式下游就得写两套解析逻辑迟早出问题。我的修复方案是定义一个共享的 schema主节点和降级节点都必须通过校验才能返回。schema 里对可空字段做了明确标注降级时这些字段填默认值而不是直接删掉字段。这样下游拿到的永远是同构数据只是某些字段的值不同。5.3 补偿节点的幂等性被忽略补偿节点异步执行最大的风险是重复触发。我实测时故意让调度器重发了一次补偿指令结果补偿节点执行了两次报告被修复了两遍第二次修复把第一次的正确内容又改错了。补偿节点必须幂等——同一个失败事件触发多次补偿结果应该一致。我的做法是给每个失败事件生成一个唯一 ID补偿节点执行前先查这个 ID 是否已处理过处理过就直接返回上次结果。这个幂等层是补偿策略能上生产的前提。5.4 失败状态的传播没有边界最后一个坑最隐蔽Node B 降级成功后状态是 DEGRADED但 Node C 拿到数据后并不知道这是降级数据它按正常数据处理最后 Node E 校验时发现数据新鲜度不达标把整份报告判为不合格。问题出在降级状态没有随数据一起传播。下游节点需要知道我拿到的数据是降级的才能调整自己的处理逻辑。我后来在数据包里加了一个_meta字段记录来源节点的状态下游节点可以选择性地读取这个字段来做决策。这个设计让整个链路的状态变得透明每个节点都知道自己的输入是正常的、降级的还是补偿的从而做出相应的处理。这才是真正的继续——不是盲目往下走而是带着上下文往下走。6. 把容错策略做成可配置的调度层6.1 策略注册表的设计实测跑通之后我把三套策略抽象成了一个策略注册表。每个节点在注册时声明自己的容错配置node_registry.register( node_idnode_b, handlerretrieval_agent, timeout3.0, retry_policy{max_retries: 2, backoff: exponential}, fallbacklocal_cache_retrieval, compensateNone, on_final_failuredegrade # 可选 skip / degrade / compensate )这个注册表的好处是容错策略从硬编码变成了配置。新增节点时只需要声明它的容错需求调度器自动套用相应逻辑。我实测时改了 5 次策略每次只改配置不改代码效率提升非常明显。6.2 调度器的决策流程调度器收到节点结果后的决策流程我整理成了这样节点返回成功 → 状态置 SUCCESS唤醒下游。节点返回可重试失败 → 检查重试次数未超限则重试超限则进入第 3 步。节点最终失败 → 查降级注册表有降级则执行降级无降级则进入第 4 步。无降级可用 → 查补偿注册表有补偿则异步触发补偿并继续无补偿则按on_final_failure配置执行跳过或终止。这个流程的关键是每一步都有明确的下一步不存在失败了不知道怎么办的中间态。我见过太多调度器在失败后陷入等待人工介入的状态这在自动化场景下等于卡死。6.3 任务最终状态的判定任务跑完之后最终状态不能简单用成功/失败来判定。我定义了一个复合状态COMPLETED所有节点成功无降级无补偿。COMPLETED_WITH_DEGRADATION有节点降级但输出完整。COMPLETED_WITH_COMPENSATION有节点补偿输出已修复。PARTIAL有节点跳过输出不完整但流程走完。FAILED关键节点失败且无任何容错路径。这个复合状态让上游系统能准确判断任务结果的可信度。比如COMPLETED_WITH_DEGRADATION的报告可以用但需要标注数据来源PARTIAL的报告只能作为草稿。这种细粒度的状态判定是容错系统真正可用的标志。7. 几个实测中总结的硬核经验经验一失败分类比失败处理更重要。你花在定义异常类型上的时间会十倍地回报在容错逻辑的清晰度上。可重试异常、不可重试异常、超时异常、资源异常这四类必须分开否则调度器只能一刀切。经验二降级路径要定期演练。我实测时发现降级节点因为长期不触发代码早就和主节点脱节了真到用的时候直接报错。后来我加了一个定时演练机制每周强制触发一次降级路径确保它始终可用。这个习惯救过我两次。经验三补偿的异步边界要清晰。补偿节点能访问哪些数据、能修改哪些状态必须严格限定。我见过补偿节点把主流程正在读的数据改了导致主流程读到脏数据。补偿只能操作主流程已不再访问的数据这个边界要用锁或者版本号来保证。经验四状态传播要轻量。_meta字段只放必要信息别把整个执行历史塞进去。我一开始把每个节点的状态都往_meta里塞数据包膨胀了 3 倍序列化开销直接拖慢了整个链路。后来精简到只放当前数据的来源状态和是否可信两个字段性能恢复正常。经验五容错配置要能热更新。生产环境出问题时你最需要的是不改代码就能调整容错策略。我把策略注册表接到了配置中心超时时间、重试次数、降级开关都能实时改。这个能力在故障演练和紧急止损时价值巨大。多 Agent 调度里的继续说到底是一个在不确定中维持确定性的工程问题。节点会失败这是常态任务要能继续这是要求。把失败当成流程的一部分来设计而不是当成异常来处理整个系统的健壮性会上一个台阶。我实测的这三套策略不是终点而是一个起点——你可以根据自己的业务场景组合出更适合的容错方案。关键是先把状态机建起来把失败分类做清楚剩下的就是配置和演练的事了。