多步智能体跑得又慢又容易出错这阵子好几个做 Agent 开发的朋友跟我吐槽同一个现象任务链路只要超过三步延迟就指数级上升而且经常在中间某个环节拿到错误结果回头查日志又看不出明显问题。我调了几个项目之后发现问题其实大多不在模型本身而在控制流编排。这里的控制流编排指的是你如何决定哪些步骤交给大模型推理、哪些步骤交给代码逻辑、步骤与步骤之间怎么衔接、每次调用带多少上下文、失败之后如何重试。同样是调用同一个模型编排方式不同性能差距可以到十倍以上。这篇文章就把我实际排查和重构过程中的经验整理出来从定位延迟来源、拆解高开销模式到具体落地分层编排和重试策略一步步说清楚为什么编排错了会导致又慢又错以及怎么改。1. 慢不一定怪模型先分清延迟花在哪条链路上1.1 三种慢的成因画像推理时延、编排开销、无效重试很多人遇到多步智能体响应慢第一反应是模型不行换更强的模型或者上下文太长模型吃不下。这两个因素确实存在但它们通常只解释一部分延迟。我实际拆解过几个项目发现慢的成因主要有三种特征完全不同。第一类是模型推理时延。这是单次调用的固有成本取决于所选模型的响应速度和输入输出长度。它的特征是单次调用耗时稳定你把一次任务里的所有 LLM 调用时间加总如果总和接近总耗时那问题就出在调用次数太多而不是模型单个请求太慢。第二类是编排开销。包括请求序列化、上下文拼装、工具返回结果处理、条件判断逻辑、数据格式转换等非 LLM 损耗。这类开销在单步任务里可以忽略但多步任务里会像手续费一样层层叠加。它的特征比较隐蔽每次调用之间的空洞时间特别长日志里能看到大段空白间隔而这些间隔里没有任何网络请求或计算。第三类是无效重试。这是最容易被忽视也是最伤性能的。流程一旦出错触发重试逻辑整条链路从头再跑一遍或者某一步反复调用模型直到成功。它的特征是总耗时是正常耗时的数倍且每次失败都发生在同一个节点附近。调优的第一步不是改代码而是先搞清楚当前慢是哪一种。把一次完整任务的时间拆开看通常能直接锁定问题区域。1.2 五分钟定位法用时间戳拆解一次完整任务要定位慢的根源不需要复杂的链路追踪工具先用最原始的方法在编排层每个关键步骤前后打时间戳跑一次端到端任务把时间线画出来。具体的做法是在以下位置分别记录时间点和耗时任务启动入口函数接收参数的时间每个 LLM 调用请求发出前的时间每个 LLM 调用返回后的时间每个工具函数开始执行和结束执行的时间每次数据转换、条件判断的前后时间每次异常抛出和重试触发的时间跑完一次完整的流程之后把时间线摊开你会直观看到哦原来步骤三和步骤四之间空了二十秒这二十秒既没有调模型也没有调工具就是在等某个队列或者做无意义的轮询。或者原来这里重试了三轮每轮都要把前面的五步重跑一遍。我见过一个很典型的案例一个总共需要八次 LLM 调用的多步任务实测总耗时一百二十秒但八次调用的模型耗时加在一起只有四十五秒。剩下的七十五秒里有三十秒花在步骤之间的串行等待上四十五秒花在两次因输出格式解析失败而触发的整链路重跑上。这里模型既不慢上下文也不长纯粹是编排设计把时间都吃掉了。所以我的建议是每次调优之前先做一次这样的时间拆解把结果贴在项目文档里。有了这张时间线图后面做任何改动都能直接对比效果而不是凭感觉说优化了。2. 三个常见编排范式为什么它们又慢又脆2.1 每步都问LLM该怎么办的ReAct误用ReAct 模式本身没有问题它是让模型在思考-行动-观察之间循环通过推理来决定下一步调什么工具。但很多团队在落地时把它用成了每步都开会做完一个工具调用把结果返回给模型让模型重新思考再决定下一个动作。这样每前进一步都需要一次完整的 LLM 推理往返。这样做的代价是双重的。慢的方面每一步都要等模型生成完整的思考链加上历史上下文不断累积后面的调用越来越慢。错的方面随着前面的工具调用结果被拼接进上下文模型被大量中间信息干扰在第五步、第六步时很容易做出前后矛盾的决定比如重复调用同一个工具或者忽略某个关键结果去执行无关动作。这不是模型能力问题而是编排把决策频率设得太高了。某些步骤之间的顺序关系是固定的根本不需要模型来判断直接由代码按顺序执行就行。正确的做法是把需要模型判断的决策点压缩到最少那些已经确定先后关系的步骤用代码逻辑按依赖关系串起来模型只在整个流程的分岔口出现。2.2 串行瀑布流水线排队效应和单点阻塞另一种常见问题是把多步任务写成串行瀑布步骤一完成后进入步骤二步骤二完成后进入步骤三每一步都必须等前一步完成哪怕这两步之间根本没有数据依赖。这种情况多见于把看起来像流程的任务拆成严格顺序的步骤。例如先查 A 数据再查 B 数据再把 A 和 B 数据合并给模型。如果 A 和 B 的查询相互独立完全可以并行发出等两个结果同时回来再合并。串行执行时总耗时是 A 的耗时加上 B 的耗时并行执行时总耗时约等于两者中较大的那个。单点阻塞则是另一种表现形式。一个多步任务里如果某一步是瓶颈——比如一个外部 API 响应特别慢超时设得又长——那后面所有的步骤都会被这个慢节点卡住。我在一个项目里看到过一个数据清洗步骤调用外部服务平均耗时八到十秒而其他步骤都是几百毫秒结果整个任务的总耗时被这一步拉高了一个数量级。解决串行瀑布的思路有两种一是把有依赖关系的任务画成有向无环图能并行的分支尽早拆出来并行执行二是对明显慢的瓶颈步做特殊处理比如加缓存、加超时上限、用近似结果替代避免它阻塞后续流程。2.3 让LLM当全局状态机的结构性风险第三种,也是最危险的一种编排范式是让一个全局上下文同时承担状态存储、决策判断和结果累积三个角色。常见实现是维持一个巨大的上下文里面既有用户请求、又有中间结果、又有模型自己的思考记录然后让模型在每次交互时读取整个上下文自己判断当前进行到哪一步、下一步该做什么。这种设计的隐患在于上下文一旦变长模型对当前状态的感知就会漂移。它可能认为某一步还没做实际已经做了或者认为某一步的结果是 A实际结果是 B。错误的根源不是模型逻辑不行而是你强加给它的状态管理方式不符合它的工作方式。全局上下文还有一个问题是成本。每次调用都携带全量历史token 消耗随步骤数线性甚至超线性增长而大部分历史内容对于当前这一步决策根本没有用处。这也解释了为什么很多多步智能体越跑越慢——不是模型变笨了是你每次都在让它重新读一遍几百页的材料然后做一个小决定。把状态从 LLM 里剥离出去用外部变量、数据库记录或结构化对象来维护当前进行到哪一步、已完成哪些步骤、各步骤结果是什么LLM 只负责接收与当前决策相关的必要信息是解决这类问题的根本方向。3. 把确定性流程交给代码把不确定判断交给模型3.1 混合编排静态DAG 动态分支经过前面两轮的对比我在实际项目中倾向于采用一种混合编排思路能提前确定的流程结构画成静态 DAG有向无环图由代码引擎负责调度真正需要灵活判断的分支点才把控制权交给模型。DAG 在这里不是抽象概念它就是一个明确的执行计划。节点代表可执行单元边代表依赖关系。执行单元可以是外部工具调用、内部函数、数据转换也可以是一个询问模型的子流程。调度引擎按依赖关系决定哪些节点可以先执行、哪些节点必须等前置节点完成。这样确定性的部分完全由代码掌控不浪费一次模型调用。动态分支的意思是DAG 的某些节点内部包含一个需要模型判断的钩子。例如一个信息提取节点完成后下一个节点是走直接生成答案路径还是继续询问用户补充信息路径这个选择由一个专门的模型调用决定。但一旦选择落定后续路径的执行顺序仍然由 DAG 引擎接管而不是让模型一边走一边想。这种混合模式的好处是每一步之间的大量协调工作等待、数据传递、状态记录、失败处理全部由代码完成速度快且稳定模型只在真正的决策点出现调用次数大幅减少上下文也变得短而专注。我理解很多团队不愿意这么做是因为觉得 Agent 就应该是模型自由编排人为界定流程边界好像限制了智能。但实际工程项目的约束恰恰来自这个限制——你给模型越多的自由它就越容易在中间步骤里失控。把确定性流程交给代码不是束缚智能而是给智能划了一个安全的活动范围。3.2 结构化输出如何帮控制流收敛控制流要稳定一个很关键的细节是模型输出的形式。很多编排错误源于模型返回了自由格式文本你再用正则或字符串匹配去解析解析失败又得重试重试又增加延迟。我建议所有决策节点的输出都强制走结构化格式。最简单的做法是用 JSON Schema 约束输出让模型必须返回固定的字段。例如一个路由节点让它返回一个包含next_step字段的 JSON字段值只能来自一个枚举列表。这样下游调度逻辑可以直接读取字段值做分支跳转不需要解析自然语言也大大降低了解析失败的概率。这类结构化约束需要写进 Prompt并且配合工具调用的 function schema 或者输出解析器来实现。实测下来把决策节点的输出从自由文本改成结构化枚举之后因格式解析失败导致的重试次数能下降 80% 以上。同时下游节点可以根据字段值直接走对应的代码逻辑整个流程的确定性显著提升。一个容易忽略的副作用是结构化输出也会从根本上限制模型的发挥空间。当模型只能在几个固定选项里选择时它几乎不会产生计划外的下一步动作这也避免了那种模型突然调用一个你没想让它调用的工具的尴尬情况。3.3 上下文精简原则只传这一次决策真正需要的信息多步智能体的上下文管理是影响性能和准确率的直接因素。很多项目在步骤推进时把之前所有的中间结果不加选择地塞给模型认为这是信息完整。结果就是上下文中混杂了大量对当前决策没有帮助的噪音模型既要过滤噪音又要判断关键信息速度和准确率一起下降。我在编排中遵循一个原则每次模型调用只携带与当前决策最相关的必要信息。具体来说是三件事当前步骤的输入数据来自上游工具的结果摘要当前步骤需要参考的少量示例或约束条件决策历史的最小摘要例如用户已确认收货地址当前需要判断是否生成退款单对较长的历史内容优先做摘要而不是直接拼接原文。这里的摘要可以由前一步的模型生成也可以由代码规则截取关键字段。对于外部检索回来的参考资料只截取命中片段不要把整篇文档塞进去。这样做的一个附加好处是模型的上下文窗口压力变小单次调用的响应速度更快。如果你发现某个多步任务在后期步骤中特别迟钝先检查一下上下文是不是越滚越大——通常就是这里出了问题。每个步骤都要想清楚这一步真正需要知道什么。无关的信息无论是之前的中间结果还是模型的思考过程都不应该被带进下一步。4. 被忽略的重试风暴超时参数不合理可以拖垮一切4.1 一个错误的超时值如何引发雪崩多步智能体最容易在重试策略上踩坑而超时参数是重试策略里最容易被随手一填的配置。举个例子某个步骤调用外部服务你设置超时是三十秒。正常情况下该服务两秒返回但由于外部波动某次它耗时二十五秒才返回超时没触发任务继续往下走。虽然慢了但至少是正常的。但如果某次外部服务真的卡住了三十秒超时触发任务进入重试逻辑重试又等了三十秒再次超时再重试……一次任务可能因为重试而拖到几分钟以上。另一种更隐蔽的雪崩模式是服务端在压力下变慢客户端超时后立刻发起重试重试的请求又叠加到服务端导致服务端更慢又触发更多重试最终整个系统陷入重试风暴。这类故障在单体接口上就有在多步智能体这种每个步骤都可能调用外部服务的场景里风险被放大了数倍。正确做法是给超时和重试设定明确的边界。超时值不能只考虑正常响应时间还要考虑尾部延迟。我会把超时设定为正常响应时间的五到十倍而不是两三倍这样避免因为一点正常波动就触发重试。更重要的是重试次数必须有硬上限并且重试之间必须加入退避间隔而不是立即重试。4.2 重试放错层级慢的问题雪上加霜重试策略另一个常见错误是层级放错。我见过不少编排在整个多步任务的外层包了一个失败自动重跑的逻辑只要任务中任何一步失败整体回到第一步重新执行。这种设计看起来简单实际上代价巨大——如果失败发生在第五步前五步的模型调用成本和时间都要白白浪费一次。正确的方式是把重试下沉到具体步骤级别。每一步制定自己的重试策略外部调用失败可以重试两到三次模型输出格式不对只重试当前这个模型调用并且修正 prompt 或输出约束数据校验失败则不重试直接走失败分支交给上层人工处理。重试还应该考虑幂等性。某些外部工具调用如果已经生效重复调用就会造成副作用比如重复扣费、重复下单。在设计工具接口时尽量让工具调用支持幂等标识让重试时不会产生重复操作。这一点在多步智能体的实际落地中特别重要也是最容易被忽略的。我在一个项目里处理过这样的情况某个步骤调用支付查询接口第一次调用其实已经成功了只是响应超时编排层误判为失败后触发重试结果产生了两次查询记录后续的数据核对就乱了。后来在接口层面加了请求 ID 去重才彻底解决。编排设计重试时必须考虑这个操作重复执行会不会有副作用这个基本问题。5. 让编排错误无处可藏可观测性和回放日志5.1 每个智能体调用该埋哪些点多步智能体调试困难核心原因是过程不透明模型为什么那样决策、哪一步产生了错误中间结果、工具调用到底返回了什么如果不加观测全是黑盒。我在所有编排节点都统一埋点至少采集以下信息节点 ID、节点类型、所属任务 ID进入节点和离开节点的时间戳该节点调用的模型名称、输入 token 数、输出 token 数、单次耗时模型返回的原始输出完整保存不要截断工具调用的请求参数和返回结果注意敏感信息脱敏当前步骤携带的上下文长度和关键字段摘要是否触发重试、重试次数、重试原因这些数据统一写入日志系统同时提供按任务 ID 聚合查询的能力。这样一旦某个任务出错可以直接拉出整条链路的完整过程看到是哪个节点产生的中间结果不对哪个节点做了一个意外的分支决策。5.2 通过trace回放定位错在哪个分支埋点采集到数据之后回放是定位问题的关键手段。我通常的做法是出错后把该任务的整条链路日志拉出来按时间顺序铺开直观地看每个节点的入参、出参、决策结果。举一个常见的定位场景任务在第五步生成了错误答案但从数据上看第五步的模型调用返回是正常的问题可能出在第四步传递过来的上下文包含了错误信息。通过回放第四步的输出发现它在某个字段上的值被错误截断或拼接。继续回放第三步发现是上游工具返回的原始数据本身有异常。这样一层层往回追很快就能锁定真正的问题节点。回放日志还有一个作用就是评估每次模型调用的必要性和质量。你可以统计每条链路的调用次数分布如果某一个任务类型平均要调用十二次模型而最优路径只需要五次说明编排中有大量冗余决策点。依据回放数据可以逐步压缩决策点、把固定流程代码化从而实现性能优化。实际做下来建立良好的可观测性需要一定工作量但它带来的调试效率提升是数倍的。没有观测能力的多步智能体优化基本靠猜出了错只能不断加日志重新跑效率很低。6. 一次典型的控制流重构从4分钟24次调用到40秒9次调用6.1 原始编排的问题拆解用一个我实际重构过的模拟项目来说明。某智能客服系统需要处理一个订单售后自动处理任务链路包括解析用户诉求、查订单、查物流、判断售后类型、生成处理方案、调用外部工单系统、回复用户。原始实现采用了一个大型 ReAct 循环模型在一轮轮思考中不断调用工具直到它认为处理完成。实测一次完整任务的平均耗时约四分钟模型调用次数在十八到二十四次之间而且时不时出现重复查询同样订单信息、判断前后矛盾、漏掉某个必要步骤等错误。从日志回放看最典型的问题是模型经常重复调用查物流这个工具——它查了一次之后又因为不确信结果而再次查询甚至出现第三次重复。问题拆解后有三点模型承担了太多流程记忆职责它必须记住查过什么、没查过什么这本来就不可靠工具调用之间存在固定的业务顺序但模型每次都需要重新推理才能保持这个顺序浪费大量调用外部工单系统偶发超时原始编排是整链路重跑一次超时可能拖垮整个任务6.2 重构后的编排设计重构时我按照前面说的混合编排思路重新设计了流程第一步把所有固定顺序的环节提取出来用代码写成一个 DAG。节点划分如下节点一解析用户诉求提取订单号这里安排一次模型调用输出结构化 JSON节点二并行调用查订单接口和查物流接口两个无依赖的查询同时发起节点三将两个查询结果摘要合并调用模型判断售后类型输出类型枚举节点四根据售后类型走不同的处理分支。退货分支走生成退货方案调用工单系统换货分支走生成换货方案调用工单系统咨询分支直接生成答复文案节点五调外部工单系统创建记录代码逻辑带幂等 ID节点六生成最终回复文案模型调用输入是前面的结构化结果摘要每个模型调用都被限制在各自的节点内部携带的上下文只包含当前决策所需字段。路由判断由节点三的枚举输出直接驱动不再经过大模型二次决策。6.3 重构前后的对比数据重构之后同一任务的实测数据变化很明显模型调用次数从平均二十一次降到六到九次且其中两次是固定查询后的解析判断单次任务耗时从约四分钟降至三十五到四十五秒因重复查询导致的错误输出消失因上下文过长导致的判断矛盾基本不再出现外部工单系统的超时影响被限制在单个节点内不再引发整链路重跑这个项目给我的启发是多步智能体的性能瓶颈往往不是单次推理慢而是编排策略让模型做了太多它本不该做的事。让模型专注于有限的判断点把流程的确定性和状态管理交给代码是减少延迟和错误的最直接路径。控制流的本质就是明确什么必须由模型决定什么不该由模型决定。这个边界画得越清楚系统就跑得越快越稳。优化多步智能体的时候不要急着换更大的模型先把这条边界重新审视一遍往往收获最明显。我记得项目上线后一个很出乎意料的收益是运维那边反馈说夜间告警少了很多。以前任务失败率高的时候经常会有工单创建完却回复失败之类的边缘案例需要人工补偿重构之后这些零星问题基本消失。控制流理顺了不只是快和准的问题整个系统的可维护性都上了一个台阶。如果你手头也有一套又慢又容易出错的多步智能体我建议你按照这个思路走一遍先拆时间线再检查模型调用次数然后看流程中哪些环节其实可以由代码决定最后重新设计重试和观测。这个过程不需要什么高级框架纯粹是对编排思路的重新梳理。我每次做完这一步都能在性能和稳定性上看到显著的提升。