1. Plan-and-Solve框架概述
Plan-and-Solve是一种将复杂问题分解为规划(Plan)和执行(Solve)两个独立阶段的AI代理设计模式。这种模式特别适合处理那些需要多步骤推理和计算的固定流程任务,比如数学应用题、逻辑推理题等。
1.1 核心思想
Plan-and-Solve的核心思想可以用一个简单的公式表示:
code复制复杂问题 → 规划阶段(生成步骤列表)→ 执行阶段(逐步求解)→ 最终答案
这种分阶段处理的方式有几个显著优势:
- 逻辑清晰:每个步骤都有明确的输入和输出
- 可预测性强:执行路径在规划阶段就已经确定
- 易于调试:可以清楚地看到每个步骤的执行结果
1.2 与ReAct模式的对比
ReAct(Reasoning and Acting)是另一种常见的AI代理模式,它与Plan-and-Solve有着本质的区别:
| 特性 | Plan-and-Solve | ReAct |
|---|---|---|
| 规划方式 | 一次性完整规划 | 动态规划 |
| 执行灵活性 | 低 | 高 |
| 适用场景 | 固定流程任务 | 工具调用任务 |
ReAct模式更适合那些需要根据执行结果动态调整策略的场景,比如需要调用外部API或进行网络搜索的任务。而Plan-and-Solve则在处理数学计算这类确定性任务时表现更优。
2. 架构设计与实现
2.1 三层架构设计
Plan-and-Solve代理采用典型的三层架构:
code复制┌─────────────────────────────────────┐
│ PlanAndSolveAgent (协调层) │
│ - 整合 Planner 和 Executor │
│ - 控制整体流程 │
└─────────────────────────────────────┘
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ Planner (规划器) │ │ Executor (执行器) │
│ - 分解问题 │ │ - 逐步执行计划 │
│ - 生成步骤列表 │ │ - 维护执行历史 │
└──────────────────┘ └──────────────────┘
这种分层设计使得系统各部分的职责明确,便于维护和扩展。协调层负责整体流程控制,规划器专注于问题分解,执行器则负责按部就班地完成任务。
2.2 Planner类实现
Planner类的核心功能是将复杂问题分解为可执行的步骤列表。以下是Python实现的关键代码:
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)
response = self.llm_client.think(prompt)
return self._parse_plan(response)
Planner的工作流程:
- 接收原始问题
- 使用特定模板构造提示词
- 调用LLM生成步骤列表
- 解析并返回结构化的计划
2.3 Executor类实现
Executor类负责按计划逐步执行任务,并维护执行历史:
python复制class Executor:
def __init__(self, llm_client: HelloAgentsLLM):
self.llm_client = llm_client
def execute(self, question: str, plan: list[str]) -> str:
"""按计划逐步执行,返回最终答案"""
history = ""
final_answer = ""
for i, step in enumerate(plan, 1):
prompt = EXECUTOR_PROMPT_TEMPLATE.format(
question=question,
plan=plan,
history=history,
current_step=step
)
response = self.llm_client.think(prompt)
history += f"步骤 {i}: {step}\n结果: {response}\n\n"
final_answer = response
return final_answer
Executor的关键职责:
- 遍历计划中的每个步骤
- 为每个步骤构造包含完整上下文的提示词
- 调用LLM获取当前步骤的结果
- 维护历史执行记录
- 返回最终答案
3. 提示词模板设计
3.1 规划阶段提示词
规划阶段的提示词模板(PLANNER_PROMPT_TEMPLATE)设计要点:
python复制PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。
问题: {question}
请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
"""
code复制
这个模板有几个关键设计:
1. 明确角色定位:"顶级的AI规划专家"
2. 强调输出格式:必须包含Python代码块标记
3. 要求步骤独立且有逻辑顺序
### 3.2 执行阶段提示词
执行阶段的提示词模板(EXECUTOR_PROMPT_TEMPLATE)设计:
```python
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。
# 原始问题:
{question}
# 完整计划:
{plan}
# 历史步骤与结果:
{history}
# 当前步骤:
{current_step}
请仅输出针对"当前步骤"的回答:
"""
执行阶段提示词的关键特点:
- 提供完整上下文:原始问题+完整计划+历史结果
- 强调专注当前步骤,避免跳步
- 要求简洁输出,不要额外解释
4. 完整执行流程示例
让我们通过一个具体例子来看Plan-and-Solve的实际工作流程:
用户问题:
"一个水果店周一卖出了15个苹果,周二是周一的两倍,周三比周二少5个,三天一共卖出了多少个苹果?"
4.1 规划阶段
Planner生成的步骤列表:
python复制["计算周一卖出的苹果数量",
"计算周二卖出的苹果数量(周一的两倍)",
"计算周三卖出的苹果数量(比周二少5个)",
"将三天卖出的苹果数量相加得到总数"]
4.2 执行阶段
步骤1/4:
- 当前步骤:"计算周一卖出的苹果数量"
- 历史:无
- 输出:
15
步骤2/4:
- 当前步骤:"计算周二卖出的苹果数量(周一的两倍)"
- 历史:
code复制步骤 1: 计算周一卖出的苹果数量 结果: 15 - 输出:
30
步骤3/4:
- 当前步骤:"计算周三卖出的苹果数量(比周二少5个)"
- 历史:
code复制步骤 1: 计算周一卖出的苹果数量 结果: 15 步骤 2: 计算周二卖出的苹果数量(周一的两倍) 结果: 30 - 输出:
25
步骤4/4:
- 当前步骤:"将三天卖出的苹果数量相加得到总数"
- 历史:
code复制步骤 1: 计算周一卖出的苹果数量 结果: 15 步骤 2: 计算周二卖出的苹果数量(周一的两倍) 结果: 30 步骤 3: 计算周三卖出的苹果数量(比周二少5个) 结果: 25 - 输出:
70
4.3 资源消耗
- LLM调用次数:1(规划)+4(执行)=5次
- 执行时间:与步骤数量成正比
5. 关键实现细节
5.1 计划解析安全性
解析LLM生成的计划时需要特别注意安全性:
python复制try:
# 提取代码块中的内容
plan_str = response_text.split("```python")[1].split("```")[0].strip()
# 使用 ast.literal_eval 安全解析
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(),避免代码注入 - 多重异常捕获,确保鲁棒性
- 类型检查,确保返回的是列表
5.2 历史记录维护
历史记录的维护对多步骤任务的执行至关重要:
python复制history = ""
for i, step in enumerate(plan, 1):
response_text = self.llm_client.think(messages=messages)
history += f"步骤 {i}: {step}\n结果: {response_text}\n\n"
final_answer = response_text
历史记录的作用:
- 为后续步骤提供上下文
- 支持步骤间的信息传递
- 便于调试和问题排查
6. 优势、局限与适用场景
6.1 优势分析
Plan-and-Solve模式具有以下显著优势:
✅ 逻辑清晰:规划和执行分离,每个步骤都有明确的输入输出
✅ 可预测性强:执行路径在规划阶段就已确定,便于监控
✅ 适合多步推理:每个步骤都能利用前面的计算结果
✅ 易于调试:可以清楚地看到每个步骤的执行结果
6.2 局限性
Plan-and-Solve也存在一些局限性:
⚠️ 缺乏灵活性:计划生成后无法根据执行结果调整
⚠️ 规划质量依赖:初始计划有误会导致整个流程失败
⚠️ 成本较高:每个步骤都需要独立的LLM调用
⚠️ 不适合动态环境:无法处理执行过程中的意外情况
6.3 适用场景
最适合的场景:
- 数学应用题
- 多步骤计算问题
- 逻辑推理题
- 流程固定的任务
不适合的场景:
- 需要工具调用的任务(如搜索、API调用)
- 执行结果不可预测的场景
- 需要根据中间结果调整策略的任务
7. 扩展与优化思路
7.1 动态重规划
当某个步骤执行失败时,可以尝试重新生成计划:
python复制def execute_with_retry(self, question: str, plan: list[str], max_retries=3):
for attempt in range(max_retries):
try:
return self.execute(question, plan)
except ExecutionError as e:
if attempt == max_retries - 1:
raise
plan = self.replan(question) # 重新生成计划
7.2 计划验证
在开始执行前,可以先验证计划的合理性:
python复制def validate_plan(self, question: str, plan: list[str]) -> bool:
validation_prompt = VALIDATION_PROMPT_TEMPLATE.format(
question=question,
plan=plan
)
response = self.llm_client.think(validation_prompt)
return "VALID" in response
7.3 并行执行
识别可以并行执行的步骤,提高效率:
python复制def identify_parallel_steps(self, plan: list[str]) -> list[list[str]]:
# 分析步骤间的依赖关系
# 返回可以并行执行的步骤组
7.4 混合模式
结合ReAct模式,在执行中允许工具调用:
python复制class HybridAgent:
def __init__(self, planner: Planner, executor: Executor, react_agent: ReactAgent):
self.planner = planner
self.executor = executor
self.react_agent = react_agent
def solve(self, question: str):
if requires_tools(question):
return self.react_agent.run(question)
else:
plan = self.planner.plan(question)
return self.executor.execute(question, plan)
8. 性能优化技巧
8.1 减少LLM调用次数
- 批量处理步骤:将多个简单步骤合并为一个复杂步骤
- 缓存结果:对相同或相似的步骤结果进行缓存
- 预计算:对确定性的计算步骤直接计算结果,不调用LLM
8.2 提高计划质量
- 多轮验证:让LLM自我验证生成的计划是否合理
- 示例引导:在提示词中提供优秀计划的示例
- 约束条件:明确指定计划必须满足的条件
8.3 错误处理机制
- 超时重试:对失败的步骤自动重试
- 备用计划:准备备用计划应对主计划失败的情况
- 异常捕获:全面捕获各种可能的异常情况
9. 实际应用案例
9.1 数学应用题求解
Plan-and-Solve特别适合解决多步骤数学问题,如:
"一个长方体的长是宽的2倍,高是长的一半。已知体积是64立方厘米,求长、宽、高各是多少?"
生成的计划:
python复制["设宽为x,表示长和高",
"根据体积公式建立方程",
"解方程求出x的值",
"计算长和高的值",
"验证结果是否符合题意"]
9.2 逻辑推理题
对于逻辑推理问题也能有效处理:
"有三个人A、B、C,其中一个人总是说真话,一个人总是说谎,一个人有时说真话有时说谎。A说'B是说谎者',B说'C是说谎者',C说'A是说真话的人'。请判断每个人的身份。"
生成的计划:
python复制["假设A是说真话的人,验证B和C的陈述",
"假设A是说谎的人,验证B和C的陈述",
"假设A是随机回答的人,验证B和C的陈述",
"排除矛盾的情况,确定唯一合理的身份分配"]
9.3 编程问题解答
即使是编程问题也能分解为清晰的步骤:
"编写一个Python函数,计算给定字符串中最长的不包含重复字符的子串长度。"
生成的计划:
python复制["理解题目要求,明确输入输出",
"设计滑动窗口算法的大致思路",
"实现窗口的扩展和收缩逻辑",
"处理边界条件和特殊情况",
"测试函数并验证正确性"]
10. 开发注意事项
10.1 提示词工程技巧
- 角色定义明确:给LLM明确的角色定位(如"顶级AI规划专家")
- 格式要求严格:明确指定输出格式,便于后续解析
- 示例引导:提供优秀输出的示例,引导LLM生成高质量结果
10.2 异常处理策略
- 计划解析失败:准备备用解析方案或重新生成计划
- 步骤执行失败:记录失败原因,尝试跳过或调整计划
- 结果验证失败:设置验证机制,确保最终结果的正确性
10.3 性能监控指标
- LLM调用次数:监控每个任务的平均LLM调用次数
- 执行时间:记录规划时间和执行时间
- 成功率:统计任务完成的成功率
- 错误类型:分类记录各种错误的发生频率
11. 与其他模式的对比分析
11.1 与ReAct对比
| 特性 | Plan-and-Solve | ReAct |
|---|---|---|
| 规划方式 | 静态规划 | 动态规划 |
| 执行灵活性 | 低 | 高 |
| LLM调用次数 | 1+N | 动态 |
| 调试难度 | 容易 | 较难 |
| 适用场景 | 确定性任务 | 不确定性任务 |
11.2 与Reflection对比
Reflection模式通过迭代优化来改进结果,而Plan-and-Solve是线性执行:
| 特性 | Plan-and-Solve | Reflection |
|---|---|---|
| 执行方式 | ���性执行 | 迭代优化 |
| 结果质量 | 依赖初始规划 | 逐步提高 |
| 资源消耗 | 中等 | 较高 |
| 适用场景 | 明确步骤任务 | 质量优化任务 |
11.3 混合模式的可能性
结合不同模式的优点,可以创建更强大的混合代理:
python复制class HybridAgent:
def solve(self, question: str):
# 先用Plan-and-Solve尝试
try:
plan = self.planner.plan(question)
return self.executor.execute(question, plan)
except PlanError:
# 失败后切换到ReAct模式
return self.react_agent.run(question)
这种混合策略可以在保持Plan-and-Solve高效性的同时,增加系统的灵活性。
12. 未来发展方向
12.1 自适应规划
开发能够根据任务复杂度自动调整规划粒度的系统:
python复制def adaptive_plan(self, question: str) -> list[str]:
complexity = estimate_complexity(question)
if complexity < THRESHOLD_SIMPLE:
return [question] # 直接解决
elif complexity < THRESHOLD_MEDIUM:
return self.default_plan(question)
else:
return self.detailed_plan(question)
12.2 多模态支持
扩展框架以支持多模态任务的规划和执行:
python复制class MultimodalPlanner:
def plan(self, question: Union[str, Image, Audio]) -> list[str]:
# 根据输入类型生成不同的计划
12.3 分布式执行
将计划中的步骤分配到不同的计算节点并行执行:
python复制def distributed_execute(self, plan: list[str]):
dependencies = analyze_dependencies(plan)
parallel_groups = group_parallel_steps(dependencies)
execute_in_parallel(parallel_groups)
13. 工程实践建议
13.1 代码组织
建议采用以下项目结构:
code复制plan_and_solve/
├── agents/
│ ├── planner.py
│ ├── executor.py
│ └── coordinator.py
├── prompts/
│ ├── planner_prompt.py
│ └── executor_prompt.py
├── utils/
│ ├── parser.py
│ └── validator.py
└── examples/
├── math_problems.py
└── logic_puzzles.py
13.2 测试策略
- 单元测试:对Planner和Executor分别测试
- 集成测试:测试整个流程的端到端功能
- 性能测试:监控LLM调用次数和执行时间
- 异常测试:模拟各种异常情况,测试系统的健壮性
13.3 部署考虑
- 缓存机制:缓存常用问题的计划,减少LLM调用
- 限流措施:控制对LLM的并发请求量
- 监控系统:实时监控系统运行状态和性能指标
- 日志记录:详细记录每个任务的执行过程,便于问题排查
14. 常见问题解答
14.1 计划质量不稳定怎么办?
解决方案:
- 优化提示词,增加更多约束和示例
- 引入多轮验证机制
- 对生成的计划进行评分,只接受高质量计划
14.2 执行过程中某个步骤失败怎么处理?
处理策略:
- 重试机制:对失败步骤自动重试
- 跳过机制:允许跳过非关键步骤
- 重新规划:当连续失败时重新生成计划
14.3 如何减少LLM调用次数?
优化方法:
- 合并简单步骤
- 缓存常见步骤的结果
- 对确定性计算直接求解,不调用LLM
14.4 系统响应时间过长怎么办?
性能优化:
- 并行执行独立步骤
- 预生成常用问题的计划
- 使用更高效的LLM API调用方式
15. 个人实践经验分享
在实际开发Plan-and-Solve系统时,我总结了以下几点经验:
-
提示词迭代至关重要:花费在优化提示词上的时间往往能带来最大的回报。通过不断调整角色定义、格式要求和示例,可以显著提高计划质量。
-
异常处理要全面:LLM的输出不可预测,必须对每种可能的异常情况做好准备。特别是计划解析阶段,要考虑到各种可能的格式变化。
-
监控指标要明确:建立清晰的性能指标(如LLM调用次数、执行时间、成功率等)对于评估系统改进效果至关重要。
-
混合模式很有价值:纯Plan-and-Solve模式有其局限性,结合ReAct或其他模式可以创建更强大的系统。根据任务特点选择合适的策略。
-
测试案例要多样:准备涵盖各种情况的测试案例,包括边界条件和异常输入,确保系统在各种场景下都能稳定工作。
-
历史记录要详细:维护完整的执行历史不仅有助于调试,还能为后续步骤提供更丰富的上下文,提高执行准确性。
-
并行化可能带来惊喜:识别可以并行执行的步骤能显著提高系统吞吐量,特别是对于计算密集型任务。
-
用户反馈很有价值:收集用户对系统输出的反馈,用于持续改进提示词和算法。真实使用场景往往能揭示测试中未发现的问题。
