Plan-and-Solve范式-先规划再执行的Agent设计
本章深入讲解 Plan-and-Solve 范式的设计思想、完整代码实现,以及与 ReAct 的适用场景对比。
Plan-and-Solve 范式:先规划再执行的 Agent 设计
本章深入讲解 Plan-and-Solve 范式的设计思想、完整代码实现,以及与 ReAct 的适用场景对比。
1.1 问题背景:ReAct 的局限
Plan-and-Solve 两阶段架构 — Planner 生成完整步骤列表,Executor 逐步忠实执行
ReAct 是"走一步看一步"的范式。它在执行时不知道自己要走多少步,每次思考都是局部视角——拿到工具结果,再想下一步怎么办。这种方式在面对开放性的信息检索任务时很有效,因为搜索会返回什么内容是未知的,需要灵活调整。
但是遇到结构清晰的多步骤任务,ReAct 会出问题。任务越复杂,中间步骤越多,它越容易在某个子问题上钻牛角尖,忘记了整体目标。典型的例子是:让 ReAct Agent 处理一道有七个步骤的数学应用题,它可能在第三步算出一个中间结果之后,开始反复验证那个数字,把后面四步全忘了。
Plan-and-Solve 解决的就是这个问题。先把任务完整拆解成步骤列表,再按顺序逐步执行。 执行阶段不再做决策,只是忠实地跑完计划。
1.2 两阶段架构
两个阶段的分工很清晰。
Planner 负责全局视角。它读入问题,输出一个完整的步骤列表。这一步要求 LLM 在动手之前想清楚全程,类似于项目启动前先写工作计划。
Executor 负责单步执行。它每次只处理一个步骤,但能看到之前所有步骤的执行结果,用于当前步骤的推理。上下文不断积累,最后一步拿到完整的历史,输出最终答案。
1.3 完整代码实现
用一道多步数学应用题来演示:
小明周一买了 15 个苹果,周二又买了周一数量的 2 倍,周三吃掉了 5 个。请问小明现在一共有多少个苹果?并计算平均每天新增(不含周三)多少个苹果?
import ast
import re
from openai import OpenAI
client = OpenAI()
# ============================================================
# Planner:接收问题,输出 Python list 格式的步骤列表
# ============================================================
class Planner:
def __init__(self):
self.system_prompt = """你是一个任务规划专家。
将用户的问题拆解成清晰的执行步骤,以 Python list 格式输出。
要求:
1. 每个步骤是一个字符串,描述具体要做什么
2. 步骤要足够细,每步只做一件事
3. 最后一步必须是"汇总前面所有结果,给出最终答案"
4. 只输出 Python list,不要有任何其他文字
输出示例:
["步骤1:计算...", "步骤2:根据步骤1的结果计算...", "步骤3:汇总前面所有结果,给出最终答案"]"""
def plan(self, question: str) -> list[str]:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": question},
],
temperature=0,
)
raw_output = response.choices[0].message.content.strip()
# 用 ast.literal_eval 安全解析,避免 eval 执行任意代码
try:
steps = ast.literal_eval(raw_output)
if isinstance(steps, list) and all(isinstance(s, str) for s in steps):
return steps
except (ValueError, SyntaxError):
pass
# 解析失败:尝试用正则提取列表内容
items = re.findall(r'"([^"]+)"', raw_output)
if items:
return items
# 兜底:把原始输出作为单步计划
return [raw_output, "汇总前面所有结果,给出最终答案"]
# ============================================================
# Executor:逐步执行,维护 history 字符串积累上下文
# ============================================================
class Executor:
def __init__(self):
self.system_prompt = """你是一个严谨的执行者。
你会收到:原始问题、完整计划、当前要执行的步骤、以及之前步骤的执行结果。
只执行当前步骤,给出该步骤的结果,不要超前执行其他步骤。
输出要简洁,只写计算过程和结果,不要废话。"""
def execute_step(
self,
question: str,
full_plan: list[str],
current_step: str,
step_index: int,
history: str,
) -> str:
plan_text = "\n".join(f"{i+1}. {s}" for i, s in enumerate(full_plan))
user_content = f"""原始问题:{question}
完整计划:
{plan_text}
之前步骤的执行结果:
{history if history else "(这是第一步,暂无历史结果)"}
当前执行步骤(第 {step_index + 1} 步):{current_step}
请执行当前步骤,给出结果:"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_content},
],
temperature=0,
)
return response.choices[0].message.content.strip()
# ============================================================
# PlanAndSolveAgent:组合 Planner 和 Executor
# ============================================================
class PlanAndSolveAgent:
def __init__(self):
self.planner = Planner()
self.executor = Executor()
def run(self, question: str) -> str:
print(f"\n问题:{question}")
print("=" * 60)
# 规划阶段
print("\n[规划阶段]")
steps = self.planner.plan(question)
for i, step in enumerate(steps):
print(f" {i + 1}. {step}")
# 执行阶段
print("\n[执行阶段]")
history = ""
for i, step in enumerate(steps):
print(f"\n步骤 {i + 1}:{step}")
result = self.executor.execute_step(
question=question,
full_plan=steps,
current_step=step,
step_index=i,
history=history,
)
print(f"结果:{result}")
# 追加到历史,供后续步骤使用
history += f"\n步骤 {i + 1}({step})的结果:\n{result}\n"
# 最后一步的结果就是最终答案
return result
# ============================================================
# 测试
# ============================================================
if __name__ == "__main__":
agent = PlanAndSolveAgent()
question = (
"小明周一买了 15 个苹果,周二又买了周一数量的 2 倍,"
"周三吃掉了 5 个。请问小明现在一共有多少个苹果?"
"并计算周一和周二平均每天新增多少个苹果?"
)
answer = agent.run(question)
print(f"\n最终答案:{answer}")
运行后的输出大概是这样:
问题:小明周一买了 15 个苹果...
============================================================
[规划阶段]
1. 计算周一购买的苹果数量
2. 计算周二购买的苹果数量(周一的2倍)
3. 计算周三之后剩余的苹果数量(扣除吃掉的5个)
4. 计算周一和周二平均每天新增苹果数
5. 汇总前面所有结果,给出最终答案
[执行阶段]
步骤 1:计算周一购买的苹果数量
结果:周一购买 15 个苹果。
步骤 2:计算周二购买的苹果数量(周一的2倍)
结果:周二购买 15 × 2 = 30 个苹果。
步骤 3:计算周三之后剩余的苹果数量
结果:总计 15 + 30 = 45 个,吃掉 5 个后剩余 40 个苹果。
步骤 4:计算周一和周二平均每天新增苹果数
结果:周一新增 15 个,周二新增 30 个,平均 (15 + 30) / 2 = 22.5 个。
步骤 5:汇总所有结果,给出最终答案
结果:小明现在共有 40 个苹果。周一和周二平均每天新增 22.5 个苹果。
1.4 实现细节
ast.literal_eval 而不是 eval。Planner 的输出是一段 Python list 的文本,如果用 eval 直接执行,LLM 可以在里面注入任意代码。ast.literal_eval 只解析字面量(字符串、数字、列表、字典),不会执行函数调用,是安全的选择。
history 字符串追加而不是追加 messages。每次执行步骤时,把之前所有结果拼成一段文本传入,而不是维护一个长 messages 列表。好处是结构清晰,Executor 的每次调用都是独立的,方便并行优化;坏处是 history 太长时 prompt 会很大,token 成本高。实际项目里可以对 history 做摘要压缩。
规划失败的容错。Planner 输出的格式可能不标准,所以有两层容错:先 ast.literal_eval,失败了用正则提取引号内的文本,都不行就把原始输出当成单步计划。不要在这里让整个任务直接崩掉。
1.5 和 ReAct 的对比
选哪种范式,本质上是看任务的结构是否确定。
ReAct 适合:需要实时获取外部信息、任务路径不确定、需要边做边决策的场景。比如让 Agent 去查一个你自己也不知道答案在哪的问题,搜索结果会影响下一步搜什么。
Plan-and-Solve 适合:任务结构清晰、步骤可以提前确定、主要靠推理而非工具调用的场景。多步数学题、结构化报告、已知流程的分析任务。
| 维度 | ReAct | Plan-and-Solve |
|---|---|---|
| 决策时机 | 每步都决策 | 只在规划阶段决策 |
| 全局视野 | 弱,容易迷失 | 强,始终有计划 |
| 适应意外 | 强,可实时调整 | 弱,计划一旦定了很难改 |
| 适合任务类型 | 信息检索、探索性 | 多步推理、结构化 |
| 实现复杂度 | 中 | 略高(两阶段) |
两者没有优劣之分,只有适不适合。
1.6 Plan-and-Solve 的局限
这个范式最大的问题是计划是静态的。
Planner 在拿到问题后,基于已有信息制定计划。但执行过程中可能发现:某个步骤的前提假设是错的,或者某步的结果让后续步骤变得没意义。这时 Executor 没有权力修改计划,只能硬着头皮按计划走,最终输出一个基于错误前提的答案。
更大的问题是,如果 Planner 本身规划有误,后面的 Executor 再努力也是白费。Planner 的质量决定了整个任务的上限。
针对这个局限,有一个改进方向是动态重规划(Dynamic Re-planning):在执行过程中,每完成一个步骤就检查一次"计划还需要调整吗"。如果需要,重新调用 Planner 更新剩余步骤。这本质上是把 Plan-and-Solve 和 ReAct 融合了——宏观上有计划,微观上可以灵活调整。LangGraph(LangChain 旗下的 Agent 工作流框架,用图结构管理 Agent 的状态和决策流程)的一些高级用法就是走这个路子的。
对于大多数结构确定的任务,静态计划已经足够好用。不要为了加复杂度而加复杂度,能解决问题的方案就是好方案。
选范式要看任务特点:一道数学题,Plan-and-Solve 比 ReAct 稳;一个开放性研究任务,ReAct 比 Plan-and-Solve 灵活。理解两者的边界,才能在对的场景用对的工具。