课程0基础Agent开发课 / Agent基础 / Plan-and-Solve范式-先规划再执行的Agent设计
— 12 min read

Plan-and-Solve范式-先规划再执行的Agent设计

本章深入讲解 Plan-and-Solve 范式的设计思想、完整代码实现,以及与 ReAct 的适用场景对比。

Plan-and-Solve 范式:先规划再执行的 Agent 设计

本章深入讲解 Plan-and-Solve 范式的设计思想、完整代码实现,以及与 ReAct 的适用场景对比。

1.1 问题背景:ReAct 的局限

用户输入

规划阶段
Planner

步骤列表
① 搜索数据
② 分析趋势
③ 生成报告

执行阶段
Executor

步骤 1

步骤 2

...

步骤 N

汇总输出
最终答案

Plan-and-Solve 两阶段架构 — Planner 生成完整步骤列表,Executor 逐步忠实执行

ReAct 是"走一步看一步"的范式。它在执行时不知道自己要走多少步,每次思考都是局部视角——拿到工具结果,再想下一步怎么办。这种方式在面对开放性的信息检索任务时很有效,因为搜索会返回什么内容是未知的,需要灵活调整。

但是遇到结构清晰的多步骤任务,ReAct 会出问题。任务越复杂,中间步骤越多,它越容易在某个子问题上钻牛角尖,忘记了整体目标。典型的例子是:让 ReAct Agent 处理一道有七个步骤的数学应用题,它可能在第三步算出一个中间结果之后,开始反复验证那个数字,把后面四步全忘了。

Plan-and-Solve 解决的就是这个问题。先把任务完整拆解成步骤列表,再按顺序逐步执行。 执行阶段不再做决策,只是忠实地跑完计划。

1.2 两阶段架构

用户输入

规划阶段 Planner

输出结构化步骤列表

执行阶段 Executor

执行步骤 1

把结果追加到历史

执行步骤 2

把结果追加到历史

...

执行最后一步

汇总输出最终答案

两个阶段的分工很清晰。

Planner 负责全局视角。它读入问题,输出一个完整的步骤列表。这一步要求 LLM 在动手之前想清楚全程,类似于项目启动前先写工作计划。

Executor 负责单步执行。它每次只处理一个步骤,但能看到之前所有步骤的执行结果,用于当前步骤的推理。上下文不断积累,最后一步拿到完整的历史,输出最终答案。

1.3 完整代码实现

用一道多步数学应用题来演示:

小明周一买了 15 个苹果,周二又买了周一数量的 2 倍,周三吃掉了 5 个。请问小明现在一共有多少个苹果?并计算平均每天新增(不含周三)多少个苹果?

python
import ast
import re
from openai import OpenAI

client = OpenAI()


# ============================================================
# Planner:接收问题,输出 Python list 格式的步骤列表
# ============================================================

class Planner:
    def __init__(self):
        self.system_prompt = """你是一个任务规划专家。
将用户的问题拆解成清晰的执行步骤,以 Python list 格式输出。

要求:
1. 每个步骤是一个字符串,描述具体要做什么
2. 步骤要足够细,每步只做一件事
3. 最后一步必须是"汇总前面所有结果,给出最终答案"
4. 只输出 Python list,不要有任何其他文字

输出示例:
["步骤1:计算...", "步骤2:根据步骤1的结果计算...", "步骤3:汇总前面所有结果,给出最终答案"]"""

    def plan(self, question: str) -> list[str]:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": self.system_prompt},
                {"role": "user", "content": question},
            ],
            temperature=0,
        )
        raw_output = response.choices[0].message.content.strip()

        # 用 ast.literal_eval 安全解析,避免 eval 执行任意代码
        try:
            steps = ast.literal_eval(raw_output)
            if isinstance(steps, list) and all(isinstance(s, str) for s in steps):
                return steps
        except (ValueError, SyntaxError):
            pass

        # 解析失败:尝试用正则提取列表内容
        items = re.findall(r'"([^"]+)"', raw_output)
        if items:
            return items

        # 兜底:把原始输出作为单步计划
        return [raw_output, "汇总前面所有结果,给出最终答案"]


# ============================================================
# Executor:逐步执行,维护 history 字符串积累上下文
# ============================================================

class Executor:
    def __init__(self):
        self.system_prompt = """你是一个严谨的执行者。
你会收到:原始问题、完整计划、当前要执行的步骤、以及之前步骤的执行结果。
只执行当前步骤,给出该步骤的结果,不要超前执行其他步骤。
输出要简洁,只写计算过程和结果,不要废话。"""

    def execute_step(
        self,
        question: str,
        full_plan: list[str],
        current_step: str,
        step_index: int,
        history: str,
    ) -> str:
        plan_text = "\n".join(f"{i+1}. {s}" for i, s in enumerate(full_plan))

        user_content = f"""原始问题:{question}

完整计划:
{plan_text}

之前步骤的执行结果:
{history if history else "(这是第一步,暂无历史结果)"}

当前执行步骤(第 {step_index + 1} 步):{current_step}

请执行当前步骤,给出结果:"""

        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": self.system_prompt},
                {"role": "user", "content": user_content},
            ],
            temperature=0,
        )
        return response.choices[0].message.content.strip()


# ============================================================
# PlanAndSolveAgent:组合 Planner 和 Executor
# ============================================================

