从原理到代码,拆解Plan-and-Solve智能体设计模式

(一)引言

上一节我们介绍了ReAct范式的智能体,其能通过构建 Thought-Action-Observation 的闭环机制走一步看一步,将推理和行动结合起来,这种范式的明显特点就是根据每一步从外界获得的 Observation 来不断调整 Thought Action,但是同时这种步进式的决策也存在着只顾眼前而忽略全局,从而陷入局部最优的局限性,为此,本文探讨一种面向全局的智能体范式------ Plan-and-Action

顾名思义,这种范式将任务处理明确分为两个阶段:先规划 (Plan)后执行(Solve)。

(二)Plan-and-Solve的工作原理

Plan-and-Solve架构范式的核心逻辑源于2023年发表的Plan-and-Solve Prompting论文,其设计灵感贴合人类解决复杂问题的直觉------先制定完整计划,再按步骤有序执行,最终整合结果形成解决方案。这种"先谋而后动"的范式的两个阶段如下图所示:

  • 规划阶段:智能体首先接收用户的完整问题,然后将问题分解,并制定出一个清晰、分步骤的行动计划。这个计划本身的制定就是调用LLM的过程。
  • 执行阶段:在获得完整的计划后,智能体进入执行阶段,按照计划中的步骤逐一执行任务,最终得出答案。

我们可以将这个过程用公式展示出来:

首先,在规划阶段,规划模型 <math xmlns="http://www.w3.org/1998/Math/MathML"> π p l a n π_{plan} </math>πplan 根据原始问题 <math xmlns="http://www.w3.org/1998/Math/MathML"> q q </math>q 生成一个包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个步骤的计划 <math xmlns="http://www.w3.org/1998/Math/MathML"> P = ( p 1 , p 2 , p 3 , . . . , p n ) P = (p_1, p_2, p_3,...,p_n) </math>P=(p1,p2,p3,...,pn) :

<math xmlns="http://www.w3.org/1998/Math/MathML"> P = π p l a n ( q ) P = π_{plan}(q) </math>P=πplan(q)

随后,在执行阶段,执行模型 <math xmlns="http://www.w3.org/1998/Math/MathML"> π s o l v e π_{solve} </math>πsolve 会逐一完成计划中的步骤。对于第 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i 个步骤,其解决方案 <math xmlns="http://www.w3.org/1998/Math/MathML"> S i S_i </math>Si 的生成会同时依赖于原始问题 <math xmlns="http://www.w3.org/1998/Math/MathML"> q q </math>q 、完整计划 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P 以及之前所有步骤的执行结果 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( S 1 , S 2 , S 3 , . . . , S i − 1 ) (S_1, S_2, S_3, ..., S_{i-1}) </math>(S1,S2,S3,...,Si−1) :

<math xmlns="http://www.w3.org/1998/Math/MathML"> S i = π s o l v e ( q , P , ( S 1 , S 2 , S 3 , . . . , S i − 1 ) ) S_i = π_{solve}(q, P, (S_1, S_2, S_3, ..., S_{i-1})) </math>Si=πsolve(q,P,(S1,S2,S3,...,Si−1))

最终的答案就是最后一个步骤的执行结果 <math xmlns="http://www.w3.org/1998/Math/MathML"> S n S_n </math>Sn 。

(三)Plan-and-Solve的工程实现

我们将从需求开始分析,通过环境准备和构建代码再到最后的测试从零手撕一个 Plan-and-Solve 范式的智能体。

1. 需求分析

为了凸显 Plan-and-Solve 范式在结构化推理任务上的优势,我们将不使用工具,而是通过提示词的设计完成一个推理任务。

这类任务的特点是,答案无法通过单次查询或者计算得出,必须先将问题分解为一系列逻辑连贯的子步骤,然后按顺序求解。

我们的目标问题是:"一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?"

2. Plan-and-Solve编码实现

