多步智能体现在几乎是每家做 AI 应用的团队都绕不开的东西,但我发现一个很有意思的现象:很多人抱怨“智能体太慢、太容易错”,问下来发现模型本身没换过,prompt 也调了无数版,最后折腾一圈,问题其实出在控制流编排上——你把一个本该走固定轨道的流程,硬生生交给了模型去自由发挥。这篇文章就以我自己的实操经验来拆一拆,多步智能体为什么又慢又易错,以及控制流编排到底应该怎么设计。
先说明白一点:多步智能体不是单次问答,它是“感知→推理→调用工具→拿到结果→继续下一步”这样一串动作的集合。这里的每一步都可能触发一次模型调用,而模型调用是有延迟的、有成本的、也有概率出错的。所以一旦流程设计不合理,时间会成倍放大,错误也会沿着链路往下传递。很多人踩的坑,本质上不是模型不够聪明,而是控制流没有把“聪明”用在正确的地方。
这篇文章适合正在构建多步智能体的开发者、AI 应用架构师,也适合那些已经上过线、但是被线上 agent 的失败率和响应时间折磨得头疼的团队。我会从问题定位开始,讲到控制流设计思路,再给出一套可以直接抄作业的实操方案,最后附上常见问题的排查表。不绕弯子,直接进入正题。
1. 问题定位:多步智能体的“慢”与“错”到底出在哪
1.1 慢的三个真实来源:串行等待、重复协商、上下文膨胀
先说我观察到的第一个现象:慢。很多人以为慢就是模型延迟高,换一个更快的模型就完事了,但真实情况往往不是这样。多步智能体的慢,通常有三个叠加在一起的来源。
第一个来源是串行等待。多步流程天然有依赖关系,比如第一步要生成一个 SQL 查询,第二步执行查询拿结果,第三步根据结果生成结论。这三步之间是严格串行的,每步都要等前一步返回,而每一步的网络开销、排队时间、Token 生成时间都会被累加。如果一个任务有 8 个步骤,每个步骤平均耗时 5 秒,那单纯串行执行就是 40 秒。这还没算重试和分支。
第二个来源是重复协商。什么叫重复协商?就是模型在自由决策模式下,会来回犹豫。我见过一个真实的案例:某个多步智能体在处理“根据用户描述创建一条客户记录并发送通知邮件”这个任务时,模型第一步先调用了“查询用户是否存在”的工具,返回为空;然后它没有直接进入创建流程,而是又调用了一次“查询客户列表”,想确认是不是数据格式不对;确认无果后,模型又决定先给自己“规划一下”再做。两个工具调用之间,模型还有大段大段的“内心独白”在消耗 Token。整个流程原本 3 步能完成,模型自己“协商”了 7 次调用才进入正题。
第三个来源是上下文膨胀。这是最隐蔽的。多步智能体的每一步都会把历史对话、中间结果、工具返回的原始数据全部塞进上下文中。到了第 5 步、第 6 步,上下文可能已经上万 Token,而每一步推理都要处理这上万 Token。你在步骤 1 时可能只要 2 秒出结果,到步骤 8 时同样的推理量可能要 5 秒甚至更久——模型处理长上下文的计算量是超线性增长的。这就形成了一个恶性循环:步骤越多,上下文越长;上下文越长,单步越慢;单步越慢,整体就越慢。
1.2 错的两个典型形态:规划漂移与错误锁死
说完慢,再说错。多步智能体的错误率,往往也不是模型单点能力的问题,而是整个流程的失控。
第一种错误形态我管它叫“规划漂移”。当模型拥有每一步的自由决策权时,它很容易偏离最初的目标。举个例子:任务是“整理本周销售数据并生成一份周报”。模型第一步读取了销售数据,第二步发现数据里有异常值,就跑去调用数据分析工具做异常检测;检测完又觉得需要看一下历史对比,于是又调取了历史数据;最后它甚至生成了一份专项分析报告,但最初要的周报反而没写。每一步单独看都是合理的,但整体偏离了原始目标。这就是自由规划模式的典型问题:模型局部最优,全局失控。
第二种错误形态是“错误锁死”。多步流程中一旦某一步出错,如果控制流没有定义好容错和重试策略,整个任务就卡死了。我见过最离谱的情况是:某一步调用的工具返回了空结果,模型判断“需要重试”,于是原样重试了 10 次;第 10 次返回同样的空结果后,模型进入了一种“自我怀疑”的循环,反复调用同一个工具,再也不往下走了。本质上就是没有设置重试上限,也没有定义“重试失败后应该怎么办”——这就是控制流缺位的典型表现。
1.3 一个关键判断:模型能力还是编排问题?
这里有个非常重要的问题:怎么判断一个多步智能体的失败到底是模型能力不行,还是编排不合理?
我自己的判断方法是看两个特征:失败是否集中在某些固定的步骤上,以及同样的任务重复执行时,失败点是否多变。如果有固定的失败步骤,比如每次都是第 4 步解析返回结果时崩,那基本可以断定是编排层的解析逻辑有漏洞,或者这一步的输入没有被上游规范化;如果失败点飘忽不定,一会儿在这一会儿在那,那有可能是上下文管理出了问题,或者模型在自由决策中走岔了路。
另外一个更简单的判断方法:把同样的任务用硬编码的流程跑一遍,如果硬编码流程稳定成功,而自由决策模式经常出错,那就说明任务本身是清晰固定的,不应该交给模型自由发挥。很多时候我们要做的不是“提升模型能力”,而是“收回模型的自由权”。这个思路贯穿了后面所有的编排设计。
2. 控制流编排的核心思路与设计取舍
2.1 从“全自由”到“带护栏”:控制流光谱
要理解控制流编排,先要理解一个光谱:一端是“全部交给模型自由发挥”,另一端是“全部硬编码写死”。
全部交给模型自由发挥,代表模式是 ReAct 式的“推理→行动→观察”循环。模型每一步都自己决定下一步做什么,理论上非常灵活,可以应对各种没见过的场景。但代价就是上一节说的:慢、不可控、容易漂移。当你把“每一步做什么”这个决策权完全让渡给模型时,就等于把整个流程的稳定性押在了模型的每一次输出上,而多步链路的失败率是每一步失败率的叠加——单步失败率 5%,十步的累计失败率就接近 40%,非常残酷。
全部硬编码则走向另一个极端。流程固定,每一步做什么写死,模型只负责填充其中某几处自然语言理解的部分。这种方案稳定、快、可控,但也有明显天花板:凡是流程里没覆盖到的场景,都会直接失败。
一个成熟的编排设计,一定是在这两个极端之间找一个“带护栏的中间态”:框架是确定的,但框架内部留有模型智能发挥的空间。用通俗的话讲,就像给一个能力很强但容易跑偏的新员工一个 SOP——SOP 规定了先做什么、后做什么、什么情况下走哪个分支、最多尝试几次,但每一步内部的具体措辞、具体决策,还是让模型自己来。这样既保住了智能体的灵活性,又避免了无节制的自由发挥。
2.2 五个控制流单元:顺序、分支、循环、并行、容错
任何复杂的多步智能体流程,拆解到最底层,都是由五个控制流单元组合而成。理解这五个单元,是设计编排方案的基础。
顺序(Sequence)是最基础的单元。步骤之间有明确的依赖关系,前一步的输出是后一步的输入。比如“生成查询 → 执行查询 → 汇总结果”,这就是一个严格的顺序流。顺序流的重点在于数据契约:前一步的输出必须能稳定地成为后一步的输入,这要求我们在每步之间做格式规范化和校验,而不是直接把模型输出原样丢给下一步。
分支(Branch)处理的是条件逻辑。模型或者上游系统的输出不同,走不同的路径。比如判断用户意图是查天气还是查机票,这就是一个分支点。分支设计的关键是分支条件要尽量客观、可判断,比如基于结构化输出的某个字段做判断,而不是让模型在每个分支里面重新理解一遍意图。
循环(Loop)是智能体最需要的单元,也是最容易失控的单元。循环用于让智能体自我修正、重试失败的步骤、或者反复处理多轮任务。循环设计的三原则是:必须有最大次数限制、必须有跳出条件、必须记录已尝试过的方案。没有这三条,循环就是一个潜在的死循环。
并行(Parallel)是提速的关键。当多个步骤之间没有依赖关系时,它们完全可以同时执行。比如一个任务需要同时查询三个数据库、调用两个外部 API,串行可能要 15 秒,并行可能只需要 4 秒。
容错(Fallback)是最后一道防线。每个可能失败的步骤,都应该有三个问题的答案:失败后重试吗?重试几次?重试还失败的话走哪条降级路径?这三个问题在流程设计阶段就应该想清楚,而不是等线上挂了再去补。
2.3 编排层与模型层的分工边界
很多人设计多步智能体时,容易陷入一个误区:把所有逻辑都塞进 prompt 里,让模型“自己看着办”。这其实是把编排层和模型层的职责搞混了。编排层负责流程的骨架——步骤顺序、分支条件、循环边界、超时时间、重试策略、降级方案;模型层负责填充骨架中的“内容”——总结一段文字、抽取一个字段、生成一个查询、给出一个判断。骨架要硬,内容要软。
我经常用自动驾驶来类比这件事。模型是那个驾驶能力很强的“司机”,但控制流是那条“路”。路上要有明确的车道线、红绿灯、限速标志、应急车道。你不能把司机扔在一片没有路的草原上,然后指望他高效地把你送到目的地。多步智能体也一样:你要给模型一条清晰的路,它在路上怎么开是它的事,但路的方向、交叉口、出口,必须是编排层定义好的。
3. 实操落地:多步智能体的编排改造方案
3.1 把“边想边做”改成“先规划后执行”
我自己在改造多步智能体时,第一个动作永远是:把自由决策模式改成“先规划后执行”模式。
具体做法是:当任务进来时,第一步先让模型生成一份结构化的任务规划,输出为严格的 JSON 格式,包含步骤列表、每一步的操作类型、依赖关系和预期输出。这一步完成后,编排层对规划做校验,把不符合约束的步骤打回去重新规划,或者直接拒绝。规划通过后,后续就按照这份规划逐步执行,不再让模型每一步临时决定下一步做什么。
这样做有三个明显的好处。第一,规划阶段只做一次决策,后续执行就变成了确定性的流程,省掉了模型每步“思考下一步”的时间和 Token 消耗。第二,规划本身就是一次“全局审视”,模型在开始执行前就能看到完整路径,比走一步看一步更容易发现潜在问题。第三,规划给编排层提供了干预点——你可以在执行前检查规划是否合理,可以在执行中对比模型行为是否偏离规划。
这里有一个实操细节:规划输出的 JSON Schema 要定义得非常严格。每个步骤必须包含step_id、action_type、input_params、depends_on这些字段。action_type必须是预先定义好的枚举值,比如llm_call、tool_call、condition_check,不能开放给模型自由发挥。我见过太多规划输出中 action_type 写得五花八门的例子,模型把tool_call写成call_tool、use_tool、execute_function,下游解析器直接崩掉。所以校验这一步绝对不能省。
3.2 循环与重试的工程化控制
第二个核心动作是把所有循环和重试逻辑从模型自由行为中拿出来,下沉到编排层控制。
拿重试来举例。一种常见的坏设计是:某一步调用工具失败后,把错误信息返回给模型,让模型“决定”要不要重试、怎么重试。这时候模型大概率会尝试换一种方式调用——但它并不知道是不是换个方式就能成功,很可能只是在碰运气,而且每一次失败重试都会消耗一次模型调用,时间成本和金钱成本都翻倍。
好的设计是:在编排层定义重试策略,模型不参与重试决策。具体来说,我会给每个步骤配置三个参数:max_retries(最大重试次数)、retry_backoff(重试退避时间)、retry_on(哪些错误类型触发重试)。比如某个步骤调用了外部 API,得到 429 限流错误,编排层自动等 2 秒重试一次;如果是 400 参数错误,那就属于不可重试错误,直接进入容错分支——因为这大概率是参数生成的问题,不是临时故障,重试多少次都没用。
循环控制的另一个关键点是幂等性。多步智能体中很多工具调用是有副作用的,比如创建订单、发送邮件、写入数据库。如果循环让同一个步骤执行了两次,就可能产生重复订单、重复邮件。解决方法是给每个步骤一个全局唯一的execution_id,工具执行前先检查这个 ID 是否已经执行过,已有结果就直接复用。这个思路在分布式系统里已经很成熟了,但在智能体编排里却常常被忽略——结果就是智能体一旦陷入重试循环,业务数据就被写脏了。
3.3 并行提速与状态快照
串行等待是多步智能体慢的主要来源之一,而并行是把时间压下来的最直接手段。当规划阶段生成的步骤列表中,存在多个互不依赖的步骤时,编排层可以用并发调用同时执行它们。
我做过一个模拟项目 X 的改造:一个数据聚合类的多步智能体,原本 12 个步骤全是串行,整体耗时约 90 秒。把其中 6 个没有依赖关系的查询步骤全部改成并行后,整体耗时降到约 32 秒,因为在同一个异步事件循环中同时发出多个工具调用,总耗时就取决于最长的那一项,而不是所有项之和。
并行提速听起来简单,但有几个细节要注意。
第一是限流。并行调用多个外部服务时,要同时遵守每个服务自己的速率限制。建议在编排层做一个统一的限流控制器,对同一上游的并发请求数进行管控,避免为了提速反而触发大规模 429。
第二是共享状态的一致性。并行步骤之间如果都要读写同一个共享变量,就要用不可变状态设计——每个步骤只能拿到自己的输入快照,不能直接修改全局状态,只有全部并行步骤完成后,编排层才把它们的结果合并成一份新的状态。这样就避免了并发写导致的脏数据和竞态问题。
第三是并行步骤的失败处理。并行执行时,如果其中一个子任务失败了,另外几个还在跑。编排层需要定义:是等待全部完成再统一处理失败,还是只要有失败就立刻取消其他任务?两种策略各有适用场景,我在实际项目中通常默认“等待全部完成再统一汇总”,因为多数情况下其他并行子任务的结果对容错判断仍然有价值。
3.4 一个最小可用的编排伪代码示例
说了这么多,我给一个最简单、可以直接抄的编排伪代码,不绑定任何特定语言,核心逻辑是通用的。
class AgentOrchestrator: def __init__(self): self.max_retries = 3 self.retry_backoff_base = 2.0 def run(self, task): # 第一步:生成规划 plan = self.plan(task) # 第二步:校验规划 self.validate_plan(plan) # 第三步:按依赖图执行 results = {} ready_queue = [s for s in plan.steps if not s.depends_on] while ready_queue: step = ready_queue.pop(0) # 已经执行过的步骤直接复用结果 if step.execution_id in results: continue if step.action_type == "tool_call": result = self.execute_with_retry(step) elif step.action_type == "llm_call": result = self.invoke_llm(step, results) elif step.action_type == "condition_check": result = self.evaluate_condition(step, results) results[step.execution_id] = result # 更新就绪队列 for candidate in plan.steps: if candidate.execution_id in results: continue deps = candidate.depends_on if all(dep in results for dep in deps): ready_queue.append(candidate) return self.assemble_final_answer(results) def execute_with_retry(self, step): last_err = None for attempt in range(self.max_retries): try: # 执行前检查幂等 if self.has_executed(step.execution_id): return self.last_result(step.execution_id) return self.call_tool(step) except RetryableError as e: last_err = e time.sleep(self.retry_backoff_base * (2 ** attempt)) # 重试耗尽,走降级分支 return self.fallback(step, last_err)这段伪代码体现了几个关键点:全局唯一的execution_id做幂等;重试有上限和退避;重试失败有降级函数;依赖满足的步骤才进入就绪队列。你可以直接把这个骨架翻译成工程代码,再往上加并发、可观测性、状态持久化这些能力。
4. 常见问题与排查技巧实录
4.1 Agent 频繁重复调用同一个工具或转入死循环
这是多步智能体最高频的问题。线上表现是:某一个工具被连续调用十几次,agent 卡在原地不往前走,日志里能看到大量相同参数的重复请求。
排查思路分三步。第一步看循环边界——这个循环有没有设置max_iterations?如果没有,先打上迭代上限,这是最紧急的止血措施。第二步看跳出条件——每一步循环结束时,是否有明确的“满足什么条件就跳出”?很多死循环是因为跳出条件写得模糊,模型判断不了“要不要继续”,只能一直尝试。第三步看是否缺少“已尝试方案记忆”——循环体内部每次迭代都产生了新的状态,但 agent 没有记录“我试过 A 方案失败了”,所以下一次迭代又走同一个分支。解决方式是在循环上下文中维护一个attempt_history列表,让模型每次决策前都能看到之前尝试过的方案和失败原因,避免原地打转。
4.2 任务中途“忘记”了最初目标
这种问题的典型表现是:前面的步骤还算正常,到流程中后段,智能体的行为开始偏离初始任务,去做一些不在规划里的事。很多人的第一反应是“模型上下文太长了,导致注意力下降”——这个解释对了一半。
更本质的原因是:每一步的 prompt 里都重复携带了完整的目标描述,但目标描述越来越被无关上下文稀释。我推荐的做法是把“目标锚点”独立出来:在每一步的 prompt 顶部固定放一段精炼的任务目标声明,比如“你的唯一目标是:生成本周销售周报。禁止执行与生成周报无关的操作。”这段声明不随历史对话增长,始终保持精简。
另一个有效做法是“阶段性子目标确认”:把整个流程按规划节点切成几个阶段,每个阶段开始时单独向模型确认一次当前阶段要达成的子目标。这相当于给多步流程加了节点检查点,即使模型在某一段跑偏了,下一阶段的确认也能把它拉回来。
4.3 分支返回格式不稳定导致下游解析报错
分支节点是错误率比较高的地方。原因在于:分支判断往往是模型输出一个 JSON 字段,比如action: "book_flight",但模型可能输出成action: "预定机票",或者多了一个空格、多了一个引号,下游解析器就挂了。
解决办法有两个层面。第一,在输出端使用结构化输出约束——主流大模型 API 基本都支持 JSON Schema 强制约束,把分支字段定义成枚举类型,模型就无法输出枚举之外的取值。第二,在输入解析端做一层容错解析,不要因为一处解析失败就整个流程崩溃。我在解析逻辑里会加一个“纠错映射表”,把常见的同义表达映射到标准枚举值,这属于经验积累,踩过的坑越多,这个映射表越厚。
还有一个更稳健的思路:分支判断不依赖模型的“意图理解”,而是依赖上游步骤返回的结构化字段。比如某个工具返回的原始结果里自带一个status字段,那么分支应该直接读这个字段,而不是让模型去重新理解一遍工具返回的内容。让模型做分支判断,往往是锦上添花的场景才该做的事,能省则省。
4.4 可观测性与断点续跑
多步智能体的线上调试绝对是噩梦级别,因为差错链路太长,一个问题往往跨越多个步骤。所以可观测性不是加分项,而是必选项。
我的最低要求是:每一步执行都要记录完整的输入输出、耗时、Token 消耗、错误信息,并且用同一个task_id串联整个流程。每一步产生的日志都能回答这三个问题:这一步的输入是哪来的?这一步产出了什么?这一步如果失败,是怎么处理的?只有信息完整,才有资格谈排查。
在可观测性之上,再往上加一层能力就是断点续跑。多步智能体执行到第 7 步时挂了,如果整个流程要从第 1 步重来,那成本太高了。我现在习惯把每一步的执行结果持久化存储,以execution_id为键。当流程恢复时,先加载已完成的步骤结果,只从失败点继续执行。这个能力在长流程智能体中价值极其明显——我之前有个 15 步左右的流程,加断点续跑后,恢复成本从全量重跑变成了单步重跑,线上失败恢复时间是原来的十分之一不到。
整理一个常见问题速查表,方便大家日常排查时对照:
| 现象 | 直接原因 | 根本隐患 | 优先解法 |
|---|---|---|---|
| 反复调用同一工具 | 循环跳出条件缺失 | 无迭代上限控制 | 设置 max_iterations 与跳出条件 |
| 任务中途跑偏 | 目标信息被上下文稀释 | 无目标锚点机制 | 每步注入精简目标声明 |
| 分支解析失败 | 模型输出非标准枚举 | 无结构化约束 | JSON Schema 枚举强制约束 |
| 失败后整体重跑 | 无法定位失败步骤 | 无断点续跑能力 | 执行结果按 execution_id 持久化 |
| 并行调用被限流 | 并发策略过猛 | 缺少限流控制 | 统一限流控制器 |
多步智能体的控制流编排,说到底是在做一道“如何平衡模型自由度和系统确定性”的题。我见过太多团队在这道题上的答案分成两个极端:要么把流程全交给模型导致生产事故频发,要么把流程写死变成毫无智能的自动化脚本。而真正落地的方案,一定是在这两个极端中间的那个点上——让模型在确定的轨道上发挥能力,而不是让模型自己去找轨道。我自己做完几个项目的改造之后,最大的体会是:先花时间把控制流图画出来,比急着让模型跑起来重要得多。把每一步的顺序、分支、循环边界、失败降级都画清楚,再谈智能体的“智能”也不迟。