这两年做强化学习(RL)的朋友应该都有同感:训练曲线跑得漂漂亮亮,真正放到环境里却经常判若两“鼠”。这个问题我自己踩了不知道多少回,每次卡到最后才发现,模型没坏、奖励没写错,真正拖后腿的往往是部署阶段那套过于朴素的“动作选择”机制。最近在某AI顶会投稿列表里看到一篇很扎心的题目:Breaking the Performance Ceiling in Reinforcement Learning requires Inference Strategies。意思是,强化学习性能天花板并不总是训练问题,而可能出在推断策略(inference strategies)上。这篇博文我想基于这个方向,把我自己的理解、方案选型思路、以及在一个模拟项目X中完整实验过的一套推断策略实操流程,从零到一讲清楚。适合正在做RL落地、做策略部署、或者想把已有训练产物“吃干榨净”的开发者参考。
1. 项目概述:性能天花板到底卡在哪里
1.1 训练上限与推断上限是两回事
我们在评估RL效果时,习惯性盯着一组训练曲线看:前期rm上升慢,中期陡增,后期收敛。曲线一旦不涨,第一反应就是调学习率、换算法、加探索奖励,但很少有人问一句:就算我把策略网络训练到接近完美,部署时的动作选择方式是否真的能拿到那个“完美”的性能?
先拆一个最简单的强化学习部署闭环。训练阶段,我们获得一个策略网络π和(通常是配套的价值网络V或Q)。到了部署阶段,最常见的行为模式是:
- 离散动作场景:取argmax,即选概率最高的动作;
- 连续动作场景:直接用策略网络输出动作均值,甚至不做任何平滑;
- 偶尔会在动作上加一点噪声,美其名曰“探索”,实际上既增加抖动,又掩盖真实性能。
这背后有一个巨大的隐性假设:策略网络已经足够准确,且推断时每一步做局部最优选择,就能得到全局最优结果。但真实世界的任务往往是稀疏奖励、非平稳分布、状态被局部遮蔽,甚至环境本身就是多阶段决策问题。此时贪心推断带来的次优性,会随着决策步数逐级放大。换句话说,训练上限是“这个模型理论上能学到多好”,推断上限是“这个系统实际能用多好”,二者之间隔着的,正是推断策略。
我在某实验室参与过一个迷宫导航模拟项目X:智能体只能看到当前位置和目标相对坐标,奖励只在到达终点时给+1,其余所有步数都是0。PPO训练到后期,训练集成功率可以到75%,但换到未见过的测试迷宫,成功率直接掉到30%。一开始怀疑过拟合,后来把同一套参数用更复杂的网络重训,训练Set指标涨了一些,测试Set依然纹丝不动。最后我们发现,测试时如果用价值网络对多条候选轨迹重新排序,而不是直接argmax,成功率能拉到接近60%。那个差距,就是训练和推断之间的天花板。
1.2 推断策略为什么在2025年成了显学
这个题目在2025年出现其实不意外。过去两年,大语言模型领域把“test-time compute”炒得非常热:同样的模型权重,推断时多花一点计算做搜索、验证、重排,效果可以大幅提升。这个逻辑迁移到强化学习上,就是:模型训练阶段的产物只是一个“可用的决策基础”,并不等于“决策上限”。你完全可以在部署时,用多条rollout采样、树搜索、价值重排、不确定性引导的自适应计算分配这些手段,从同一个模型中榨出更高性能。
这类做法并不是新东西。AlphaGo系列早就靠MCTS把同一个策略网络玩出远超纯策略推断的水平;在线规划、MPC这些领域也一直在用“想一想再动”的思路。但问题是,过去大部分RL论文的评测流程默认“训练完就贪心部署”,导致很多人对推断策略的认知停留在“后处理”层面,甚至觉得“这是工程优化,不是算法贡献”。2025年前后大家忽然看清一个关键点:如果推断阶段能带来肉眼可见的性能突破,那么它本身就是RL系统设计的一部分,而不是可有可无的补丁。
落地层面,这意味着两件事:第一,做RL项目时,训练阶段产出的策略网络和critic网络要尽量为推断策略预留接口;第二,评估实验时,不能只报告“训练模型直接部署”的效果,而应该对比“朴素推断”和“推断策略加持”的差异。这篇文章后面所有的实操环节,都围绕这个思路展开。
2. 方案选型:为什么推断策略能打破性能天花板
2.1 贪心推断的隐性假设与失效场景
先明确一下“贪心推断”到底在做什么。离散控制下,贪心推断的策略是在当前状态s_t下选择使得策略网络输出概率或Q值最大的动作a_t,执行后进入下一个状态,再重复这个过程。这个机制成立的前提有三:
- 策略网络和价值函数都是有偏低的估计误差,至少不能出现系统性高估;
- 当前状态的局部最优动作,不会把决策推向一个发生概率很低的全局死胡同;
- 环境满足马尔可夫性,且单步决策之间没有隐藏的长期依赖。
这三条在学术benchmark里有时成立,但在真实工程里几乎没有一条是严格成立的。以模拟项目X为例,智能体在迷宫拐角处,策略网络能看到的局部信息严重不足,argmax动作经常是在两个相邻的高概率动作之间犹豫,一旦选错方向,后面就跑进死路;而价值网络实际上知道“下一步只要能绕出拐角,后续成功概率很高”。这种“单步概率高、累积价值低”的情况,光靠训练改进很难解决,因为训练数据的分布主要来自局部成功路径,对于一些关键岔路口的区分度天然不足。
另一个极端是连续控制里的“均值执行”模式。很多基于高斯策略的RL训练,训练时采样的动作是有方差的,推断时却直接把均值当成确定性输出。这个做法默认策略分布是单峰的,且峰足够尖,但真实训练出来的策略分布往往是多峰或者带有大量偶然性偏置的。忽略分布信息去执行均值,会把策略“去随机化”成一条中穿过障碍物边界的路径,导致部署性能雪崩。这个现象在一些机械臂抓取任务里特别明显,同样一个策略,直接执行均值和从分布中采样多个候选再选优,成功率能差好几个百分点。
2.2 三种可落地的推断策略路线
既然问题出在“决策方式过于简单”,那推断策略可以怎么做?我在实际操作中把常见方案收敛成三条路线,分别对应不同场景和限制条件。
第一条是候选轨迹生成与价值重排(rollout reranking)。核心思路是:既然单次动作选择可能看走眼,那就从策略网络里采样多个候选动作、展开多条轨迹,然后用训练好的价值网络对每条轨迹的期望回报打分,最后执行最优轨迹的第一步。这条路线实现成本最低,几乎不需要修改训练流程,只要具备两个条件即可:策略网络自带随机性,以及critic网络有一定可靠性。
第二条是决策时树搜索(search-based inference)。把当前状态当成根节点,利用策略网络作为先验分布生成子节点候选,利用价值网络对叶节点做评估,通过多次playout把值反向传播回根节点,再选择经过搜索加权的动作。这套方案的性能通常最高,但需要定义推理时的状态转移模型,在仿真环境或存在环境模型的场景下很合适;如果没有模型,就只能生成开环候选轨迹,相当于第一条路线的复杂版本。
第三条是自适应计算分配(adaptive compute allocation)。不是每个状态都需要同等规模的推断计算。有些区域策略网络很自信、价值网络的不确定性很低,直接贪心执行就够;有些区域是高价值冲突区,才需要启动重排或搜索。用一个轻量级的不确定性估计器来判断“这次推断要不要加大计算量”,可以在不明显牺牲性能的前提下,大幅降低平均推断成本。
这三条路线不是互斥的。模拟项目X里,我实际用的是“自适应开关+多候选重排+关键节点有限深度树搜索”的组合:先判断状态是否属于高不确定区域,如果是,就临时把候选数扩到64条,同时对顶层动作做一次深度12的树搜索。这个组合在效果上最稳,在计算上也可控。
2.3 选型判断:该上哪种推断策略
做选型时,我习惯按四个维度打分:动作空间类型、critic可靠性、是否有环境模型、计算预算高低。
| 维度 | 偏“轻”方案 | 偏“重”方案 |
|---|---|---|
| 动作空间 | 离散且动作数少 | 连续或动作组合爆炸 |
| critic可靠性 | critic明显可信,与真实return相关 | critic估计噪声大或容易过乐观 |
| 环境模型 | 无法快速获取下一状态 | 有仿真器或可学习的近似模型 |
| 计算预算 | 每决策毫秒级响应 | 允许秒级甚至更长的决策时间 |
- 离散动作+critic可靠+无模型:优先上多候选重排,N从8起测,效果不够再叠加树搜索。
- 连续动作+critic可靠+有模型:优先上MPC或CEM式的规划推断,把训练好的policy作为采样先验,而不是直接当作答案。
- critic不可靠:先修critic,否则任何依赖价值评估的推断策略都会越修越歪,甚至出现“越搜索越差”。
- 计算预算超级紧张:考虑自适应分配,让90%的普通状态走贪心,10%的高风险状态走重搜索,整体收益依然可观。
我的核心建议是,不要一上来就贪图搜索型推断。先看一个便宜的“多候选重排”能在你的任务上带来多少增益,如果增益很小甚至为负,问题大概率不在推断算法,而在底层模型的可信度。磨刀不误砍柴工。
3. 核心实操:在模拟项目X中突破性能瓶颈
3.1 环境设计与基线训练
模拟项目X的环境定义如下:一个15×15的网格迷宫,墙随机生成,智能体从随机的起点出发,目标是到达唯一终点。每回合最多100步,动作只有上下左右四选一。状态输入是11维的张量,包含当前位置坐标、目标相对坐标、四周是否墙的判断,以及一个4×4的局部视野。奖励函数刻意设计成极度稀疏:到达终点返回+1,其他任何情况都返回0。这样设计的目的是构造一个典型的“训练拿不到足够有效梯度、部署时容易局部短视”的性能天花板场景。
基线训练使用PPO,额外加一个以好奇心驱动的内在奖励模块,鼓励智能体访问未见过的区域。超参数如下:actor和critic共享一个两层128维的MLP主干,学习率3×10^-4,GAE系数0.95,batch大小64,clip系数0.2,每回合总步数上限10万步。训练过程做了5个seed,取成功率中位数。
结果如预期,训练集上中位成功率约78%,但测试集上直接贪心部署只有32%。这里点名一个问题:测试集表现远低于训练集,不一定全是过拟合。因为我们使用了不同的迷宫布局,策略网络在训练时见过的“局面类型”有限,遇到未见过的局部构型时,单步argmax会迅速积累误差。这个现象在稀疏奖励任务里特别常见,因为它本身就没有足够的“中间信号”让策略对每个局面建立稳健的偏好。
3.2 推断策略层:候选轨迹生成与价值重排
先实现最便宜的多候选重排。思路很简单:在当前状态s_t下,从策略网络π_θ(·|s_t)采样N个动作作为起点,每个起点都用策略网络自动回归展开一条完整的轨迹到回合结束(或者到提前终止条件),然后用critic网络V_φ对每条轨迹的累计回报做估计。因为稀疏奖励环境下直接看累计return是大量零,所以我们实际用的是“critic在每条轨迹末尾状态上的估计值+已经获得的奖励”。选择这条轨迹中“价值估计最高”的那条,执行它的第一步动作,然后进入下一状态重复。
下面是我实际使用的Python伪代码骨架:
def infer_with_reranking(state, policy, critic, num_candidates, horizon): trajectories = [] for _ in range(num_candidates): traj = [] s = state.clone() done = False while not done and len(traj) < horizon: action_probs = policy(s) action = sample_action(action_probs) next_s, reward, terminated = env.step(action) traj.append((action, reward, next_s)) s = next_s done = terminated or len(traj) >= horizon last_value = critic(s) if not terminated else 0.0 total_score = sum(r for _, r, _ in traj) + last_value * discount_factor trajectories.append((total_score, traj)) trajectories.sort(key=lambda x: x[0], reverse=True) best_traj = trajectories[0][1] return best_traj[0][0]关键点在于动作采样:如果策略网络输出的分布比较尖锐,直接采样N条很容易得到雷同的轨迹,重排失去意义。我实际测试时发现,在动作概率后面加一个温度参数τ,把概率分布“摊平”一点,再采样,候选多样性会明显增加。τ取0.8到0.9之间通常比较好,太低则多样性差,太高则候选退化到随机策略。
N的选择也有讲究。N=1就是普通采样推断;N=8时测试成功率从32%涨到48%;N=32时达到55%;N=64时大约57%,再往上基本平缓。这个边际递减效应很正常,候选轨迹再多,如果价值估计不准确,提升空间终究有限。所以第一版推断策略建议把N固定在32,不要盲目堆算力。
3.3 搜索型推断:树搜索与回溯剪枝
多候选重排本质上是一种“开环序列重排”,它仍然依赖策略网络自回归展开,一旦中途走偏就没有回头路。要更进一步,就得在“决策时”把搜索树展开,让前面的选择能根据后面节点的评估回传修正。我采用的实现是简化版蒙特卡洛树搜索,可以理解成:以当前状态为根,迭代进行选择、扩展、求值、回传四个步骤。
每一步迭代的流程:
- 从根节点出发,按UCB公式选择子节点,直到到达尚未扩展的节点;
- 在该节点调用策略网络生成若干个候选动作,扩展出子节点;
- 对叶子节点调用critic(或执行一条模拟轨迹)来获取价值估计;
- 把价值沿访问路径回传到根节点,更新每个动作的平均胜率。
当迭代预算耗尽,选择根节点下访问次数最多或平均价值最高的动作执行。这里需要注意,不要直接选“平均价值最高”的动作,因为某个动作只要被探索过一次且恰好拿到一个乐观估值,它就会占据榜首;访问次数更均衡的动作通常更可靠。我用的是“访问次数为主、平均价值为辅”的排序方法,效果比纯价值排序稳。
关键代码骨架如下:
class SearchNode: def __init__(self, state, prior): self.state = state self.prior = prior self.visits = 0 self.value = 0.0 self.children = [] def tree_search(root_state, policy, critic, num_simulations, expand_depth): root = SearchNode(root_state, prior=1.0) for _ in range(num_simulations): node = root path = [] while node.children: node = select_by_ucb(node) path.append(node) if node.state.is_terminal(): v = node.state.terminal_value() else: v = critic(node.state) for n in reversed(path + [node]): n.visits += 1 n.value += (v - n.value) / n.visits if not node.state.is_terminal() and len(node.state.to_actions()) > 0: actions_probs = policy(node.state) for a in top_k_actions(actions_probs, expand_depth): child_state = node.state.transition(a) node.children.append(SearchNode(child_state, prior=actions_probs[a])) return root在实际迷宫环境里,expand_depth取4、num_simulations取32时,测试成功率提升到63%左右。这个版本比多候选重排高了约8个点,但单步决策耗时也上去了,平均每步约35毫秒,多候选重排只用了不到8毫秒。如果部署环境允许这个开销,搜索型推断是更优的选择;如果延迟敏感,就需要自适应分配来缓解。
3.4 关键参数与效果对照
把三种模式放在一起对比,数据会更直观:
| 推断策略 | 测试成功率 | 平均完成步数 | 单步决策开销 |
|---|---|---|---|
| 贪心argmax | 32% | 52 | 2ms |
| 候选重排N=32 | 55% | 41 | 8ms |
| 树搜索+重排组合 | 63% | 35 | 35ms |
| 自适应分配混合 | 61% | 37 | 12ms |
自适应分配混合的做法是在普通状态走贪心,在不确定性高的状态触发重排或树搜索。我用critic的ensemble方差作为不确定性指标:两个独立训练的critic对当前状态估值差超过0.15时,认为该状态需要重排;超过0.3时,升级成树搜索。这样平均单步开销只有12毫秒,性能却接近满搜索版本,这是工程部署最推荐的折中。
有几个参数需要特别提一下。候选数N、温度τ、树搜索预算三者是需要一起调的联动变量。我见过不少人把N加到很大,温度却不调,结果候选轨迹全部雷同;也有人树搜索预算很高,但UCB系数没调,导致搜索始终集中在某个前期表现好的子树里。UCB系数C建议从1.0起步,如果搜索过程中发现“某个动作从头到尾没被访问过”,就适当增大C;如果发现根节点下访问分布极度不均衡,就减小C。用户环境不同最优值差异很大,不要迷信默认参数。
4. 常见问题与排查技巧实录
4.1 推断增益消失:分布偏移与价值误估
最典型的失败现象是:加了推断策略之后,训练指标没变,但测试性能不升反降。这个我见过太多次,根源通常不在推断算法本身,而在于critic对推断时遇到的状态给出的估值是错的。训练时critic主要见过“策略正常探索”生成的状态分布;推断时如果你用更强的搜索或更多候选,找到的轨迹可能会把智能体带进训练分布中极少出现的新局面,此时critic的估值会发生很大的外推偏差,甚至出现“系统性的乐观估计”。
我自己踩过最狠的一次坑是,用MCTS在模拟项目X里搜索,搜索结果找出一条critic估值高达0.9的路径,但实际执行时智能体第一步就走进了死胡同。事后检查发现,那条路径在训练数据里几乎没出现过,critic对“看起来像是死胡同但估值很高”的状态完全没有分辩能力。所以我在后来所有的推断策略实验里都加了一条规则:所有候选轨迹展开后,不仅要看critic估值,还要检查轨迹中出现过的状态是否落在训练分布的“支持区间”内。用简单办法衡量就是保存训练时所有状态特征的均值,推断时对偏差过大的状态施加惩罚折扣。这个偏保守的做法会减少一部分理论最优增益,但能防止灾难性的外推翻车。
4.2 计算预算、延迟与吞吐的平衡
推断策略的代价不能只看“多采几条轨迹”,要看整体系统延迟。在真实环境里,单步决策如果花费几十毫秒,对于游戏AI或仿真推演还能接受,对于工业控制、高频交易、在线推荐这类场景就是灾难。解决思路有三个层级:
第一层是降低单次推断成本。比如模型蒸馏:训练一个结构更小的“推断专用头”,只在需要重排或搜索时使用,主网络保持大模型精度,小模型负责快速采样候选。第二层是并行化评估:多条候选轨迹在推断时是互相独立的,可以用多进程或GPU batch一次性展开,把N条轨迹的评估从串行的“N倍时长”变成并行的“单倍时长”。第三层是异步预计算:对可以预知的状态转移路径,提前缓存搜索结果,命中缓存时直接返回。
自适应计算分配还有一个常见变体,按episode阶段分配:开局阶段探索需求大,重排/搜索预算就调高;接近结束时策略相对确定,直接阈值判断。模拟项目X里,我在前20步使用N=64重排,中间40步用N=32,最后40步用贪心,结果测试成功率几乎不变,平均开销降到了原来的三分之一。做延迟敏感项目时,强烈建议先试这种简单的分段预算分配,再上复杂的不确定性模型。
4.3 训练阶段要为推断策略留出余地
推断策略不是部署阶段才需要想的事情。如果训练阶段没有留下可用接口,后面所有推断技巧都会绑手绑脚。我在实践中最基础的四个动作是:
- 至少保存两个不同checkpoint,不要只留“最后一代”模型。不同训练阶段的策略分布不同,推断时做集成或切换,往往比单个末期模型稳。
- 训练时在critic之外,额外记录每条状态的不确定性度量,比如用dropout多次前向的方差,或者维护一个小型的密度估计器。这个度量在自适应计算分配里是刚需。
- 不要让critic只输出一个期望回报标量,可以考虑再加一个“置信度头”输出熵或方差。只要在训练loss里加一个辅助建分母,成本很低,但推断时它会很有用。
- 如果条件允许,在训练收尾阶段用“推断策略”产生的轨迹做一点微调。让模型浅浅地见过一些搜索会选择的状态分布,可以有效减少部署时的分布偏移。
这些动作说起来简单,但大多数RL训练框架没有默认支持。我用过的一些现成库,要么只能保存单一checkpoint,要么critic输出就是一个标量,导致到了部署阶段寸步难行。如果项目目标明确包含“提升最终部署性能”,请在搭训练流程的第一天就考虑到推断层的需求,不然后面要返工很久。
4.4 一份自查清单
最后整理一份我在项目实战中反复核对的问题清单,按优先级排列:
| 排查项 | 判断方法 | 参考解法 |
|---|---|---|
| 基线性能是否意外偏低 | 先用N=8重排,看增益是否为正 | 无增益则先修critic |
| critic是否高估 | 对比critic估值和真实return | 保守化处理、ensemble取低位 |
| 候选轨迹是否单一 | 打印动作概率分布,看熵值 | 调温度τ、增加随机化 |
| 搜索是否偏移到坏子树 | 观察根节点动作访问次数分布 | 调UCB系数、增加剪枝 |
| 推断分布是否离训练太远 | 检查状态的密度估计 | 加行为约束或状态惩罚 |
| 计算开销是否失控 | 测量P95单步延迟 | 上自适应分配、分段预算 |
| 评估口径是否公平 | 对比同一checkpoint下贪心/重排/搜索 | 统一随机seed与评估集 |
这套清单可以帮助快速定位“推断策略没有效果”的问题出在哪个环节。本质上,推断策略设计得再好,也弥补不了底层模型与训练流程的结构性问题。
我自己踩过几次坑之后最大的体会是:强化学习项目的性能天花板,很多时候不是被训练算法焊死的,而是被部署时“只用argmax”这个默认习惯焊死的。如果模型本身没问题,你花半天时间写一个多候选重排,就可能拿到比调一个月超参数更大的收益。而如果重排带来的是负增益,也不要急着否定推断策略这条路,先回头看看critic是不是在自欺欺人。这个方向后续的扩展空间还很大,比如在连续控制里引入模型预测规划、在多智能体场景中引入交互式推断,但最值得做的,永远是先把“部署决策机制”这块短板补上。