奖励究竟检查了什么:答案、格式、测试与过程反馈

成长演进

具备清晰骨架、初步数学推导或工程验证,持续补充论据与实测。

ℹ数字花园

🌿 什么是数字花园与思想演进?

遵循 Maggie Appleton 与 Andy Matuschak 的常青笔记理念:这里不是按时间沉底的流水账,而是持续修剪打磨的知识生态。文章按成熟度经历:

  • 🌱 萌芽 (Seedling): 发散灵感、读书摘录、开放猜想
  • 🌿 演进 (Budding): 结构渐明、初步代码推导、积极修正
  • 🌳 常青 (Evergreen): 逻辑闭环、持久经得起检验的基石
  • 🔬 实验待证 (Speculative): 结合沙盒与仿真探针的理论探索
浏览全站数字花园与笔记全景 →

模型得了一分,这一分究竟证明什么?

可能只证明回答能解析成 JSON,也可能证明最终数值正确,还可能只是某个评审模型觉得它写得好。这几种“一分”不能互换。强化学习会利用评分规则中的差异;如果规则漏掉任务要求,优化得越认真,错误方向也可能走得越远。

沿用上一篇的整数任务:输入非负整数 a、b,输出满足 3n+a≥b 的最小非负整数 n,只允许一个 JSON 字段 n。本篇把“能核对”展开成具体代码和反例。代码用于教学检查,不是实际模型训练结果。

先写出通过意味着什么#

对 a=2、b=12,正确值是 4。要判定一次输出合格,至少需要同时满足三件事。

第一,输出是完整、无歧义的 JSON 对象。第二,它只有 n 字段,值是非负整数。第三,这个整数是满足条件的最小值。

下面几条失败分别对应不同原因:

输出为什么不合格
{"n":3}太小,不满足不等式
{"n":5}满足不等式,但不是最小值
{"n":"4"}数字被写成字符串
{"n":4,"explanation":"正确"}多出任务不允许的字段
答案是4没有遵守接口格式
{"n":3,"n":4}重复字段,解释可能有歧义

这不是说所有应用都应拒绝解释或字符串数字,而是本任务已经作了明确约定。若允许兼容转换,就把转换范围写进协议;不能训练时宽松,部署时严格,然后把失败都归因于模型。

格式检查决定接口能否使用,语义检查决定回答是否完成任务。两者都重要,但一种检查通过不能替另一种作证。

最小值条件不能漏掉#

如果只检查 3n+a≥b,那么 1000000 也能通过例题。检查器实现很短,却实现了另一个任务。

一种完整检查方式是先算参考值:

n∗=max⁡(0,⌈b−a3⌉).n^*=\max\left(0,\left\lceil\frac{b-a}{3}\right\rceil\right).

另一种方式不直接使用这条公式,而是验证两个条件:当前 n 满足不等式,并且当 n>0 时,n−1 不满足。因为左边随 n 单调递增,这足以证明它是最小非负整数。

这两个实现适合互相核对。在小范围内还可以直接从零枚举,找到第一个满足条件的值。三个实现都相同并不自动构成对所有工程细节的证明,但独立思路更容易暴露向上取整、边界和符号错误。

一个可以运行的教学检查器#

下面固定输入 a、b 为非负整数。输入任务非法时抛出异常,不把它记成模型答错;输出不合格则返回零分和原因。这里给输出设 256 字符上限,用来明确本教学接口的接受范围,不声称这是完整的资源隔离方案。

import json
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate key")
result[key] = value
return result
def reject_constant(value):
raise ValueError("non-finite constant")
def grade(text, a, b):
if type(a) is not int or type(b) is not int or min(a, b) < 0:
raise ValueError("invalid task input")
if not isinstance(text, str) or len(text) > 256:
return 0, "output_limit"
try:
obj = json.loads(
text,
object_pairs_hook=unique_object,
parse_constant=reject_constant,
)
except (ValueError, RecursionError):
return 0, "parse"
if type(obj) is not dict or set(obj) != {"n"}:
return 0, "schema"
n = obj["n"]
if type(n) is not int or n < 0:
return 0, "type_or_range"
if 3 * n + a < b:
return 0, "too_small"
if n > 0 and 3 * (n - 1) + a >= b:
return 0, "not_minimal"
return 1, "pass"

这里有几个容易忽略的细节。

Python 默认 JSON 解码接受重复名称,并保留最后一个值;也接受 NaN 等扩展常量。因此“解析没报错”不代表已经满足本任务的严格约定,上面显式拒绝这些情况。Python 3.13 JSON 文档

