本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。

ReAct 的循环里,下一步查什么要等上一步的结果回来才定。流程明确的长任务用这种方式,决策开销会重复花在同一件事上。Plan-and-Solve 把顺序倒过来:先让模型把整件事拆成一个有序的步骤列表,再逐步执行,末尾汇总成答案。

三段流水线

Planner 一次调用,拿用户问题和工具清单,输出一个 JSON 数组。Executor 按顺序执行每一步,每步内部是一个带工具的小循环。Aggregator 再调用一次,把计划与各步结果汇总成最终答案。三个角色各管一段上下文,谁也不用看到全部历史。以课程任务一为例,Planner 拆出来的列表大致是查北京当前天气、按天气推荐两个景点、汇总结论。步骤描述里带着城市和数量,Executor 拿到就能直接调工具;写成“了解北京”“给出建议”这类空话,下一步的模型还得重新想一遍要做什么。

Planner 的 prompt 要把输出格式写死:

1
2
3
4
5
6
7
8
9
10
11
12
PLANNER_PROMPT = """把用户的问题拆成有序的子任务列表。

可用工具:
{tools}

规则:
- 每步都能独立执行并拿到中间结果
- 步骤数控制在 2 到 5 个
- 严格输出 JSON 数组,每项一个字符串,不要输出其他文字

用户问题:{question}
"""

约束写进 prompt 只是一半。模型仍可能用代码块包裹数组、在数组前后多写一段、或者输出对象而不是数组。解析代码得处理这几种偏差,解析失败时退回单步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import json

def parse_plan(raw: str) -> list[str]:
raw = raw.strip()
start, end = raw.find("["), raw.rfind("]")
if start == -1 or end == -1:
return []
try:
steps = json.loads(raw[start : end + 1])
except json.JSONDecodeError:
return []
if not isinstance(steps, list):
return []
return [s for s in steps if isinstance(s, str)]

调用方拿到空列表就用问题本身兜底,把原任务当成只有一步。宁可少拆,也不让解析失败把整个流程打断。要绕开这类解析,可以在请求里打开 JSON 模式,或者用 pydantic 做结构化输出。

还有一种情况要区分:解析失败退回单步骤,看起来像“这题只需要一步”,实际是容错兜底。观察 demo 输出时把真规划和降级分开看,否则会误判规划器的能力。

跟 ReAct 的分工

维度 ReAct Plan-and-Solve
决策时机 看到上一步的观察结果再决定 开始前把所有步骤定完
灵活度 高 低
适用任务 信息收集、探索型 流程明确、多步任务
计划失效 在循环里自然换路 计划错了整轮白跑
token 步数越多越贵 一次规划加多次短执行

ReAct 适合不知道下一步要查什么的任务,Plan-and-Solve 适合步骤多但流程清楚、步骤之间弱依赖的任务,比如分别查三家公司的财报再汇总对比。课程 demo 的五个测试任务共用同一套工具,两个范式跑同样的题,差别只在循环怎么组织。

省 token 的说法要打个折扣。Executor 每一步都带上前序结果,Aggregator 还要把计划和结果重新发一遍,省下的是每步重看全部历史,代价是多了一次规划调用。

计划错了怎么办

一个具体的失效场景:计划写着查北京天气、推荐户外景点、汇总,第 1 步查到今天大雨,第 2 步还按户外走就没有意义。课程 demo 做的是静态版,Executor 拿到上一步的结果,看到大雨会自己改推室内景点。更稳的做法是动态 Replan,关键假设一变就回到 Planner 重排,Devin 这类产品大量用动态版。

重排不是免费的。每重排一次就多一次 Planner 调用,已经执行过的步骤要不要保留、结果怎么合并,都得额外判断。工业实现通常只在两类信号上触发:某一步直接失败,或者关键假设被推翻,比如查到的天气、库存、价格与规划时的前提相反。

前序结果以纯文本拼接进 user 消息,步数越多这段上下文自己也在膨胀。真做长任务,得换成摘要或者只传重要字段。

适合与不适合

课程讲义把适用条件写成三条:步骤多而流程明确、步骤之间弱依赖、token 预算有限。前两条决定能不能用,第三条决定值不值得用。

反过来,信息收集型的任务不适合,比如查一个 bug 是怎么引入的,在查明之前根本不知道该拆几步。步骤之间强依赖的任务同样不适合,前一步的结果决定后一步做不做,计划从一开始就不成立。

工业里常见的做法是双层:外层 Plan 做宏观规划,子任务内部再跑 ReAct 做探索,一次 Function Calling 能解决的步骤直接调用。规划粒度粗一点,局部保留应变能力。

这对搭 Agent 意味着什么

规划把思考提前,也让错误提前。Planner 拆错一步,后面几步全部白跑,ReAct 至少能在循环里换路。工程上的对策有两条:给计划加校验与重排,或者把计划做粗、把灵活性留给 Executor。课程 demo 把步骤数限制在 2 到 5 步,Executor 每步最多 4 轮工具调用,这两个数字就是给偏差留的余量。

Plan-and-Solve 之后还可以加一层:答案写完,让它自己回头检查一遍。这是下一篇的 Reflection。