规划与 ReAct:从「无计划」到「依赖图」的四种形态

Agent; 规划; ReAct; Planning; 多步任务 2252 字 12 min read

前面七篇搭好了 Agent 的躯干

、对话循环、工具契约、上下文、记忆、状态。但有一件事一直没讲——面对一个任务,Agent 到底应该怎么组织它的行动?

一步到位?列个清单逐条做?边做边改计划?还是拆成一张可以并行的任务图?

这就是规划(Planning) 层要回答的问题。它不是”永远多规划一点”,而是为任务匹配最合适的规划形态——并且知道什么时候该停下来重新规划。

本篇原理部分译自 agentic 课程 Ch.09,案例来自 agent开发系列的工作流编排横评。

为什么规划这件事值得单独讲

没有计划,Agent 会空转(thrash)

,第 4 步走错了却一直没发现,调一个工具去做上一个工具已经做完的事。

但计划太多,同一个 Agent 会把一半 token 花在提出一个 20 步的蓝图上——而这个蓝图在第一个工具结果和它冲突的那一刻就作废了。

代价体现在 token、延迟,以及最糟的一种:自信地给出了解决错误问题的最终答案

修复方法不是”多规划”,而是把规划形态匹配到任务,并知道何时重新规划。

四种形态,并排看

flowchart TD
    subgraph s1["① 无计划 — 反应式"]
        a1["用户提问"] --> a2["模型每轮<br/>挑下一个动作"]
    end
    subgraph s2["② 清单"]
        b1["模型写有序列表"] --> b2["逐步执行<br/>做完就划掉"]
    end
    subgraph s3["③ 规划-执行-重规划"]
        c1["模型提出计划"] --> c2["执行一步"] --> c3["观察结果"] --> c1
    end
    subgraph s4["④ 依赖图"]
        d1["模型输出任务 DAG"] --> d2["可运行节点<br/>并行执行"] --> d3["汇聚点综合"]
    end

判断一个任务需要哪种形态,问两个问题:目标有多明确?第 1 步的结果有多大概率改变第 2 步的计划? 目标明确、发散度低的任务适合前两种;后两种是为路径真正不确定、或存在独立分支的任务准备的。

形态一 · 无计划(反应式)

Agent 挑一个工具、运行、然后要么回答、要么挑下一个工具。这是短问答、一次性查询、简单转换的默认形态。

“装的 node 是什么版本?” 不需要计划。

快、便宜、流畅。但在任何真正多步的任务上都容易空转。

形态二 · 清单

模型在动手前写一个简短的有序列表,边做边划掉。

type ChecklistPlan = {
  objective: string;
  steps: Array<{
    id:     string;
    text:   string;
    status: "pending" | "in_progress" | "done" | "skipped";
  }>;
};

这个列表本身就是记忆——提示词构建器每轮把当前列表注入,模型对照它自我纠正。适合 3-8 个有序步骤、模型大致能记住整个计划但进度追踪有帮助的场景。OpenCode 的 TodoWriteTool 是最清晰的参照。

形态三 · 规划-执行-重规划

模型提出计划,执行一步或几步,观察结果,然后在结果改变全局时重新提出计划

stateDiagram-v2
    [*] --> 规划
    规划 --> 执行 : 跑下一步
    执行 --> 观察 : 工具结果
    观察 --> 重规划 : 新信息改变了计划
    观察 --> 执行 : 计划仍有效
    观察 --> 完成 : 目标已满足
    重规划 --> 执行
    完成 --> [*]

适合调查、调试、研究——任何你在第 3 步做完之前不知道第 5 步长什么样的任务。代价是每次重规划多一次模型调用,收益是 Agent 不会被一个过时的计划锁死。

这就是大多数人口中的 “ReAct”(Reason + Act)

→行动→观察的循环。它稳定、通用,是目前 Coding Agent 的主流形态。

形态四 · 依赖图

对于有独立分支的任务(并行审查三个文件;从三个源拉取再合并),把计划表达成一张有向图,并行执行可运行节点。

