从零学习 Agentic RL(四)—— 超越 ReAct 的线性束缚:深入解析 Tree-of-Thoughts (ToT)
从零学习 Agentic RL(四)—— 超越 ReAct 的线性束缚:深入解析 Tree-of-Thoughts (ToT)
摘要:
本文是“从零学习 Agentic RL”专栏的第四篇。在(三)中,我们实现了 ReAct 框架,它通过 T-A-O (思考-行动-观察) 循环赋予了 LLM 执行能力。然而,ReAct 的线性思考链在面对复杂规划或需要探索的任务时(例如数学难题、棋局)显得十分脆弱,一旦某一步思考出错,整个任务便会失败。
为解决此问题,本文将深入探讨 Tree-of-Thoughts (ToT) 框架。ToT 将 Agent 的思考过程从一条“链”扩展为一棵“树”,允许 Agent 同时探索多条推理路径、评估(剪枝)不同分支,并进行回溯 (Backtracking)。本文将包含 ToT 的核心原理、与 ReAct 的详细对比表格、一个简化的 ToT 框架 Python 实战(实现 BFS 搜索算法),以及 PPO 如何优化 ToT 的进阶讨论和相关面试问题。
文章目录
- 从零学习 Agentic RL(四)—— 超越 ReAct 的线性束缚:深入解析 Tree-of-Thoughts (ToT)
- 🦈 一、前言:ReAct 的“线性”困境
- 二、[原理] 从“链”到“树”:ToT 的核心思想
- 2.1 ToT 的四大核心组件
- 三、[实战] 从零实现一个简化的 ToT 框架
- 3.1 步骤 1:定义“评估器” (Evaluator)
- 3.2 步骤 2:定义“生成器” (Generator)
- 3.3 步骤 3:定义“搜索算法” (BFS Executor)
- 3.4 运行与分析
- 四、[进阶] ToT, PPO 与 ReAct 的“大一统”
- 4.1 ToT vs ReAct:成本与收益的权衡
- 4.2 PPO 如何优化 ToT?
- 五、🧠 专栏面试问题角 🧠
- 六、总结与参考链接
🦈 一、前言:ReAct 的“线性”困境
在上一篇文章中,我们构建的 ReAct Agent 已经可以解决“苹果 CEO 家乡”这类多步查询任务。其工作流是一个单线程的 T-A-O 循环:
Task -> Thought 1 -> Action 1 -> Observation 1 -> Thought 2 -> ... -> Finish
这个模式的致命弱点在于:它是一条“单行道”,无法“掉头”或“探索岔路”。
想象一下,如果 Agent 在 Thought 2 这一步做出了一个次优甚至错误的决策(例如,错误地搜索了一个不相关的人名),ReAct 框架没有原生的机制去回溯 (Backtrack) 到 Thought 1 并尝试另一条路径。它只能“硬着头皮”在错误的基础上继续下去,导致任务最终失败。
这种“线性”的特性,使得 ReAct 在处理以下任务时力不从心:
- 复杂规划:例如需要多步权衡的旅行规划。
- 数学与逻辑:例如“24点游戏”或逻辑谜题,第一个思路很可能是错的。
- 探索性任务:例如“写一个有创意的押韵短诗”,需要尝试多种措辞。
``
(图 1:ReAct 的线性思考链及其"死胡同"困境)
为了解决这个问题,研究者们提出了 Tree-of-Thoughts (ToT),其核心思想是:与其“一条路走到黑”,不如“广撒网,多探索”。
二、[原理] 从“链”到“树”:ToT 的核心思想
ToT 框架(源自论文 Tree of Thoughts: Deliberate Problem Solving with Large Language Models)将 LLM 的问题解决过程,从一个“序列 (Sequence)”建模为一个“树 (Tree)”。
- ReAct 是链 (Chain):State0→State1→State2→...State_0 \rightarrow State_1 \rightarrow State_2 \rightarrow ...State0→State1→State2→...
- ToT 是树 (Tree):在任何一个 StateStateState 节点,都可以分岔出 NNN 个可能的下一步 StateStateState。
``
(图 2:从“链式思考” (左) 到“树状思考” (右) 的演变)
2.1 ToT 的四大核心组件
ToT 框架的实现,依赖于四个关键组件的协同工作:
- 分解 (Decomposition):
- 作用:将一个复杂的大任务,分解为 KKK 个有序的“思考步骤”(即树的 KKK 层深度)。
- 类比:ReAct 的 T-A-O 循环是隐式的、一步一步的分解。ToT 则是有意识地将问题规划为多个阶段。
- 生成 (Generation):
- 作用:在树的任何一个节点(一个部分思考),调用 LLM 生成 NNN 个不同的、可能的“下一步思考”(即树的 NNN 个分支)。
- 实现:通过修改 Prompt,例如 “Based on the current plan, propose 3 different next steps.”
- 评估 (Evaluation):
- 作用:这是 ToT 的灵魂。你需要一个“评估器 (Evaluator)”来判断 NNN 个新生成的“思考分支”中,哪一个“更靠谱”。
- 实现:评估器可以是:
- 启发式 (Heuristic):一个简单的、基于规则的函数(例如,在24点游戏中,“计算结果是否更接近24”)。
- LLM 自我评估:调用 LLM,让它自己给这 NNN 个分支打分(例如 “Rate these 3 thoughts from 1-10 on their likelihood of success.”)。
- 价值函数 (Value Function):(剧透) 这就是 PPO 的 Critic 可以发挥作用的地方!
- 搜索 (Search):
- 作用:有了 NNN 个分支和它们的“评估分数”,你需要一个“搜索算法”来决定接下来探索哪条分支。
- 实现:可以是:
- 广度优先搜索 (BFS):一层一层地探索所有分支。
- 深度优先搜索 (DFS):先沿着一条“最有希望”的分支一路走到底。
- A* 搜索:更高级的启发式搜索。
三、[实战] 从零实现一个简化的 ToT 框架
我们来手写一个 ToT Agent。为了聚焦核心原理(生成、评估、搜索),我们选择一个简单的逻辑谜题,而不是依赖外部 API。
- 目标任务:一个简单的“物品分配”谜题。
- 已知:有3个盒子 (A, B, C) 和 3 个物品 (钥匙, 硬币, 钻石)。
- 线索 1:盒子 A 里不是钥匙。
- 线索 2:盒子 C 里是钻石。
- 求解:A, B, C 分别是什么?
一个 ReAct Agent 可能会“猜” A 是硬币,然后一条路走下去。但 ToT 可以同时探索 A 是硬币和 A 是钻石(虽然线索2马上会否定后者)的路径。
3.1 步骤 1:定义“评估器” (Evaluator)
我们的“评估器”是一个启发式函数,它负责检查一个“部分解”是否与线索冲突。
# 代码块 1: 定义评估器 (Heuristic Evaluator)# 谜题的线索 (我们的“环境”)
CLUES = {"clue1": "A is not 钥匙","clue2": "C is 钻石"
}def evaluate_thought(solution: dict) -> str:"""评估一个“部分解”(thought) 是否有效。Args:solution (dict): e.g., {'A': '硬币', 'B': '?', 'C': '钻石'}Returns:str: 'valid' (有效), 'invalid' (无效/冲突), 'complete' (完整且有效)"""# 检查线索 1if solution.get('A') == '钥匙':return 'invalid'# 检查线索 2if solution.get('C') and solution.get('C') != '钻石':return 'invalid'if solution.get('C') == '钻石' and (solution.get('A') == '钻石' or solution.get('B') == '钻石'):return 'invalid' # 物品不能重复# 检查物品是否重复items = [v for v in solution.values() if v != '?']if len(items) != len(set(items)):return 'invalid' # 发现了重复物品# 检查是否完成if all(v != '?' for v in solution.values()):# 确保所有物品都用上了if set(items) == {'钥匙', '硬币', '钻石'}:return 'complete'else:return 'invalid' # 物品不全# 如果没有冲突,且未完成,则为有效的部分解return 'valid'print("[System] 评估器 (Evaluator) 已定义。")
3.2 步骤 2:定义“生成器” (Generator)
我们的“生成器”模拟 LLM,它在当前状态下,生成所有可能的下一步“思考”。
# 代码块 2: 定义“思考”生成器 (Thought Generator)ALL_ITEMS = ['钥匙', '硬币', '钻石']def generate_thoughts(current_solution: dict, all_items: list) -> list[dict]:"""在当前解的基础上,生成所有可能的下一步“思考” (新解)"""thoughts = []# 找到第一个未分配的盒子box_to_fill = Nonefor box in ['A', 'B', 'C']:if current_solution[box] == '?':box_to_fill = boxbreakif box_to_fill is None: # 已经填满了return []# 尝试所有可能的物品for item in all_items:new_solution = current_solution.copy()new_solution[box_to_fill] = itemthoughts.append(new_solution)return thoughtsprint("[System] 生成器 (Generator) 已定义。")
3.3 步骤 3:定义“搜索算法” (BFS Executor)
这是 ToT 的“执行器”。我们使用广度优先搜索 (BFS),它会一层一层地探索所有可能的分支。
# 代码块 3: ToT 执行器,使用广度优先搜索 (BFS)from collections import dequedef run_tot_executor(initial_task: dict, max_steps: int = 10):"""ToT 的主执行器,使用 BFS 搜索算法。"""# 搜索队列,每个元素是一个 (solution, path_str) 元组# path_str 用于追踪思考路径queue = deque([(initial_task, "Start")])# 记录已访问过的状态,防止循环visited = set()step = 0while queue and step < max_steps:step += 1current_solution, current_path = queue.popleft()# 1. 评估当前“思考”status = evaluate_thought(current_solution)# 打印搜索轨迹print(f"--- Step {step} ---")print(f" [Exploring] {current_path}")print(f" [Solution] {current_solution}")print(f" [Status] {status.upper()}")# -----------------------------------# 2. 检查状态# -----------------------------------if status == 'complete':print(f"\n======= 任务成功 (Task Complete) =======\n")print(f"最终解: {current_solution}")print(f"思考路径: {current_path}")return current_solutionif status == 'invalid':print(" [Pruning] 此分支无效,剪枝。")continue # 剪枝,不再探索此路径# -----------------------------------# 3. 生成下一步“思考”# -----------------------------------# 将 solution 转换为不可变类型 (tuple) 以便存入 setsolution_tuple = tuple(sorted(current_solution.items()))if solution_tuple in visited:print(" [Pruning] 已访问,跳过。")continuevisited.add(solution_tuple)# 这是一个 'valid' 的部分解,继续生成分支next_thoughts = generate_thoughts(current_solution, ALL_ITEMS)if not next_thoughts:print(" [Info] 无更多分支。")for thought in next_thoughts:# 将新分支加入队列new_path = f"{current_path} -> {thought}"queue.append((thought, new_path))print(f"\n======= 任务失败 (Task Failed) =======\n在 {max_steps} 步内未找到解。")# --- 运行我们的 ToT Agent ---
initial_state = {'A': '?', 'B': '?', 'C': '?'}
run_tot_executor(initial_state)
3.4 运行与分析
当你运行 run_tot_executor 时,你会在控制台看到一个清晰的“搜索树”:
[System] 评估器 (Evaluator) 已定义。
[System] 生成器 (Generator) 已定义。
--- Step 1 ---[Exploring] Start[Solution] {'A': '?', 'B': '?', 'C': '?'}[Status] VALID
--- Step 2 ---[Exploring] Start -> {'A': '钥匙', 'B': '?', 'C': '?'}[Solution] {'A': '钥匙', 'B': '?', 'C': '?'}[Status] INVALID[Pruning] 此分支无效,剪枝。
--- Step 3 ---[Exploring] Start -> {'A': '硬币', 'B': '?', 'C': '?'}[Solution] {'A': '硬币', 'B': '?', 'C': '?'}[Status] VALID
--- Step 4 ---[Exploring] Start -> {'A': '钻石', 'B': '?', 'C': '?'}[Solution] {'A': '钻石', 'B': '?', 'C': '?'}[Status] VALID
... (BFS 会继续探索 Step 3 和 4 的分支) ...
... (例如,探索 Step 3 的分支: 'A': '硬币', 'B': '钥匙', 'C': '?') ...
... (它会探索到 {'A': '硬币', 'B': '钥匙', 'C': '钻石'}) ...
--- Step X ---[Exploring] Start -> {'A': '硬币', 'B': '?', 'C': '?'} -> {'A': '硬币', 'B': '钥匙', 'C': '?'} -> {'A': '硬币', 'B': '钥匙', 'C': '钻石'}[Solution] {'A': '硬币', 'B': '钥匙', 'C': '钻石'}[Status] COMPLETE======= 任务成功 (Task Complete) =======最终解: {'A': '硬币', 'B': '钥匙', 'C': '钻石'}
...
分析:
- 在
Step 2,Agent 探索了“A 是钥匙”的路径。我们的“评估器”立刻发现这违反了线索1,判为INVALID,ToT 框架便自动“剪枝”了这条路径。 - ReAct 如果第一步猜了“A 是钥匙”,它就会卡死。
- ToT 则会继续探索
Step 3(“A 是硬币”) 和Step 4(“A 是钻石”) 的路径,最终找到正确答案。
``
(图 3:本实战的 ToT-BFS 搜索树简图)
四、[进阶] ToT, PPO 与 ReAct 的“大一统”
我们已经掌握了 ReAct 和 ToT。那么在 Agentic RL 的大框架下,它们是什么关系?
4.1 ToT vs ReAct:成本与收益的权衡
ToT 并不总是优于 ReAct。它是一种“用计算换准确率”的策略。
表格 1:ReAct 与 ToT 的关键权衡
| 特性 | ReAct (链式) | Tree-of-Thoughts (ToT) (树状) |
|---|---|---|
| 思考模式 | 线性,单路径 | 并行,多路径,可回溯 |
| 适用任务 | 简单查询、直接任务、事实获取 | 复杂规划、数学、逻辑、探索性任务 |
| 主要弱点 | 脆弱,一步错则全错 | 成本极高(计算量呈指数增长) |
| LLM 调用成本 | 低 (任务 LLL 步 ≈\approx≈ LLL 次 LLM 调用) | 极高 ( LLL 步, NNN 分支 ≈\approx≈ O(NL)O(N^L)O(NL) 次调用) |
| 实现复杂度 | 简单 (一个循环) | 复杂 (需实现搜索算法、评估器) |
4.2 PPO 如何优化 ToT?
这再次把我们专栏的(一)、(二)、(四)篇串联了起来。
在我们的“手写实战”中,“生成器”和“评估器”都是基于规则的 (Rule-based)。但在真实世界中,问题是开放的,我们必须用 PPO 来“训练”这两个组件。
- 训练“生成器” (Policy Network):
- 目标:PPO 可以训练“生成器” LLM,使其从一开始就倾向于生成“更有希望”的分支。
- 方法:在 PPO 中,LLM Generator 就是策略 (Policy)。如果一条分支最终导向了“成功”(高 Reward),PPO 就会增加生成这条分支(这个
Thought)的概率。
- 训练“评估器” (Value Network):
- 目标:PPO 可以训练“评估器” LLM,使其能准确预测一个“部分解 (Thought)”的未来潜在价值。
- 方法:这完美对应 PPO 中的 Critic (Value Function)!
- 在专栏(一)中,Critic V(s)V(s)V(s) 预测的是游戏状态 sss 的未来总回报。
- 在这里,Critic V(thought)V(\text{thought})V(thought) 预测的就是这个“思考” thoughtthoughtthought 未来的成功概率。
- 有了一个 PPO 训练的强大 Critic,ToT 的“搜索算法”就可以更智能:优先探索那些 KaTeX parse error: Unexpected end of input in a macro argument, expected '}' at end of input: …(\text{thought) 分数更高的分支。
五、🧠 专栏面试问题角 🧠
Q1:ToT (Tree-of-Thoughts) 相比 ReAct,核心解决了什么问题?
A1:ToT 核心解决了 ReAct 的**“线性思考”和“脆弱性”问题。ReAct 无法从错误的决策中回溯,而 ToT 通过引入多路径探索**、评估和搜索机制,允许 Agent 在一个思考节点上生成多个可能的下一步,并评估它们的好坏,然后选择最优路径或进行回溯,极大地提高了在复杂规划和推理任务上的鲁棒性和准确性。
Q2:ToT 框架最大的实现“瓶颈”或“成本”在哪里?
A2:计算成本(或 LLM 调用成本)。ToT 的搜索空间是指数级的。如果一个任务需要 L=5L=5L=5 步,每一步都探索 N=3N=3N=3 个分支,理论上最多需要 35≈2433^5 \approx 24335≈243 次 LLM 调用(生成+评估)。而 ReAct 只需要 5 次。这导致 ToT 的延迟非常高且成本昂贵。
Q3:在 ToT 中,“评估器 (Evaluator)” 是如何实现的?它必须是 LLM 吗?
A3:不必。评估器是 ToT 的灵魂,其实现方式多样:
- 启发式 (Heuristic):如我们代码实战中,使用一个基于规则的 Python 函数。它速度快、成本低,但只适用于规则明确的领域(如下棋、24点)。
- LLM 评估 (Self-Correction):用 LLM 本身来评估分支。例如,向 LLM 提问:“这三个方案中,哪个最有可能解决问题?请打分。”
- 训练的价值模型 (Value Model):(Agentic RL 的做法) 单独训练一个模型(Critic),其唯一工作就是给“部分思考”打分。这个模型可以用 PPO 等 RL 算法来优化,使其能准确预测该分支的未来价值。
Q4:在你的项目中,你会优先使用 ReAct 还是 ToT?
A4:这是一个权衡 (Trade-off) 问题。
- 我会默认使用 ReAct。对于 90% 的任务(如信息提取、API 调用、简单问答),ReAct 成本低、速度快,已经足够。
- 我只会在那些“高风险、高复杂度”的任务上使用 ToT。例如,需要深度规划的“法律合同分析”、“多步骤的科学实验设计”或“关键的数学推导”。在这些场景下,准确性远比成本和延迟更重要,ToT 的“指数级成本”是值得付出的代价。
六、总结与参考链接
- 总结:今天我们从 ReAct 的“线性困境”出发,深入学习了 ToT 框架。我们知道了 ToT 是如何通过分解 (Decomposition)、生成 (Generation)、评估 (Evaluation) 和搜索 (Search) 四大组件,将思考模式从“链”升级为“树”的。我们还从零手写了一个基于 BFS 搜索的 ToT 执行器,直观地看到了它“剪枝”无效路径的过程。
- 串联:我们再次打通了专栏的知识。ToT 的“生成器”和“评估器”正是 PPO (专栏一) 可以大显身威的地方——PPO 的 Policy 网络可以优化“生成”,而 PPO 的 Value 网络可以优化“评估”。
- 展望(下一步):我们已经解决了“如何做”(ReAct) 和“如何深入思考”(ToT)。但目前为止,Agent 的“知识”完全依赖于 LLM 内部的参数。如果任务需要**“此时此地”的外部知识**(例如:“总结一下这篇刚发布的 100 页财报”),Agent 该怎么办?
- 这就是我们专栏的下一篇要探讨的核心问题:RAG (Retrieval-Augmented Generation),即 Agent 如何拥有“外部记忆”。
参考链接:
- ToT 原始论文 (必读):Yao, S., et al. (2023). Tree of Thoughts: Deliberate Problem Solving with Large Language Models. arXiv:2305.10601
- ToT 的 GitHub 实现 (参考):Original Implementation for Game of 24
