一、为什么任务规划是 Agent 的分水岭
上一篇文章讲 Function Calling,解决的是"模型如何调用工具";但工具再多,没有规划能力,Agent 就只能做"一次调用"的简单问答。真正复杂的业务诉求------"帮我对比三家供应商的报价并生成采购建议"------需要 Agent 自己拆解成多个子任务、决定执行顺序、根据中间结果调整后续步骤。这个"拆解与决策"的过程,就是任务规划。
目前业界主流的规划范式有三条路线:ReAct (边想边做)、Plan-and-Execute (先计划后执行)、Tree-of-Thought(多路径搜索)。很多人把三者当成"选一个用",实际上它们解决的是不同层次的问题,适用场景、成本、稳定性差异巨大。这篇文章用三个最小实现把三者讲透,再给出一张真实的选型决策表。
二、三种范式速览
| 维度 | ReAct | Plan-and-Execute | Tree-of-Thought |
|---|---|---|---|
| 核心思想 | 推理与行动交替 | 先规划后逐步执行 | 同时探索多条推理路径 |
| 规划时机 | 边走边规划(动态) | 一次性规划(静态) | 每步分支搜索 |
| LLM 调用次数 | 每步 1 次,总次数不定 | 计划 1 次 + 每步 1 次 | 每步多次(分支×候选) |
| 成本 | 中 | 低 | 高 |
| 可解释性 | 强(每一步可审计) | 强(计划先于行动) | 中(并行路径难追踪) |
| 最适合 | 需要查工具/环境的动态任务 | 步骤明确的多阶段任务 | 高难度推理、试错型问题 |
一句话概括:ReAct 适合"环境会变"的任务,Plan-and-Execute 适合"步骤固定"的任务,Tree-of-Thought 适合"一步错全盘错"的难题。
三、ReAct:边想边做
ReAct(Reasoning + Acting)是三者里最接近"人"的工作方式:每一步先思考(Thought)、再决定动作(Action)、观察结果(Observation),循环往复。它的核心优势是动态性------计划可以随环境反馈实时修正。
python
def react_agent(task: str, tools: dict, max_steps: int = 6) -> str:
"""
ReAct 最小实现:Thought -> Action -> Observation 循环
tools: {"search": fn, "calc": fn, ...}
"""
history = [] # 保留 Thought/Action/Observation 三元组
for step in range(max_steps):
# 1) 让模型基于历史产出"下一步思考+动作"
prompt = f"""任务:{task}
可用工具:{list(tools.keys())}
已执行步骤:{history}
请输出 JSON:{{"thought": "你的推理", "action": "工具名", "args": {{...}}}}
如果任务已完成,action 输出 "finish",args 里放最终答案。"""
resp = llm(prompt) # 结构化输出
if resp["action"] == "finish":
return resp["args"]["answer"]
# 2) 执行工具,观察结果
try:
obs = tools[resp["action"]](**resp["args"])
except Exception as e:
obs = f"工具错误:{e}"
# 3) 把三元组写回历史,供下一轮推理
history.append(f"Thought:{resp['thought']} | Action:{resp['action']} | Observation:{obs}")
return "已达最大步数,未能完成任务"
生产中的关键点:
- history 要截断:ReAct 每步都往上下文追加内容,长任务下 token 会爆炸,务必保留最近 N 步(通常 4~6 步);
- 步骤上限必须设:没有 max_steps,模型可能陷入"搜索→观察→再搜索"的死循环,成本失控;
- 结构化输出是前提:要求模型输出 JSON,而不是自由文本,否则解析成本极高。
ReAct 在"需要实时查数据"的场景(客服查单、多轮检索)表现最好,因为每步都能利用最新环境信息修正方向。代价是步数不可控,长任务成本线性上涨。
四、Plan-and-Execute:先计划,后执行
Plan-and-Execute 把规划从执行中剥离:先用一次 LLM 调用生成完整计划(计划器),再按计划逐条执行(执行器)。规划是静态的,执行是线性的------计划器不参与每步执行,只在下游规划失败时才介入。
python
def plan_and_execute(task: str, tools: dict, max_steps: int = 8) -> str:
# 阶段一:计划器,一次性产出步骤清单
plan = llm(f"""任务:{task}
可用工具:{list(tools.keys())}
请输出分步执行计划,每步一个 JSON 对象,
格式:[{{"step": 1, "action": "工具名", "args": {{...}}, "goal": "这一步的目的"}}]
最多 {max_steps} 步。""")
# 阶段二:执行器,线性执行,可带局部重规划
results = []
for step in plan:
try:
out = tools[step["action"]](**step["args"])
results.append(f"Step{step['step']}({step['goal']}): {out}")
except Exception as e:
# 失败时只重规划"后续步骤",不重跑已完成的
plan = replan_remaining(task, step["step"], results, e)
return finalize(task, results)
相比 ReAct 的三个优势:
- 成本低且可预测:计划 1 次 + 每步 1 次调用,总调用次数 ≈ 1 + 步数,上限确定;
- 可解释性强:先给你看完整计划,再开始干活------这在需要审批、审计的业务里是刚需;
- 失败隔离:某一步失败,只需重规划剩余步骤,已完成的结果不浪费。
但它有一个致命弱点:计划是静态的 。如果第一步执行后环境发生了变化(比如库存没了),而计划还是按旧假设编排的,后续步骤就会基于错误前提执行。所以生产上几乎都用它的变体------Plan-and-Solve / 带重规划的 Plan-and-Execute:执行器每完成 N 步,让计划器核对"计划是否仍然成立",不成立就重新规划。
python
def plan_execute_with_replan(task, tools, replan_every: int = 3):
plan = make_plan(task, tools)
for i, step in enumerate(plan):
out = execute(step, tools)
if (i + 1) % replan_every == 0 and not plan_still_valid(task, plan, i):
plan = make_plan(task + f"\n已完成:{i+1} 步,请基于最新情况重排剩余计划", tools)
return final_answer(plan, results)
适用场景 :报表生成、数据管道、多阶段审批流------步骤明确、顺序固定、每步结果可预期的任务。它也是三种范式里最容易做产品化的:计划本身就是一份可以给用户看、给审批人看的工作清单。
五、Tree-of-Thought:多路径搜索
Tree-of-Thought(ToT)不是"规划器"本身,而是一种推理增强框架:每一步不是只走一条路,而是让模型生成多个候选思路,评估打分,保留最有希望的若干分支继续探索。适合"一步错、全盘错"的高难度问题(数学证明、逻辑谜题、复杂代码调试)。
python
def tree_of_thought(problem: str, branches: int = 3, depth: int = 3) -> str:
"""
最小 ToT:每层生成 branches 个候选,保留 top-1 继续
完整版应保留 top-k 并做宽度优先搜索
"""
frontier = [{"path": [], "state": problem}]
for level in range(depth):
new_frontier = []
for node in frontier:
# 1) 基于当前状态,生成 branches 个不同思路
candidates = llm(f"""问题:{problem}
当前推理路径:{node["path"]}
请给出 {branches} 种不同的下一步解法,分别用编号列出。""")
# 2) 评估每个候选的价值(用一次 LLM 打分)
for cand in candidates:
score = llm(f"给这个思路的可行性打分(0-10):{cand}")
new_frontier.append({
"path": node["path"] + [cand],
"score": float(score),
})
# 3) 保留分数最高的分支继续
new_frontier.sort(key=lambda n: -n["score"])
frontier = new_frontier[:max(1, len(new_frontier) // 2)]
# 对最终保留的分支做收敛
return llm(f"基于这些思路给出最终答案:{frontier[0]['path']}")
成本是它的硬伤 :每层调用次数 = 分支数 × (生成 + 打分),三层 3 分支就是 18 次调用起步,而且每个候选的评估本身也不可靠------LLM 给自己打分,经常"觉得自己都对"。生产上 ToT 通常只用在:
- 复杂推理类任务(数学、代码难题、策略分析);
- 前端给用户展示"多种方案对比"的场景;
- 结合 ReAct 做"规划+搜索"混合(即 ReAct + 分支探索)。
六、真实业务选型:一张决策表
前面是原理,下面是实战。我按"任务类型"给出推荐组合,这三条几乎覆盖了绝大多数 AI 应用:
| 业务场景 | 推荐范式 | 理由 |
|---|---|---|
| 客服查单、多轮检索问答 | ReAct | 环境实时变化,需要每步修正 |
| 报表生成、数据管道、审批流 | Plan-and-Execute(带重规划) | 步骤固定、成本可控、计划可审计 |
| 复杂代码调试、数学/逻辑难题 | Tree-of-Thought | 需要多路径试错,容忍高成本 |
| 采购分析、方案对比(输出多种建议) | ReAct + ToT 混合 | 主干用 ReAct,关键决策点用 ToT 分支 |
三个最容易踩的坑:
- 拿 ToT 处理简单任务:普通问答用 ToT,成本翻十几倍,收益为零------先问自己"这个任务真的需要多路径试错吗";
- 纯 Plan-and-Execute 不加重规划:计划一旦过期,整条执行链全错,必须做"计划健康检查";
- ReAct 不做步数上限和上下文截断:线上 80% 的"Agent 失控"都来自这两个缺失。
成本与质量的最终权衡 :预算敏感选 Plan-and-Execute,质量敏感选 ReAct+重规划,只有难题才值得 ToT。生产上更常见的做法是三级路由:先意图分类,简单任务直接问答,中等任务走 Plan-and-Execute,复杂任务才升级到 ReAct/ToT------让 90% 的请求跑在便宜路径上,把贵的算力留给真正需要的地方。
七、总结
- ReAct = 动态修正,适合环境会变的交互型任务,注意步数上限;
- Plan-and-Execute = 静态计划 + 线性执行,适合步骤明确的流程型任务,记得加重规划;
- Tree-of-Thought = 多路径搜索,适合高难度推理,成本最高,慎用;
- 三者不是互斥的,生产中按"任务难度 × 成本预算"组合使用,才是工程化的正确姿势。
规划器没有银弹,只有"在正确的场景用正确的范式,并控制好成本与失败边界"。