从ReAct到ToT:Agent决策规划四范式全解析
做过 Agent 项目的朋友应该都有这种体验:模型确实能调用工具、能搜索、能写代码,但遇到稍微复杂的任务,就开始东一榔头西一棒子,要么抓到一步就闷头执行,把后面的约束全忘了;要么在同一个错误上反复打转,仿佛失去了记忆;更常见的是,一条路走到黑,明明方案已经不对了,就是不肯回头。
我最早做客服 Agent 时,任务链路只有"意图识别 → 查库 → 回答"三步,模型还经常在第二步就答非所问。后来陆续尝试了 LangChain、Dify 这类框架,发现底层逻辑大同小异,真正的差异全在"模型如何决策、如何规划执行路径"这一层。市面上把这类抽象统称为 Agent 的决策与规划,其中 ReAct、Plan-and-Execute、Reflexion 和 Tree of Thoughts 是绕不开的四个方向。
这篇内容我结合自己的落地经验,把四个范式从头到尾捋一遍,不绕弯子,直接讲它们各自解决什么问题、怎么用、有哪些坑,以及做项目时到底该怎么选。适合正在搭 Agent、或者准备把 Agent 从 Demo 推向生产环境的同学参考。
1. 为什么 Agent 需要专门研究"决策与规划"
先想清楚一个前提:大模型本身只是"会生成文本的引擎",它并不天然具备"完成任务"的能力。所谓 Agent,本质是在模型外面套了一层"感知 → 决策 → 行动 → 观察"的循环。循环跑得好不好,很大程度上依赖决策和规划的调度方式。
如果只调用一次模型,那只是单轮问答;但如果让模型连续调用工具、修正计划、评估中间结果,就需要一种明确的循环机制。早期不少人直接把"模型 + 工具"堆在一起,以为能自动工作,结果发现模型经常忘记上一轮在做什么。原因不是模型不够聪明,而是缺少决策框架来约束它的行为边界。
1.1 Agent 的决策链路拆解
一个完整的 Agent 执行周期可以拆成四个环节:
- 目标解析:把用户的高层诉求拆成可执行子目标,比如"帮我查这个月的销售数据并绘图"需要拆成"查库 → 聚合计算 → 选图表 → 渲染输出"。
- 行动选择:在当前状态下,决定是该调用搜索、执行代码,还是直接回答用户,这一步要求模型结合历史和工具返回结果判断。
- 执行与观察:工具执行后会产生新的观察,例如报错、空结果、部分成功,这些观察又会成为下一步决策的输入。
- 循环终止:判断目标是否达成,或者是否需要放弃、请求人工介入。
这四个环节中,行动选择和循环终止最依赖策略设计。ReAct 强调"边想边做",把推理和行动交错进行,让决策随时能被结果修正;Plan-and-Execute 则强调"先想后做";Reflexion 关注"失败后怎么办";Tree of Thoughts 关注"如果一条路走不通,能不能多开几条路一起探"。
1.2 四个范式解决的问题域
建议先把四个范式当成"行为策略"而不是"具体工具"。同一个 Agent 工程里,你可以混用它们,就像人会先定计划、执行中保持反思、遇到岔路时再横向比较多条方案。
我自己的经验是:判断该用哪个范式,先看任务结构。任务只需要少量顺序步骤时,ReAct 足够;任务步骤多、逻辑长时,Plan-and-Execute 更稳;任务容易反复失败、需要纠错时,Reflexion 见效快;任务属于"开放探索型",没有唯一正确路径时,Tree of Thoughts 才值得出手。下面逐个展开。
2. ReAct:让模型"边想边做"的基础范式
ReAct 是 Reasoning + Acting 的缩写,最早来自 2022 年底 Google Research 那篇论文。思路非常朴素:模型在完成一个工具调用前,先输出一段思考(Thought),然后决定动作(Action),拿到工具结果后,再基于观察(Observation)继续思考,直到得出最终答案。
第一次接触时觉得这没什么神奇,不就是思维链加了个工具调用吗?但真正去复现一遍,才发现这套机制把模型当前的能力调度得最自然。因为模型本身就是在做文本生成,思考、行动、观察全部用文本表示,不需要额外训练,只要在提示词里给出几个示例,模型就能照着格式产出决策序列。
2.1 ReAct 的核心机制
一个经典 ReAct 轨迹长这样:
Thought: 用户想查询杭州明天的天气,我需要先获取杭州的天气数据接口。 Action: search["杭州 天气预报"] Observation: 明天多云,22~28℃。 Thought: 查到数据了,可以直接回答。 Action: finish["杭州明天多云,22~28℃,适合出行。"]注意这里的 Action 不是随便写的,通常会通过提示词约定成一个固定格式,比如 JSON 或者带方括号的函数名,再交给一个解释器去解析、执行。模型的思考隐藏在 Thought 字段里,这也是后续调试最值得看的地方。
这个机制的关键,是"观察结果"会重新进入上下文。模型能用上一轮工具的真实返回修正自己的判断,这就避免了纯思维链那种"自说自话"的问题。另一个关键点是格式约束:提示词里必须明确规定哪些字段对应什么职责,否则模型会一股脑把思考、动作、答案混在一起,解析器根本拆不出来。
2.2 手工实现一个极简 ReAct
为了演示逻辑,我用 Python 写一个迷你版,不依赖 LangChain,方便看清楚核心流程。
import json from openai import OpenAI client = OpenAI() tools = { "search": lambda q: f"模拟搜索结果: {q} 相关百科内容", "calc": lambda expr: str(eval(expr)) } system_prompt = """你是一个任务助手。请按以下格式循环: Thought: 你的思考 Action: {"name": "工具名", "args": {"参数": "值"}} Observation: 工具返回结果 直到得到结论后输出: Thought: 我认为已经完成 Action: {"name": "finish", "args": {"answer": "最终答案"}} """ def run_agent(task, max_steps=5): messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": task}] for step in range(max_steps): resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, temperature=0 ) text = resp.choices[0].message.content print(f"[step{step}] {text}") # 简略解析 Action if "finish" in text: try: action = json.loads(text.split("Action: ")[1].strip()) return action["args"]["answer"] except Exception: return text try: action = json.loads(text.split("Action: ")[-1].strip()) name = action["name"] result = tools[name](**action["args"]) messages.append({"role": "assistant", "content": text}) messages.append({"role": "user", "content": f"Observation: {result}"}) except Exception as e: messages.append({"role": "assistant", "content": text}) messages.append({"role": "user", "content": f"Observation: 解析出错: {e}"}) return "max_steps 达到,停止" print(run_agent("帮我查一下'大型语言模型',然后计算 12*8"))这个实现把最关键的三件事做了:一是把思考与动作追加进消息历史,二是把工具观察作为用户消息回传,三是用 max_steps 限制死循环。实际项目中,解析动作要用更严谨的 JSON 提取,工具注册也要改成带 schema 的形式,但原理完全一致。
2.3 常见坑与调优
我遇到过最典型的问题是"模型不按格式输出"。解决方式不是反复改提示词,而是把示例写足,最好包含一个失败重试的示例,让模型知道中途工具报错也正常,它可以换个参数再试。
第二个坑是动作参数类型。模型生成{"name": "search", "args": "xxx"}而不是字典,解析时容易崩。我给工具调用加了 pydantic 校验,参数不合法就返回给模型"参数格式错误,请重新生成",通常下一轮就会自己修正。
第三是上下文膨胀。每轮 Thought、Action、Observation 都会增加 token,任务一长很容易超出窗口,后面的关键信息被挤掉。这时可以在每轮开始前,把更早的观察压缩成摘要,或只保留最近 N 轮轨迹。ReAct 本身没有记忆管理,这部分需要工程上补。
3. Plan-and-Execute:用计划拆分降低长任务翻车率
ReAct 的问题在于"走一步看一步",一旦步骤很多,模型很容易在第五步忘记了第二步的目标,或者被某个无关观察带偏。Plan-and-Execute 的思路就是不要边走边想,而是先让模型一次性生成完整计划,然后按照计划逐步执行,每执行完一步再回到计划上检查进度。
类比一下,ReAct 像不查地图直接开车,遇到路口再想怎么走;Plan-and-Execute 像先在高德上规划好整条路线,再按导航逐段行驶,虽然中途也可能遇到堵车需要绕路,但至少大方向不会跑偏。
3.1 两阶段设计:先规划,再执行
这套模式把 Agent 拆成两个独立模块:
- 规划器(Planner):负责把用户请求分解为有序步骤,输出结构化的 Step 列表,每一阶段可以调整,但调整频率远低于 ReAct 的每轮思考。
- 执行器(Executor):按当前计划执行单步,调用相应工具,返回执行结果,并把结果反馈给规划器。
这样做最大的好处是可观测性。计划本身是显式的,用户可以随时看到 Agent 现在执行到哪一步,以及后续要做什么。相比 ReAct 的黑盒思考,Plan-and-Execute 在 To B 场景下更受青睐,因为"可解释"比"聪明"更重要。
3.2 落地示意:一个简化版执行循环
我常用下面这段伪代码来描述这套流程,让你一眼看明白编排关系:
planner_prompt = "将任务拆分为步骤,输出 JSON 数组,每步含 step_id, description, tool, args" steps = LLM(task, planner_prompt) for index, step in enumerate(steps): result = execute_tool(step.tool, step.args) if result.status == "error": revised_steps = LLM(f"第{step}步失败,失败原因是{result},请重新规划剩余步骤") steps = merge_completed_and_revised(steps, revised_steps) else: mark_step_done(step_id) step_result_cache[index] = result final_answer = LLM(f"用户任务是{task},所有步骤结果如下:{step_result_cache},请汇总回答")这里的关键设计是"失败时重规划剩余步骤",而不是推翻整个计划。比如"查库 → 算指标 → 画图"三步,第二步算指标时发现某字段为空,只需要重规划"跳过该指标或补默认值",没必要从查库重新开始。
3.3 适用场景、注意事项与踩坑记录
Plan-and-Execute 适合数据分析、报表生成、调研整理这类"步骤可预先枚举"的任务。但如果任务本身开放度很高,比如"帮我设计一个营销方案",硬拆计划反而会限制思路。所以使用前要判断任务是否存在"计划漂移"风险。
踩坑最多的是步骤间的数据传递。计划里写了"步骤2使用步骤1的输出",但执行器如果没有上下文管理,下一步根本拿不到上一步的变量。我的做法是在计划中显式声明input_refs和output_key,执行完每步后把输出存进一个共享字典,这样重规划时也能从字典中取已完成步的结果。
另外要严格控制规划器输出的步骤数。让模型拆到 15 步以上,通常后面几步质量会下降。出现这种情况时,我会让规划器先输出"阶段级步骤",每个阶段执行前再展开为"动作级步骤",看起来是两段式,但实际落地稳定很多。
4. Reflexion:让 Agent 从失败中迭代
带反射机制的 Agent 思路不复杂,却非常实用。ReAct 和 Plan-and-Execute 都是假设模型能在当前的轮次里完成任务,但如果任务第一次做就错了呢?常规做法是重新跑一遍,可重跑之后模型大概率犯同样的错,因为没有任何反馈信号沉淀下来。
Reflexion 的核心改进是:每一次执行失败后,额外让模型对自己刚才的行动轨迹做一次复盘,把"哪里错了、下轮应该怎么改"写成一段简短的记忆,然后在下一轮执行开始时把这段记忆注入提示词。这相当于给 Agent 配了一本"错题本"。
4.1 从"失败"到"改进"的闭环逻辑
Reflexion 的闭环可以分成四步:
- 执行:用 ReAct 或 Plan-and-Execute 跑一遍当前任务;
- 评估:用规则或 LLM 判断当前结果是否达到目标;
- 反思:如果未达标,让模型读一遍轨迹和工具返回,总结失败原因,提出改进策略;
- 重试:带着新生成的反思记忆再执行一轮,如果仍然失败,继续追加反思记录。
这里最容易被忽略的是评估环节。如果评估标准模糊,反思就会变成套话。比如"我觉得回答不够好"没有任何指导意义;应当改成"回答中缺少具体数字""引用了 2021 年以前的数据""步骤2 的工具传参缺失"这种可操作描述。反思质量直接影响重试效果,而反思质量又依赖评估反馈的细粒度。
4.2 用最少代码实现一个"会认错"的 Agent
下面这段代码展示 Reflexion 的关键结构,严格说它只是把一个反思字符串追加到上下文,但效果立竿见影。
def run_with_reflexion(task, max_attempts=3): memory = [] for attempt in range(max_attempts): trajectory = run_react(task, extra_memory=memory) score = evaluate(trajectory, task) # 返回 0~100 分与原因 if score >= 90: return trajectory reflection = generate_reflection(task, trajectory, score) memory.append(reflection) print(f"attempt {attempt+1} 反思: {reflection}") return trajectory注意 highlight:extra_memory不是简单地把反思拼在新一轮消息末尾,而是要在系统提示词中单独隔离一个区域,比如"历史失败复盘"段落。如果只拼在消息尾部,模型在长对话里很容易忽略它。放到系统提示词开头,效果会明显提升。
4.3 什么时候该用 Reflexion,什么时候不该用
Reflexion 非常契合两类任务:代码生成(编译报错 → 修复 → 再编译)、工具链路长但可自动验证的任务(比如用搜索查资料后写报告,用规则检查引用是否存在)。因为这两类任务失败信号清晰,反思容易获得有效输入。
但也要小心两个过度使用问题。第一,对于单步问答任务,Reflexion 完全多余,它只会增加延迟和成本。第二,反思本身也可能产生幻觉,模型会"编造"失败原因,尤其在观察结果不完整的时候。我的办法是给反思模块加上工具上下文完整性校验,如果观察本身为空,就禁止生成反思,直接要求重试。
5. Tree of Thoughts:暴力但有效的多路径搜索
如果说前三种范式都是"逐步推进",Tree of Thoughts 完全是另一个维度。它把解决问题的过程看成一棵树,每个节点代表一个中间"思考步骤",模型在每个节点需要生成多个候选分支,再用评估器给分支打分,最后用搜索算法(比如 BFS 或 DFS)选择最优路径继续扩展。
这已经不是"让模型做一个决策",而是"让模型批量生成大量决策,再从中挑一个"。
5.1 ToT 和前三者的本质区别
ReAct 的每个 Thought 只有一个方向,错了就整条路径报废;ToT 在同一节点上保留多个备选方向,如果其中一个方向在后续评估中失败,还能回溯到上一个节点尝试另一个分支。
区别就在于回溯能力。以解数学题为例,ReAct 从头算到尾,中间哪步算错了只能重新开场;ToT 则会在关键步骤同时生成"方案A、方案B、方案C",算完 A 发现结果不对,还能回到岔路口试 B。代价也很明显:每一步的模型调用量不是 1,而是若干个候选乘以若干次评估。成本高一个数量级。
5.2 关键实现细节:节点生成、状态评估与搜索策略
真正实现 ToT 时,有三个点要设计好:
- 如何生成候选分支:对同一个节点,用较高 temperature(比如 0.7)采样 N 次,让模型给出不同方向的思考;或者显式提示"请给出 K 种可能的下一步做法,不要重复"。
- 如何评估分支:评估有两种主流方式,一种是用单独的 LLM 打分("该分支对最终答案的帮助程度 1~10"),一种是让模型投票("从以下分支中选择一个最有希望的方向,并说明理由")。实测下来投票比打分更稳定,因为模型比较擅长挑选而不是打分。
- 搜索策略:常用 BFS,因为树宽可控;每一步保留 Top-K 个节点继续迭代。DFS 适合路径越走越长、且很容易判断局部是否失败的任务,代码修复场景就很适合 DFS。
这里要特别提醒:Tree of Thoughts 的提示词写法和普通 Agent 完全不同。普通 Agent 只需要"一步步推理",ToT 则需要模型明确区分"当前状态"、"候选动作"、"候选评估"三类信息,建议用结构化 JSON 传递状态,否则解析会非常痛苦。
5.3 成本和效果权衡:别被论文带偏
ToT 在论文里的效果确实不错,涉及"24 点游戏""创意写作"这类任务时,比 Chain-of-Thought 高出一截。但论文任务往往步骤数可控、评估标准清晰,现实项目里不是这样。
我建议不要把 ToT 作为 Agent 的默认框架。它的启动成本体现在三个地方:模型调用量成倍增加、评估器的准确性难以保证、调试难度也显著上升。只有任务满足两个条件时才值得用:一是有明确的中间评估点(比如每解一步都能打分),二是单条路径失败代价高(比如需要大量 token 才能执行一步,失败后重跑很亏)。
另外一个变通办法是"约束宽度"。不用经典的 BFS 展开太多节点,只让模型在你的规划器给出的每个规划节点上生成 3 个候选方案,选 1 个执行,效果已经比 ReAct 稳不少,成本增加也还合理。这种"轻量版 ToT"对于多方案设计类任务非常实用。
6. 四者对比与选型策略
四个范式讲完,你可能会觉得"这也分不清到底用哪个"。其实不用纠结名字,它们并不互斥,很多成熟框架里都是混用的:外层用 Plan-and-Execute 控制流程,内部每个计划步骤用 ReAct 执行,出错后用 Reflexion 记录经验,遇到高难度关键节点再用 ToT 选最优路径。组合的学问比单个范式本身更重要。
6.1 横评表
我做了个简化的对比表格,方便你记选型时的几个核心维度:
| 范式 | 核心思路 | 决策方式 | 回溯能力 | 适用任务 | 成本 |
|---|---|---|---|---|---|
| ReAct | 推理与行动交错 | 每步即时决策 | 无,错到底 | 简单工具调用、问答 | 低 |
| Plan-and-Execute | 先计划再执行 | 按计划逐步执行 | 计划级有限回溯 | 多步骤、流程固定任务 | 中 |
| Reflexion | 失败后自我反思并重试 | 多轮尝试改进 | 通过迭代补救 | 代码修复、可验证任务 | 中高 |
| Tree of Thoughts | 多路径并行探索 | 分步生成多个候选再评估 | 全面回溯搜索 | 高难度推理、方案设计 | 很高 |
这里的"成本"需要展开说一下。很多人只看 API 费用,忽略了工程成本。ReAct 最便宜不仅是 token 少,而是工程实现简单,出了 bug 也好追。Plan-and-Execute 则多出一套状态管理。Reflexion 多一个评估器和反思提示词。ToT 则要写树结构、缓存、评估调度,代码量完全不在一个量级。
6.2 项目里到底怎么选,我的最终建议
如果你刚开始做 Agent,我的建议很明确:第一版先老老实实实现 ReAct,把工具调用、历史管理、异常捕获跑通。ReAct 是最基础的地基,后面所有范式都建立在这套"行为 → 观察"循环之上,不把地基打牢,直接上复杂框架只会让你分不清是模型的问题还是工程的问题。
第二步根据任务特征增量调整。任务步骤多,引入 Plan-and-Execute 的长计划缓存与重规划机制;任务反复失败,在循环外层包一层 Reflexion;任务需要多方案对比,在某个关键决策节点引入 ToT 的候选生成与投票选择。不要一上来就 A/B 测试全部框架,而是每次只加一个变量,观察效果和成本的变化。
最后我想分享一点个人心得:很多人误以为 Agent 效果不好是模型能力不够,于是不停换大模型。但我在实际项目里发现,很多"翻车"根本不是推理能力的问题,而是缺少明确的停止条件、缺少失败反馈机制、缺少状态管理。框架的选择永远是为约束模型行为服务的,而不是为炫技。一个设计恰当的 ReAct + 简单反思,很多时候比强行套用 Tree of Thoughts 更能稳定地产出结果。
如果你正在为 Agent 的"不听话"头疼,不妨先退回来,把你的任务轨迹打印出来,人工标出哪些步骤是决策错了,哪些是工具返回没处理好,再决定要不要换范式。把决策和规划这层想清楚,Agent 的可靠性才能真正上一个台阶。