当 ReAct 在多步骤推理中频频偏航,一种更冷静的范式出现了------先把路看清楚,再一步一步走。
前言:ReAct 的另一种选择
在 上一篇博客 中,我们从零构建了一个 ReAct 智能体,亲身体验了它"思考 → 行动 → 观察"的核心循环。ReAct 让 LLM 边想边做、动态调整,很多场景下确实够用。
但问题也随之而来:当任务需要超过三步的连续推理 时,ReAct 容易"偏航"。它不是不够聪明,而是缺乏全局规划------每一步只盯着眼前的观察结果做决策,走着走着就忘了最初的目标。
Plan-and-Solve 正是为解决这个痛点而生的。它的思路朴素而有效:把"想"和"做"彻底分开,先在纸上画好路线图,再埋头执行。
一、Plan-and-Solve 的本质:分阶段推理
顾名思义,Plan-and-Solve 将任务处理明确地分为两个独立的阶段:
PlanAndSolve.png
Planning Phase(规划阶段) :智能体接收用户的完整问题后,第一件事不是去解决问题,也不是调用工具,而是将问题分解成一个清晰、分步骤的行动计划。这个计划本身就是一次独立的 LLM 调用。
Solving Phase(执行阶段) :获得完整计划后,智能体严格按计划中的步骤逐一执行。每一步都是一次独立的推理调用,上一步的输出作为下一步的上下文输入,直到所有步骤完成,得出最终答案。
这种"先谋后动"的策略,让智能体在处理需要长远规划的复杂任务时,能够始终保持较高的目标一致性。
二、什么样的任务适合它?
不是所有问题都需要 Plan-and-Solve。它真正发光的地方在于那些结构性强的、可以被清晰分解的复杂任务:
| 场景 | 为什么适合 |
|---|---|
| 多步数学应用题 | 需要先列出计算步骤,再按序求解 |
| 多源信息整合与报告撰写 | 需先规划结构(引言 → 数据A → 数据B → 总结),再逐一填充 |
| 复杂代码生成 | 需先构思函数/类/模块的骨架,再逐一实现细节 |
本质上,只要你在动笔之前需要"先列个大纲",那就是 Plan-and-Solve 的地盘。
三、动手实现:从规划器到完整智能体
下面我们直接用代码来走一遍完整的实现。思路很清晰:先造一个能制定计划的 Planner ,再造一个能执行计划的 Executor ,最后用一个 PlanSolveAgent 把它们串起来。
3.1 Planner:让 LLM 输出结构化计划
Planner 的核心挑战不是"让模型分解问题"------这本身就是 LLM 擅长的事。真正的工程难点在于:怎么让模型输出的计划,能被代码稳定地解析和消费?
如果让模型输出自然语言描述的计划,你就得面对各种不规则的文本格式和解析容错。更好的做法是:在提示词中强制约束输出格式。
ini
PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。
问题: {question}
请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
"""
这个提示词有几个设计要点值得注意:
- 角色设定:"顶级 AI 规划专家"------用角色标签激活模型的相关能力区域
- 格式约束:要求输出 Python 列表,代码块前后缀固定------这让后续解析变得简单可靠
- 语义约束:强调"独立的、可执行的"------防止模型输出模糊或嵌套的步骤描述
接下来把提示词逻辑封装成一个 Planner 类:
python
import ast
class Planner:
def __init__(self, llm_client: AgentLLM):
self.llm_client = llm_client
def plan(self, question: str) -> list[str]:
"""制定计划"""
prompt = PLANNER_PROMPT_TEMPLATE.format(question=question)
messages = [{"role": "user", "content": prompt}]
print("------------正在生成计划------------")
response_text = self.llm_client.think(messages)
print(f"计划已生成: \n{response_text}")
try:
# 提取```python和```之间的内容
plan_str = response_text.split("```python")[1].split("```")[0].strip()
# 用ast.literal_eval安全解析字符串为Python列表
plan = ast.literal_eval(plan_str)
return plan if isinstance(plan, list) else []
except (ValueError, SyntaxError, IndexError) as e:
print(f"解析计划时出错: {e}")
return []
这里选择 ast.literal_eval 而不是 eval(),是出于安全性考虑------literal_eval 只解析字面量表达式(字符串、数字、列表、字典等),不会执行任意代码。在生产环境中这是一个重要的工程细节。
3.2 Executor:状态管理是真正的核心
规划器把蓝图画好了,但执行器要做的远比"逐个调用 LLM"复杂。它承担着一个至关重要的职责:状态管理。
每一步的结果必须被记录,并作为上下文传递给后续步骤。如果缺少这个机制,步骤 3 就不知道步骤 1 和步骤 2 做了什么,整个计划链条就会断裂。
执行器的提示词设计也值得仔细看:
ini
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。
# 原始问题:
{question}
# 完整计划:
{plan}
# 历史步骤与结果:
{history}
# 当前步骤:
{current_step}
请仅输出针对"当前步骤"的回答:
"""
这个提示词传递了四个关键信息给模型:
| 信息 | 作用 |
|---|---|
| 原始问题 | 确保模型始终牢记最终目标 |
| 完整计划 | 让模型了解当前步骤在整个任务中的位置 |
| 历史步骤与结果 | 将已完成的工作作为当前步骤的直接输入 |
| 当前步骤 | 明确指示现在需要解决的具体子任务 |
其中历史步骤与结果正是状态管理的载体------它是整个执行链条中信息流动的通道:
python
class Executor:
def __init__(self, llm_client: AgentLLM):
self.llm_client = llm_client
def execute(self, question: str, plan: list[str]) -> str:
"""执行计划"""
history = []
print("------------正在执行计划------------")
for i, step in enumerate(plan):
print(f"\n-> 正在执行步骤 {i+1}/{len(plan)}: {step}")
prompt = EXECUTOR_PROMPT_TEMPLATE.format(
question=question,
plan=plan,
history=history if history else "无",
current_step=step
)
messages = [{"role": "user", "content": prompt}]
response_text = self.llm_client.think(messages) or ""
# 将当前步骤的结果追加到历史记录中
history += f"步骤 {i+1}: {step} \n结果-> {response_text}\n\n"
print(f"\n✅步骤 {i+1} 已完成,执行结果: {response_text}")
# 最后一个步骤的结果就是最终答案
return response_text
history 列表就是状态管理的核心数据结构。它从空开始,每完成一步就追加一条记录,下一步的 prompt 中会包含完整的 history,形成一条不断增长的信息链。
3.3 PlanSolveAgent:把一切串联起来
最后一步,将 Planner 和 Executor 组合成一个统一的智能体:
python
class PlanSolveAgent:
def __init__(self, llm_client: AgentLLM):
self.planner = Planner(llm_client)
self.executor = Executor(llm_client)
def run(self, question: str):
"""运行智能体,规划并解决问题"""
print(f"使用规划智能体处理问题:\n {question}")
# 第一步:制定计划
plan = self.planner.plan(question)
if not plan:
print("计划为空,无法解决问题。")
return
print(f"计划已生成: {plan}\n开始执行计划\n")
# 第二步:严格执行计划
final_answer = self.executor.execute(question, plan)
print(f"👍🏻任务完成,最终答案: {final_answer}")
return final_answer
整个智能体的架构可以用一句话概括:Planner 负责"想清楚",Executor 负责"做到底" 。职责边界清晰,各自独立可测试。
四、跑一个实际案例
理论讲完了,来看一个真实运行的例子。我们用一道典型的多步数学应用题来测试:
"一个水果店周一卖出了 15 个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了 5 个。请问这三天总共卖出了多少个苹果?"
运行代码:
ini
if __name__ == '__main__':
llm = AgentLLM()
agent = PlanSolveAgent(llm_client=llm)
question = "一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?"
agent.run(question)
输出日志清晰地展示了整个两阶段流程:
makefile
使用规划智能体处理问题:
一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?
------------正在生成计划------------
🧠调用模型 Qwen/Qwen3.5-35B-A3B 生成回复...
模型响应成功
计划已生成:
["计算周二卖出的苹果数量(周一数量×2)",
"计算周三卖出的苹果数量(周二数量-5)",
"将三天卖出的苹果数量相加得出总数"]
开始执行计划
------------正在执行计划------------
-> 正在执行步骤 1/3: 计算周二卖出的苹果数量(周一数量×2)
🧠调用模型 Qwen/Qwen3.5-35B-A3B 生成回复...
模型响应成功: 30
✅步骤 1 已完成,执行结果: 30
-> 正在执行步骤 2/3: 计算周三卖出的苹果数量(周二数量-5)
🧠调用模型 Qwen/Qwen3.5-35B-A3B 生成回复...
模型响应成功: 25
✅步骤 2 已完成,执行结果: 25
-> 正在执行步骤 3/3: 将三天卖出的苹果数量相加得出总数
🧠调用模型 Qwen/Qwen3.5-35B-A3B 生成回复...
模型响应成功: 70
✅步骤 3 已完成,执行结果: 70
👍🏻任务完成,最终答案: 70
从这个日志中可以清楚地看到 Plan-and-Solve 的三个关键优势:
- 计划可视化 :在执行任何计算之前,智能体已经将问题分解为
[步骤1, 步骤2, 步骤3],整个推理路径一目了然 - 信息有序传递:步骤 1 算出 30 传给步骤 2,步骤 2 算出 25 传给步骤 3,上下文不丢失
- 最终结果可追溯:如果答案有误,你可以定位到具体是哪一步出了问题,而不是在一个黑箱中猜测
五、与 ReAct 的对比:什么时候用哪个?
现在回头来看,Plan-and-Solve 和 ReAct 并不是"谁更好"的问题,而是"谁更适合当前任务"的问题。
| 维度 | ReAct | Plan-and-Solve |
|---|---|---|
| 核心策略 | 边想边做,动态调整 | 先规划,后执行 |
| 决策粒度 | 每一步基于当前观察做下一个决策 | 全盘规划好之后再行动 |
| 优势场景 | 需要外部工具查询、信息不确定、需灵活调整的任务 | 步骤可预见的、结构化的多步推理任务 |
| 劣势场景 | 步骤超过 3 步时容易偏离目标 | 需要中途根据中间结果调整方向时显得僵化 |
| 扩展性 | 容易加入新工具,环内决策 | 工具调用需在计划中预设 |
一个简单的判断法则:如果"解题步骤"可以事先想清楚,用 Plan-and-Solve;如果需要"边走边看",用 ReAct。
当然,这两者也并非互斥。在实际工程中,更常见的做法是将它们组合------先 Plan 出大方向,再在每一步中用 ReAct 风格的 Tool-Use 去灵活执行。这正是后续更复杂的 Agent 架构(如 Reflexion、AutoGPT 风格)的构建基础。
六、总结与实践建议
Plan-and-Solve 的精髓用一句话概括:把 LLM 的推理能力限定在一个清晰的框架内,用结构化的计划来对抗推理漂移。
如果你准备在自己的项目中落地这个范式,几点实践建议:
- 提示词设计决定解析稳定性:让模型输出 JSON 或 Python 字面量,而不是自由文本。这能省去大量脆弱的文本解析代码
- 状态管理不要偷懒:每一步的历史结果必须完整传递,不要只传上一步的结果------后续步骤可能需要更靠前的中间数据
- 做好计划为空的防御:模型可能输出格式错误导致解析失败,要有 fallback 逻辑
- 考虑 Plan + ReAct 的组合:大多数实际场景下,纯 Plan-and-Solve 的灵活性不够,值得在计划每一步嵌入工具调用能力
理解了 Plan-and-Solve,你就掌握了智能体架构设计的第二条核心思路。从 ReAct 到 Plan-and-Solve,再到后续的 Reflection,每一种范式都在解决一类特定的推理失败模式。把握住这个脉络,你就不再是框架的使用者,而是能根据问题特征自主设计智能体架构的创造者。