Skip to content

Plan-and-Solve ​

标签
AI/agent/范式
字数
3691 字
阅读时间
15 分钟

Plan-and-Solve 把任务处理明确切成两个阶段:先规划(Plan),后执行(Solve)。

先分清两件同名的事 ​

这个名词在两个层面被使用,混在一起讲会失真:

提示技巧层面的 PS Prompting本文后半讲的 PS Agent
是什么零样本 prompt 技巧——在提示里加两句话多阶段智能体架构——Planner 生成计划、Executor 逐步执行
实现载体一段提示文本两个类 + 多次 LLM 调用
出处Wang et al., 2023, arXiv:2305.04091《Hello-Agents》第四章的工程实现
解决的问题零样本 CoT 的漏步错误复杂任务中「执行到中途忘了目标」

两者共享同一个直觉(先谋后动),但一个改提示、一个改架构。下面先讲提示技巧,再讲工程实现。

同一个名词在两个层面被使用,混在一起讲会失真。

层面一:PS Prompting(提示技巧层面)
   是什么      零样本 prompt 技巧 —— 在提示里加两句话
   实现载体    一段提示文本
   解决的问题  零样本 CoT 的漏步错误
        │
        ▼
层面二:PS Agent(工程实现的用法)
   是什么      多阶段智能体架构 —— Planner 生成计划、Executor 逐步执行
   实现载体    两个类 + 多次 LLM 调用
   解决的问题  复杂任务里「执行到中途忘了目标」
        │
        └─ 两者共享同一个直觉(先谋后动),
           但一个改提示、一个改架构 —— 这是它们唯一相通的地方

提示技巧部分:PS 是对零样本 CoT 的改进 ​

Wang, L., et al. Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models. arXiv:2305.04091, 2023

Zero-shot-CoT 的三个缺陷 ​

「Let's think step by step」有三类失败:

缺陷表现
计算错误(calculation errors)中间步骤算错
漏步错误(missing-step errors)跳过了关键的中间计算步骤
语义误解错误(semantic misunderstanding)理解错了题意

PS 主要针对「漏步错误」——先要求模型把问题拆成子任务,再按计划执行,这样难以跳过步骤。

PS 与 PS+ 的提示模板 ​

PS 只是在提示里加两句话:

Q: [问题]
Let's first understand the problem and devise a plan to solve it.
Then, let's carry out the plan and solve the problem step by step.

PS+ 在此基础上再加三条指令:

Q: [问题]
Let's first understand the problem and devise a plan to solve it.
Then, let's carry out the plan to solve the problem step by step.
- Extract relevant variables and their corresponding numerals.
- Calculate intermediate results (pay attention to calculation errors).
- Check your answer for reasonableness.

三条附加指令各自对应一类错误:第一条把变量和数值显式拎出来(对抗语义误解),第二条对计算错误,第三条做合理性自检。

性能(text-davinci-003,全部零样本) ​

方法MultiArithGSM8KAddSubAQuASingleEqSVAMP平均
Zero-Shot CoT83.856.485.338.988.169.970.4
PS87.258.288.142.589.272.072.9
PS+91.859.392.246.094.775.776.7
Manual-CoT(8-shot)93.658.491.648.493.580.377.6

关键结论:PS+ 用零样本就逼近 8 个手工示例的 CoT(平均 76.7 vs 77.6),并在 MultiArith、AddSub、SingleEq 上反超。GSM8K 上也略高(59.3 vs 58.4)。

代价是:8-shot 手工示例仍然在更难的数据集上更稳(AQuA 48.4 vs 46.0、SVAMP 80.3 vs 75.7)。

错误类型分布:改进发生在本来的三个缺陷上,但只改了两个 ​

该文从 GSM8K 里抽样 100 道各方法都答错的题,人工分型:

方法计算错误漏步错误语义误解
Zero-Shot CoT7%12%27%
Zero-shot PS7%10%26%
Zero-shot PS+5%7%27%

这张表比准确率表更能说明 PS 的边界:PS+ 把计算错误压到 5%、漏步错误压到 7%(相对 Zero-Shot CoT 分别降了约 29% 和 42%),但语义理解错误一点没改善(27%)。

PS 治的是「步骤走偏」,治不了「题意理解错」。 题读错了,计划再完整也没用——这条判据决定了什么时候不该指望 PS。

