Plan-and-Solve 翻译过来是规划和执行,他的执行逻辑顾名思义,先规划(Plan)再执行(Solve)。
ReAct 与 Plan-and-Solve 的区别
在了解 Plan-and-Solve 之前,我们经常会对比另一种经典的 Agent 范式:ReAct (Reason + Act)。它们在处理复杂任务时的核心逻辑有显著的区别:
执行模式不同:
- ReAct 采用的是“交替式”的思考与行动(Interleaved Reasoning and Acting)。它会在每走一步之前先进行思考(Thought),然后执行一个动作(Act),观察结果(Observation)后,再决定下一步怎么走。走一步看一步。
- Plan-and-Solve 则是将“规划”和“执行”完全解耦。它会在一开始统揽全局,直接将复杂问题拆解为一个包含多个子步骤的完整执行计划,然后再按照计划列表逐一去解决(Solve)这些子问题。
长期任务的表现(长程推理):
- ReAct 在面对需要很多步骤的长流程任务时,非常容易“迷失方向”。因为每次动作只聚焦当前状态,缺乏全局视野,容易导致错误累积、陷入重复调用同一个工具的“死循环”,或者遗忘最初的目标。
- Plan-and-Solve 通过提前制定全局计划,大大提升了长程任务的稳定性和成功率。它能更好地掌控任务进度,清楚地知道当前处在整个计划的哪一个环节。
灵活性与容错:
- ReAct 的灵活性极高,每一次行动都会根据最新的真实环境反馈来调整,非常适合高度动态和需要探索的信息收集任务。
- Plan-and-Solve 初始计划的灵活性相对固化。如果在第一步执行时就遇到了突发情况导致环境改变,后续排好的计划可能就不再适用了。因此,高级的 Plan-and-Solve 架构通常还需要引入**重新规划(Re-plan)**机制来弥补这一缺陷。
简单来说,ReAct 就像是一个“边走边看、摸着石头过河”的探险家,而 Plan-and-Solve 则像是一个“先画好施工图纸,再按图搬砖”的工程师。
Plan-and-Solve 的工作原理
简单来说,Plan-and-Solve 会把整个流程分成两部分:
- 规划阶段 (Planning Phase):第一部分并不会着急去解决问题或调用工具,而是将问题分解,并制定一个清晰、分步骤的计划。这个计划本身就是一次大语言模型的调用产物。
- 执行阶段 (Solving Phase):在获得完整的计划后,智能体进入执行阶段。它会严格按照计划中的步骤,逐一执行。每一步的执行都可能是一次独立的 LLM 调用,或者是对上一步结果的加工处理,直到计划中的所有步骤都完成,最终得出答案。
我们可以将这个两阶段过程进行形式化表达。首先,规划模型 根据原始问题 生成一个包含 个任务的计划 :
随后,在执行阶段,执行模型 会逐一完成计划中的步骤。对于第 个步骤,其解决方案 的生成会同时依赖于原始问题 、完整计划 以及之前所有步骤的执行结果 :
最终的答案就是最后一个步骤的执行结果 。
Plan-and-Solve 尤其适用于那些结构性强、可以被清晰分解的复杂任务,例如:
- 多步数学应用题:需要先列出计算步骤,再逐一求解。
- 需要整合多个信息源的报告撰写:需要先规划好报告结构(引言、数据来源A、数据来源B、总结),再逐一填充内容。
- 代码生成任务:需要先构思好函数、类和模块的结构,再逐一实现。
编码实现
为了彰显范式的特性,这次我们不使用现成的高级工具库,而是通过提示词的设计与代码封装,从零完成一个简单的推理任务。
目标问题如下:
“一个水果店周一卖出了 15 个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了 5 个。请问这三天总共卖出了多少个苹果?”
这种连环数学题无法通过单次大模型查询得出准确实时的计算结果,必须先将问题分解为逻辑连贯的子步骤,然后再按顺序求解。这正是检验 Plan-and-Solve “先规划,后执行” 核心能力的绝佳场景。
1. 规划阶段 (Planner)
提示词部分,规划阶段和执行阶段需要两段不同的 Prompt。在规划阶段,我们需要明确告诉模型:只做规划拆解,不要进行计算和执行。
1 2 3 4 5 6 7 8 9 10 11 12 13
| PLANNER_PROMPT_TEMPLATE = """ 你是一个顶级的 AI 规划专家。你的任务是将用户提出的复杂问题,分解成一个由多个简单步骤组成的行动计划。 请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。 你的输出必须是一个 Python 列表,其中每个元素都是一个描述子任务的字符串。
问题: {question}
请严格按照以下格式输出你的计划,```python 与 ``` 作为前后缀是必要的: ```python ["步骤1", "步骤2", "步骤3", ...] ``` """
|
这个提示词通过以下几点确保了输出的质量和稳定性:
- 角色设定:定义为“顶级的 AI 规划专家”,激发大模型的专业推理能力。
- 任务描述:清晰定义了“仅分解问题”的边界目标。
- 格式约束:强制要求输出为
Python 列表格式的字符串,这极大地简化了我们在代码中的解析工作,使其比解析自然语言更稳定可靠。
接下来,我们将这个逻辑封装成一个 Planner 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import ast
from system_prompt import PLANNER_PROMPT_TEMPLATE
class Planner: def __init__(self, llm_client): 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=messages) or "" print(f"✅ 计划已生成:\n{response_text}") try: plan_str = response_text.split("```python")[1].split("```")[0].strip() plan = ast.literal_eval(plan_str) return plan if isinstance(plan, list) else [] except (ValueError, SyntaxError, IndexError) as e: print(f"❌ 解析计划时出错: {e}\n原始响应: {response_text}") return [] except Exception as e: print(f"❌ 解析计划时发生未知错误: {e}") return []
|
2. 执行器与状态管理 (Executor)
规划器勾勒好清晰的蓝图后,我们需要一个 Executor (执行器) 逐一完成计划中的分解任务。执行器不仅负责调用大语言模型解决子问题,还要承担状态管理的核心职责:它必须记录每一步的具体计算结果,并将其作为上下文“击鼓传花”提供给后续步骤。
执行器的提示词需要包含以下关键信息:
- 原始问题:确保模型不偏离最终目标。
- 完整计划:让模型了解当前步骤的全局站位。
- 历史步骤与结果:也就是状态,作为当前步骤的先决已知条件。
- 当前步骤:明确当下的具体微小任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| EXECUTOR_PROMPT_TEMPLATE = """ 你是一位顶级的 AI 执行专家。你的任务是严格按照给定的计划,一步步地解决问题。 你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。 请你专注于解决“当前步骤”,并仅输出针对该步骤的最终答案,不要输出任何额外的解释或对话。
# 原始问题: {question}
# 完整计划: {plan}
# 历史步骤与结果(已知条件): {history}
# 当前步骤: {current_step}
请仅输出针对“当前步骤”的精简回答: """
|
在 Executor 类的实现中,我们通过一个循环来驱动执行,并利用 history 变量维护执行流转的上文状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from system_prompt import EXECUTOR_PROMPT_TEMPLATE
class Executor: def __init__(self, llm_client): self.llm_client = llm_client
def execute(self, question: str, plan: list[str]) -> str: """根据计划,逐步执行并汇总解决问题""" history = "" final_answer = "" print("\n--- 正在执行计划 ---") 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=messages) or "" history += f"步骤 {i+1}: {step}\n结果: {response_text}\n\n" print(f"✅ 步骤 {i+1} 已完成,结果: {response_text}") final_answer = response_text
return final_answer
|
3. 编排合并 (PlanAndSolveAgent)
最后,我们需要一个智能体调度器将 Planner 和 Executor 进行整合并对外暴露能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class PlanAndSolveAgent: def __init__(self, llm_client): """初始化智能体,挂载规划器与执行器实例""" self.llm_client = llm_client self.planner = Planner(self.llm_client) self.executor = Executor(self.llm_client)
def run(self, question: str): """运行智能体的完整生命周期:先规划 -> 后执行""" print(f"\n--- 开始处理问题 ---\n问题: {question}") plan = self.planner.plan(question) if not plan: print("\n--- 任务终止 --- \n无法生成有效的行动计划。") return
final_answer = self.executor.execute(question, plan) print(f"\n--- 任务完成 ---\n🎉 最终答案: {final_answer}")
|
该部分设计极好地体现了**“组合优于继承”**的架构原则。Agent 作为一个协调者 (Orchestrator),自身没有冗余的复杂逻辑,通过流水线一样调用各个明确分工的组件,使得代码变得极具可维护性。
4. 运行实例
将这套代码运行起来后,我们可以从终端日志一窥模型思考的全过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| --- 开始处理问题 --- 问题: 一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?
--- 正在生成计划 --- 🧠 正在调用大语言模型... ✅ 计划已生成: ```python ["计算周二卖出的苹果数量:周一卖出的15个苹果乘以2", "计算周三卖出的苹果数量:周二卖出的数量减去5个", "计算三天卖出的苹果总数:将周一、周二、周三卖出的数量相加"] ```
--- 正在执行计划 ---
-> 正在执行步骤 1/3: 计算周二卖出的苹果数量:周一卖出的15个苹果乘以2 🧠 正在调用大语言模型... ✅ 步骤 1 已完成,结果: 30
-> 正在执行步骤 2/3: 计算周三卖出的苹果数量:周二卖出的数量减去5个 🧠 正在调用大语言模型... ✅ 步骤 2 已完成,结果: 30 - 5 = 25
-> 正在执行步骤 3/3: 计算三天卖出的苹果总数:将周一、周二、周三卖出的数量相加 🧠 正在调用大语言模型... ✅ 步骤 3 已完成,结果: 15 + 30 + 25 = 70
--- 任务完成 --- 🎉 最终答案: 70
|
完整代码及输出可以查看 Plan-and-Solve 示例代码
至此,一个从设计到实现的标准 Plan-and-Solve 范式智能体便跃然纸上。相比起 ReAct 范式的见机行事,Plan-and-Solve 确实更加稳健,更像是“运筹帷幄之中,决胜千里之外”。