class PlanAndSolveAgent:
    def __init__(self):
        self.planner = Planner()
        self.executor = Executor()

    def run(self, question: str) -> str:
        print(f"\n问题:{question}")
        print("=" * 60)

        # 规划阶段
        print("\n[规划阶段]")
        steps = self.planner.plan(question)
        for i, step in enumerate(steps):
            print(f"  {i + 1}. {step}")

        # 执行阶段
        print("\n[执行阶段]")
        history = ""

        for i, step in enumerate(steps):
            print(f"\n步骤 {i + 1}{step}")
            result = self.executor.execute_step(
                question=question,
                full_plan=steps,
                current_step=step,
                step_index=i,
                history=history,
            )
            print(f"结果:{result}")

            # 追加到历史,供后续步骤使用
            history += f"\n步骤 {i + 1}{step})的结果:\n{result}\n"

        # 最后一步的结果就是最终答案
        return result


# ============================================================
# 测试
# ============================================================

if __name__ == "__main__":
    agent = PlanAndSolveAgent()

    question = (
        "小明周一买了 15 个苹果,周二又买了周一数量的 2 倍,"
        "周三吃掉了 5 个。请问小明现在一共有多少个苹果?"
        "并计算周一和周二平均每天新增多少个苹果?"
    )

    answer = agent.run(question)
    print(f"\n最终答案:{answer}")

运行后的输出大概是这样:

code
问题:小明周一买了 15 个苹果...
============================================================

[规划阶段]
  1. 计算周一购买的苹果数量
  2. 计算周二购买的苹果数量(周一的2倍)
  3. 计算周三之后剩余的苹果数量(扣除吃掉的5个)
  4. 计算周一和周二平均每天新增苹果数
  5. 汇总前面所有结果,给出最终答案

[执行阶段]

步骤 1:计算周一购买的苹果数量
结果:周一购买 15 个苹果。

步骤 2:计算周二购买的苹果数量(周一的2倍)
结果:周二购买 15 × 2 = 30 个苹果。

步骤 3:计算周三之后剩余的苹果数量
结果:总计 15 + 30 = 45 个,吃掉 5 个后剩余 40 个苹果。

步骤 4:计算周一和周二平均每天新增苹果数
结果:周一新增 15 个,周二新增 30 个,平均 (15 + 30) / 2 = 22.5 个。

步骤 5:汇总所有结果,给出最终答案
结果:小明现在共有 40 个苹果。周一和周二平均每天新增 22.5 个苹果。

1.4 实现细节

ast.literal_eval 而不是 eval。Planner 的输出是一段 Python list 的文本,如果用 eval 直接执行,LLM 可以在里面注入任意代码。ast.literal_eval 只解析字面量(字符串、数字、列表、字典),不会执行函数调用,是安全的选择。

history 字符串追加而不是追加 messages。每次执行步骤时,把之前所有结果拼成一段文本传入,而不是维护一个长 messages 列表。好处是结构清晰,Executor 的每次调用都是独立的,方便并行优化;坏处是 history 太长时 prompt 会很大,token 成本高。实际项目里可以对 history 做摘要压缩。

规划失败的容错。Planner 输出的格式可能不标准,所以有两层容错:先 ast.literal_eval,失败了用正则提取引号内的文本,都不行就把原始输出当成单步计划。不要在这里让整个任务直接崩掉。

1.5 和 ReAct 的对比

选哪种范式,本质上是看任务的结构是否确定。

ReAct 适合:需要实时获取外部信息、任务路径不确定、需要边做边决策的场景。比如让 Agent 去查一个你自己也不知道答案在哪的问题,搜索结果会影响下一步搜什么。

Plan-and-Solve 适合:任务结构清晰、步骤可以提前确定、主要靠推理而非工具调用的场景。多步数学题、结构化报告、已知流程的分析任务。

维度 ReAct Plan-and-Solve
决策时机 每步都决策 只在规划阶段决策
全局视野 弱,容易迷失 强,始终有计划
适应意外 强,可实时调整 弱,计划一旦定了很难改
适合任务类型 信息检索、探索性 多步推理、结构化
实现复杂度 略高(两阶段)

两者没有优劣之分,只有适不适合。

1.6 Plan-and-Solve 的局限

这个范式最大的问题是计划是静态的

Planner 在拿到问题后,基于已有信息制定计划。但执行过程中可能发现:某个步骤的前提假设是错的,或者某步的结果让后续步骤变得没意义。这时 Executor 没有权力修改计划,只能硬着头皮按计划走,最终输出一个基于错误前提的答案。

更大的问题是,如果 Planner 本身规划有误,后面的 Executor 再努力也是白费。Planner 的质量决定了整个任务的上限。

针对这个局限,有一个改进方向是动态重规划(Dynamic Re-planning):在执行过程中,每完成一个步骤就检查一次"计划还需要调整吗"。如果需要,重新调用 Planner 更新剩余步骤。这本质上是把 Plan-and-Solve 和 ReAct 融合了——宏观上有计划,微观上可以灵活调整。LangGraph(LangChain 旗下的 Agent 工作流框架,用图结构管理 Agent 的状态和决策流程)的一些高级用法就是走这个路子的。

对于大多数结构确定的任务,静态计划已经足够好用。不要为了加复杂度而加复杂度,能解决问题的方案就是好方案。

选范式要看任务特点:一道数学题,Plan-and-Solve 比 ReAct 稳;一个开放性研究任务,ReAct 比 Plan-and-Solve 灵活。理解两者的边界,才能在对的场景用对的工具。

本页目录