type(n) is int 有意拒绝布尔值;在 Python 中,宽泛的 isinstance(True, int) 会成立,但本接口不接受 true 当整数。4.0 虽然数值上等于 4,也不满足此处的整数类型约定。

检查器不从文字里提取“最后一个数字”,也不修补截断 JSON。否则回答同时含有 3、4、5 时,到底哪个算答案,会变成隐藏在评测脚本里的规则。若要评价带推导的回答,应另行定义最终答案区域,不能随意改变本接口。

怎样测试检查器,而不是只测试模型#

先准备反例集合。除了表里的六条,还应包含合法空白、空对象、数组、负数、布尔值、浮点值、非有限常量、多个顶层对象和输出被截断的情况。

再准备数值边界:b≤a 时 n=0;b−a 为 1、3、4 时,分别覆盖向上取整与恰好整除。对一个小输入范围,枚举所有任务并比较公式、逐个尝试和检查器结果。特别测试 n*−1 和 n*+1:前者不应被接受,后者也不应因为满足不等式而蒙混过关。

最后故意写几个错误版本。例如删掉最小值判断,或把 < 写成 <=,观察测试能否发现。这叫变异测试:不是给正确实现重复贴标签,而是检查测试有没有能力拦住真实可能发生的错误。

EvalPlus 研究通过扩充测试输入检查生成代码,并讨论用变异错误衡量测试的检错能力。这里借用的是测试思路,不使用该论文成绩来证明我们的整数检查器已经全面正确。EvalPlus,第 2 节

只检查最终答案,不能验证推导#

假如把任务改为“先解释,再给 n”,模型可能写出“3×4+2=12,所以 n=4”。最终值正确,但中间等式错误。

结果反馈可以认可最后的 4;它没有检查每一步。若只用最终答案奖励训练,就不能据此宣布整个推导可靠。反过来,表达步骤都正确,也不一定符合输出接口或完成了题目要求。

过程反馈试图给中间步骤提供判断。在这个例子里,可以单独检查乘法、加法和最小性论证;复杂自然语言推导则往往需要人工或学习得到的评分模型,评分本身也会出错。

《Let’s Verify Step by Step》区分最终结果监督和逐步骤监督,其方法使用人工步骤标注训练过程奖励模型。该研究主要比较奖励模型及其用于选择解答的效果,不能直接读成“给任何策略加过程奖励就一定更好”。论文方法章节

步骤怎么切分、漏写一步如何处理、多个步骤分数怎样聚合,都会改变目标。把每步的正分直接相加,可能鼓励把同一事实拆成更多步骤;取最小分又可能对一个评分误差过于敏感。这些是需要验证的设计取舍,不存在一个脱离任务的通用合成办法。

代码通过测试,也有覆盖边界#

把“返回 n”改为“写一个求 n 的函数”,检查器就从比较一个数变成运行程序。

如果测试只有 (2,12),一个总返回 4 的函数就能过。增加 (4,2) 会暴露它,却仍不能证明程序对任意输入都正确。需要根据接口约定覆盖边界、输入规模及不同分支,而不是单纯堆积相似样例。

测试样例也必须合法。若题目只允许非负整数,就不能悄悄用负数判失败,再称它是泛化不足。想扩展输入范围,应先修改任务定义和训练、评测口径。

执行代码还需要限制运行时间、内存和可访问资源;程序超时与评测服务故障要分开。前者可能是被评程序的任务失败,后者是测量无效。如果服务偶发不可用就给模型零分,训练会把基础设施故障混进学习信号。本篇检查器只解析文本,不执行模型生成的代码。

多种奖励相加后,目标会改变#

一种看似方便的设计是:格式正确给 0.2 分,数值正确再给 0.8 分。这样的稠密反馈可能帮助早期模型产生合法输出,但总分就不再等于完整任务成功率。

做一个自写算例。方案 A 的 100 次输出全是合法格式,其中 50 次数值正确;方案 B 有 60 次完整正确,其余 40 次无法解析。按上述规则:

RA=0.2+0.8×0.50=0.60,R_A=0.2+0.8\times0.50=0.60,RB=0.2×0.60+0.8×0.60=0.60.\begin{aligned} R_B&=0.2\times0.60\\ &\quad+0.8\times0.60\\ &=0.60. \end{aligned}

两者平均奖励相同,但完整成功率分别为 50% 和 60%。这不是训练结果,而是权重定义直接造成的差别。若业务只关心完整成功,应至少把这一指标单列;训练辅助奖励可以保留,但不能替代它。

