我们最早把智能化任务做成“一个Prompt干完所有事”的时候根本想不到后期会那么痛苦。一个工单自动处理的模拟项目最初就是单次模型调用读原始信息、判断类型、提取关键字段、生成回复、做合规检查全部塞进一个超长Prompt里。表面上看链路短、代码简单实际一跑就发现只要模型在中间任何一步输出不符合预期整条链路就废了日志里只有一次“开始”和一次“超时失败”完全不知道卡在哪。后来我把它彻底改成多Agent串行流水线把任务拆成可重试、可续跑的Pipeline节点链才真正解决了不可观测、不可重试、不可恢复这三个大坑。这篇文章就是把这次改造的完整过程和踩坑记录写下来。如果你也在做类似的多Agent任务编排或者你在纠结“为什么不能一个Agent干到底”那这篇内容应该能帮上忙核心是节点建模、状态机、数据契约、重试策略和断点续跑这几块每一块我都会讲清楚为什么这样设计以及实测下来哪些参数和做法最靠谱。1. 为什么需要串行流水线一个长任务被单次模型调用卡死的全流程复盘先讲讲当初那个“单次大Prompt”方案具体是怎么翻车的。任务本身不复杂系统收到一份用户提交的工单文本需要自动提取关键信息、判断工单类型、分配合适的处理方式、生成给用户的回复最后还要做一轮合规检查。最初实现时就是把所有步骤的指令写在一个Prompt里让模型一次性输出一段JSON然后程序解析这段JSON。1.1 大Prompt的隐性瓶颈不可观测、不可重试、不可恢复先说不可观测。一次调用内部做五件事模型把五个步骤穿插在思考过程里你只能看见最终输出。如果某个字段漏了、格式不对、分类错误你只能对着最终结果猜是哪一步出了问题。更麻烦的是模型内部没有明确的“步骤边界”你想定位是“信息提取错了”还是“回复生成错了”没有任何中间产物可以参考。再说不可重试。一次调用失败你没有任何“部分成功”的概念。比如信息提取成功了、分类也完成了但最后生成回复时超时了那么整条调用失败只能整体重跑。重跑意味着前面所有步骤的token消耗全部浪费而且第二次运行可能因为模型的不确定性给出完全不同的中间结果等于把问题重蹈覆辙一遍。最难受的是不可恢复。工单文本几万字、上下文很长模型调用超时之后你想“从生成回复这一步接着跑”是完全做不到的因为中间结果没有落盘程序只能从零开始。我那次真实遇到的情况是一个长文本的工单处理三次从头重跑花了大量成本最后勉强出了一份结果但关键字段和第一次跑出来的还不一样后续人工核对气得想删库。1.2 串行节点链到底改了什么后来我把“一件事五步做”改成“五件事串行、每件事一个Agent节点”。每个节点只负责一件具体的事情输入是上一个节点的输出加上少量全局上下文输出是一个结构化的局部产物同时把执行状态写入持久化存储。整体链路变成一条节点链节点A信息提取节点B类型判断节点C方案匹配节点D回复生成节点E合规检查这个结构最核心的改变就是把“不可分割的大过程”变成“有边界的小过程”。每个节点独立执行、独立记录日志、独立重试、独立续跑。节点A成功之后哪怕节点B重试十次节点A的结果也不会丢不需要从头再来。用流水线工厂来类比就很好理解以前是一个老师傅从头干到尾做砸了只能整件返工现在是分工流水线每个工位只做一道工序工序之间用交接单传递半成品某道工序出了问题只需要把半成品退回这个工位重新加工不用把整条流水线停下来从第一个工位重新投料。这种设计也不是没有代价最明显的是流程编排变复杂了每个节点都要写输入输出逻辑还要处理状态流转。但我后来发现这点代价换来的收益是巨大的出错之后能定位到具体节点能精准重试能断点续跑还能并行优化单个节点的Prompt而不用担心影响其它节点。对于任何需要稳定交付的生产级Agent任务这都是值得的投资。2. 节点链的建模状态机、数据契约与幂等语义串行流水线不是简单地把函数按顺序调用一遍它背后需要一套严格的节点模型。我刚开始改造时走了弯路直接用数组保存任务列表节点执行完就更新一下内存状态结果服务一重启全丢了。后来才意识到节点链的本质是一个“可持久化的状态机集合”。2.1 节点状态机设计每个节点都有一份状态我用的状态集合包含这几种状态含义允许的流转pending未开始等待前序节点完成检查依赖通过后转为runningrunning执行中可能正在调用模型或处理外部服务完成转completed失败转failed人工干预转pausedcompleted执行成功产物已落库终态failed执行失败可能尝试重试达到重试次数后转paused或blockedpaused暂停等待人工介入或外部条件满足人工确认后转running或completedskipped不需要执行条件不满足终态这个状态机是所有机制的地基。它解决了一个关键问题任何时候系统都能回答“这条流水线现在跑到哪一步了、哪些步骤已经完成、哪些步骤需要重跑”。实现时我用了一张任务表加一张节点表。任务表保存整条流水线的元信息比如任务ID、当前所在节点、全局上下文节点表保存每个节点的状态机信息、输入输出快照、执行次数、最近一次错误信息。两张表用任务ID关联。2.2 每个节点的输入输出契约串行流水线最容易乱的地方是节点之间到底传什么。如果没有明确的输入输出契约就会出现节点B偷偷改了节点A的产物、节点C读到了本该由节点D生成的字段最后整条链路的依赖关系变成一团乱麻。我的做法是给每个节点定义两个结构input_schema和output_schema。input_schema声明这个节点需要从全局上下文拿哪些字段、从上游节点产物中拿哪些字段output_schema声明这个节点输出什么字段。节点执行前先做schema校验字段缺失直接失败不做下游透传。举个例子信息提取节点的output_schema大概是{ type: object, required: [customer_name, issue_type, content_summary], properties: { customer_name: {type: string}, issue_type: {type: string}, content_summary: {type: string} } }下游分类节点只允许读这三个字段作为输入不允许直接读原始工单全文。这样做的目的是把每个节点的输入面收窄。模型在处理窄输入时更容易稳定输出同时一旦某个字段出问题你能立刻定位到是哪个节点产出的。2.3 幂等键重试不重复执行的基石节点可以重试但重试必须配合幂等设计否则后果很严重。我见过的一个场景回复生成节点第一次调用成功生成了回复文本但程序在写数据库时挂了于是标记为失败并发起重试第二次又生成了一份回复结果库里出现两条内容相似但细节略有差异的记录人工根本不知道哪条是“最终版本”。处理办法是给每个节点执行动作加一个幂等键。节点进入running状态时先生成一个唯一的execution_id所有外部动作调用模型、写数据库、发通知都携带这个幂等键。如果重试时发现同一个execution_id已经执行过就直接读取之前的执行结果不再重复执行。具体到模型调用幂等键可以用来做请求级去重把execution_id嵌入Prompt或请求元数据让模型端或者网关层知道这是同一请求落到业务侧则是“先查结果再执行”的流程。不要小看这个设计没有幂等键之前重试只是一个放大错误的开关有了幂等键重试才是真正的“安全网”。3. 多Agent接力上下文传递、角色边界与依赖校验拆成多Agent之后很多人以为就是“把大Prompt拆成几个小Prompt按顺序调一次”。这么做虽然比单次调用强一点但依然会踩到角色混淆、上下文污染、脏数据传递的问题。要真正实现稳定的“接力”需要严格设计三件事全局上下文与局部产物的分离、Agent提示词的角色边界、前序节点的输出校验。3.1 全局上下文与局部产物的界限我们需要区分两类数据。全局上下文是整条流水线都需要的、相对稳定的信息包括任务原始输入、业务规则、用户画像、历史处理记录等局部产物是单个节点加工出来的中间结果比如信息提取节点抽出来的字段、合规检查节点给出的结论。我的原则是全局上下文只读任何节点都允许读它但禁止改写局部产物按节点隔离每个节点只能读写自己的产物经过显式声明才能把某个上游产物作为输入引用。这样做一开始会觉得麻烦要手写配置哪些字段给哪个节点但后期排查问题时受益巨大——你永远知道某个数据是从哪来的。3.2 Agent角色提示词与输出结构强制每个Agent节点本质上是“一个角色加一套规则”。角色提示词不要写得过于宽泛要像岗位说明书一样明确。举例来说信息提取Agent的角色定位是“只负责从工单原文中抽取结构化字段不负责判断工单该由谁处理不负责生成回复”。分类Agent的定位是“只负责根据已有字段判断类型不重读原文不修改提取结果”。光靠提示词约束不够还要在程序层强制输出结构。我的做法是给每个Agent定义好输出模板要求模型严格按照JSON结构返回并进行schema校验。校验不通过就按失败处理进入重试逻辑。这样才能保证下游节点拿到的一定是结构合法的数据而不是“模型今天心情好多输出了一段废话”这种随机结果。3.3 前序节点校验宁可卡住也不传脏数据“脏数据”是串行流水线最大的污染源。节点A输出里混入了一个多余字段节点B可能因为这个字段被模型误读产生错误结论节点B的错误结论又传给节点C最终整条链路的输出全部跑偏。而且这种错误往往不是直接崩溃而是“看起来有结果、用起来全是坑”。所以在节点启动之前我会强制做一次上游产物校验。校验规则包括必填字段是否存在、字段类型是否符合预期、值域是否合法比如状态字段只能是那几种值、可选字段是否携带标记。校验不通过时流水线不继续往下走直接把节点置为paused或failed等待修复。这个“宁可卡住也不传脏数据”的决策非常重要。很多同学觉得“先往下传后面再人工修”结果是把小问题滚成了大问题。流水线卡住并不可怕可怕的是把错误扩散到所有后续节点导致整条任务的结果都不可信。4. 可重试策略怎么定退避、超时与失败归类让节点可重试不是简单地把代码包在一个“重试三次”的循环里。重试策略设计不好要么重试太频繁放大成本要么重试太少导致任务频繁失败。我这边按照“失败分类、超时设置、退避策略、幂等执行”四个方面来做目前跑下来效果比较稳。4.1 失败为什么必须分类我给每个失败标记了一个类型只有可恢复的失败才允许重试。可恢复的失败主要有这几种模型调用超时、服务端临时不可用、网络抖动、输出格式不符合schema假设模型有概率修复输出格式。不可恢复的失败则包括全局上下文缺失、上游产物校验不通过、业务规则明确拒绝、提示词配置错误。为什么要分类因为盲目重试只会让“确定性失败”重复消耗预算。比如“必填字段缺失”这种错误重试一百次也一样缺但“模型超时”这种错误重试往往能成功。所以我给每个节点配置两个参数可重试的错误类型列表、最大重试次数。不在列表里的错误直接进入人工介入流程。4.2 超时与重试窗口的经验参数超时参数的设置是个细节活。模型调用本身的超时时间我习惯设置为120秒如果连续两次超时基本可以判断是上游服务不稳定而不是任务本身的问题。每次重试之间的间隔我采用指数退避加随机抖动公式是wait_time base * (2 ^ attempt) random_jitterbase选择5秒attempt从0开始那么第一次重试等待约5秒第二次约10秒第三次约20秒依次增长。随机抖动控制在±2秒内避免多个节点同时重试造成流量尖峰。最大重试次数我一般定4到5次再往上基本没有收益只会放大成本。4.3 重试幂等的边界外部副作用也要考虑重试不只是“再调一次模型”这么简单它可能连带触发外部副作用比如发送通知、更新工单状态、调用第三方接口。模型调用本身算一次副作用外部系统调用又是新的副作用。我踩过的一个坑是节点在调用外部接口时网络超时实际外部接口已经执行成功了但程序这边判断为失败并重试结果外部系统收到两次请求产生了重复变更。这个问题的根源不是重试本身而是“判断成败的时机”。我后来在外部调用环节引入了两阶段设计第一步先发一个“预提交”请求并记录请求ID第二步在节点输出落库后再执行“确认提交”。如果确认提交失败系统可以查询请求ID对应的实际状态决定是补提交还是回滚。这套设计说起来有点重但对“可续跑”的可靠性要求高时非常值得。至少你在做重试之前要想清楚一个问题这次动作如果实际上成功了但程序认为失败重试会带来什么后果回答不清晰就不要轻易重试。5. 断点续跑检查点、持久化与手动修复链路断点续跑是串行流水线相对传统方案最强的能力之一。它解决的是假设任务跑到节点D时挂了而且短时间无法自动恢复那么等环境恢复之后能不能直接从节点D继续而不是从头跑一遍答案是可以前提是你把检查点设计和持久化做好。5.1 检查点粒度选择检查点粒度直接决定续跑的精度。我采用的是“每个节点完成后立即持久化”的粒度。节点进入completed状态时把它的输出产物、执行元数据耗时、消耗token数、重试次数、当时的全局上下文版本号一起写入节点表。这样任意时刻只要节点表里有数据系统就知道每个节点是否完成、产物在哪。有同学会问要不要做得更细模型调用过程中每收到几个token就存一次检查点我不建议这么做收益极低且复杂度爆炸。模型调用本身没有可恢复的中间态你要做的是“保住每个节点完成之后的结果”而不是“保住模型生成到一半的token流”。节点粒度已经是性价比最好的平衡点。5.2 恢复执行时的上下文重建续跑的时候最难的不是找到未完成的节点而是把上下文重新组装起来。因为流水线可能重启过内存里的全局上下文可能丢了我采用的是“持久化上下文加按需重建”的策略。全局上下文在任务创建时就序列化保存后续节点如果依赖前序节点的某个输出字段不是直接读内存而是通过统一的上下文服务去查节点表。恢复执行的基本流程是这样扫描任务下所有节点状态找到第一个状态不是completed且不是skipped的节点从该节点开始重新执行执行之前从上下文服务拉取它依赖的所有字段重新拼装Prompt。这样即使跑过了十几个节点哪怕中间业务逻辑升级过也能保证续跑时使用的是正确版本的数据。5.3 人工介入修复的场景断点续跑不止用于“系统挂了之后自动恢复”也用于“模型输出有问题但需要人工修正”的场景。我在这条流水线里设置了一个paused状态当节点连续重试达到上限、或者输出校验不通过时任务自动转入暂停等待人工处理。人工处理的方式不是去改代码而是通过管理界面查看节点的输入输出快照、手动修正局部产物然后一键把任务推回流水线继续执行。这个设计让我少加了很多班。以前出现模型输出异常只能整条任务作废、重新创建现在直接在节点层面修正单点修复其他所有已完成节点的结果原样保留。人工介入的每一步也都会记录操作日志方便后续复盘模型为什么在某个节点上反复出现问题。6. 落地之后的坑我从真实链路里总结的避坑清单如果只看前面的设计可能会觉得这套串行流水线很顺。实际上我落地过程中踩了不少坑有些是设计层面的有些是参数层面的这里挑三个典型问题整理成清单给正在做类似方案的朋友提个醒。6.1 上下文漂移后置节点被前序节点带偏第一个坑是上下文漂移。串行链路跑久了后置节点容易“被带偏”。原因很常见信息提取节点输出的summary字段里包含了主观判断分类节点读了这个summary之后把主观判断当成事实继续加工到了回复生成节点整条输出的语气和结论都偏离了原始工单的真实内容。对策是两句话一是严格控制每个Agent只能读它该读的字段二是在输出契约中添加“禁止输出主观结论”的约束并在schema校验阶段用规则引擎过滤掉包含主观倾向词汇的内容。说白了上下文漂移的本质是数据越权解决方式就是把权限收死。6.2 重试放大成本最坏情况下的预算估算第二个坑是成本失控。一个节点失败后自动重试5次每次都要重新消耗模型调用预算。如果有10个节点、每个节点都失败重试最终成本可能是正常执行的数倍甚至十数倍而且结果未必变好。我们曾经出现过一条任务因为上游服务持续抖动多个节点反复重试最终模型账单令人揪心的情况。后来我在任务调度层加了一个预算护栏。每一条任务进入流水线之前先估算总成本上限每个节点每次重试之前检查当前任务的累计消耗超过上限就停止重试并转人工。这个护栏听起来简单但能有效防止“重试风暴”。我常用的一句话是没有预算控制的可重试机制本质上是在给失控买保险。6.3 可观测性没有Trace就没有排查能力第三个坑是排查问题太慢。节点多了以后如果日志只是零散地打印在每个节点函数里一旦出现问题需要人工翻开几十条日志硬找。我后来给流水线引入了贯穿全链路的追踪ID从任务创建开始生成每个节点的请求都携带这个ID。节点表里额外记录每个节点的输入摘要、输出摘要、耗时、重试次数、错误堆栈。排查问题时我通常会先看节点表哪几个节点状态是failed各自的重试次数和错误信息是什么然后按追踪ID去查看详细的Prompt和输出记录基本能在几分钟内定位问题。上线追踪体系最大的感受是以前处理一个“模型输出不对”要半小时起步现在十分钟内能判断是提示词问题还是数据问题还是上游依赖问题。最后说一点个人体会。这套多Agent串行流水线改完并稳定运行之后我最大的感触是不要迷信“一个Agent会解决所有问题”工程上真正难的是把大任务拆小、把节点之间的契约定清楚、把失败和恢复的路径做扎实。如果你准备做类似方案不用一上来就追求复杂的并行编排先把串行节点链、状态机、幂等重试和检查点这四件事做好系统稳定性就已经比绝大多数“单次大Prompt”方案高一个数量级。之后再谈并行或者动态编排也不迟。希望这篇记录能帮你在自己的Pipeline落地时少踩几个坑。