Agent 的规划-执行分离:计划表示、重规划触发与并发调度

很多团队在做 Agent 时,第一版几乎都是同一个样子:一个 while 循环,把用户目标、历史消息、工具列表一股脑塞进上下文,让模型输出下一步动作,执行完再塞回去。这个结构在演示阶段非常好用,因为它足够简单,也足够"像人"。但只要任务超过五步,它就开始暴露出三个很难绕开的问题:模型忘记最初目标、在无关分支上反复打转、以及在某个工具连续失败后仍然固执地重试同一条路径。

问题的根源不在模型不够强,而在于"边想边做"这种结构本身缺少一个稳定的中间层。人在做复杂工作时不会把全部注意力都放在"下一步做什么"上,而是先在心里形成一个粗糙的计划,再在执行过程中不断修正它。Agent 也一样,需要把"计划"从一个隐式的、每轮重新生成的东西,变成一个显式的、可被检查、可被修改、可被回滚的数据结构。

一、为什么要把规划和执行拆开

先明确一个概念上的边界。编排(orchestration)和规划(planning)不是一回事。编排是代码层面的控制流,决定第 1 步和第 2 步谁先谁后;规划是模型层面的决策,决定"要做哪些事才能达成目标"。前者是确定性的,后者是概率性的。如果把两者混在一起写,代码就会变成一堆 if-else 里夹着模型调用,改一处逻辑要重新跑一遍全流程,测试也无从下手。

规划-执行分离的核心收益有三个。

第一是可观测。计划本身是一个结构化对象,你可以把它打印出来、存进数据库、在管理后台展示给运营看。用户问"这个 Agent 现在在干什么",你能直接回答"它在第 3 步,正在查订单,前面两步已完成"。而纯 ReAct 的循环里,你只能给用户看一段流水账。

第二是可干预。计划可以被人工修改。高风险动作(退款、发信、删数据)可以在计划层面就标红,让人先确认再执行,而不是等到工具已经调用出去才做拦截。这一点在企业场景里是刚需,因为业务方不会接受一个"自己决定要不要给客户退款"的系统。

第三是可复用。同一类任务的计划结构往往高度相似。把"查订单—核对金额—判断是否符合退款条件—生成回复"这套流程固化成模板,下次遇到同类请求时只需要填充参数,不必让模型从零推理。这直接决定了 token 成本和首字延迟。

但拆开也有代价。多一次模型调用意味着多一次延迟和多一份成本;计划一旦做错,后面的执行会全部白费,这就是典型的"垃圾进垃圾出"。所以真正的问题不是"要不要拆",而是"在什么粒度上拆、什么时候重新规划"。

二、计划该长什么样

计划的数据结构设计,决定了整个系统的上限。实践中常见的错误是让模型输出一段自然语言步骤,比如"1. 先查一下订单 2. 再看看物流 3. 然后回复用户"。这种计划人看着舒服,程序没法用。

一个可执行的计划至少需要包含四类信息:步骤标识、动作类型、参数、依赖关系。

