这篇重点学什么
本篇重点:比较 Beam Search 与 MCTS / UCT 怎样选择候选,推演尝试新选择(探索)、利用已有好选择与搜索预算之间的关系。
开始读正文 ↓学过相近内容?查看哪些可以跳过
两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。
工作流与决策搜索
共同基础任务、状态、依赖与预算是共同语言,固定调度与搜索未知动作分别学习。
另一条路线补充什么?
画出没有循环依赖的任务图(DAG),找出耗时最长的依赖路径(关键路径),再规定角色、预算和失败后受影响的任务。
读完这篇,你能做什么
Java复现窄束丢失较好完整路径;Python在有限树执行选择、扩展、模拟和回传并核对预算。
学习理由、准备要求与知识点
为什么学它
搜索依赖候选评分;评分有偏差时,多搜也可能放大错误。增加计算量,不保证任务真正做得更好。
先读这些内容
知识点清单
- 束宽与累计分数
- 前缀剪枝的不可逆性
- UCT均值与探索项
- 访问次数回传
- 候选评分与真实奖励
- 预算公平比较
- 可逆环境与副作用
- 树搜索最坏规模
运行前先确认UCT ECML 2006、OpenSpiel v1.6.9、ToT论文v2;Java17束搜索与Python3.14标准库有限树实验。未运行语言模型或GPU。
01先弄懂它是什么
搜索时不想把所有可能都继续展开,可以每轮只保留分数最高的几个未完成候选,这叫 Beam Search,束宽 W 决定最多留几个。生成文本常按累计对数概率打分,规划任务则可能看预期价值或代价。要先说清楚优化的是哪种分数:模型觉得更常见的文字,不一定更能在环境中完成任务。没有明确评分目标,就很难讨论搜到的结果是不是更好。
只保留当前最高分,可能过早丢掉后劲更好的路线。例如第一步 A 得 -.1、B 得 -.2;第二步 A 再加 -10、B 再加 -.1。若 W=1,先留下 A,最终得 -10.1;若 W=2,才能保留并找到最终得 -.3 的 B 路线。这个例子说明,前半段暂时领先,不保证整条路线最好;增加束宽有时能改善结果,也不等于任务成功率必然严格上升。
蒙特卡洛树搜索(MCTS)把有限尝试次数分给不同路线。一次搜索先沿已有树选路,再在需要时扩展新分支,接着通过模拟、价值估计或终局结果获得反馈,最后沿走过的路线更新记录,这一步叫回传。
UCT 是一种选路规则:既看某个动作目前的平均回报,也给还没充分尝试的动作一些探索机会。这样做是在分配搜索预算,并不自动证明评分可靠。论文中的收敛和有限样本结论有模型、奖励等前提,不能直接说任意语言推理树多搜几次就一定正确。
文中这几个词,先认一认
- 束宽
- 每一轮最多保留多少个未完成的候选前缀。
- UCT
- 在树搜索中,用已有平均回报和“不确定、值得多试”的程度共同决定接下来探索哪个动作。
- 回传
- 一次搜索得到结果后,沿刚才走过的路线往回更新各节点的累计奖励和访问次数。
02看看它怎么工作
UCT常见形式为Q_i+c×sqrt(log(N)/n_i),Q_i是子节点平均回报,N是父节点访问数,n_i是子节点访问数,c控制探索强度。未访问节点需要显式优先扩展,不能直接除以零。奖励尺度变化会改变Q与探索项的相对作用,因此比较配置时应统一奖励范围或同步调整c。
Python实验是一棵深度二的单人二叉树,四个叶子回报固定为0、.3、.7、1。一次模拟只新建一个节点,随后用固定随机数生成器走到叶子,把同一回报沿路径累加。单人任务不翻转符号;在对弈中不同节点所属玩家不同,回传视角必须另行设计,不能照搬本实验。
Agent树节点可能是完整环境状态,也可能只是文本前缀。若工具动作会真实发邮件、扣库存或改变网页状态,搜索多个分支需要独立快照、沙箱或可逆模拟,不能在同一生产环境里试遍分支再假装只执行最终答案。文本自评也不是真实执行反馈,评价器误差会被搜索重复选择而放大。
先跑通一个最小例子
Python3.14标准库执行experiment.py。在四叶单人树上运行400次UCT模拟,未调用模型或真实工具。
展开收起代码 · experiment.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import math
import random
from dataclasses import dataclass, field
@dataclass
class Node:
path: tuple
visits: int=0
total: float=0.
children: dict=field(default_factory=dict)
def search(budget, seed=23):
if type(budget) is not int or budget<1: raise ValueError("budget")
rng=random.Random(seed)
root=Node(())
rewards={(0,0):0.,(0,1):.3,(1,0):.7,(1,1):1.}
for _ in range(budget):
node=root;path=[node]
while len(node.path)<2 and len(node.children)==2:
def uct(child):
return child.total/child.visits+math.sqrt(2*math.log(node.visits)/child.visits)
node=max(node.children.values(),key=lambda child:(uct(child),tuple(-x for x in child.path)))
path.append(node) # 选择阶段沿已有树,平局规则固定。
if len(node.path)<2:
action=next(a for a in (0,1) if a not in node.children)
child=Node(node.path+(action,));node.children[action]=child
node=child;path.append(node) # 每轮最多扩展一个新节点。
rollout=node.path
while len(rollout)<2: rollout+=(rng.randrange(2),)
value=rewards[rollout]
for ancestor in path:
ancestor.visits+=1;ancestor.total+=value # 单人任务同一回报向根回传。
best=max(root.children,key=lambda a:(root.children[a].visits,-a))
return best,root
best,root=search(400)
assert best==1
assert root.visits==400
assert sum(c.visits for c in root.children.values())==400
assert all(0<=c.total<=c.visits for c in root.children.values())
again,copy=search(400)
assert again==best and [c.visits for c in copy.children.values()]==[c.visits for c in root.children.values()]
assert search(1)[0] in (0,1)
try: search(0)
except ValueError: pass
else: raise AssertionError("zero budget")
print(f"best_root_action={best} simulations={root.visits}")
print("PASS 7 checks; finite single-player tree, not language-model search")
核对运行结果best_root_action=1、simulations=400,输出PASS 7 checks;仅针对固定有限树。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
N0["声明状态动作与搜索评分"]
N1["设置束宽或模拟预算"]
N2["扩展并评价候选分支"]
N3["按规则剪枝或回传统计"]
N4["执行独立成功判定与成本对照"]
N0 --> N1
N1 --> N2
N2 --> N3
N3 --> N4按顺序看,每一步做了什么
- 声明状态动作与搜索评分
- 设置束宽或模拟预算
- 扩展并评价候选分支
- 按规则剪枝或回传统计
- 执行独立成功判定与成本对照
03对照实现代码
OpenSpiel v1.6.9;UCT ECML 2006 · SearchNode.uct_value / MCTSBot.mcts_search
原创UCT标量模型,参考固定OpenSpiel实现;不复制完整棋类引擎、PUCT或多玩家回传逻辑。
import math
def uct(mean_return, child_visits, parent_visits, coefficient):
if child_visits == 0:
return float("inf") # 未访问分支优先探索,避免除零。
exploration = coefficient * math.sqrt(math.log(parent_visits)/child_visits) # 不确定性奖励。
return mean_return + exploration # 均值和探索项使用同一奖励尺度。
固定源码SearchNode保存explore_count、total_reward、player、children等字段。uct_value在节点已有可证明结果时使用该结果,否则对未访问节点返回无穷大,再对已访问节点计算均值加探索项。本文短函数只展示最后两类逻辑,没有复现已解子树或多玩家结果处理。
MCTSBot把树策略选择、叶节点评估和回传连接起来;RandomRolloutEvaluator复制状态后随机执行合法动作直到结束或约定长度。复制状态是重要边界,它避免候选模拟直接破坏原始局面。真实外部工具未必具有clone能力,不能因为库里能复制棋盘就假设网页或业务数据库也能回滚。
UCT与PUCT等规则使用的先验和探索方式不同,参数名称相似不代表相同算法。阅读实现时要确认实际选择函数、是否启用已解状态传播以及最终按访问数还是均值选动作。实验把这些选择固定为单人UCT与根访问数,便于读者逐项核验。
Tree of Thoughts展示以较大语言片段组织候选并用评价和搜索推进任务,但并不意味着所有此类方法都等同MCTS。本文把束搜索、树搜索框架和UCT实例分别实现或解释,保留它们的假设差异;没有调用语言模型,也没有重现实验论文的任务成绩。
代码先调用谁,再调用谁
- 读取合法动作与可复制状态
- 按UCT选择已有节点
- 扩展新动作并评估叶子
- 按玩家或任务视角回传
- 按明确规则选最终根动作
04动手写一个例子
有限深度图上的束搜索反例
输入非正有限边分数、起点、终点、束宽和深度上限,返回保留束中完成路径的最高分;终点候选保留,不再扩展。
- 输入 1
A前缀-.1后续-10,B前缀-.2后续-.1,W=1- 输出 1
-10.1
- 输入 2
相同图,W=2- 输出 2
-.3
每轮从当前至多W个候选扩展,完成候选原样保留,其余按出边生成新分数;按累计分数降序及节点编号作确定排序后截取前W个。这里没有合并到同一状态的路径,也没有路径重建,仅返回分数,使剪枝行为能够用最小程序观察。
不变量只保证每层保留的是该层实际生成候选中的前W名,而不是完整指数路径集合中的全局前W名。之前被剪掉的路径不会重新出现,因此不能从局部排序推出全局最优。起点就是目标时零长度得分为零,其他零深度情况无解,这些边界直接体现在测试里。
先验证整个邻接表再开始搜索,拒绝NaN、正边分数和越界节点。有限深度阻止有环图无限展开,但宽度上限并不消除每层排序成本。复杂度必须包括图校验与候选排序,神经模型生成和评分的真实成本则不属于这个给定边分数模型。
为什么这样写是对的frontier至多W项,是上一轮实际生成候选按约定排序后的前W项;完成项不扩展。
D为深度,b=max(1,最大出度);先校验V个节点E条边,再每层排序至多Wb项。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static void near(double expected,double actual) {
if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
throw new AssertionError(expected+" != "+actual);
}
static void check(boolean ok) { if(!ok) throw new AssertionError(); }
static void bad(Runnable r) {
try { r.run(); } catch(IllegalArgumentException e) { return; }
throw new AssertionError("invalid input accepted");
}
record Edge(int to,double logp) {}
record Candidate(int node,double score) {}
static OptionalDouble beam(List<List<Edge>> graph,int start,int goal,int width,int depth) {
if(graph==null||start<0||goal<0||start>=graph.size()||goal>=graph.size()||width<1||depth<0)
throw new IllegalArgumentException("configuration");
for(List<Edge> edges:graph) {
if(edges==null) throw new IllegalArgumentException("row");
for(Edge e:edges) if(e==null||e.to()<0||e.to()>=graph.size()||!Double.isFinite(e.logp())||e.logp()>0)
throw new IllegalArgumentException("edge");
}
List<Candidate> frontier=List.of(new Candidate(start,0));
Comparator<Candidate> rank=Comparator.comparingDouble(Candidate::score).reversed().thenComparingInt(Candidate::node);
for(int d=0;d<depth;d++) { // 深度上限保证有环图也终止;这不是UCT访问回传。
List<Candidate> next=new ArrayList<>();
for(Candidate c:frontier) {
if(c.node()==goal) {next.add(c);continue;} // 完成项保留,不继续生成动作。
for(Edge e:graph.get(c.node())) {
double score=c.score()+e.logp();
if(!Double.isFinite(score)) throw new IllegalArgumentException("overflow");
next.add(new Candidate(e.to(),score));
}
}
next.sort(rank); // 按当前累计分数剪枝,并非按未知最终成功率排序。
frontier=new ArrayList<>(next.subList(0,Math.min(width,next.size())));
if(frontier.isEmpty()) break;
}
return frontier.stream().filter(c->c.node()==goal).mapToDouble(Candidate::score).max();
}
public static void main(String[] args) {
List<List<Edge>> g=List.of(List.of(new Edge(1,-.1),new Edge(2,-.2)),List.of(new Edge(3,-10)),List.of(new Edge(3,-.1)),List.of());
near(-10.1,beam(g,0,3,1,2).orElseThrow());
near(-.3,beam(g,0,3,2,2).orElseThrow());
near(0,beam(g,3,3,1,0).orElseThrow());
check(beam(g,0,3,2,0).isEmpty());
check(beam(List.of(List.of(new Edge(0,0))),0,0,1,4).isPresent());
check(beam(List.of(List.of(),List.of()),0,1,1,3).isEmpty());
bad(()->beam(g,0,3,0,2));
bad(()->beam(List.of(List.of(new Edge(0,Double.NaN))),0,0,1,1));
System.out.println("PASS 8 cases");
}
}
这些特殊情况也要试一试
- 起点即目标返回零
- 深度零不额外扩展
- 无出边可使候选集清空
- 非法宽度和
NaN分数拒绝
进阶选读 · 带昂贵验证器的预算规划
场景一次模型评价或工具执行耗时很高,搜索预算成为主要成本。
做法缓存可复用状态评分,记录每次模型调用、token与工具预算;用独立验证器评估最终候选,比较同计算预算下的贪心、束搜索与UCT。
复杂度完整深度D、分支b的树可达O(b^D);预算化MCTS的成本随模拟数和路径长度增长,不是一般规划的多项式最优解法。
好处与代价更多探索可能找到更好路径,也可能重复优化错误评价器并增加尾延迟。
05项目里怎么用
固定任务集与总预算后比较搜索方法,至少同时报告成功率、生成量、验证次数和耗时。只给某方法更多候选然后比较最终成功率,会混淆算法改进与计算投入。随机树搜索应记录种子并用多个种子评估,单个确定测试只验证实现没有明显错误。
搜索中的评分器负责挑候选,最终检查器负责确认任务成功,两者应分开。语言任务尤其要检查模型自评是否偏袒自己的答案,以及候选是否只是重复说法。十条高度相似的路线,并不等于十次独立探索;缓存去重应根据真实状态区分,而不只看展示文字。涉及外部写入时,先在可隔离环境里规划,最终提交仍要检查权限并防止重复执行。
模型训练与推理搜索是两种改变性能的途径。搜索得到好轨迹可以成为后续训练数据,但需要保留筛选条件和失败分布。本文有限树实验没有训练神经网络,不测真实工具延迟,也不证明大规模搜索收敛速度,报告中应明确这些范围。
容易踩的坑
- 把高概率文本当高任务奖励
- 把局部束排序称全局最优
- 未访问节点直接除零
- 对弈回传忘记玩家视角
- 搜索分支共享不可逆外部状态
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 贪心 | 成本低、容易复现 | 早期错误难恢复 | 简单低风险路径 |
| 束搜索 | 保留多个高分前缀 | 早期剪枝不可逆 | 评分较可靠的序列生成 |
| UCT搜索 | 预算自适应分配探索 | 依赖评估和可模拟环境 | 可回放且奖励可判断的规划 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01更宽束一定提高任务成功吗?
展开参考答案与推导
不保证,优化的是声明的评分,评分与真实任务可能不一致。
02UCT探索项为何含访问次数?
展开参考答案与推导
访问少的分支不确定性更高,随着访问增加探索奖励下降。
03MCTS是否自动训练模型?
展开参考答案与推导
不是,它是搜索过程;训练需要另外定义数据和参数更新。
换个情况,还会处理吗
01搜索后更容易奖励作弊怎么办?
展开参考答案与推导
独立审计评价器,限制漏洞并用最终任务状态验证,不只扩大搜索预算。
02工具无法回滚如何做树搜索?
展开参考答案与推导
使用沙箱、只读模拟或可复制快照;不可在生产状态上随意展开分支。
03不同方法耗时不等如何比较?
展开参考答案与推导
在相同计算或调用预算下绘制质量成本对照,单独报告额外计算。
写代码试一试
01复现窄束反例。
- 输入 / 条件
两条前缀及后续分数如正文- 输出 / 预期
W1=-10.1,W2=-.3
提示 逐层剪枝
展开参考答案与推导
固定图和比较器,展示被删路径不会恢复。
02处理零访问UCT。
- 输入 / 条件
子节点访问0- 输出 / 预期
优先探索
提示 不要除零
展开参考答案与推导
返回无穷大或先显式扩展未访问动作。
03检查回传守恒。
- 输入 / 条件
400次单人根模拟- 输出 / 预期
根访问400,子访问和400
提示 每次路径只更新一次
展开参考答案与推导
固定种子并检查所有累计奖励落在允许范围。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
Java复现窄束丢失较好完整路径;Python在有限树执行选择、扩展、模拟和回传并核对预算。
本篇重点加练
搜索依赖价值尺度和评价器真实性,加练分数变换对探索行为的影响。
- UCT均值与探索项
- 候选评分与真实奖励
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
自建深度 3、每层至多 3 个分支的树,给出边分数和叶子任务奖励;分别手算束宽 1、2 的保留过程。
- 提交内容
- 搜索树及剪枝轨迹
- 怎样算完成
- 累计分数定义一致
- 前缀被删后不隐式恢复
练到的知识点束宽与累计分数 · 前缀剪枝的不可逆性
- 作业 02原理问答
为一个含未访问节点的 UCT 选择时刻列出访问次数、价值估计与探索项,说明叶子评估与真实任务奖励的证据差异。
- 提交内容
- 节点状态表与定义
- 怎样算完成
- 未访问策略显式
- 统计量来源可追踪
练到的知识点UCT均值与探索项 · 访问次数回传 · 候选评分与真实奖励
- 作业 03代码推演
自定三子节点的访问次数、平均奖励和探索系数,比较奖励统一乘十、统一加常数两种变换下 UCT 选择;完整声明未访问节点政策。
- 提交内容
- 三种评分表与选择依据
- 怎样算完成
- 奖励和探索项尺度明确
- 节点统计合法
练到的知识点UCT均值与探索项 · 候选评分与真实奖励
综合应用
- 作业 04编程实践
实现 CPU 小树 MCTS,保存每次选择、展开、评估及回传日志;用可枚举树核对访问计数与最终动作。
- 提交内容
- 搜索器及审计日志
- 怎样算完成
- 节点计数可独立核对
- 终止与预算处理完整
练到的知识点UCT均值与探索项 · 访问次数回传 · 树搜索最坏规模
- 作业 05排错实验
在固定树上故意构造偏好错误叶子的评分器,比较不同搜索预算下真实成功率;同时报告评分值与任务奖励。
- 提交内容
- 受控评分偏差实验
- 怎样算完成
- 评分器和成功判据独立
- 预算增加的结果完整保留
练到的知识点候选评分与真实奖励 · 预算公平比较
- 作业 06面试场景
环境动作会产生不可回滚的实际变更。审阅将真实环境直接作为搜索分支的方案,定义可用于研究比较的模拟边界与偏差验证。
- 提交内容
- 实验环境设计审阅
- 怎样算完成
- 副作用边界明确
- 模拟误差有核验方案
练到的知识点预算公平比较 · 可逆环境与副作用
- 作业 07编程实践
在 CPU 固定小树上保持真实叶子奖励不变,仅改变评分器的尺度和偏置,运行相同扩展预算,记录访问分布及最终真实回报。
- 提交内容
- 搜索实验与节点分布表
- 怎样算完成
- 评价器与真奖励分开
- 预算及随机源相同
练到的知识点UCT均值与探索项 · 候选评分与真实奖励
- 作业 08设计评审
搜索器用同一个评分器生成候选并选择最终答案。设计独立评价协议,加入评分器最不擅长的保留子任务,并说明如何识别选择放大偏差。
- 提交内容
- 独立审计与子任务划分
- 怎样算完成
- 最终评价不复用选择信号
- 困难子任务事先定义
练到的知识点候选评分与真实奖励
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
比较束搜索与 MCTS,在等节点扩展预算及等 CPU 时间两种条件下评估一组保留树;说明两种公平口径能支持的结论。
- 提交内容
- 双预算比较与报告
- 怎样算完成
- 成本定义可复核
- 保留任务未参与选超参
练到的知识点束宽与累计分数 · UCT均值与探索项 · 预算公平比较 · 树搜索最坏规模
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
把 UCT 迁移到算法配置试验时,一次试验成本差异很大,原访问次数还能代表相同预算吗?设计两种预算计量并比较可能的选择偏好。
- 提交内容
- 异成本试验模型与对照
- 怎样算完成
- 成本与奖励单位明确
- 迁移后的假设变化列明
练到的知识点UCT均值与探索项 · 候选评分与真实奖励
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
UCT原论文课程镜像 (新标签页)↗
阅读生成模型假设、选择规则与理论条件。
- 实践参考文档英文
OpenSpiel v1.6.9 MCTS (新标签页)↗
核对UCT、状态复制和回传视角。
- 深入参考文档英文
Tree of Thoughts v2 (新标签页)↗
理解语言片段搜索,不将其一概称为MCTS。