3. 规划,让 Agent 先想后做 - Plan

有了 Agent Loop 和 Memory 之后,Agent 已经能处理简单任务了。“统计文件数,写进 count.txt”——几轮循环搞定。

但换一个任务试试:“为这个项目添加用户认证模块。”

Agent 就开始乱窜了。它一会儿读 package.json,一会儿翻到某个完全无关的 util 函数里研究起来,一会儿又在 node_modules 里迷路了。跑了十几轮循环之后,你发现它忘了最初要干什么。

这不是 Agent 笨。是 ReAct 模式(思考→行动→观察→思考)天然有一个缺陷:每一步只看到上一步的结果,全局方向靠 LLM 自己把握。 任务一复杂,LLM 就跟没地图的司机差不多——每个路口都在做正确选择,但整体上完全偏离了目的地。

Claude Code 早期收到了大量类似的反馈。用户最常说的一句话是:“先别写代码,想清楚再动手。” 所以最初的 Plan 功能就是个简单的 prompt 注入——告诉 LLM 先别急着写,把计划列出来。

Plan 的核心思路

Plan 本身也是一次 LLM 调用,但有两个关键区别:

  1. 只给只读工具。 Plan 阶段的 LLM 只能浏览项目结构、读文件、搜索代码——不能创建、修改、删除任何东西。它负责侦察,不负责执行。
  2. 要求结构化输出。 指定 JSON 格式,用 zod 校验。通过了就拿去执行,没通过就降级——把整个 task 当成唯一的 subtask,至少不会卡住。
import { agentLoop } from "./core";
import { readOnlyTools } from "./tools";
import z from "zod";
 
const schema = z.object({ subTasks: z.array(z.string()) });
 
async function createPlan(task: string) {
  const msg = getPlanMessage(task);
  const { result } = await agentLoop(task, msg, undefined, {
    tools: readOnlyTools, // 只读:能看,不能碰
  });
 
  try {
    return schema.parse(JSON.parse(result)).subTasks;
  } catch {
    return [task]; // 校验失败 → 降级,原样执行
  }
}
 
function getPlanMessage(task: string) {
  return [{
    role: "system",
    content: `Break down the task into 3-5 actionable steps. Return ONLY valid JSON:
{
  "subTasks": string[]
}
No markdown, no explanation, no commands.`,
  }];
}

这里的 agentLoop 就是第一篇里实现的同一个函数。Plan 没有另起炉灶,它复用了 Agent Loop,只是换了一套工具和 prompt。这种复用是刻意的——Plan 不是一个独立模块,是 Agent Loop 的一种特殊运行模式。

zod 校验被包在 try-catch 里,校验失败不抛异常,而是默默降级为 [task]。这种防御式写法在 Agent 开发里特别常见:LLM 的输出不可靠,你永远需要一个 fallback。 与其让程序崩掉,不如把原始任务当成计划本身——“拆不出来?那就整件干。“

两种范式:ReAct vs Plan-Then-Execute

Agent Loop 和 Plan 代表了 Agent 领域的两种底层范式。

ReActPlan-Then-Execute
怎么做走一步看一步,每步根据当前状态决定下一步先列计划,再按计划逐步执行
优势灵活,能应对意外变化有全局视角,不容易跑偏
短板复杂任务容易迷失方向计划本身可能不准,执行中情况会变

这里容易有一个误解:Plan 模式就是”先写一个大纲然后照着执行”。其实 Plan 模式的关键不是大纲本身,而是用 subtask 给每次 ReAct 执行划定边界。每个 subtask 跑一轮独立的 Agent Loop,上下文不会无限膨胀,LLM 每次只专注一件事。

Plan 模式在业界有一些争议。一部分人认为应该依赖模型自身的规划能力,强制拆分反而限制了发挥——模型在一次长推理中可能自己找到更优路径,拆成小块反而打断了它的思路。这个争论没有定论,实际项目中通常两种都支持,让用户选。

Plan-as-Tool:让 Agent 自己决定什么时候该规划

前面讲的 Plan 需要人手动切换——用户得在命令行加 --plan,或者点一个”规划模式”按钮。问题是,大部分时候你根本不记得切。

更好的做法:把 Plan 做成一个工具,让 Agent 自己判断要不要用。

Agent 收到一个复杂任务 → 它想”这个好像有点大,拆一下吧” → 调用 plan 工具 → plan 工具把任务拆成 subtask → 逐个执行 → 把结果汇总返回给 Agent。

const planFunc = async (args: { task: string }) => {
  const subtasks = await createPlan(args.task);
  const results = [];
 
  for (let i = 0; i < subtasks.length; i++) {
    results.push(
      await agentLoop(
        `[Subtask ${i + 1}/${subtasks.length}]: ${subtasks[i]}`,
        subtaskSystemMessages, // 不含 plan 工具
        undefined,
        { tools: executeTools }, // 不含 plan 工具
      ),
    );
  }
 
  return results.map((r, i) => `Task ${i + 1}: ${r.result}`).join("\n");
};

这段代码里有一个很容易被忽略的细节:执行 subtask 的 agentLoop 不带 plan 工具。

如果带了呢?Agent 执行 subtask 的时候觉得”这个 subtask 也好复杂,我调一下 plan 拆掉它”,plan 里的 subtask 再调 plan……无限递归,token 当场炸穿。

所以 plan 工具的逻辑是:只有主 Agent(顶层)具备 plan 工具,subtask 执行器只能干活,不能”再想想怎么干”。这是 Agent 开发里一条实用经验——给 LLM 工具的时候,想清楚哪些工具在什么层级可以用,什么层级不能用。 不然它会把简单的任务一层层拆下去,直到 context 窗口爆掉。

什么时候该 Plan,什么时候不该

Plan 不是银弹。“统计文件数”这种任务强行走 Plan 模式,等于拿大炮打蚊子——多绕了一圈 LLM 调用,除了消耗 token 没有任何收益。

Plan 适合的场景:

  • 任务涉及多个独立步骤,步骤之间有依赖关系
  • 你在心里已经能列出”大概要做这几件事”,只是不想自己一个个敲
  • 任务范围很大,直接丢给 ReAct 容易跑偏

不适合的场景:

  • 任务本身简单明确,一步就能搞定
  • LLM 自己已经表现出良好的规划能力(模型越强,越不需要外部规划)

Plan-as-Tool 之所以是比较优雅的方案,就是因为它把这个选择权交给了 Agent。任务简单它就不调 plan,任务复杂它自己拆。你不用操心。


下一篇4. 拓展能力 - Rules、Skills、MCP