{
  "goal": "处理用户关于订单 A1234 延迟发货的投诉",
    "steps": [
        {
              "id": "s1",
                    "action": "tool",
                          "tool": "query_order",
                                "args": { "order_id": "A1234" },
                                      "depends_on": [],
                                            "risk": "low"
                                                },
                                                    {
                                                          "id": "s2",
                                                                "action": "tool",
                                                                      "tool": "query_logistics",
                                                                            "args": { "order_id": "A1234" },
                                                                                  "depends_on": ["s1"],
                                                                                        "risk": "low"
                                                                                            },
                                                                                                {
                                                                                                      "id": "s3",
                                                                                                            "action": "llm",
                                                                                                                  "instruction": "根据订单与物流信息判断延迟原因,若为仓库缺货则给出补偿方案",
                                                                                                                        "depends_on": ["s1", "s2"],
                                                                                                                              "risk": "low"
                                                                                                                                  },
                                                                                                                                      {
                                                                                                                                            "id": "s4",
                                                                                                                                                  "action": "tool",
                                                                                                                                                        "tool": "send_reply",
                                                                                                                                                              "args": { "template": "delay_apology" },
                                                                                                                                                                    "depends_on": ["s3"],
                                                                                                                                                                          "risk": "high",
                                                                                                                                                                                "require_approval": true
                                                                                                                                                                                    }
                                                                                                                                                                                      ]
                                                                                                                                                                                      }
                                                                                                                                                                                      ```
这个结构里有几个关键点值得展开。

`depends_on` 是让计划从"列表"变成"图"的关键。有了依赖关系,调度器才能判断哪些步骤可以并行。上面例子中 s1 和 s2 其实都只依赖订单号,理论上可以并发;如果物流查询需要订单里的仓库编码,那就必须串行。很多团队的计划结构里没有依赖字段,导致所有步骤被迫串行执行,端到端延迟直接翻倍。

`risk` 和 `require_approval` 是安全阀。把风险等级写进计划,而不是写进工具实现里,好处是同一个工具在不同上下文下可以有不同风险等级。比如 `send_reply` 在"回复常见问题"时是低风险,在"回复涉及赔付的投诉"时就是高风险。风险由规划器根据语义判断,而不是由工具静态声明。

`action` 字段区分了 `tool` 和 `llm` 两种步骤。这看起来是小事,实际上很重要:它让调度器知道哪些步骤需要走外部 API、哪些步骤只是在做文本推理。前者要设超时和重试,后者要设 token 预算,两者的失败语义完全不同。

还有一点常被忽略:**计划要带版本号**。计划在执行过程中可能被修改(重规划),如果不带版本,日志里会出现同一个步骤 id 对应两份不同内容的情况,排查问题时极其痛苦。给每次计划变更打一个递增的 revision,执行记录里同时记下 plan_id 和 revision,回溯时就能精确还原当时到底执行的是哪一版。

## 三、重规划的触发条件

计划做出来之后,什么时候该推翻重来?这是整个架构里最容易做错的地方。太敏感,模型会频繁重规划,成本爆炸且行为不稳定;太迟钝,计划早就失效了还在硬跑。

工程上建议把触发条件分成三类,按优先级从高到低。

**第一类:硬失败。** 工具返回明确的业务错误,比如订单不存在、权限不足、参数校验失败。这类错误不应该重试,因为重试的结果一定相同。正确的做法是标记该步骤失败,触发重规划,让模型换一条路径。比如 `query_order` 返回"订单不存在",模型应该转而让用户确认订单号,而不是重试三次。

**第二类:软失败与超时。** 网络抖动、下游限流、临时不可用。这类错误应该先按指数退避重试,重试 N 次仍失败才升级为重规划。这里的关键是区分"这个工具挂了"和"这个任务做不了"。前者可以通过降级(比如换成缓存数据)绕过,后者才需要改计划。

**第三类:语义偏离。** 前两类都是技术层面的,第三类才是真正需要模型判断的。执行到某一步后,模型发现新信息与计划前提矛盾。比如计划里假设"用户投诉的是物流延迟",查完发现订单其实是被系统自动取消的,那整个计划的前提就不成立了。这类偏离无法用规则捕获,必须由执行器在每步完成后,把结果交给一个轻量的校验调用,判断"计划是否仍然成立"。

这里有个成本优化的技巧:**不要让主模型做校验**。校验只需要回答一个是非问题,用一个小模型甚至一个分类器就够了。每步都调用大模型做校验,成本会翻好几倍,而收益只是偶尔发现几次偏离。更划算的做法是只在"关键步骤"后校验,比如所有写操作之前、所有涉及金额的判断之后。

另外必须设**重规划次数上限**。工程上建议 2 到 3 次。超过这个次数往往说明任务本身超出了系统能力边界,或者用户输入有问题,这时候正确的做法是诚实告知"我无法完成这个任务,因为……",而不是继续烧 token。一个没有上限的重规划循环,是所有 Agent 事故里最常见的一种。

## 四、并发调度:把 DAG 跑起来

计划是图,执行就应该是图的遍历。最朴素的做法是拓扑排序后串行执行,这已经比纯 ReAct 快很多,但还没榨干并行的收益。

真正有效的调度器应该做到三件事。

**第一,按依赖分层。** 把没有未完成依赖的步骤放进就绪队列,每次从队列里取一批并发执行。上面例子里 s1 和 s2 如果都只依赖输入,就可以同时发起。这一层并行的收益在 I/O 密集型的工具调用上非常明显,两个 800ms 的接口并发执行,端到端就从 1600ms 降到 900ms 左右。

**第二,控制并发度。** 无限制并发会打爆下游服务,也会让日志难以阅读。实践中建议给调度器设一个全局并发上限(比如 4 到 8),再给每个工具单独设一个并发上限。有些工具本身就是有状态的,比如浏览器自动化,同一个会话里并发操作会互相干扰,这类工具必须串行。

**第三,处理"部分失败"。** 并发执行时最容易出问题的是错误处理。如果 s1 成功、s2 失败,s3 依赖两者,那 s3 应该被跳过还是带着不完整输入执行?答案取决于业务语义。工程上建议把这种决策显式化:在计划里给每个步骤标注 `on_dependency_failure`,取值为 `skip`、`degrade` 或 `abort`。默认 `abort` 最安全,但会让整个任务因为一个不重要的步骤失败而全盘重来,所以关键路径上的步骤才用 `abort`,边缘步骤用 `skip` 更合理。

一个常被低估的细节是**结果缓存的键设计**。并发执行时,两个步骤可能调用同一个工具、传同样的参数(比如都查同一个订单)。如果缓存键只用工具名加参数的哈希,就能直接复用结果,省掉一次调用。但要注意缓存的有效期:查订单可以缓存几秒,查库存就必须实时。给每个工具声明一个 `cache_ttl`,比全局统一设置要靠谱得多。

## 五、一个容易被忽略的问题:计划的"可恢复性"

计划执行到一半,进程挂了、用户关掉了页面、上游超时了。这时候怎么办?

如果你的计划只存在内存里,答案就是"从头再来"。但很多任务是有副作用的,重来一次可能重复发信、重复扣款。所以计划必须**持久化**,而且要在每一步执行前先落盘"即将执行"的状态,执行后再更新为"已完成"。这是典型的 write-ahead 思路,和数据库的 redo log 是同一个道理。

恢复时的逻辑要区分两种状态。一种是"已提交但结果未知",比如发信请求发出去了但没收到响应。这类步骤不能简单重试,必须有幂等键。给每个写操作生成一个基于 plan_id 和 step_id 的幂等键,下游服务据此去重,是最稳妥的做法。另一种是"已执行且结果已知",直接读结果继续即可。

还有一点:**恢复不等于重放**。有些团队为了简单,恢复时直接把整个计划重跑一遍。如果计划里有 LLM 步骤,重跑会产生不同的输出,导致最终结果和原来不一致。对于需要强一致性的场景,应该把 LLM 步骤的输出也存下来,恢复时直接复用,而不是重新生成。

## 六、什么时候不该用这套架构

最后说点反面的。规划-执行分离不是银弹,它在三类场景下是负收益。

**任务只有一两步时。** 为了一个"查天气并回答"的请求做一次规划调用,纯属浪费。判断标准很简单:如果计划步骤数稳定小于 3,直接用单轮工具调用更划算。

**任务高度确定时。** 如果业务方已经明确知道流程是 A→B→C,那就应该写成代码,而不是让模型规划。模型规划的价值在于处理不确定性,确定性的流程用工作流引擎(甚至一个状态机)更可靠、更便宜、更好测试。

**对延迟极度敏感时。** 规划本身要花几百毫秒到一两秒,对于要求首字延迟低于 500ms 的交互式场景,这个开销不可接受。这种情况下可以把规划结果缓存成模板,命中时跳过规划,只做参数填充。

架构选型的本质是权衡,而不是追求先进。规划-执行分离真正解决的问题是"长任务下模型会迷路",如果你的任务根本不长,那这个架构带来的只有复杂度。

## 七、小结

把 Agent 从一个循环拆成"规划器 + 调度器 + 执行器",本质上是在给概率系统加上确定性的骨架。计划是数据,不是提示词;重规划是异常处理,不是主流程;并发是图的遍历,不是简单的 for 循环。

落地时可以先从最小闭环做起:定义好计划 schema,实现一个串行的执行器,把所有步骤和结果落盘,跑通之后再逐步加并发、加校验、加人工审批。很多团队一上来就想做通用编排引擎,结果半年过去还在设计抽象层,而业务方要的其实只是一个能稳定跑完十步的流程。

工程上的成熟,往往不体现在架构图有多漂亮,而体现在"出问题时能不能定位、能不能重放、能不能人工接住"。这三点做到了,Agent 才算真正可用。


Logo

更多推荐