配套的相关性分析印证了这一点:变量定义的存在与计划的存在,都与计算错误、漏步错误呈负相关。另外,随机抽 100 例检查,90 例的预测里确实出现了计划——说明当代模型(实验用的是 GPT-3.5/GPT-4 代)已经具备被提示出来的规划能力。

更值得看的是错误类型分布,它比准确率表更能说明 PS 的边界。

GSM8K 里抽样 100 道「各方法都答错」的题,人工分型

             零样本 CoT   零样本 PS   零样本 PS+
计算错误      7%          7%         5%     ← 相对 CoT 降了约 29%
漏步错误     12%         10%         7%     ← 相对 CoT 降了约 42%
语义误解     27%         26%        27%     ← 一点没改善

   └─ PS 治的是「步骤走偏」,治不了「题意理解错」
      题读错了,计划再完整也没用 —— 这条判据决定了什么时候不该指望 PS

配套的相关性分析印证了这一点:变量定义的存在与计划的存在,
都与计算错误、漏步错误呈负相关。
另外随机抽 100 例检查,90 例的预测里确实出现了计划 ——
说明当代模型已经具备被提示出来的规划能力。

两条使用边界 ​

  • 计划粒度是权衡:计划太粗没有收益(等于没拆),太细会增加错误面
  • 不是普适优势:在常识推理等任务上 PS+ 的增益明显变小甚至消失(不同模型上结论不一致),数学与符号推理才是它的主场

工程部分:把它做成智能体 ​

该文只改提示,03-ReAct 那种「走一步看一步」的节奏在长任务里会漂移。把 PS 落成架构,就是把规划和执行拆成两个组件。

两者的性格差异可以这么记:ReAct 像侦探,根据现场蛛丝马迹一步步推理、随时调整方向;Plan-and-Solve 像建筑师,动工前先把蓝图(Plan)画完,然后严格照图施工(Solve)。

两阶段 ​

阶段做什么
规划阶段接收用户完整问题。第一个任务是把问题分解、制定出清晰分步骤的行动计划,而不是解决问题或调工具。计划本身就是一次 LLM 调用的产物
执行阶段拿到完整计划后,严格按计划步骤逐一执行。每步可能是一次独立的 LLM 调用,也可能是对上一步结果的加工,直到所有步骤完成

形式化:

P=πplan(q)si=πsolve(q,P,(s1,…,si−1))

规划模型 πplan 由问题 q 生成含 n 步的计划 P;执行时第 i 步的解 si 同时依赖原始问题、完整计划和之前所有步骤的结果。最终答案是最后一步的结果 sn。

注意 si 的三个依赖项——原始问题一直在场,这是避免「执行到第三步忘了要干什么」的机制。

两阶段是两个组件,而状态管理是执行器的真正职责。

用户完整问题 q
   │
   ▼
┌─ 规划阶段 ────────────────────────────────────────────┐
│  π_plan(q) ──▶ P                                      │
│  第一个任务是把问题分解成清晰的分步骤计划,              │
│  不解决问题、也不调工具 —— 计划本身就是一次 LLM 调用的产物│
│  输出强制成 Python 列表,让解析从自然语言处理变成         │
│  ast.literal_eval(不用 eval:只解析字面量,不执行代码)  │
│  解析失败就返回空列表并在 run() 里终止,不带着残缺计划往下跑│
└────────────────────────┬─────────────────────────────┘
                         │ 完整计划 P
                         ▼
┌─ 执行阶段 ────────────────────────────────────────────┐
│  for i, step in enumerate(P):                         │
│      s_i = π_solve(q, P, (s_1, …, s_{i-1}))             │
│      把 s_i 追加进 history,作为下一步的输入             │
│                                                        │
│  提示词明确要求「仅输出该步骤的最终答案,不要额外解释」——   │
│  执行阶段不需要推理过程,否则 history 会被废话撑爆        │
└────────────────────────┬─────────────────────────────┘
                         │ 最终答案 = 最后一步的结果 s_n
                         ▼

s_i 的三个依赖项,每一项都在防一类失败
   q              原始问题一直在场,防止「执行到第三步忘了要干什么」
   P              完整计划,让模型知道当前步骤在整个任务里的位置
   (s_1 … s_{i-1}) 之前的步骤与结果,当前步骤的直接输入

