Plan-And-Solve 智能体模式:深入解析与实践指南

Plan-And-Solve 智能体模式:深入解析与实践指南

一、引言

在人工智能应用领域,如何让大语言模型(LLM)处理复杂问题是关键课题。Plan-And-Solve(计划与执行)模式是一种经典的多步骤推理框架,它将复杂问题分解为多个简单步骤,逐一解决,最终得到正确答案。

本文以Plan_and_solve.py为例,深入剖析该模式的设计理念与实现细节。


二、什么是 Plan-And-Solve?

2.1 核心思想

Plan-And-Solve 的核心理念:“先规划,后执行”

┌──────────────┐ ┌──────────────┐ │ Planner │────▶│ Executor │ │ (规划器) │ │ (执行器) │ └──────────────┘ └──────────────┘ │ │ ▼ ▼ 生成计划 执行计划 分解问题 逐步解决

2.2 解决的问题

问题场景传统方式Plan-And-Solve
复杂数学题LLM 可能算错分步骤计算,每步可追溯
多步骤任务容易遗忘或重复按计划执行,不遗漏
需要中间结果最终答案错误难排查每步有结果,可追踪

三、代码结构解析

3.1 整体架构

# 两大核心组件PLANNER_PROMPT_TEMPLATE# 规划器提示词模板EXECUTOR_PROMPT_TEMPLATE# 执行器提示词模板# 三大核心类Planner# 负责分解问题Executor# 负责执行计划PlanAndSolveAgent# 整合两者,协调工作

3.2 工作流程图

用户提问 │ ▼ ┌─────────────────────────┐ │ PlanAndSolveAgent │ │ .run() │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Planner │ │ .plan() │ │ │ │ 调用 1 次 LLM │ │ 返回步骤列表 │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Executor │ │ .execute() │ │ │ │ 循环 N 次(每步骤1次) │ │ 返回最终答案 │ └─────────────────────────┘ │ ▼ 输出最终答案

LLM 调用总次数 = 1 + N(其中 N 为计划步骤数)


四、核心组件详解

4.1 规划器 (Planner)

4.1.1 提示词模板设计
PLANNER_PROMPT_TEMPLATE=""" 你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。 请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。 你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。 问题:{question}请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:```python["步骤1","步骤2","步骤3",...]

“”"

**设计要点**: - 明确要求 LLM 输出 Python 列表格式 - 指定 ` ```python ` 作为格式标记,便于后续解析 #### 4.1.2 Planner 类实现 ```python class Planner: def __init__(self, llm_client: HelloAgentsLLM): self.llm_client = llm_client def plan(self, question: str) -> list[str]: prompt = PLANNER_PROMPT_TEMPLATE.format(question=question) messages = [{"role": "user", "content": prompt}] response_text = self.llm_client.think(messages=messages) or "" # 解析 LLM 返回的内容 plan_str = response_text.split("```python")[1].split("```")[0].strip() plan = ast.literal_eval(plan_str) return plan if isinstance(plan, list) else []

关键步骤解析

步骤代码作用
1.split("```python")[1]提取 ```python 后的内容
2.split("```")[0]提取结束标记前的内容
3.strip()去除首尾空白
4ast.literal_eval()安全地将字符串转为列表
4.1.3 LLM 返回示例

输入

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

LLM 返回

好的,我来帮你分析这个问题。 ```python ["计算周二卖出的苹果数量(15×2=30)", "计算周三卖出的苹果数量(30-5=25)", "计算三天卖出的苹果总数(15+30+25=70)"]

这是你的解决计划。

**解析后得到**: ```python ["计算周二卖出的苹果数量(15×2=30)", "计算周三卖出的苹果数量(30-5=25)", "计算三天卖出的苹果总数(15+30+25=70)"]

4.2 执行器 (Executor)

4.2.1 提示词模板设计
EXECUTOR_PROMPT_TEMPLATE=""" 你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。 你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。 请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。 # 原始问题: {question} # 完整计划: {plan} # 历史步骤与结果: {history} # 当前步骤: {current_step} 请仅输出针对"当前步骤"的回答: """

设计要点

  • 传递原始问题(保持上下文)
  • 传递完整计划(让 LLM 知道全局)
  • 传递历史步骤与结果(关键创新点)
  • 明确当前步骤(明确任务)