// 可运行 = pending 且所有依赖已完成
function runnableNodes(nodes: PlanNode[]) {
  const done = new Set(
    nodes.filter(n => n.status === "done").map(n => n.id)
  );
  return nodes.filter(n =>
    n.status === "pending" && n.dependsOn.every(id => done.has(id))
  );
}

实践中这种形态住在比纯规划高一层的地方——委派(下一篇 Ch.09 多智能体编排),可运行节点通常变成子 Agent 调用。适合有明确并行度和汇聚点的工作流;不适合任何看起来线性的东西——那样图只是为复杂而复杂。

怎么选

任务形状规划形态
一个明显的动作无计划
3-8 个有序步骤清单
路径不确定、结果改变下一步规划-执行-重规划
独立分支带汇聚依赖图

两个要警惕的反模式:

  • 因为依赖图看起来高级就用它,而清单本可以搞定;
  • 对一个明显需要结构的任务赖在”无计划”模式里

模型对这两种都会顺着你——得靠你的设计推回去。

计划该放在哪

计划就是文本,但放在哪很重要。三个地方:

  • 工具结果里
    todo_write,结果是新列表,像任何工具结果一样出现在易变尾部。
  • 工作记忆里(上篇的可变草稿纸)
    WorkingMemory.currentPlan 的一部分,每轮渲染进提示词。
  • 一个用户可编辑的独立文件里(plan.md)
    和用户共享这个产物,谁都能改。

跨轮次把计划放在同一个地方,这样模型知道去哪找。别一会儿写文件、一会儿返回工具结果——挑一个。

规划型 Agent vs 构建型 Agent

真实系统做的一个有用区分:做规划的 Agent 和做构建的 Agent 用不同的工具集

  • 规划型 Agent
    ,不编辑、不 shell,输出一个结构化计划。往往用便宜模型就够。
  • 构建型 Agent
    ,含写入、编辑、shell。贵的模型住在这里。

交接

(由用户或策略),然后构建 Agent 以这个计划为起始上下文运行。OpenCode 注册了独立的 planbuild Agent 配置,正是这个形状。

这也呼应了 Claude Code 的 Plan 模式——先只读探索出计划,批准后再切到可写执行。“先想后做”是跨系统反复出现的模式。

隐式 vs 显式计划

有些 Agent 从不写计划——它们只是不断挑下一个工具。有些则把写计划当作第一个动作。隐式在小任务上更快;显式能早early catch”脆弱假设”这种失败模式。

一个有用的默认:如果任务描述里点了两个或更多交付物,就要求一个显式计划;否则让模型每轮自己决定。

最便宜的推动方式

,对看起来多步的任务加一句 “先写出你的计划”。模型通常会照做,而它写出的计划本身就是有用的信号——如果模型连计划都说不清,说明任务本身欠规约。

什么时候该重新规划

四个”计划已失效”的信号:

  1. 一个工具结果和计划的前提矛盾——比如计划假设某文件存在,结果它不存在。
  2. 同一步反复失败——重试两三次仍不过,说明方案本身有问题,不是执行问题。
  3. 出现了计划没预见的新子任务——发现要先修一个历史遗留问题才能继续。
  4. 目标本身被用户修正了——中途用户补充或改变了需求。

任意一个触发,就该停下来重新规划,而不是硬着头皮把过时的计划执行完。

小结

规划的本质是为任务匹配最简单够用的形态,并识别任务什么时候在要求你升到上一级:

  • 一步 → 无计划
  • 几步有序 → 清单
  • 路径不定 → 规划-执行-重规划(ReAct)
  • 独立并行 → 依赖图(通向多智能体编排)

计划要放在固定的地方、当作记忆管理;规划和构建可以用不同工具集的 Agent 分离;显式计划的最大价值,是逼出”任务是否欠规约”这个信号。

下一篇进入多智能体编排——当依赖图里的节点变成一个个子 Agent 时,谁来决定并行度?这是整个专题最烧脑的一章。


本文主要参考:

  • agentic-ai-system-course Ch.09 Planning Patterns(四形态、replan 触发、plan-only vs build)
  • agent开发系列 / 工作流编排(真实项目的规划实现)
  • Claude Code Plan 模式(先想后做的哲学,见本站《Claude Code 专题·工作流篇》)