虽然上述问题对于大模型而言并不算困难,但是其中包含了一个先规划(先分别求解再加和计算)后执行(分别求解每天)的逻辑链条。在后续面对某些复杂的逻辑难题时,如果智能体不能给出高质量的推理答案,可以尝试这个参考这个模式来设计。

(1)规划阶段

规划阶段的目标是让我们的LLM能够接收原始问题,并输出一个清晰、分步骤的行动计划。

(i)规划阶段提示词设计

提示词应该明确地告诉模型它的角色和任务,并给出一个输出格式的实例。

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

问题: {question}

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

以上提示词主要设置了以下内容:

  • 角色设定:"顶级的AI规划专家"
  • 任务描述:将复杂问题分解为一个由多个简单步骤组成的行动计划
  • 格式约束:强制要求输出为python列表,且每个元素均为描述子任务的字符串。
(ii)规划阶段Planner类封装
python 复制代码
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}")
		
		# 解析LLM输出的列表字符串
		try:
			# 找到 ```python and ```之间的内容
			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}")
			print(f"原始响应: {response_text}")
			return []
		except Exception as e:
			print(f"❌ 解析计划时发生未知错误: {e}")
			return []

(2)执行阶段

在规划器(Planner)生成了清晰地行动蓝图后,我们就需要一个执行器(Executor)来逐一完成计划中的任务。执行器不仅负责调用LLM来解决每个子问题,还承担着状态管理的角色:记录每一步的执行结果,并将其作为上下文提供给后续步骤,确保信息在整个任务链条中顺畅流动。

(i)执行阶段提示词设计

执行器的提示词的目标是在已有上下文的基础上,专注解决当前这一个步骤:

python 复制代码
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。

# 原始问题:
{question}

# 完整计划:
{plan}

# 历史步骤与结果:
{history}

# 当前步骤:
{current_step}

请仅输出针对"当前步骤"的回答:
"""

以上提示词主要包含以下关键信息:

  • 原始问题:确保模型能够了解最终目标
  • 完整计划:让模型了解当前步骤在整个任务中的位置
  • 历史步骤与结果:提供至今为止已经完成的工作,作为当前步骤的直接输入。
  • 当前步骤:明确指示模型现在需要解决哪一个具体任务。
(ii)执行阶段Executor类封装

我们将执行逻辑封装到Executor类中。这个类将循环遍历计划,调用LLM,并维护一个历史记录(状态)。

python 复制代码
class Executor:
	def __init__(self, llm_client):
		self.llm_client = llm_client
	def execute(self, question: str, plan: list[str]) -> str:
		# 根据计划,逐步解决问题
		history = ""
		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 ""
			
			# Refresh history, prepare for next step
			history += f"步骤 {i}: {step}\n结果: {response_text}\n\n"
			print(f"✅ 步骤 {i} 已完成,结果: {response_text}")
		# 最后一步的响应就是最终答案
		final_answer = response_text
		return final_answer

(3)PlanAndSolve智能体类

我们将(1)创建的 Planner 类和(2)创建的 Executor 类整合到一个统一的智能体 PlanAndSloveAgent 中,并赋予它解决问题的完整能力。这个主类的职责是:接收一个LLM客户端,初始化内部的规划器和执行器,并提供一个 run 方法来启动整个流程。

python 复制代码
class PlanAndSolveAgent:
	def __init__(self, llm_client):
		# 初始化智能体,创建规划器和执行器实例
		self.llm_client = llm_client
                # 将planner和executor组合到主类内部
		self.planner = Planner(self.llm_client)
		self.executor = Executor(self.llm_client)
		
	def run(self, question: str):
		# 运行智能体的完整流程: 先规划再执行
		print(f"\n--- 开始处理问题 ---\n问题: {question}")
		
		# 1. 调用规划器生成计划
		plan = self.planner.plan(question)
		if not plan:
			print("\n--- 任务终止 --- \n无法生成有效的行动计划。")
			return
		
		# 2. 调用执行器执行计划
		final_answer = self.executor.execute(question, plan)
		print(f"\n--- 任务完成 ---\n最终答案: {final_answer}")

PlanAndSolveAgent 类的设计体现了"组合优于继承"的原则,它本身不包含复杂的逻辑,而是作为一个协调者来调用其内部组件完成任务,提高了代码的灵活性和扩展性。

(4)PlanAndSolve智能体完整实现

以Ubuntu22.04系统,Python3.10版本为例进行展示。

首先我们新建一个项目文件夹 Plan_and_SolveAgent_demo 来存放我们的所有文件。 Plan_and_SolveAgent_demo/

├── .env

├── llm_client.py

├── Plan_and_Solve.py

└── README.md

Plan_and_Solve.py

python 复制代码
# Plan_and_Solve.py

import os
import ast
from llm_client import HelloAgentsLLM
from dotenv import load_dotenv
from typing import List, Dict

try:
	load_dotenv()
except FileNotFoundError:
	print("警告:未找到 .env 文件,将使用系统环境变量。")
except Exception as e:
	print(f"警告:加载 .env 文件时出错: {e}")


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

问题: {question}

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

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}")
		
		# 解析LLM输出的列表字符串
		try:
			# 找到 ```python and ```之间的内容
			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}")
			print(f"原始响应: {response_text}")
			return []
		except Exception as e:
			print(f"❌ 解析计划时发生未知错误: {e}")
			return []
			

EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。

# 原始问题:
{question}

# 完整计划:
{plan}

# 历史步骤与结果:
{history}

# 当前步骤:
{current_step}

请仅输出针对"当前步骤"的回答:
"""

