第 13 章 · Planning 规划
一份“三家竞品对比报告”的请求
那个卖家又来了。这次他甩过来一句话:“帮我调研 Anker、Baseus、UGREEN 这三家在亚马逊上的旗舰充电器,整理成一份对比报告——价格、评分、差评里最常被骂的点,我下午要开选品会。”
这不是“查个价格”那种一步到位的活儿。它至少要做九件事:分别打开三家的商品页、各自抓价格和评分、各自翻差评、把三家的差评归类、再横向拉成一张表、最后写结论。任何一步漏了、顺序乱了,报告就是残的。
如果 Agent 上来就闷头干,常见的翻车是这样的:查完 Anker 兴致勃勃写了一大段,轮到 UGREEN 时上下文已经很长,它忘了前面还要“横向对比”,草草收尾;或者三家的差评它只翻了一家,剩下两家凭印象编。任务一复杂,它就顾头不顾尾。
人接到这种活儿会怎么做?不会立刻动手——会先在草稿纸上列个提纲:第一步查 Anker,第二步查 Baseus……最后一步写结论。有了这张清单,做一项划掉一项,既不漏也不乱。让 Agent 学会这件“先列提纲再动手”的事,就是本章的主题:规划(Planning)。
什么是任务分解(task decomposition)
规划的第一块基石,叫任务分解(task decomposition):把一个笼统的大目标,拆成一串更小、更明确、能逐个完成的子任务。
“整理三家竞品对比报告”是个大目标,模型直接对着它使劲,无从下手。但如果先拆成——
1. 抓取 Anker 旗舰充电器的价格、评分、Top 差评
2. 抓取 Baseus 的同类数据
3. 抓取 UGREEN 的同类数据
4. 把三家差评归类(发热 / 虚标 / 做工 / 兼容性…)
5. 拉成一张横向对比表
6. 写选品建议
——每一项都具体到“知道下一步该调哪个工具、传什么参数”。任务分解的价值就在这里:它把“我该干嘛”这个模糊的大问题,翻译成一串“我现在干这个”的小问题。 大模型不擅长一口气盯着一个庞杂目标,却很擅长完成一个界定清晰的小步骤——分解就是在迁就模型的这个特点。
分解出来的这串步骤,通常以一个待办清单(todo list)的形式存在。它可以只是模型脑子里想过一遍的文本,也可以是一份被显式记录、能勾选状态的结构化清单——后者正是本章后面要用 pi 实现的东西。
先规划后执行(plan-and-execute)的好处
有了分解,下一个问题是:什么时候分解? 一种鲜明的做法是先分解、再动手,业界叫 plan-and-execute(先规划后执行):让 Agent 在真正调用任何工具之前,先产出一份完整的计划,然后照着计划一步步执行。
这套做法通常把职责拆成两个角色,术语值得记牢:
- planner(规划器):只负责把目标拆成有序步骤,产出计划,不碰具体执行。
- executor(执行器):拿着计划,逐条去调工具、干活,把每步结果攒起来。
为什么值得先花一步专门做计划?好处有三个,恰好治的就是开头那份报告的病:
- 不遗漏:计划就是一张清单,做一项划一项,“横向对比”这种容易被长上下文冲掉的步骤,白纸黑字钉在那里,漏不掉。
- 有方向:计划本身留在上下文里,每一轮都在提醒 Agent“整体目标是什么、现在走到第几步”,它不会做着做着跑偏。
- 可调整:执行中发现计划不对——比如 UGREEN 没有对标机型——可以回头改计划,而不是硬着头皮按错的清单往下做。
把 planner 和 executor 分开还有个隐性好处:规划这一步往往需要更强的推理,执行这一步更看重稳和快,两者分开就能各用各的策略,甚至各用不同的模型。
一次性规划(plan-once)vs 交错式规划(interleaved planning)
先规划后执行,规划到底“规划几次”,又分成两条路线,这是本章最该讲透的一组对立术语。
一次性规划(upfront planning / plan-once):开头就把完整计划一次性列全,之后原样照做,中途不再重新规划。它像出发前把整条路线都规划死。优点是省事、可预测、便于展示进度;缺点是吃不消意外——一旦执行中冒出计划没预料到的情况(某家竞品页面改版抓不到评分),照着老计划走就会撞墙。它适合结构清晰、步骤基本确定的任务,比如“把这个目录下所有图片压缩后上传”。
交错式规划(interleaved planning):不追求一开始就想全,而是规划一步、执行一步、再根据结果规划下一步,规划和执行交替进行。它像开着导航走,每到一个路口再根据实时路况决定下一步。优点是灵活、能吸收执行中的新信息;代价是每步都要重新想,更慢、也更难提前展示完整进度。它适合不确定性高、边做边才知道下一步的任务,比如“排查这个线上故障”——你根本没法在看到第一条日志之前就把排查步骤列全。
两者不是二选一的死对头,实践中常常混用:先用一次性规划列一个粗提纲(六大步),执行每一大步时再用交错式规划细化。开头那份竞品报告就适合这样——大框架(“查三家 → 对比 → 写结论”)一次性定死,但“查某一家”具体怎么查、遇到页面改版怎么办,留给交错式临场应变。
还有一点常被忽略:计划是一种可以持久化的状态。它不只是模型某一轮的临时念头,而应该被显式存下来,跟着会话一起保存。这样即使中途压缩了上下文、甚至关掉重开,Agent 依然“记得”整体计划和进度。这个观念,直接引出下面用 pi 的实现。
用 pi 实现:把计划变成显式状态
回顾第 3 章,pi 的循环靠模型“自己决定下一步”。规划的落地思路,就是通过系统提示引导 + 扩展,把本来藏在模型脑子里的计划,拽出来变成看得见、存得住的东西。
pi 的系统提示本身就带了对“技能工作流”和分步执行的引导——它会提示模型接到复杂任务时先拆解、按步骤推进,而不是一头扎进去。这是“引导模型规划”的第一层,零代码。
第二层,是用一个扩展(第 8 章)把待办清单变成 Agent 的显式状态。做法是注册一个 update_plan 工具,让模型把计划写下来、并随执行更新每步状态:
export default function planningExtension(pi) {
// 注册一个 update_plan 工具,让模型把计划显式写下来
pi.registerTool({
name: "update_plan",
description:
"把任务分解为有序步骤,并标记每步的完成状态。接到多步骤任务时先调用它列计划,每完成一步再更新状态。",
parameters: {
type: "object",
properties: {
steps: {
type: "array",
items: {
type: "object",
properties: {
task: { type: "string", description: "这一步要做什么" },
status: {
type: "string",
enum: ["todo", "doing", "done"],
description: "todo=未开始, doing=进行中, done=已完成",
},
},
required: ["task", "status"],
},
},
},
required: ["steps"],
},
// execute 从会话上下文 ctx 拿到状态,把计划写进去
async execute({ steps }, ctx) {
ctx.state.plan = steps; // 计划成为可持久化的会话状态
return { plan: steps };
},
});
}
这里有个细节值得说:初稿里 execute 用到了 ctx 却没在签名里声明,靠的是外层闭包里恰好有个同名变量——这在真实运行时会拿到 undefined 或错误的对象。正确写法是从 execute 的参数里接住运行时传入的 ctx(如上),这样 ctx.state.plan 写的才是当前会话的状态。
有了这个工具,一次完整的规划-执行就长这样:模型接到“竞品报告”任务,先调 update_plan 列出六步(全部 todo);开始查 Anker 时把第一步标 doing,查完标 done、把第二步标 doing……每次调用都把最新计划写回 ctx.state.plan。
关键在最后这句:计划作为状态留在会话里,Agent 每一轮都“看得见”整体目标和进度。因为它是持久化状态,上下文压缩(第 9 章)时可以优先保住它,长任务里也不会把计划弄丢。这正是很多交互式编码 Agent 里那个“待办列表”面板的本质——它不是 UI 装饰,而是模型的规划状态被显式渲染了出来。至于什么时候只列一次、什么时候边做边改,由系统提示和模型自己按任务性质决定,update_plan 只提供“把计划落成状态”的能力。
动手看看
pi 的交互式编码 Agent(pi-coding-agent)自带一个待办列表,是观察规划最好的活样本。给它一个明显多步的任务,比如“帮我把这个项目里所有 console.log 换成结构化日志,并加上测试”,然后盯着它的待办列表看:
- 它是一次性把所有步骤列全,还是做一步冒出一步(交错式)?
- 每一步的状态(todo/doing/done)是怎么随执行流转的?
- 中途如果某步失败,它是硬着头皮按原计划走,还是回头改了计划?
再去翻 pi 的系统提示,找找里面关于“分步执行”“技能工作流”的措辞——正是这些话在引导模型接到复杂任务时先拆解。对照着看,你会发现“待办列表”这个看起来很产品化的功能,底下就是本章讲的任务分解 + 计划持久化状态。
实战中的几个坑
坑一:计划列得太粗或太细。
- 现象:要么一步“完成报告”大得没法执行,要么拆成几十个琐碎小步,光更新计划就耗掉半天。
- 原因:没给模型合适的粒度示范,任务分解失了准头。
- 对策:在系统提示或工具描述里给粒度范例——一步大致对应“一次能验证结果的动作”,别大到无从下手,也别细到鸡毛蒜皮。
坑二:列了计划却不照着走。
- 现象:模型开头调了
update_plan,后面执行时把计划抛到脑后,该更新状态时不更新。 - 原因:计划只被当成一次性的开场白,没有机制反复把它拉回模型视野。
- 对策:每轮或关键节点把当前计划(带状态)回填进上下文,并在提示里明确“每完成一步就更新计划”。
坑三:死守一次性计划,不肯变通。
- 现象:执行中环境变了(页面改版、接口报错),模型仍机械照老计划往下撞。
- 原因:任务本该交错式规划,却被当成一次性规划硬跑。
- 对策:对不确定性高的任务,在提示里明确允许“发现计划不对就重新规划”,让它有权改计划而非死磕。
坑四:计划没持久化,压缩后就丢了。
- 现象:长任务跑到后半程,Agent 忘了整体目标,开始重复或跑偏。
- 原因:计划只活在对话消息里,一旦上下文压缩就被冲掉。
- 对策:把计划存成显式的会话状态(如
ctx.state.plan),压缩时优先保留,确保它始终在场。
对比:其他框架
同样是“让 Agent 先规划再执行”,各家给你的抽象层次差别很大:
| 框架 | 规划怎么做 | 特点 |
|---|---|---|
| LangGraph | 一等公民。经典 “Plan-and-Execute” 模板:专设 planner 节点出计划、executor 节点逐步执行,执行后可回 planner 更新计划 | 最显式、控制流全在图里,也最啰嗦 |
| CrewAI | 开启 planning 选项,执行前自动为任务列计划再分派给各角色;角色“目标”也隐含分步倾向 | 抽象层次高,规划半自动 |
| PydanticAI | 无内建 plan-and-execute 原语。常见做法:让模型返回结构化“步骤列表”(发挥类型安全),再由你的代码驱动执行 | 规划要自己组织,但步骤有类型保障 |
| Agno | 无专门“规划器”抽象,靠引导 Agent 显式产出待办再执行;配合 memory 让计划跨步保留 | 规划是引导出来的,非一等特性 |
| pi | 系统提示引导 + 扩展把计划显式化为 update_plan 工具/状态,作为可持久化会话状态留在上下文 | 轻量灵活,计划即状态 |
本质都是同一件事:把计划从模型脑子里拽出来、显式化、留在上下文里指引后续步骤。差别只在于框架是塞给你一个现成的 planner 节点,还是让你自己把计划变成一份状态。
小结
- 任务分解(task decomposition) 是规划的基石:把笼统的大目标拆成一串界定清晰的小步骤,迁就模型“不擅长盯大目标、擅长做小步骤”的特点。
- 先规划后执行(plan-and-execute) 把职责分成 planner(规划器) 和 executor(执行器),换来不遗漏、有方向、可调整三大好处。
- 规划分 一次性规划(plan-once) 和 交错式规划(interleaved planning):前者省事可预测但吃不消意外,后者灵活能吸收新信息但更慢;实践中常粗提纲一次性、细步骤交错式地混用。
- 计划是一种可持久化的状态,不只是临时念头;存成显式状态才能扛住上下文压缩、跨会话不丢。
- pi 用 系统提示引导 +
update_plan扩展 落地规划:把 待办清单(todo list) 变成看得见、存得住的会话状态,这正是交互式编码 Agent 里那个待办面板的本质。
规划给了 Agent 方向感,让它知道要走哪几步。可计划一旦开始执行,难免有步骤做错、结果不对——光有计划还不够,Agent 还得能回头审视自己刚做的事、发现问题再纠正。这种自我检查的能力,就是第 14 章要讲的 Reflection 反思。