Plan & Solve:先列计划,再逐步执行
有些任务失败不是因为模型做不了某一步,而是因为它回答太快。
旅游助手接到"杭州三天两夜"的请求,直接开写行程——漏掉天气、忽略步行距离、忘记打包建议。问题不是能力不够,而是没有先想清楚要做哪几件事。
Plan & Solve 加了一个最简单的暂停:先写计划,再按计划逐步执行。
一句话说清楚
Plan & Solve 把"直接回答"改成"先列步骤、再逐步完成",让多步任务有一个可检查的骨架。
解决什么问题
| 问题 | 表面看起来 | 实际风险 |
|---|---|---|
| 直接给答案 | 快 | 容易跳步骤 |
| 边想边写 | 自然 | 任务拆解过程不可见 |
| 没有计划产物 | 简洁 | 约束条件难以检查 |
旅游助手一口气写行程,看着完整,但是"下午下雨"这个约束从来没出现在它的推理里。你无法在 trace 里找到它到底想过没有。
引入什么复杂度
- 多了一次模型调用(生成计划)。
- 计划太长会变成自己的管理难题。
- 计划写完就固定了——执行过程中发现天气变了,计划不会跟着改。
和其他模式的关系
| 模式 | 谁决定下一步 | 什么时候用 |
|---|---|---|
| Prompt Chaining | Python 写死步骤 | 流程已知 |
| Plan & Solve | 模型先写计划 | 任务需要拆解,但计划不会中途失效 |
| PER | Replanner 可以改计划 | 计划可能过时 |
| LATS | 搜索控制器比较候选 | 一个计划不稳定 |
Plan & Solve 是最轻量的规划。比 Prompt Chaining 灵活(步骤由模型写),比 PER 简单(没有重规划)。
这个模式改变了什么
| 谁 | 负责什么 |
|---|---|
| Planner(模型) | 接到任务后生成步骤列表 |
| Solver(模型) | 按步骤逐个执行 |
| Python | 存计划、依次调用步骤、记 trace |
- 谁决定下一步:计划决定。Python 按顺序取下一步。
- 谁拥有状态:Python 持有计划列表和已完成的步骤输出。
- 谁能调用工具:Solver 在执行步骤时可调用工具。
- 谁给最终答案:Solver 在最后一步综合输出。
- 什么时候停止:所有步骤执行完毕。
- trace 记录什么:
plan_and_solve.plan(步骤数)、plan_and_solve.step(每步 index 和内容)、plan_and_solve.done。
走一遍真实轨迹
用户请求:"杭州三天两夜,预算 3000,不想走太多路。"
| 阶段 | 输出 | 为什么到这一步 |
|---|---|---|
| 生成计划 | ["提取偏好","查天气","设计路线","打包建议"] |
模型先列出子任务 |
| 执行步骤 1 | "偏好:预算 3000,少步行,三天两夜" | 把约束提取为结构化信息 |
| 执行步骤 2 | "第二天下午有雨" | 查到天气 |
| 执行步骤 3 | "Day1 西湖游船→灵隐寺(打车);Day2 上午茶博物馆、下午室内宋城..." | 已知下雨,安排室内 |
| 执行步骤 4 | "带伞、舒适鞋" | 基于路线给打包建议 |
| 综合 | 完整行程 + 预算明细 + 打包清单 | 所有步骤输出拼合 |
关键:步骤 3 能看到步骤 2 的天气结果——但如果计划本身没列"查天气",这条信息就不会出现。
流程图
flowchart TD
T["用户任务"] --> P["模型生成计划"]
P --> S1["执行步骤 1"]
S1 --> S2["执行步骤 2"]
S2 --> S3["执行步骤 N"]
S3 --> F["综合最终答案"]
完整实现
下面是一个完整的旅游助手 Plan & Solve 示例。用 MockLLM 模拟模型,方便离线运行。
from __future__ import annotations
from pathlib import Path
from agent_patterns_lab.patterns.plan_and_solve import plan_and_solve
from agent_patterns_lab.runtime import MockLLM, Tracer
def main() -> None:
tracer = Tracer()
# 模型会依次返回:计划 JSON、每步输出、最终综合
model = MockLLM(
[
# 第一次调用:返回计划
'{"plan":["提取用户偏好","查询杭州天气","设计三天路线","给出打包建议"]}',
# 步骤 1:提取偏好
"偏好:预算 3000 元,少步行,三天两夜,杭州。",
# 步骤 2:查天气
"杭州未来三天:Day1 晴 28°C,Day2 下午雷阵雨 25°C,Day3 多云 27°C。",
# 步骤 3:设计路线
(
"Day1:西湖游船(上午)→ 灵隐寺(下午,打车前往)→ 河坊街晚餐\n"
"Day2:中国茶叶博物馆(上午)→ 宋城室内演出(下午,避雨)→ 酒店附近晚餐\n"
"Day3:良渚博物院(上午)→ 返程"
),
# 步骤 4:打包建议
"必带:折叠伞、舒适运动鞋、充电宝。",
# 最终综合
(
"【杭州三天两夜行程】\n"
"预算:约 2800 元(住宿 1200 + 餐饮 600 + 交通 400 + 门票 600)\n"
"Day1:西湖游船 → 灵隐寺 → 河坊街\n"
"Day2:茶博物馆 → 宋城(室内,避雨)\n"
"Day3:良渚博物院 → 返程\n"
"打包:折叠伞、运动鞋、充电宝"
),
]
)
result = plan_and_solve(model, task="杭州三天两夜,预算 3000,不想走太多路。", tracer=tracer)
print("=== 计划 ===")
for i, step in enumerate(result.plan, 1):
print(f" {i}. {step}")
print("\n=== 最终答案 ===")
print(result.answer)
trace_path = tracer.export_jsonl(Path(".traces") / "50_plan_and_solve_zh.jsonl")
print(f"\n[trace] {trace_path}")
if __name__ == "__main__":
main()
运行:
PYTHONPATH=src python examples/50_plan_and_solve.py
完整运行记录
=== 计划 ===
1. 提取用户偏好
2. 查询杭州天气
3. 设计三天路线
4. 给出打包建议
=== 最终答案 ===
【杭州三天两夜行程】
预算:约 2800 元(住宿 1200 + 餐饮 600 + 交通 400 + 门票 600)
Day1:西湖游船 → 灵隐寺 → 河坊街
Day2:茶博物馆 → 宋城(室内,避雨)
Day3:良渚博物院 → 返程
打包:折叠伞、运动鞋、充电宝
[trace] .traces/50_plan_and_solve_zh.jsonl
trace 文件中可以看到:
{"event":"plan_and_solve.plan","steps":4}
{"event":"plan_and_solve.step","step_index":0,"step":"提取用户偏好"}
{"event":"plan_and_solve.step","step_index":1,"step":"查询杭州天气"}
{"event":"plan_and_solve.step","step_index":2,"step":"设计三天路线"}
{"event":"plan_and_solve.step","step_index":3,"step":"给出打包建议"}
{"event":"plan_and_solve.done"}
踩坑与诊断
| 坑 | 现象 | trace 信号 | 修法 |
|---|---|---|---|
| 计划太长 | 10+ 步骤,后面几步变成"总结""检查""再检查" | steps 数远大于预期 |
max_plan_steps 限制在 4-6 |
| 计划太模糊 | 步骤是"深入分析""仔细思考" | 步骤输出和前一步几乎一样 | system prompt 要求每步写明具体动作和产出 |
| 执行跑偏 | Solver 忽略计划,自己发挥 | 步骤输出和计划内容无关 | 每步 prompt 带上 step_id 和计划原文 |
| 计划过时 | 第二步发现暴雨,第三步还安排户外 | 最终答案包含矛盾信息 | 升级到 PER(加重规划) |
关键诊断技巧:检查每一步的 prompt 是否包含了前面步骤的输出。如果步骤 3 的 prompt 里没有步骤 2 的天气结果,问题在 _step_prompt 的拼接逻辑。
工程备忘
成本公式:
总调用 = 1(计划) + N(步骤) + 1(综合) = N + 2
4 步计划 = 6 次模型调用。每加一步,多一次调用。
生产注意事项:
max_plan_steps建议 4-6。超过 6 步的计划通常质量下降。- 计划是 JSON 结构化输出,走
structured_complete,有校验和重试。 - 步骤执行是普通
model.complete,没有结构化校验——如果你需要中间步骤也返回 JSON,需要自己加 parser。 - Plan & Solve 是一次性计划。如果你在生产中发现"计划写完就不对了"的频率超过 20%,不要在这个模式上修补,直接换 PER。
读完以后
Plan & Solve 是最轻量的规划模式。
- 如果执行过程会让计划失效,读 PER。
- 如果需要在多个计划之间搜索,读 LATS。
- 如果计划里的步骤之间有依赖和并行关系,读 LLM Compiler。