4.2.2 Executor 类实现
classExecutor:def__init__(self,llm_client:HelloAgentsLLM):self.llm_client=llm_clientdefexecute(self,question:str,plan:list[str])->str:history=""# 历史记录final_answer=""# 最终答案fori,stepinenumerate(plan,1):# 构建提示词prompt=EXECUTOR_PROMPT_TEMPLATE.format(question=question,plan=plan,history=historyifhistoryelse"无",current_step=step)messages=[{"role":"user","content":prompt}]# 调用 LLMresponse_text=self.llm_client.think(messages=messages)or""# 更新历史记录history+=f"步骤{i}:{step}\n结果:{response_text}\n\n"final_answer=response_textreturnfinal_answer

执行流程示例

┌─────────────────────────────────────────────────┐ │ 第1次循环:计算周二卖出的苹果数量 │ │ history = "无" │ │ LLM 返回: "周二卖出30个苹果" │ │ history 更新为: │ │ "步骤 1: 计算周二卖出...结果: 周二卖出30个苹果" │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ 第2次循环:计算周三卖出的苹果数量 │ │ history = "步骤1...周二卖出30个苹果" │ │ LLM 返回: "周三卖出25个苹果" │ │ history 更新为: │ │ "步骤1...周二卖出30个苹果\n步骤2...周三卖出25个" │ └─────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ 第3次循环:计算三天卖出的苹果总数 │ │ history = "步骤1...周二卖出30个苹果\n步骤2...周三卖出25个" │ │ LLM 返回: "三天总共卖出70个苹果" │ │ final_answer = "三天总共卖出70个苹果" │ └─────────────────────────────────────────────────┘

五、关键知识点

5.1enumerate(plan, 1)的妙用

fori,stepinenumerate(plan,1):

作用:同时获取索引和值,且索引从 1 开始。

参数含义示例
plan要遍历的列表["A", "B", "C"]
1起始值(可灵活调整)从 1 开始

输出对比

enumerate(plan, 1) → 1, A / 2, B / 3, C enumerate(plan, 0) → 0, A / 1, B / 2, C (默认值)

应用场景

  • 人性化展示:1/50/5更直观
  • 字母编号:enumerate(plan, ord('A'))可得到A, B, C...

5.2 字符串解析:split()的链式调用

plan_str=response_text.split("```python")[1].split("```")[0].strip()

解析过程

原始文本: "我来帮你分析...\n```python\n["步骤1", "步骤2"]\n```\n这是计划。" 步骤1: split("```python") ['我来帮你...\n', '["步骤1", "步骤2"]\n```\n这是计划。'] 步骤2: [1] '["步骤1", "步骤2"]\n```\n这是计划。' 步骤3: split("```") ['["步骤1", "步骤2"]\n', '\n这是计划。'] 步骤4: [0] '["步骤1", "步骤2"]\n' 步骤5: strip() '["步骤1", "步骤2"]'

5.3ast.literal_eval()vseval()

# 推荐:安全的方式plan=ast.literal_eval('["步骤1", "步骤2"]')# 返回: ["步骤1", "步骤2"]# 不推荐:存在安全风险plan=eval('["步骤1", "步骤2"]')# 返回: ["步骤1", "步骤2"]

区别

方法安全性用途
ast.literal_eval()✅ 安全仅解析字面量(列表、字典、数字、字符串等)
eval()❌ 危险执行任意 Python 代码

5.4history的设计哲学

history+=f"步骤{i}:{step}\n结果:{response_text}\n\n"

为什么需要 history?

  1. 上下文传递:让 LLM 记住前序步骤的结果
  2. 避免遗忘:LLM 在长对话中可能忘记早期信息
  3. 结果可追溯:每一步都有记录,便于调试

简化写法

history=historyifhistoryelse"无"# 等价于history=historyor"无"

六、模式优势与局限

6.1 优势

优势说明
✅ 可追溯每一步都有结果,便于调试
✅ 逻辑清晰问题分解后更容易解决
✅ 错误隔离单步错误不会影响整体
✅ 易于维护Planner 和 Executor 职责分离

6.2 局限

局限说明
❌ LLM 调用多每个步骤调用一次,成本较高
❌ 步骤依赖如果规划不合理,可能出错
❌ 错误累积前一步错误可能影响后续

七、总结

Plan-And-Solve 模式是一种简单但强大的多步骤推理框架:

  1. Planner:一次性调用 LLM,将复杂问题分解为步骤计划
  2. Executor:遍历每个步骤,携带历史记录调用 LLM 执行
  3. History:通过传递历史记录,解决 LLM 上下文遗忘问题
  4. 最终答案:返回最后一步的结果

这种模式特别适合需要多步骤推理、结果可追溯、逻辑严密的场景,如数学计算、复杂分析、任务规划等。