class Executor:
	def __init__(self, llm_client):
		self.llm_client = llm_client
	def execute(self, question: str, plan: list[str]) -> str:
		# 根据计划,逐步解决问题
		history = ""
		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 ""
			
			# Refresh history, prepare for next step
			history += f"步骤 {i}: {step}\n结果: {response_text}\n\n"
			print(f"✅ 步骤 {i} 已完成,结果: {response_text}")
		# 最后一步的响应就是最终答案
		final_answer = response_text
		return final_answer
		

class PlanAndSolveAgent:
	def __init__(self, llm_client):
		# 初始化智能体,创建规划器和执行器实例
		self.llm_client = llm_client
                # 将planner和executor组合到主类内部
		self.planner = Planner(self.llm_client)
		self.executor = Executor(self.llm_client)
		
	def run(self, question: str):
		# 运行智能体的完整流程: 先规划再执行
		print(f"\n--- 开始处理问题 ---\n问题: {question}")
		
		# 1. 调用规划器生成计划
		plan = self.planner.plan(question)
		if not plan:
			print("\n--- 任务终止 --- \n无法生成有效的行动计划。")
			return
		
		# 2. 调用执行器执行计划
		final_answer = self.executor.execute(question, plan)
		print(f"\n--- 任务完成 ---\n最终答案: {final_answer}")
		
		
if __name__ == '__main__':
	try:
		llm_client = HelloAgentsLLM()
		agent = PlanAndSolveAgent(llm_client)
		question = "一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?"
		agent.run(question)
	except ValueError as e:
		print(e)

llm_client.py,可以去参考笔者的上一篇文章

从原理到代码,拆解ReAct 智能体设计模式ReAct范式将推理和行动结合起来,通过构建"Thought-Action- - 掘金

python 复制代码
# llm_client.py
import os
from openai import OpenAI
from dotenv import load_dotenv # 从.env文件中导入环境变量
from typing import List, Dict

# 导入环境变量
load_dotenv()

