在上一篇《ReAct:模型如何让推理与行动交替运转》里,我们把 ReAct 的局限性说清楚了:
ReAct 是一个“反应式”范式——它在每一步根据当前观察决定下一步,没有全局规划。对于需要提前规划、分解目标、并行执行的复杂长任务,纯 ReAct 往往不够。
这篇文章就是要回答:不够用的时候,怎么办。
先给结论
- ReAct 的核心缺陷是“只看当下”。 它每一步只根据最新的 Observation 决定下一步,没有对整个任务的全局视图。任务越长、越复杂,这个缺陷就越明显。
- Planning 的本质是“先想清楚再做”。 在执行任何动作之前,先生成一份结构化的执行计划,把目标分解成可执行的子任务序列。
- Plan-and-Execute 是最常见的 Planning 范式。 规划阶段生成计划,执行阶段按计划推进,检查点阶段评估是否需要调整。三个阶段分工明确。
- Planning 不是 ReAct 的替代品,而是它的上层结构。 好的 Agent 系统里,Planning 负责任务分解和全局协调,ReAct 负责每个子任务内部的推理与工具调用。
- 计划的刚性是工程上最重要的设计决策。 计划太刚性,遇到意外就崩;计划太灵活,和没有计划没区别。找到合适的刚性程度,是 Planning 系统设计的核心挑战。
ReAct 在哪里失效
先通过一个具体场景理解 ReAct 的天花板。
假设任务是:“分析我们产品过去一个季度的用户流失情况,找出主要原因,并给出改进建议。”
这个任务包含:
- 从数据库查询流失用户数据
- 对数据进行多维度分析(时间趋势、用户分群、行为路径)
- 查询用户反馈和客服记录
- 综合多个来源的信息得出原因假设
- 验证假设
- 生成结构化报告
用 ReAct 来做,大致会发生什么:
Thought: 我需要分析用户流失情况,先查查数据。
Action: query_database("SELECT * FROM churned_users WHERE ...")
Observation: 返回了 5000 条记录...
Thought: 数据量很大,我来看看流失趋势。
Action: analyze_data("按月统计流失率")
Observation: 1月 3.2%,2月 4.1%,3月 5.8%...
Thought: 流失率在上升,我来看看具体原因...
(继续一步一步走,每步只看当前结果)
问题在哪里:
没有全局结构。 ReAct 不知道“分析流失原因”这件事总共需要几个步骤,哪些步骤是必须的,哪些可以并行,哪些依赖于其他步骤的结果。它只是走一步看一步。
容易迷失在细节里。 当某个工具返回了有趣的数据,ReAct 很容易被吸引过去深挖,偏离了原始目标。没有全局计划约束,任务路径很难保持连贯。
无法保证完整性。 没有计划意味着没有“完成检查表”。任务执行完后,你不知道是否覆盖了所有必要的分析角度,很容易遗漏关键步骤。
对长任务来说上下文消耗失控。 前面上下文与记忆那篇已经讲过这个问题:没有结构的执行会让 context window 很快被工具结果填满,导致后续推理质量下降。
这些问题,都不是换个更强的模型能解决的。它们是“反应式执行”这个设计本身的局限。
Planning 的核心思想
Planning 的答案直接了当:在开始执行之前,先生成一份结构化的计划。
这份计划要回答三个问题:
- 这个任务可以分解成哪些子任务?
- 子任务之间的依赖关系是什么?(哪些必须先做,哪些可以并行)
- 每个子任务完成的判断标准是什么?
有了这份计划,执行阶段就不再是“走一步看一步”,而是在一个明确的框架内推进。
用上面的流失分析任务来对比,有了 Planning 之后:
[规划阶段]
Goal: 分析 Q1 用户流失原因并给出改进建议
Plan:
Step 1: 提取流失数据(数据库查询)
→ 依赖:无
→ 完成标准:获得完整的流失用户列表和基础指标
Step 2: 流失趋势分析(基于 Step 1 结果)
→ 依赖:Step 1
→ 完成标准:得出按时间、用户群体的流失率变化
Step 3: 用户行为路径分析(基于 Step 1 结果)
→ 依赖:Step 1
→ 完成标准:识别流失前的关键行为模式
Step 4: 用户反馈分析(可与 Step 2、3 并行)
→ 依赖:无
→ 完成标准:提取主要投诉和流失原因关键词
Step 5: 综合分析,生成假设
→ 依赖:Step 2、3、4
→ 完成标准:列出 3-5 个流失原因假设,每个有数据支撑
Step 6: 生成改进建议报告
→ 依赖:Step 5
→ 完成标准:结构化报告包含原因、证据、建议三部分
[执行阶段]
按计划逐步执行,每步完成后更新状态
这样,Agent 在任何时刻都知道:整个任务的全貌、当前在哪一步、还有哪些步骤没完成、当前步骤的完成标准是什么。
Plan-and-Execute 范式
把上面的思路结构化,就是 Plan-and-Execute 范式。它包含三个阶段:
规划阶段(Plan):给定目标,生成结构化执行计划。
执行阶段(Execute):按计划逐步执行,每个子任务内部可以用 ReAct 来处理工具调用。
检查点阶段(Checkpoint):每完成一个子任务,评估结果,判断是否需要调整后续计划。
三个阶段的关系是:规划是起点,执行是主体,检查点是保险——它让计划在遇到意外时能够自我修正,而不是崩溃或继续执行一个已经不适用的计划。
代码实现
规划阶段
规划器的职责是把一个自然语言目标转化成结构化的执行计划:
import openai
import json
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
client = openai.OpenAI()
class StepStatus(Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
SKIPPED = "skipped"
@dataclass
class PlanStep:
id: str
description: str
depends_on: list[str]
success_criteria: str
tools_needed: list[str]
status: StepStatus = StepStatus.PENDING
result_summary: Optional[str] = None
notes: Optional[str] = None
@dataclass
class ExecutionPlan:
goal: str
steps: list[PlanStep]
definition_of_done: str
constraints: list[str] = field(default_factory=list)
def get_next_steps(self) -> list[PlanStep]:
"""返回当前可以执行的步骤(依赖已完成,自身未开始)"""
completed_ids = {s.id for s in self.steps if s.status == StepStatus.COMPLETED}
return [
s for s in self.steps
if s.status == StepStatus.PENDING
and all(dep in completed_ids for dep in s.depends_on)
]
def is_done(self) -> bool:
return all(
s.status in (StepStatus.COMPLETED, StepStatus.SKIPPED)
for s in self.steps
)
def summary(self) -> str:
lines = [f"目标:{self.goal}", ""]
for s in self.steps:
icon = {"pending": "⬜", "running": "🔄",
"completed": "✅", "failed": "❌", "skipped": "⏭️"}[s.status.value]
result = f" → {s.result_summary[:80]}..." if s.result_summary else ""
lines.append(f"{icon} [{s.id}] {s.description}{result}")
return "\n".join(lines)
def create_plan(goal: str, context: str = "") -> ExecutionPlan:
"""规划阶段:把目标分解成结构化执行计划"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"你是一个任务规划专家。"
"根据给定目标,生成清晰的结构化执行计划。"
"计划要完整覆盖目标,步骤之间的依赖关系要准确。"
"只输出 JSON,不要有任何其他文字。"
)
},
{
"role": "user",
"content": f"""
目标:{goal}
{f"背景信息:{context}" if context else ""}
生成执行计划,格式如下:
{{
"goal": "目标描述",
"definition_of_done": "整体任务完成的判断标准",
"constraints": ["约束条件1", "约束条件2"],
"steps": [
{{
"id": "step_1",
"description": "步骤描述",
"depends_on": [],
"success_criteria": "这步完成的判断标准",
"tools_needed": ["tool_name"]
}}
]
}}
"""
}
],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
steps = [PlanStep(**s) for s in data["steps"]]
return ExecutionPlan(
goal=data["goal"],
steps=steps,
definition_of_done=data["definition_of_done"],
constraints=data.get("constraints", [])
)
执行阶段
执行阶段负责按计划推进,每个子任务内部用 ReAct 处理:
def execute_step(step: PlanStep, plan: ExecutionPlan, tools: list) -> str:
"""执行单个子任务,内部使用 ReAct 范式"""
system = f"""你是一个任务执行 Agent。
当前任务计划:
{plan.summary()}
你现在需要执行的步骤:
- ID: {step.id}
- 描述: {step.description}
- 完成标准: {step.success_criteria}
- 可用工具: {', '.join(step.tools_needed)}
执行完成后,输出一段简洁的结果摘要,说明完成了什么、关键发现是什么。
"""
messages = [
{"role": "system", "content": system},
{"role": "user", "content": f"请执行步骤 {step.id}:{step.description}"}
]
# 内部用 ReAct 执行(工具调用循环)
for _ in range(10): # 最多 10 轮
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)
choice = response.choices[0]
if choice.finish_reason == "stop":
return choice.message.content
if choice.finish_reason == "tool_calls":
messages.append(choice.message)
for tc in choice.message.tool_calls:
args = json.loads(tc.function.arguments)
result = dispatch_tool(tc.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": str(result)
})
return "步骤执行超时,未能完成。"
检查点阶段
每完成一个步骤后,评估结果,判断是否需要调整计划:
def checkpoint(plan: ExecutionPlan, completed_step: PlanStep) -> tuple[ExecutionPlan, bool]:
"""
检查点:评估已完成步骤的结果,判断是否需要调整后续计划。
返回 (更新后的计划, 是否需要重新规划)
"""
# 构建当前完成情况的摘要
completed_summaries = [
f"[{s.id}] {s.description}: {s.result_summary}"
for s in plan.steps
if s.status == StepStatus.COMPLETED
]
pending_steps = [
f"[{s.id}] {s.description}"
for s in plan.steps
if s.status == StepStatus.PENDING
]
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": f"""
任务目标:{plan.goal}
已完成步骤:
{chr(10).join(completed_summaries)}
刚完成的步骤:[{completed_step.id}] {completed_step.description}
结果:{completed_step.result_summary}
待执行步骤:
{chr(10).join(pending_steps)}
请判断:
1. 根据刚才的结果,后续步骤是否需要调整?
2. 是否发现了计划中遗漏的重要步骤?
3. 任务是否已经可以提前完成?
输出 JSON:
{{
"needs_replan": true/false,
"reason": "说明原因",
"updated_pending_steps": [
{{"id": "step_x", "description": "...", "depends_on": [], "success_criteria": "...", "tools_needed": []}}
],
"can_finish_early": true/false
}}
"""
}
],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
if result["needs_replan"] and result.get("updated_pending_steps"):
# 只更新待执行的步骤,已完成的不动
completed = [s for s in plan.steps if s.status != StepStatus.PENDING]
new_pending = [PlanStep(**s) for s in result["updated_pending_steps"]]
plan.steps = completed + new_pending
return plan, result.get("can_finish_early", False)
def run_plan_and_execute(goal: str, tools: list, context: str = "") -> str:
"""完整的 Plan-and-Execute 执行循环"""
# 1. 规划阶段
print("=== 规划阶段 ===")
plan = create_plan(goal, context)
print(plan.summary())
# 2. 执行阶段
print("\n=== 执行阶段 ===")
while not plan.is_done():
next_steps = plan.get_next_steps()
if not next_steps:
print("没有可执行的步骤,可能存在依赖死锁。")
break
# 取第一个可执行步骤(并行执行可以在这里扩展)
step = next_steps[0]
step.status = StepStatus.RUNNING
print(f"\n执行: [{step.id}] {step.description}")
result = execute_step(step, plan, tools)
step.result_summary = result
step.status = StepStatus.COMPLETED
print(f"完成: {result[:100]}...")
# 3. 检查点
plan, can_finish = checkpoint(plan, step)
if can_finish:
print("检查点判断:任务可以提前完成。")
break
# 生成最终输出
return generate_final_output(plan)
完整执行流程回顾
三个阶段串起来的完整流程如下图所示。规划阶段生成结构化计划后进入执行循环:每轮先找出依赖已满足的待执行步骤,用 ReAct 执行,经过检查点评估,直到 is_done() 为真才退出循环生成最终输出。否则分支经左侧回环,重新进入 get_next_steps()。
图 1:Plan-and-Execute 由规划、执行、检查点三部分构成;当 is_done() 为否时,流程会回到 get_next_steps() 继续推进。
计划的刚性:最重要的设计决策
前面提到,Planning 设计中最核心的挑战是:计划应该有多刚性?
这不是一个有标准答案的问题,需要根据任务性质做权衡。
完全刚性计划
规划阶段生成计划后,执行阶段严格按照计划走,中途不允许修改。
# 完全刚性:不调用 checkpoint,直接按顺序执行
def strict_execute(plan: ExecutionPlan, tools: list):
for step in plan.steps:
if all(
s.status == StepStatus.COMPLETED
for s in plan.steps
if s.id in step.depends_on
):
result = execute_step(step, plan, tools)
step.result_summary = result
step.status = StepStatus.COMPLETED
适用场景:任务路径非常确定,每一步输出都可以预期,意外情况极少。比如定期运行的数据报告、标准化的文件处理流程。
风险:一旦某个步骤结果意外,后续步骤可能在错误的前提上继续执行,产生错误传播。
完全动态计划
每一步执行完都重新规划,计划随时可以完全推翻重来。
def dynamic_execute(goal: str, tools: list):
context = ""
for _ in range(20): # 最大步数
# 每步都重新规划
plan = create_plan(goal, context)
next_step = plan.get_next_steps()[0]
result = execute_step(next_step, plan, tools)
context += f"\n已完成:{next_step.description} → {result}"
if plan.is_done():
break
适用场景:任务路径高度不确定,每一步结果都可能完全改变后续方向。
风险:每步都重新规划开销很大,而且容易失去对整体进度的把控,变成 ReAct 的另一种形式。
带检查点的自适应计划(推荐)
这是上面代码实现的方式:计划在执行中保持稳定,但在每个检查点有机会做局部调整。
核心原则:
- 已完成的步骤不可撤销
- 当前步骤完成后才触发检查点
- 检查点只修改待执行的步骤,不推翻整个计划
- 检查点的调整有明确触发条件(结果异常、发现遗漏、可提前完成)
这个设计在稳定性和灵活性之间找到了平衡:计划足够稳定,让执行有全局方向感;同时足够灵活,能应对执行过程中的意外。
Planning 与 ReAct 的关系
一个容易产生的误解是:引入 Planning 之后,就不需要 ReAct 了。
不是这样的。两者在 Agent 系统里处于不同的层次,解决不同的问题:
Planning 解决的是宏观问题:
- 任务应该分解成哪些子任务?
- 子任务的执行顺序和依赖关系是什么?
- 整体进度如何追踪?
ReAct 解决的是微观问题:
- 在执行某个子任务时,应该调用哪个工具?
- 工具返回了意外结果,下一步怎么调整?
- 当前子任务是否已经完成?
在实际的 Agent 系统里,它们是嵌套关系:Planning 负责把大任务分解成子任务,ReAct 负责执行每一个子任务。execute_step() 函数里的工具调用循环,本质上就是 ReAct。
可以用一句话概括它们的分工:Planning 决定做什么,ReAct 决定怎么做。
常见的 Planning 失败模式
1. 计划太细,失去灵活性
把每一步都规划得非常具体(“调用 search_web,关键词用 X,过滤条件用 Y”),导致计划成了一个固化的脚本。任何意外都会让计划失效。
好的计划应该在“目标层”描述步骤,而不是在“实现层”。“查找竞品的最新定价信息”是好的步骤描述;“用 Google 搜索‘竞品名称 price 2026’”是实现细节,不应该写进计划。
2. 计划太粗,无法指导执行
步骤描述过于抽象,每个步骤都是一个大任务,没有给执行阶段足够的约束。比如“分析用户数据”这个步骤太宽泛,执行阶段不知道从哪里开始,分析什么维度,达到什么程度算完成。
3. 忽略依赖关系
所有步骤都写成串行的,没有标注真正的依赖关系。这样就无法发现哪些步骤可以并行,也无法在某步失败时判断哪些后续步骤受影响。
4. 检查点调整过于激进
每个检查点都大幅修改计划,导致计划频繁重构,执行进度难以追踪,而且重新规划本身也有成本(时间、token)。检查点的设计应该是“微调”而非“重建”。
5. 没有完成标准
步骤没有明确的 success_criteria,执行阶段不知道什么时候算完成,容易过度执行或过早停止。每个步骤的完成标准应该是可以客观判断的,而不是“做得差不多”。
什么时候需要 Planning
不是所有任务都值得引入 Planning。它会增加规划阶段的时间和 token 开销,也会增加系统复杂度。
适合 Planning 的场景:
- 任务包含 5 个以上有依赖关系的子任务
- 任务需要跨越多轮工具调用,且整体结构可以提前预判
- 任务失败的代价较高,需要在执行前做好全局把控
- 任务输出需要保证完整性(如报告、分析文档),不能遗漏关键步骤
不适合 Planning 的场景:
- 任务简单,ReAct 或单次工具调用就能完成
- 任务路径完全无法预判,每一步都依赖上一步的结果才能决定方向(这种情况纯 ReAct 反而更合适)
- 对响应时间非常敏感(规划阶段本身需要一次模型调用)
总结
ReAct 的价值在于让推理与行动交替,形成闭环。但它是“反应式”的,只看当前不看全局。当任务变得复杂,这个局限就开始显现。
Planning 不是来取代 ReAct 的,而是在它上面加了一层全局视图:
- 规划阶段把目标分解成结构化的子任务序列
- 执行阶段用 ReAct 处理每个子任务内部的工具调用
- 检查点阶段评估进度,在保持整体稳定的前提下做局部调整
这三层加在一起,才是一个真正能处理复杂任务的 Agent 结构。
计划的刚性是最需要根据场景权衡的设计决策。太刚性,遇到意外就崩;太灵活,和没有计划没区别。带检查点的自适应计划,是大多数场景下合理的起点。
下一篇:如何评测一个 Agent——不只是看它“回答得好不好”,而是看它能不能稳定地把任务做完