3. 规划,让 Agent 先想后做 - Plan
为什么需要规划?
在之前实现的AgentLoop中,LLM面对简单的任务已经能处理的很好,但是在面对复杂任务的时候,比如“为项目添加一个xxx模块”,LLM 容易迷失在项目实现细节中,甚至产生幻觉,忘记最初的任务目标。Claude Code 当时收到了很多用户的反馈,他们使用最多的就是告诉 Agent:“先不要编码”,所以最初的 Plan 功能就是简单的告诉 LLM 先不要输出编码,等待规划完成后才进行编码。
规划的实现
这里我引入了一个规划阶段,通过 createPlan 函数,将 task 分解成数个 subtasks,有几个要点:
- 使用 LLM进行规划任务:Plan 本身也是一次LLM调用,但是只携带了只读的工具,这样 LLM 就能探索项目的目录和文件,进行任务规划。
- 输出 & 降级:通过 Prompt 要求 LLM 结构化输出,然后对 LLM 的输出结果进行校验,如果校验失败则降级到 [task]。这种防御性编程在 Agent 开发中非常重要,经过设计可以让 Agent 自己修复问题(Self Healing)。
import { agentLoop } from "./core";
import { ChatMessage } from "./type";
import { readOnlyTools } from "./tools";
import z from "zod";
const schema = z.object({
subTasks: z.array(z.string()),
});
export async function createPlan(task: string): Promise<string[]> {
console.log("[Planning] Start Planning...");
const msg: ChatMessage[] = getPlanMessage(task);
const { result } = await agentLoop(`[Task]: ${task}`, msg, undefined, {
tools: readOnlyTools,
});
try {
const json = JSON.parse(result);
const subTasks: string[] = schema.parse(json)?.subTasks;
console.log("[Plan] Created:");
console.log(subTasks.map((t, i) => `${i + 1}. ${t}`).join("\n"));
return subTasks;
} catch (e) {
console.log(
"[Plan] Failed to create plan. Let's just use the original task as the plan.",
);
return [task];
}
}
function getPlanMessage(task: string): ChatMessage[] {
return [
{
role: "system",
content: `Break down the task into 3-5 simple, readable, actionable steps. Must include test. Return a JSON array of strings.
Schema:
{
"subTasks": string[],
}
- Return ONLY valid JSON
- No Markdown
- No Explanation
- No Command
**Available Tools:**
${readOnlyTools.map((t) => `- ${t.type}: ${t.type}`).join("\n")}
Important Notes:
- ONLY use the tools listed above
- DO NOT create, write, edit, modify or delete file during planning
`,
},
];
}ReAct 和 Plan-Then-Execute 范式
Agent Loop 和 Plan 展示了 Agent 领域的两种范式,即 ReAct 和 Plan-Then-Execute。ReAct 灵活,但是容易迷失,Plan-then-Execute有全局视角,但是规划可能不准确,而且随着运行,计划可能会有变动。
Plan 模式,正如流程图所绘,先通过LLM获得subtasks,然后用subtask循环执行多次 ReAct,通过控制上下文的方式,强行让 ReAct 每次执行一个 subtask。上下文传递是多步执行的关键,通过传递上下文,不同的 ReAct 能够共享信息并执行任务。Plan 模式不在于 “先写一个大纲”(图中的 Simple Plan-Then-Execute),而在于先“建立约束和分工”,Plan 是为了让迭代不乱跑。
Plan 模式存在一些争议,很多业内人士认为,应该依赖于模型自身的规划能力,而非强制拆分任务,来充分发挥模型的能力。
Plan 模式的改进 - Plan-as-Tool
每次使用 Agent 的时候进行复杂任务的时候,都手动切换使用 plan 模式,或者有时候忘记了切换到 plan 模式,导致任务的执行效果不佳。不如把任务交给 Agent,让它自己
实现
我们可以给 Agent 提供一个 plan 工具。传入参数是用户输入的任务,这个工具能够分解一个复杂任务,创建一个多个子任务,并分步执行。
在如下 plan 工具的实现中,如果 Agent 认为任务复杂,则会使用 plan 工具,plan 工具会将任务分解并分派执行,最终将结果汇总给 Agent。我通过传入不同的工具,即具体执行任务的 LLM 不具备 Plan 工具,来防止无限调用。
import { createPlan } from "../../plan";
import { agentLoop } from "../../core";
import { subtaskPrompt } from "../../prompt";
import { ChatMessage } from "../../type";
import { ToolKit } from "..";
import { loadMcpTools } from "../../mcp";
import { EventStream, EventType } from "../../eventStream";
const eventStream = EventStream.getInstance();
interface PlanArgs {
task: string;
}
export const planFunc = async (args: PlanArgs): Promise<string> => {
const { task } = args;
eventStream.submit({ type: EventType.PLANNING, message: `Creating plan for task`, data: { task } });
const tasks = await createPlan(task);
const taskResults = [];
// 为 subtask 创建专用的系统消息(不包含 plan 工具提示)
const subtaskSystemMessages: ChatMessage[] = [
{
role: "system",
content: subtaskPrompt,
},
];
for (let i = 0; i < tasks.length; i++) {
const t = tasks[i];
eventStream.submit({ type: EventType.PLANNING, message: `Executing subtask ${i + 1}/${tasks.length}`, data: { subtask: t } });
const mcpTools = loadMcpTools();
const res = await agentLoop(
`[Subtask ${i + 1} of ${tasks.length}]: ${t}`,
subtaskSystemMessages,
undefined,
{
tools: [...ToolKit.executeTools, ...mcpTools],
},
);
taskResults.push(res);
}
const res = `[Plan Execution Results]\n${taskResults
.map((r, i) => `Task ${i + 1}: ${r.result}`)
.join("\n")}`;
eventStream.submit({ type: EventType.PLANNING, message: `Plan execution completed`, data: { totalTasks: tasks.length } });
return res;
};