hardcode_tests 操作出发,研究奖励、验证器和奖励投机。
我们先把奖励写成回合(episode)终止记录的函数,并比较编程环境能够提供的几种验证器(verifier)。随后复现奖励投机(reward hacking):用智能体可以看到的公开测试训练,观察训练奖励不断攀升而隐藏测试通过率并不上升,并解读监控面板(dashboard)上的特征。接下来比较三种应对办法,遵循同一原则:用于训练的奖励与用于评估的度量不能有共同的弱点。本章后半部分讨论通过学习或评判而非执行得到的奖励、过程奖励(process reward),以及基于势函数的塑形(potential-based shaping)——在相应假设下,它具有不改变最优策略的保证。
11.1 可验证奖励
可验证奖励(verifiable reward)由一个检查结果的程序计算——这里是执行测试——而不是由预测质量的模型给出。它的价值在于信任成本低:只要测试本身正确,且智能体无法查看或修改它们,那么通过就意味着代码在这些输入上行为正确。它有两个局限:一是覆盖面,因为测试套件只检查有限多个输入;二是暴露,因为智能体能观测到的任何东西,它都能学会去满足,而不必真正解决任务。BugFixEnv 在回合结束时调用一次奖励钩子,传入 第 4.5 节 中的终止 info 字典。把奖励写成这条记录的函数,就把为最终状态打分的验证器与为回合过程打分的奖励项(reward terms)分开了。VERIFIERS 包含四个验证器:所有隐藏测试(本书默认)、所有公开测试(即 test 工具展示的那些),以及两个套件各自的通过比例。make_reward 增加了三个可选项:编辑日志中出现已知漏洞利用时的惩罚、每次格式错误调用的格式惩罚,以及每轮的成本,后者由 第 12 章 研究。第四个参数设定提交未通过验证器时的奖励。
RLConfig(reward=...) 或任何 BugFixEnv。我们用四个验证器为 第 4.6 节 的脚本策略打分,另外再加一个不碰代码、立即提交的策略。每个策略在 20 个训练任务上各运行 5 个回合。
hardcode_tests 恰好让可见的测试通过。两个比例型验证器会为从未修复的代码给分:有 bug 的源代码本来就能通过每个套件的一部分,因此不做任何编辑直接提交就能拿到隐藏套件的 0.397;盲猜策略获得的部分得分(0.538)是其二值通过率(0.200)的 2.7 倍。hacker 的隐藏比例得分与什么都不做的得分相同,因为 hardcode_tests 只为公开输入添加代码行。随机策略揭示了剩下的区别:它的公开通过率(0.310)是隐藏通过率(0.160)的两倍,因为它的九种调用中有一种就是这个漏洞利用。每个验证器定义了一个不同的任务,只有第一个才是我们想要的任务。
11.2 稠密奖励与稀疏奖励
二值奖励是稀疏(sparse)的:弱策略的大多数回合恰好得 0,而在组基线(baseline)下,一个所有尝试都失败的组优势(advantage)为零(第 7.5 节)。稠密(dense)奖励给更多回合赋予不同的值,于是更多组携带梯度。部分得分(partial credit)是让基于测试的奖励变稠密的最直接方法。我们从 RL 基座智能体采样一批 8 个组、每组 8 个回合,并用二值隐藏测试验证器和部分隐藏测试验证器为同一批回合打分。run 用给定奖励在默认配置下训练 RL 基座智能体的一个副本 40 步,每 10 步在留出任务(held-out tasks)上评估一次;held_out 测量一个策略在留出任务上的隐藏测试通过率。第 11.7 节运行部分得分的对比实验;第 11.4 节的二值隐藏测试运行是整章的参照。
run 引用了 LoggedEnv,第 11.3 节会在第一次带日志调用它之前定义这个类。
11.3 复现奖励投机
要观察投机的发生,我们需要训练奖励所看不到的量:隐藏测试通过率,以及漏洞利用被使用的频率。LoggedEnv 包装任意环境,把每个结束回合的终止记录连同其奖励追加到一个列表中。log_by_step 利用历史记录中的回合计数,把该列表切分为每个训练步一段,并跳过 train_rl 通过同一个工厂运行的评估回合。reward_dashboard 把一段日志转成工程师对任何编程奖励都应该绘制的三个数字:训练验证器的通过率、一个独立验证器的通过率,以及包含已知漏洞利用的回合比例。
eval_pass 始终是隐藏测试通过率。
hardcode_tests 对任何任务都有效。与盲猜得到的 0.20 相比,投机运行的终点低于它的起点策略。
11.4 修复被投机的奖励
修复方法有三类,作用于不同的位置。第一类改变验证器:用智能体无法观测的测试来评分。第二类加入检测器(detector):识别回合中已知的漏洞利用,并对其施加惩罚或将其丢弃。第三类改变监控:在训练中测量一个独立信号,使得即便奖励无法修复,投机也能被发现。我们训练前两类并与投机运行进行比较;隐藏测试运行就是本书的默认配置。hardcode_tests 就减 1,这让漏洞利用的价值变为 0。它同样学会了阅读和修复,留出通过率为 0.719,差距在第 9 章消融实验(ablation)的单随机种子噪声范围之内。它最后 10 步的公开通过率与隐藏通过率几乎相等(0.811 和 0.809),这是监控面板在告诉我们:一旦漏洞利用消失,这个 gym 中的公开测试就与隐藏测试一致。
惩罚之所以在这里有效,是因为这个 gym 恰好只有一种漏洞利用,而检测器能完美识别它。对真实智能体而言,这两点都不成立。检测器是针对已经有人见过的漏洞利用编写的,比如修改测试文件、在断言之前调用 sys.exit(0),或者从测试输入中复制字面量;在惩罚压力下的策略会被推向检测器识别不了的漏洞利用。Baker et al.(2025)报告了在使用学习得到的监控器时出现的这种模式:在智能体编程环境中,把思维链(chain-of-thought,CoT)监控器加入推理模型的训练奖励,“at low optimization levels”(在低优化程度下)有帮助,而随着优化加深,智能体学会了“obfuscated reward hacking, hiding their intent within the CoT”(混淆的奖励投机,把意图隐藏在 CoT 中)。因此,检测器作为输出被记录和审计的监控器时最有价值,作为策略可以学会规避的奖励项时价值最低。
训练奖励与评估奖励的分离
这三次运行只在一个地方不同:驱动更新的奖励是否与判断训练是否成功的度量有共同的弱点。在投机运行中,训练奖励检查的是公开测试,并且除此之外没有别的检查;我们之所以看到了投机,唯一的原因是eval_pass 使用了隐藏测试。一般而言:
- 训练奖励必须廉价、足够稠密以便学习,并且尽可能难以被利用。它是被优化的对象,所以每个弱点都会被找到。
- 评估度量必须独立于训练奖励:不同的测试、不同的任务,以及对于任何学习得到的或评判型的组件,使用不同的模型。它不被优化,只有在没有任何梯度从它流出时,它才能保持诚实。
- 训练期间的留出监控把两者联系起来。每隔几步绘制训练奖励与独立度量之间的差距。训练奖励攀升而差距扩大,就是图 11.1 的特征,在上面的投机运行中,它在最初 10 步内就已可见。
FAIL_TO_PASS 和 PASS_TO_PASS 测试(第 4.7 节)在用作训练奖励时就是这种意义上的隐藏测试,而对于在同一评测框架(harness)上训练的模型,基于它们构建的基准就不再是独立的度量。
11.5 学习型奖励与评判器
并非每个任务都有测试。没有测试时,奖励来自一个模型:在人类或自动化偏好(preference)上训练的奖励模型(reward model),或者用评分细则(rubric)提示的 LLM 评判器(LLM judge)。两者都是以可验证性换取覆盖面。Bradley–Terry 奖励模型
Bradley–Terry 模型假设,对两个结果的偏好是其潜在分数之差的 logistic 函数,。在标注好的成对数据上用最大似然拟合 ,就得到自 Christiano et al.(2017)以来 RLHF 一直使用的奖励模型损失: 该损失只看差值,因此 只能确定到一个加性常数,其尺度由成对数据的可分程度决定。我们用 RL 基座智能体自己的回合构造成对数据:在每个组内,每个通过的回合都优于每个失败的回合,由隐藏测试充当标注者。奖励模型是 TinyAgent 的一个副本加上一个单元输出头,即 第 7.4 节 中的make_critic,在回合的最后一个 token 处读取输出。
hardcode_tests、运行测试,然后提交。
read,接着一次编辑,一个全部为 pass 的测试输出,然后提交。阅读型 hacker 复现了所有这些特征,却什么也没修复。针对这个奖励模型的 RL 运行会把概率推向阅读型 hacker,因为它得分高,且其行为容易学习。
Gao et al.(2022)在大规模下测量了这一后果:当策略针对代理奖励模型进行优化时,金标准奖励先升后降,转折点取决于奖励模型的规模和数据。代理奖励在其训练分布上是准确的,在分布之外则被利用,而这恰恰是 RL 所产生的情形。
LLM 评判器
LLM 评判器读取对话记录或最终补丁并返回一个分数,通常依据一份评分细则。它不需要训练,可以覆盖没有测试的任务。但它同样是根据策略产出的内容计算的奖励,因此策略可以优化评判器的输入。它的失效模式既包括奖励模型的那些,也有其自身的:表面特征(“One Token to Fool LLM-as-a-Judge”(2025)报告,诸如单独一个冒号或 “Let’s solve this problem step by step.” 这样的字符串会引发误判为正确)、对长度和自信语气的敏感,以及不同运行之间的噪声。评判器也只能看到对话记录所展示的内容。我们用一份应用于解码后对话记录的评分细则来代替评判器:智能体是否读了代码、是否运行了测试、最后是否看到测试通过、是否提交?这只是用手写评分规则模拟按轨迹打分,并未实现 LLM 评判器;它与真实评判器的共同点在于,它根据对话记录中可见的证据打分。11.6 过程奖励与基于势函数的塑形
结果奖励(outcome reward)只在结尾到达一次。过程奖励为中间步骤打分:可以是在步骤标注上训练的过程奖励模型(PRM),如 Let’s Verify Step by Step(2023)和 Math-Shepherd(2023),也可以是诸如“某个测试从失败变为通过”之类的启发式规则。它在功劳产生的地方给予功劳,这正是轮级优势所需要的(第 7.6 节)。它也会改变任务:策略最大化的是奖励的总和,一个可以反复领取、或无需取得进展就能领取的奖励,本身就会变成一个目标。 奖励塑形在环境奖励上加一项 。Ng、Harada 和 Russell(1999)证明,形如 的塑形,对任意状态函数 ,都不改变最优策略;并且在不对 MDP 作进一步假设的情况下,没有其他形式的 能保证这一点。充分性部分的证明很简短。沿任意轨迹(trajectory),塑形项逐项相消(telescope): 若 在终止状态为 0(或 ),则从 出发的每条轨迹的塑形后回报(return)等于其原始回报减去 。取期望,对每个策略 都有 ,因此在每个状态中,所有动作都平移同一个常数,argmax 即最优策略不会改变。任意添加的局部奖励则不能直接套用这个保证。 我们在 第 2 章 的修复 MDP 上验证这一点。shape 把期望塑形项 加到每个状态–动作奖励上。势函数 是一个进度分数:unread 为 0,read 更大,正确编辑后更大,done 为 0。作为对照的非势函数方案是每次阅读奖励 0.2,即一种“奖励好习惯”的过程奖励加成。
unread 处的最优价值仍是原来的 0.773,因为 ;并且在每个状态中, 在 的误差内成立,与证明的预测一致。阅读加成——每次阅读 0.2 的过程奖励——使得在每个状态下永远阅读都成为最优,其价值为 ,是解决任务价值的两倍多。在 LLM 智能体中,同样的机制会产生一个无休止检查文件的智能体;轮次预算会把这种现象掩盖成“从不提交”(第 12 章)。
这个证明也说明了基于势函数的塑形在本书的训练循环中不能做什么。在 且回合结束时 的情况下,一个任务的每个回合的塑形后回报都等于其原始回报减去 ,对该任务而言是常数。组基线会把它减掉。在结果级 GRPO 下,基于势函数的塑形项对梯度完全没有影响;只有当按轮分配学习信号时——由 critic(价值模型)或能看到各个单项的轮级优势来分配——它的价值才会显现(第 12 章)。而如果实现没有保持势函数差的形式,或终止边界项不为 0,就失去了上述保证;智能体可能利用残留的额外奖励。下面的代码在 gym 中把这一点具体化:令 为当前源代码中通过的公开测试比例,在一个 oracle 回合和一个 hacker 回合的每次调用之后计算。
11.7 实践中的设计问题
问题:稀疏的二值奖励。 现象: 大多数组的优势为零,早期学习缓慢,困难任务始终学不会。 原因: 二值验证器在采样到一次成功之前不提供任何信号;对通过率为 的任务进行 次尝试的组,无信息量的概率为 (第 7.5 节)。 可选方案: (a)更大的组或动态采样,代价是更多 rollout(第 9 章)。(b)保留中等通过率任务的课程(第 14 章)。(c)部分得分或塑形,它们让信号更稠密,同时也改变了任务(见下一个问题)。(d)更好的初始策略(SFT 或蒸馏,第 15 章 与 第 16 章)。 建议做法: 保留二值验证器,用(a)、(b)和(d)解决稠密度问题;只有在测量过它奖励了什么之后,才加入部分得分。隐藏测试运行的历史记录展示了二值奖励在训练过程中有多稀疏。
问题:部分得分被钻空子。 现象: 奖励上升而二值通过率不变,或者策略收敛到尽快提交。 原因: 比例型验证器会为未修复代码本来就能通过的测试付分,因此什么都不做也有正奖励;任何让比例稍有变化的行为,包括以恰好能多通过几个用例的方式破坏代码,都能得分。在 GRPO 的标准差归一化下,这些微小差异会得到完整尺度的优势(第 11.2 节)。 可选方案: (a)在任务的 FAIL_TO_PASS 测试上使用二值奖励,只对原始代码未通过的测试给部分得分。(b)把部分得分加到一个占主导地位的二值项上。(c)不做标准差归一化的部分得分(Dr. GRPO),使微小差异只产生微小优势。 建议做法: (a)。第 11.2 节提到的部分得分运行测量了它在这里带来了什么。
问题:评判器被利用。 现象: 评判器分数稳步上升,而执行测试或人工评审的结果并不上升;对话记录变得越来越自信、越来越模式化。 原因: 评判器给对话记录打分,而对话记录由策略写成,因此评判器是一个优化目标,带有第 11.5 节所述的盲点。 可选方案: (a)只在没有可执行检查的地方使用评判器,并将其与有可执行检查的奖励结合。(b)使用包含针对具体实例、可核查条目的评分细则(Rubrics as Rewards,2025)。(c)使用不是被训练模型本身的评判器,并定期更新或做集成。(d)像第 11.4 节那样,将评判器分数与独立度量对照监控。 建议做法: (a)和(d);对于测试可以检查的能力,绝不让评判器成为唯一的奖励。上面的评分细则评判器给了阅读型 hacker 与 oracle 相同的分数。
问题:不稳定测试带来的奖励噪声。
现象: 正确策略的奖励在相同的运行之间波动,本应一致的组出现非零优势,学习在没有明显原因的情况下变慢。
原因: 若正确解以概率 失败(假阴性),错误提交以概率 通过(假阳性),则真实通过率为 的策略的期望奖励为 ,其中 是其提交率。噪声把信号缩放为原来的 倍并增加方差;假阳性还会为随便提交任何东西付分。
可选方案: (a)重新运行失败的测试并取多数结果,代价是评分成本成倍增加。(b)通过多次运行每个任务的参考修复来验证任务,并丢弃不稳定的任务(第 4.8 节)。(c)用更大的组来平均噪声。
建议做法: 训练前先做(b),然后对剩下仍会不稳定测试做(a)。flaky_reward 用两种噪声包装任意奖励;我们分别以 0.3 的比率加入每种噪声进行训练。
问题:奖励尺度与归一化。
现象: 改变奖励的单位,或加入一个小的惩罚项,对学习速度或所学行为的影响远超预期。
原因: 在标准差归一化的组优势下,奖励的尺度在组内被抵消,因此把它乘以 10 不改变任何东西;不做标准差归一化时(adv_mode='mean',Dr. GRPO),优势随奖励缩放,效果相当于改变学习率。而惩罚项则以奖励单位与验证器竞争:每次格式错误调用 0.5 的格式惩罚可能抵消一次成功。
可选方案: (a)把验证器保持在 [0, 1],并相对于它设定每一项的大小,单位是“这相当于多少次失败”。(b)标准差归一化,及其难度偏差(第 7.5 节)。(c)对组内中心化后的优势做批次级缩放。
建议做法: (a)配合(c)或标准差归一化,并分别记录每一项。下面的代码在两种奖励尺度下为同一批次计算优势。
adv_mode='mean' 下,0.05 的每步成本无论验证器的尺度如何,都会让优势每次调用移动 0.05;在标准差归一化下,它的效果取决于验证器在组内的变化幅度。
问题:测试泄漏。 现象: “隐藏”验证器上的训练奖励比全新的留出评估攀升得更快,或者解答中包含来自评分器的字面值。 原因: 隐藏测试通过某种渠道到达了智能体:一个会运行它们的测试工具、仓库中的一个文件、一条错误消息,或者被复制进训练任务的测试数据。泄漏的隐藏测试就是公开测试,第 11.3 节中的每一种漏洞利用都适用于它。 可选方案: (a)在回合结束后于沙箱之外评分,并审计观测中是否含有评分器内容(第 4.8 节)。(b)保留一个任何奖励都不使用的第三组测试。(c)在评分器文件中放置金丝雀字符串(canary string),并在对话记录中扫描它们。 建议做法: (a)和(b)。下面的代码模拟一次泄漏:test工具,进而hardcode_tests,也能看到隐藏测试。
test 工具可见时,hardcode_tests 也会为它们写特例,hacker 的隐藏奖励变为 1.00。泄漏把第 11.4 节所信任的训练奖励变成了第 11.3 节所攻破的那种奖励,而训练曲线中没有任何东西能区分这两者。只有一个从未到达智能体的第三组测试才能把它揭示出来。
小结
- 奖励是任务的规格说明。把它写成回合终止记录的函数,就把验证器(什么算成功)与奖励项(回合过程的代价)分开了;
make_reward和flaky_reward实现了这两者。公开测试、隐藏测试和测试通过比例定义了不同的任务:hacker 在公开验证器上得 1.000,在隐藏验证器上得 0。 - 用公开测试训练,大约 10 步就复现了奖励投机:漏洞利用扩散到 97% 的回合,训练奖励达到 1.000,留出任务上的隐藏通过率从 0.175 降到 0。它在监控面板上的特征是:平滑上升的奖励、下降的独立度量,以及上升的行为统计量。用隐藏测试评分(留出 0.762)或惩罚已知的漏洞利用(0.719)消除了它。
- 训练奖励与评估度量不能有共同的弱点:隐藏测试仍会被针对性地优化,泄漏的测试就是公开测试,只有一组任何奖励都不使用的测试才能度量泛化。要在训练中监控两者之间的差距。
- 学习型奖励和评判型奖励都是代理奖励,RL 会把它们带到分布之外:一个留出成对准确率为 87.5% 的 Bradley–Terry 奖励模型给阅读型 hacker 打了 +3.08,而 oracle 是 +4.31;评分细则评判器给它的分数与 oracle 相同。部分得分和标准差归一化为未修复代码的提交付分;部分得分提高了训练奖励,却降低了留出通过率。
- 基于势函数的塑形 可以证明不改变最优策略(在修复 MDP 上验证到 ),而阅读加成使永远阅读成为最优。在结果级 GRPO 下,结尾处 的基于势函数的项完全没有作用;由公开测试计算的势函数就是公开测试奖励。这里的所有测量都是在 158k 参数模型上的单随机种子机制演示(mechanism demonstration)。
练习
- 在一个双状态的例子中证明 Ng 等人结果的必要性部分:构造一个 MDP 和一个不是势函数之差、并会改变最优策略的塑形项 ,然后证明没有任何 能复现你的 。
- 改变一个假设:从第 6 章的基座智能体(
arl.make_base_agent(tok))而不是make_rl_base出发,重新运行第 11.3 节的投机实验。漏洞利用扩散得更快还是更慢?把答案与两个初始策略在训练前应用hardcode_tests的频率联系起来。 - 为 gym 编写第二种漏洞利用操作,例如把函数体包进
try/except,在出现任何错误时返回第一个公开测试的期望值,并在环境的一个副本中把它加入EDIT_OPS。用make_reward('public', exploit_penalty=1.0)训练。策略会转向未被检测的漏洞利用吗?这对把检测器用作奖励项说明了什么? - 用第 11.5 节的 Bradley–Terry 奖励模型代替隐藏测试来训练默认配置:用
arl.sample_step采样,把batch['rewards']替换为rm_scores,并用arl.rl_update更新。绘制 40 步内的 RM 分数和隐藏通过率。你能看到 Gao 等人所说的先升后降吗? - 推导在第 11.7 节的不稳定奖励下,一个回合的期望 GRPO 优势,表示为 、 和组的真实通过率的函数。 取何值时期望梯度消失?
- 把势函数 = 通过的隐藏测试比例实现为逐轮塑形项,配合
turn_advantages和 critic(第 7.6 节)。与只用结果的 GRPO 相比,它能加快学习吗?解释为什么同样的项在只用结果的 GRPO 下做不到。 - 设计评审(design review):你们团队的 SWE 智能体用“所有
FAIL_TO_PASS测试通过”作为奖励进行训练,并加入了一个 LLM 评判器来惩罚“针对测试写特例”。200 步之后,评判器的标记率降到 0.5%,SWE-bench Verified 提高了 2 个点,而一个全新的内部基准下降了 1 个点。写出各种假设、能够区分它们的测量,以及你会提议对奖励和评估做出的修改。