从零学习 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)](#从零学习 Agentic RL(四)—— 超越 ReAct 的线性束缚:深入解析 Tree-of-Thoughts (ToT))
-
- [🦈 一、前言:ReAct 的"线性"困境](#🦈 一、前言:ReAct 的“线性”困境)
- [二、[原理] 从"链"到"树":ToT 的核心思想](#二、[原理] 从“链”到“树”:ToT 的核心思想)
-
- [2.1 ToT 的四大核心组件](#2.1 ToT 的四大核心组件)
- [三、[实战] 从零实现一个简化的 ToT 框架](#三、[实战] 从零实现一个简化的 ToT 框架)
-
- [3.1 步骤 1:定义"评估器" (Evaluator)](#3.1 步骤 1:定义“评估器” (Evaluator))
- [3.2 步骤 2:定义"生成器" (Generator)](#3.2 步骤 2:定义“生成器” (Generator))
- [3.3 步骤 3:定义"搜索算法" (BFS Executor)](#3.3 步骤 3:定义“搜索算法” (BFS Executor))
- [3.4 运行与分析](#3.4 运行与分析)
- [四、[进阶] ToT, PPO 与 ReAct 的"大一统"](#四、[进阶] ToT, PPO 与 ReAct 的“大一统”)
-
- [4.1 ToT vs ReAct:成本与收益的权衡](#4.1 ToT vs ReAct:成本与收益的权衡)
- [4.2 PPO 如何优化 ToT?](#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) : S t a t e 0 → S t a t e 1 → S t a t e 2 → . . . State_0 \rightarrow State_1 \rightarrow State_2 \rightarrow ... State0→State1→State2→...
- ToT 是树 (Tree) :在任何一个 S t a t e State State 节点,都可以分岔出 N N N 个可能的下一步 S t a t e State State。
``
(图 2:从"链式思考" (左) 到"树状思考" (右) 的演变)
2.1 ToT 的四大核心组件
ToT 框架的实现,依赖于四个关键组件的协同工作:
- 分解 (Decomposition) :
- 作用 :将一个复杂的大任务,分解为 K K K 个有序的"思考步骤"(即树的 K K K 层深度)。
- 类比:ReAct 的 T-A-O 循环是隐式的、一步一步的分解。ToT 则是有意识地将问题规划为多个阶段。
- 生成 (Generation) :
- 作用 :在树的任何一个节点(一个部分思考),调用 LLM 生成 N N N 个不同 的、可能的"下一步思考"(即树的 N N N 个分支)。
- 实现:通过修改 Prompt,例如 "Based on the current plan, propose 3 different next steps."
- 评估 (Evaluation) :
- 作用 :这是 ToT 的灵魂 。你需要一个"评估器 (Evaluator)"来判断 N N N 个新生成的"思考分支"中,哪一个"更靠谱"。
- 实现 :评估器可以是:
- 启发式 (Heuristic):一个简单的、基于规则的函数(例如,在24点游戏中,"计算结果是否更接近24")。
- LLM 自我评估 :调用 LLM,让它自己给这 N N N 个分支打分(例如 "Rate these 3 thoughts from 1-10 on their likelihood of success.")。
- 价值函数 (Value Function):(剧透) 这就是 PPO 的 Critic 可以发挥作用的地方!
- 搜索 (Search) :
- 作用 :有了 N N N 个分支和它们的"评估分数",你需要一个"搜索算法"来决定接下来探索哪条分支。
- 实现 :可以是:
- 广度优先搜索 (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)
我们的"评估器"是一个启发式函数,它负责检查一个"部分解"是否与线索冲突。
python
# 代码块 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' (完整且有效)
"""
# 检查线索 1
if solution.get('A') == '钥匙':
return 'invalid'
# 检查线索 2
if 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,它在当前状态下,生成所有可能的下一步"思考"。
python
# 代码块 2: 定义"思考"生成器 (Thought Generator)
ALL_ITEMS = ['钥匙', '硬币', '钻石']
def generate_thoughts(current_solution: dict, all_items: list) -> list[dict]:
"""
在当前解的基础上,生成所有可能的下一步"思考" (新解)
"""
thoughts = []
# 找到第一个未分配的盒子
box_to_fill = None
for box in ['A', 'B', 'C']:
if current_solution[box] == '?':
box_to_fill = box
break
if box_to_fill is None: # 已经填满了
return []
# 尝试所有可能的物品
for item in all_items:
new_solution = current_solution.copy()
new_solution[box_to_fill] = item
thoughts.append(new_solution)
return thoughts
print("[System] 生成器 (Generator) 已定义。")
3.3 步骤 3:定义"搜索算法" (BFS Executor)
这是 ToT 的"执行器"。我们使用广度优先搜索 (BFS),它会一层一层地探索所有可能的分支。
python
# 代码块 3: ToT 执行器,使用广度优先搜索 (BFS)
from collections import deque
def 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 = 0
while queue and step < max_steps:
step += 1
current_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_solution
if status == 'invalid':
print(" [Pruning] 此分支无效,剪枝。")
continue # 剪枝,不再探索此路径
# -----------------------------------
# 3. 生成下一步"思考"
# -----------------------------------
# 将 solution 转换为不可变类型 (tuple) 以便存入 set
solution_tuple = tuple(sorted(current_solution.items()))
if solution_tuple in visited:
print(" [Pruning] 已访问,跳过。")
continue
visited.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 时,你会在控制台看到一个清晰的"搜索树":
python
[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 调用成本 | 低 (任务 L L L 步 ≈ \approx ≈ L L L 次 LLM 调用) | 极高 ( L L L 步, N N N 分支 ≈ \approx ≈ O ( N L ) 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) 预测的是游戏状态 s s s 的未来总回报。
- 在这里,Critic V ( thought ) V(\text{thought}) V(thought) 预测的就是这个"思考" t h o u g h t thought thought 未来的成功概率。
- 有了一个 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 = 5 L=5 L=5 步,每一步都探索 N = 3 N=3 N=3 个分支,理论上最多需要 3 5 ≈ 243 3^5 \approx 243 35≈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