一个实例的观察:算「三天共卖多少苹果」时,规划器输出的 4 步里
已经把答案算出来了(每步都带着结果),执行阶段只是把数值再输出一遍
(15 / 30 / 25 / 70)—— 规划模型够强时,执行更像是在做校验与格式化。

规划阶段:把计划强制成可解析的格式 ​

规划器的提示词核心是格式约束:强制输出 Python 列表,让解析从自然语言处理变成 ast.literal_eval。

python
PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。

问题: {question}

请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]

"""


解析侧:

```python
plan_str = response_text.split("```python")[1].split("```")[0].strip()
plan = ast.literal_eval(plan_str)          # 不用 eval
return plan if isinstance(plan, list) else []

两个工程点:用 ast.literal_eval 而不是 eval(只解析字面量,不执行代码);解析失败时返回空列表并在 run() 里终止流程,而不是带着残缺计划往下跑。

执行阶段:状态管理是执行器的真正职责 ​

执行器不只是「调 LLM」,它承担状态管理——记录每一步结果,作为上下文喂给后续步骤。它的提示词要包含四样东西:

内容作用
原始问题保证模型始终知道最终目标
完整计划让模型知道当前步骤在整个任务里的位置
历史步骤与结果当前步骤的直接输入
当前步骤明确现在要解决哪一个
python
for i, step in enumerate(plan):
    prompt = EXECUTOR_PROMPT_TEMPLATE.format(
        question=question, plan=plan,
        history=history if history else "无",
        current_step=step)
    response_text = self.llm_client.think(messages=[{"role": "user", "content": prompt}]) or ""
    history += f"步骤 {i+1}: {step}\n结果: {response_text}\n\n"
final_answer = response_text

提示词里明确要求「仅输出该步骤的最终答案,不要输出任何额外的解释或对话」——执行阶段不需要模型的推理过程,只需要结果,否则历史会被废话撑爆。

一个实例的观察 ​

问题:周一卖 15 个苹果,周二卖周一的两倍,周三比周二少 5 个,三天共多少?

规划器输出的计划(4 步):

python
["计算周一卖出的苹果数量: 15个",
 "计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个",
 "计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个",
 "计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个"]

值得注意的一点:计划里已经把答案算出来了(每一步都带着结果)。执行阶段只是把每步的数值单独再输出一遍(15 / 30 / 25 / 70)。这说明规划模型的能力足够时,执行阶段更像是在做校验与格式化,而不是真正的计算——这与该文的结论一致:PS 治步骤不治理解。

与 ReAct 的选择 ​

ReActPlan-and-Solve
决策节奏走一步看一步先谋后动
对环境的依赖强,每步靠 Observation 修正弱,计划在执行前已固定
目标一致性可能漂移高,计划全程在场
适合探索性、需要外部工具输入的任务逻辑路径确定、内部推理密集的任务

适合 Plan-and-Solve 的典型任务:多步数学应用题、需要整合多个信息源的报告撰写、代码生成(先构思函数/类/模块结构,再逐一实现)。

两者的差别集中在一处:计划什么时候定下来。

ReAct(走一步看一步)
   计划在每一步重新形成,靠 Observation 修正方向
   ├─ 对环境依赖强:没有外部反馈就退化成纯 CoT
   ├─ 目标一致性弱:没有东西强制它记住原始目标,可能漂移
   └─ 适合:探索性任务、需要外部工具输入的交互式任务

Plan-and-Solve(先谋后动)
   计划在执行前一次性定下来,执行阶段严格照图施工
   ├─ 对环境依赖弱:计划已固定,执行不必等外部反馈
   ├─ 目标一致性强:每一步的依赖项里都有原始问题
   └─ 适合:逻辑路径确定、内部推理密集的任务
       多步数学应用题 / 需要整合多个信息源的报告撰写 /
       代码生成(先构思函数、类、模块结构,再逐一实现)

代价是不对称的
   计划一旦定错,PS 后面每一步都在错的方向上执行;
   ReAct 在每一步都还有机会靠 Observation 纠回来。
   └─ 所以「逻辑路径确定」是选 PS 的前提,不是「任务复杂」。

相关 ​

参考 ​

贡献者 ​

文件历史 ​