class HelloAgentsLLM:
	# 调用兼容OpenAI接口的服务, 使用默认的流式响应(将响应内容分为chunks逐个发送给客户端,而不是等待响应完毕一次性发送)
	def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
	
		# 初始化客户端,优先使用本地传入参数,否则加载环境变量参数
		self.model = model or os.getenv("LLM_MODEL_ID")
		apiKey = apiKey or os.getenv("LLM_API_KEY")
		baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
		timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
				
		if not all([self.model, apiKey, baseUrl]):
			raise ValueError("model ID and apiKey must be provided in .env file")
		self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)
		print("✅ 大模型配置成功")
		
	def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
		# 调用大模型进行思考,并返回思考结果
		print(f"🧠 正在调用 {self.model} 模型...")
		try:
			response = self.client.chat.completions.create(
				model=self.model,
				messages=messages,
				temperature=temperature,
				stream=True, # 开启流式响应
			)
			
			# 流式响应逻辑
			print("✅ 大语言模型响应成功:")
			collected_content = []
			for chunk in response: 
				content = chunk.choices[0].delta.content or "" # 从当前数据块提取刚产生的数据
				print(content, end="", flush=True)
				collected_content.append(content) # 将每次新产生的数据进行添加
			print() # 流式输出结束后换行
			return "".join(collected_content) # 将列表元素合成为完整字符串
		except Exception as e:
			print(f"❌ 调用LLM API时发生错误: {e}")
			return None

.env

python 复制代码
# .env 文件
LLM_API_KEY="YOUR-API-KEY"
LLM_MODEL_ID="YOUR-MODEL"
LLM_BASE_URL="YOUR-URL"

3. 测试效果

在终端输入 python Plan_and_Solve.py,观察到以下输出:

(四)Plan-and-Solve的特点与局限性

(1)特点

  • 全局规划与任务拆解:先谋后定,有效避免陷入局部最优。
  • 高度可解释性:由于任务被拆解成了明确的计划列表,每一步的输入和输出都有迹可循,如果最终结果出错,可以快速定位到具体是哪个步骤出了问题。
  • 支持并行化执行:在规划阶段,系统可以识别出哪些子任务是相互独立的。这意味着在执行阶段,可以同时调用多个工具或处理多个数据块。
  • 具备动态重规划能力:优秀的 Plan-and-Solve 架构通常包含"重规划"机制。当执行过程中遇到意外(如数据缺失、工具调用失败)时,系统不会直接崩溃,而是能触发重规划。

(2)局限性

  • 初始延迟高,不适合实时交互:执行前必须花费时间生成完整的计划蓝图,用户发出请求后往往需要等待数秒甚至更久才能看到第一个实质性输出。
  • 对初始规划质量高度依赖:如果 Planner(规划器)在第一步就理解偏差或遗漏了关键维度,后续执行得再完美也可能导致全盘皆输。这要求底层的大语言模型具备极强的任务建模和逻辑推理能力。
  • 容易将简单任务复杂化:对于简单的问题,强制使用Plan-and-Solve不仅增加了不必要的 Token 消耗和时间成本,还可能因为过度拆解导致死循环。
相关推荐
阿里云云原生1 小时前
从“玩具”到“工具”的进化:AgentRun如何构建企业级AI运维中台?
agent
.柒宇.2 小时前
AI-Agent入门实战-AI私厨
人工智能·python·langchain·agent·fastapi
HackerTom2 小时前
claude解决edge页面劫持
ai·edge·agent·claude·劫持
狐狐生风4 小时前
LangGraph 工具调用集成
python·langchain·prompt·agent·langgraph
Chef_Chen4 小时前
Agent-自我反思机制
agent
古茗前端团队5 小时前
Agent Skills 原理及其在中后台页面中的实践
agent
RxGc5 小时前
多Agent协作的真实瓶颈:为什么2个Agent比1个强,10个反而更差
人工智能·agent
Lazy_zheng5 小时前
LangChain + RAG 入门实战:从模型调用到完整 RAG 流水线
langchain·llm·agent
小马过河R6 小时前
从官方定义读懂智能体的时代分量
人工智能·语言模型·大模型·llm·agent·ai编程·多模态