把规划和执行分开,是 Agent 从玩具走向生产的关键分水岭
ReAct 是一个反应式的模式——每一步只看当下,决定下一步做什么。这在短任务里没问题,但任务一长就出问题。
一个典型场景:你让 Agent 做"帮我分析 data/ 下所有 CSV 文件,找出销售额 top 5 的产品,生成一份报告并存到 output/report.md"。这是一个 6~10 步的任务。ReAct 跑到第 4 步时,上下文里已经是一大堆工具调用和观察结果。模型在决定"下一步做什么"时,最初的目标已经被淹没在观察结果堆里。它可能跑去生成报告,忘了先过滤 top 5;也可能在某个 CSV 解析出错后,再也想不起还有其他 CSV 没读。
这种现象叫 goal drift——目标漂移。ReAct 天然不抗这个,因为它没有"全局视图"。
Plan-and-Execute 的核心洞察
解法很简单但很有效:把规划和执行分开。
在任务开始时,让一个 Planner(通常是更强的推理模型)一次性生成完整的计划——N 个步骤,每步做什么、用什么工具、预期产出是什么。然后一个 Executor 按这个计划逐步执行,每做完一步更新计划状态。
用户任务: 分析 data/ 下 CSV,找 top 5 产品,生成报告
Planner 输出:
Step 1: list_files("data/", ext="csv") -> 文件列表
Step 2: for each file: read_csv(f) -> dataframe
Step 3: merge all dfs, group by product, sum sales
Step 4: sort desc, take top 5
Step 5: generate markdown report
Step 6: write_file("output/report.md", content)
Executor 按顺序执行,每步拿到 Observation 后:
- 如果结果符合预期: 推进到下一步
- 如果偏离: 触发 Replan
这个架构的意义在于:全局目标不再依赖模型每轮从观察堆里"记起来",而是作为一个数据结构固定下来。Executor 每一步只需要回答"按计划,这一步做什么",不需要做长程推理。
最小可跑版本
用 Python 手写一个最小的 Plan-and-Execute Agent,不依赖任何框架:
import json
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class PlanStep(BaseModel):
id: int
action: str # 工具名或 "think"
args: dict
goal: str # 这一步要达成的子目标
status: str = "pending" # pending | done | failed
result: str | None = None
class Plan(BaseModel):
task: str
steps: list[PlanStep]
def make_plan(task: str, available_tools: list[str]) -> Plan:
sys = f"""你是一个规划专家。给定用户任务,拆成有序步骤。
可用工具: {', '.join(available_tools)}
返回严格的 JSON,格式为: {{"task": "...", "steps": [{{"id":1,"action":"tool_name","args":{{...}},"goal":"..."}}, ...]}}"""
resp = client.chat.completions.create(
model="o4-mini",
messages=[{"role": "system", "content": sys},
{"role": "user", "content": task}],
response_format={"type": "json_object"},
)
return Plan.model_validate_json(resp.choices[0].message.content)
def execute_step(step: PlanStep, tools: dict) -> PlanStep:
try:
fn = tools[step.action]
step.result = str(fn(**step.args))
step.status = "done"
except Exception as e:
step.result = f"错误: {e}"
step.status = "failed"
return step
def should_replan(plan: Plan, new_obs: str) -> bool:
sys = """你在审查一个执行计划是否还有效。看最新一步的结果,判断:
- 如果结果符合预期,后续步骤可以正常执行,回答 "continue"
- 如果结果和预期严重偏离,需要重新规划,回答 "replan"
只输出一个词。"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": sys},
{"role": "user", "content": f"计划: {plan.model_dump_json()}\n最新结果: {new_obs}"},
],
)
return "replan" in resp.choices[0].message.content.lower()
def run(task: str, tools: dict, max_replan: int = 2):
plan = make_plan(task, list(tools.keys()))
replan_count = 0
while True:
pending = [s for s in plan.steps if s.status == "pending"]
if not pending:
return plan # 所有步骤完成
step = execute_step(pending[0], tools)
if step.status == "failed" or should_replan(plan, step.result):
if replan_count >= max_replan:
return plan # 达到 replan 上限
replan_count += 1
# 重规划:把当前进度喂回去
new_task = f"""原任务: {task}
已完成: {[s for s in plan.steps if s.status == 'done']}
刚失败: {step}
请生成剩余步骤的新计划"""
plan = make_plan(new_task, list(tools.keys()))
这段代码的灵魂在三个地方:
make_plan——用推理模型一次性出计划。推理模型之所以重要,是因为规划需要全局考虑所有步骤之间的依赖,这是 CoT 能发挥最大作用的地方。
should_replan——这是 Plan-Execute 和纯"线性脚本"的分水岭。每一步执行完都让模型判断"计划还对不对"。如果偏了,触发重规划。
状态化的 Plan 对象——所有步骤的状态和结果都存在 Plan 里。无论重规划多少次,已完成的工作不丢,不需要重做。
重规划的策略:全重还是局部改
重规划分两种模式:
全量重规划——把当前进度和失败原因整个喂给 Planner,让它从"当前状态"出发生成剩余计划。上面的代码是这种。优点是简单、能应对大幅偏离。缺点是频繁重规划时上下文会膨胀。
局部修补——只让 Planner 修改"从失败步骤往后"的那几步,前面已完成的保持不变。更省 token,但实现复杂一点,需要让 Planner 明确返回"要替换哪些步骤"。生产 Agent(LangGraph、CrewAI)倾向用这种。
实务上经验是:失败的原因如果是"工具返回和预期不一致",用局部修补;如果是"我对任务理解错了",用全量重规划。前者只需要换一两步,后者需要重新思考整个路径。
Plan-Execute 的几个衍生变体
这一套"分规划和执行"的思想出来后,衍生了几个变体,每个都值得了解:
ReWOO (Reasoning WithOut Observation)——2023 年的论文。让 Planner 出完整计划,所有步骤并行执行(如果没有依赖),最后 Solver 综合所有观察给答案。比传统 Plan-Execute 更省 token,因为不需要每步都回到模型做判断。适合步骤相互独立的任务,比如"查 10 个人的信息"。
LLMCompiler——Plan 阶段直接输出一个 DAG,Executor 按依赖关系并行执行。本质上是把 Agent 编译成一个可并行的计算图。对"大量独立工具调用"的场景延迟减少 30~50%。
LATS (Language Agent Tree Search)——把 Plan-Execute 和 Tree Search 结合。每步有多个可能的 Action,用 Monte Carlo 搜索评估不同路径。计算成本高,但对难题成功率显著提升。下一篇 Reflection 和第 06 篇 Tree of Thoughts 会涉及类似思路。
LangGraph 里的 Plan-Execute
如果你用 LangGraph,上面的逻辑基本就是它的 StateGraph 模式。官方甚至提供了 create_planner_worker 这样的 helper。LangGraph 把"状态"和"节点之间的流转"做成了显式的图结构:
from langgraph.graph import StateGraph, END
def planner_node(state): ...
def executor_node(state): ...
def should_continue(state) -> str:
if all_done(state): return END
if should_replan(state): return "planner"
return "executor"
graph = StateGraph(State)
graph.add_node("planner", planner_node)
graph.add_node("executor", executor_node)
graph.add_edge("planner", "executor")
graph.add_conditional_edges("executor", should_continue, {
"planner": "planner", "executor": "executor", END: END,
})
对比我们上面手写的版本,LangGraph 的价值在于:状态流转变成声明式的图,调试时能可视化地看到每次从哪个节点跳到哪个节点。对长任务是决定性的可观测性优势。第 18 篇会系统对比 LangGraph 和其他框架。
什么时候用 Plan-Execute,什么时候不用
不是所有任务都适合。经验:
用 Plan-Execute:任务超过 5 步;步骤之间有明确依赖;有明显的"整体策略"需要固定;需要展示进度给用户。
用纯 ReAct:任务 1~3 步就完;每一步的结果决定下一步做什么(高分支);对话式、探索式任务,不适合预先规划。
一个实用的混合模式:用 Plan-Execute 做主干,用 ReAct 做步内。例如 Plan 里的某一步是"调研竞品"——这个步内部需要多轮搜索和筛选,那就让 Executor 在执行这一步时跑一个小 ReAct 循环,做完返回结果。这是 Claude Code、Cursor 这类工具实际采用的架构。
下一步
Plan-Execute 解决了"全局目标钉住"。但它有个弱点——Planner 出的计划不一定对,Executor 按计划执行不代表结果合格。
下一篇 Reflection 就是补这个短板:让 Agent 执行完一个步骤或整个任务后,自己审视结果、挑毛病、重做。这是把 Agent 从"能跑完"推到"跑得好"的关键一步。
相关阅读
- Plan-and-Solve Prompting (2023) — Plan-and-Execute 理论基础
- ReWOO: Decoupling Reasoning from Observations — 并行 Plan-Execute 变体
- LLMCompiler: Parallel Function Calling — Agent 的编译视角
- LangGraph Docs: Plan-and-Execute
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:04. Plan-and-Execute:先规划,再执行,失败时重规划
本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/04-Plan-and-Execute/
本文最后一次更新为 天前,文章中的某些内容可能已过时!