同理,加入速度、长度、风格奖励后,要记录每一项。检查奖励上升时,先看是哪项在涨,再判断是否改善了任务。只看汇总 reward,可能把“格式更稳定”和“答案更准确”混为一谈。

训练检查器不能充当唯一的裁判#

优化过程会反复接触训练检查器。如果检查器有漏洞,模型可能逐步集中到漏洞附近。因此独立评测应尽量有不同的信息来源:另一个实现、保留的边界题、人工抽检,或者更严格的测试集合。

独立不等于换一个文件名。把同一段错误逻辑复制两份,仍然会共同漏掉最小值。也不能看到测试失败后不断针对性修改训练,再继续把同一批测试叫作未见测试。

本任务可以保留新数值范围和不同条件组合,逐类报告格式通过、数值正确、完整通过与各失败原因。若加入过程评分,再分别抽查“最终正确但过程错误”和“过程看似合理但最终错误”的回答。

下一轮比较要回答什么#

先冻结输入、输出协议与检查器版本,再比较“只有完整成功奖励”和“额外加入格式奖励”。使用相同的起始模型、题目分布、生成预算和独立测试,记录完整成功率及各奖励分量。

预先写下可能的观察:格式奖励可能降低解析失败,也可能主要增加合法但错误的回答;若完整成功率没有提高,就不能凭训练总奖励上升宣布改进。这个对照尚未执行,本篇实际可验证的是检查代码与算例逻辑。

第一道自检题:为什么只判断 3n+a≥b 的检查器,不能用于训练“最小 n”的任务?请给出一个一定能揭露漏洞的输出。

第二道自检题:两个模型总奖励相同,为什么完整任务成功率可能不同?若你增加过程评分,又需要补充哪些独立检查?

有了定义清楚的反馈,下一篇再讨论策略梯度:奖励是一个数,为什么对生成概率求导,就能改变高奖励答案出现的机会?

文章概念网络

从章节进入正文概念。实线表示所属层级,虚线连接跨章节重复出现的概念,不代表因果关系。

41 / 41 个节点 · 正文标记术语与关键词概念

奖励究竟检查了什么:答案、格式、测试与过程反馈同一概念 / Shared concept同一概念 / Shared concept同一概念 / Shared concept同一概念 / Shared concept同一概念 / Shared concept奖励究竟检查了什么:答案、格式、测试与过程反馈奖励究竟检查了什么:答案、格式、测试与过程反馈先写出通过意味着什么先写出通过意味着什么答案是4答案是4格式检查格式检查语义检查语义检查最小值条件不能漏掉最小值条件不能漏掉一个可以运行的教学检查器一个可以运行的教学检查器Python 3.13 JSON 文档Python 3.13 JSON 文档type(n) is inttype(n) is intisinstance(True, int)isinstance(True, int)truetrue4.04.0怎样测试检查器,而不是只测试模型怎样测试检查器,而不是只测试模型反例集合反例集合变异测试变异测试EvalPlus,第 2 节EvalPlus,第 2 节只检查最终答案,不能验证推导只检查最终答案,不能验证推导结果反馈结果反馈过程反馈过程反馈代码通过测试,也有覆盖边界代码通过测试,也有覆盖边界多种奖励相加后,目标会改变多种奖励相加后,目标会改变训练检查器不能充当唯一的裁判训练检查器不能充当唯一的裁判下一轮比较要回答什么下一轮比较要回答什么强化学习强化学习利用利用优化优化语义语义PythonPython变异变异奖励奖励反馈反馈策略策略选择选择奖励奖励rewardreward反馈反馈测试集测试集优化优化奖励奖励策略梯度策略梯度反馈反馈
奖励究竟检查了什么:答案、格式、测试与过程反馈

奖励究竟检查了什么:答案、格式、测试与过程反馈

回到原文 ↗
BIBTEX

引用本文

@article{2026-10-23-llm-training-rl-02-verifiers,
  author  = {枫语},
  title   = {奖励究竟检查了什么:答案、格式、测试与过程反馈},
  journal = {枫语},
  year    = {2026},
  month   = {oct},
  url     = {https://meiluosi.github.io/posts/2026-10-23-llm-training-rl-02-verifiers/}
}
🌐

本篇知识星丛与滑动侧栈

奖励究竟检查了什么:答案、格式、测试与过程反馈 当前主轴

搜索

正在载入索引…