深蓝黑背景封面。左侧是奖励模型训练的流程示意:最左灰色卡片「提示 x」经箭头进入「SFT 模型」卡片,模型采样出上下两张回答卡片——青色描边卡片「回答 A ✓ 被选中 chosen」与紫色描边卡片「回答 B ✗ 被淘汰 rejected」,两张卡片共同汇入右侧「RM 标量分」卡片;下方一行分数对比「r(x, A) = 7.2 > r(x, B) = 3.1」,再往下一条青色描边长条写出排序损失「L = −log σ(r_w − r_l):让胜出回答的分数压过被淘汰回答」,底部两行灰色小字注明「一个提示通常采样多个候选回答,两两组合成偏好对」与「RM 参数从 SFT 初始化,只把输出头换成标量头」。右侧大标题「RLHF 的奖励模型」「是怎么训练出来的」分两行,副标题「人类偏好 → 标量分数:奖励模型的训练闭环」,小注「RM 本身不生成文本,它只负责给回答打分」。左上标注「面试经典 · 偏好对齐」,右上圆角胶囊「Day 07 · 12-04」。 图 1:本文封面,从提示到标量分的奖励模型训练闭环。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。RLHF 的经典三步大家都知道个名字:SFT、训练奖励模型、PPO 强化。这道题考的是中间这一步拆开后你知不知道细节——奖励模型(RM)从哪来的、偏好数据长什么样、损失函数为什么那么设计。

一句话结论:RM 就是把 SFT 模型的语言建模头换成一个标量回归头,再用「同一提示下两个回答谁更好」的成对偏好数据,按排序损失训练出来的打分器。

先看模型从哪来:不是从零训,是改造。

RM 的主干直接拿第一步产出的 SFT 模型初始化,去掉输出词表分布的 LM 头,换一个只输出单个标量的回归层。输入是「提示 + 回答」拼成的文本,输出是一个分数 r(x, y)——打分是回归任务,RM 本身不生成任何文本。为什么不从零训一个?主干在 SFT 阶段已经学会读文本、理解指令,评分只需在这个理解之上加一个价值判断头;而且偏好数据靠人工排序,又贵又慢,撑不起从零训练。

再看数据:成对偏好,不是绝对打分。

对提示集合里的每个提示,让 SFT 模型采样出多个候选回答——2022 年发表的 InstructGPT 论文(arXiv:2203.02155)用的配置是每个提示 4 到 9 个——交给标注员按偏好从好到差排序。K 个回答的完整排序可以拆成 C(K, 2) 个偏好对:4 个回答就是 6 对,训练样本就是三元组 (x, y_w, y_l):提示、胜出的回答、被淘汰的回答。一个值得记住的工程细节:论文把同一提示的所有偏好对放进同一个 batch,否则同一回答反复出现容易过拟合。为什么不直接让标注员打 1 到 10 分?人对绝对尺度的把握很不一致,而「A 比 B 好」的相对比较稳定得多,标注质量是整套方案的地基。

最后是损失:分差过 sigmoid,最大化它。

RM 对同一提示分别打出 r(x, y_w) 和 r(x, y_l),把分差送进 sigmoid,σ(r_w − r_l) 就解读为「w 确实比 l 好」的概率,最大化它等价于最小化 L = −E[log σ(r_w − r_l)]。这正是经典的 Bradley–Terry 偏好模型。关键理解:RM 学的是相对排序而不是绝对好坏,损失只关心分差的方向和大小,分数的绝对值没有意义。

用一个最小可运行模型把整条链跑通(Node.js 22.15.0,node rm.js):4 个候选回答各带两个特征,标注排序拆出 6 个偏好对,RM 是线性打分器,从零开始按排序损失梯度下降:

// 最小奖励模型:Bradley-Terry 成对排序损失
const sigmoid = (z) => 1 / (1 + Math.exp(-z));
const F = [[3, 4], [1, 6], [5, 2], [2, 3]]; // 同一提示下 4 个候选回答的特征
// 标注排序 回答2 > 回答0 > 回答3 > 回答1,拆成 C(4,2)=6 个偏好对 (胜, 负)
const pairs = [[2, 0], [2, 1], [2, 3], [0, 1], [0, 3], [3, 1]];
let w = [0, 0]; // RM 参数从零开始
const score = (i) => w[0] * F[i][0] + w[1] * F[i][1];
const loss = () => pairs.reduce((s, [c, r]) =>
  s - Math.log(sigmoid(score(c) - score(r))), 0) / pairs.length;
console.log(`训练前: 平均损失 ${loss().toFixed(4)}, 标量分 [${F.map((_, i) => score(i).toFixed(2)).join(', ')}]`);
for (let e = 0; e < 300; e++) {
  for (const [c, r] of pairs) {
    const d = sigmoid(score(r) - score(c)); // 胜者分越低、败者分越高,梯度越大
    w[0] += 0.05 / pairs.length * d * (F[c][0] - F[r][0]);
    w[1] += 0.05 / pairs.length * d * (F[c][1] - F[r][1]);
  }
}
console.log(`训练后: 平均损失 ${loss().toFixed(4)}, 标量分 [${F.map((_, i) => score(i).toFixed(2)).join(', ')}]`);
console.log(`权重 w = [${w.map(x => x.toFixed(2)).join(', ')}],分数顺序复现标注排序`);

实测输出:

训练前: 平均损失 0.6931, 标量分 [0.00, 0.00, 0.00, 0.00]
训练后: 平均损失 0.0483, 标量分 [3.66, -0.90, 8.22, 2.29]
权重 w = [1.83, -0.45],分数顺序复现标注排序

训练前损失 0.6931 正好是 ln 2——所有分数相同,每对胜率都是一半;训练后损失压到 0.05,分数顺序 2 > 0 > 3 > 1 完整复现标注排序。这个玩具模型浓缩了真 RM 的全部要点:信号是两两比较,目标是拉开胜者与败者的分差。

深蓝黑背景概念图。顶部一行小字「概念图 · 奖励模型训练」,主标题「SFT 换个标量头,用成对偏好学排序」。第一区是结构改造图:灰色卡片「SFT 模型(Transformer 主干 + LM 头)」经标注「去掉 LM 头」的箭头,变成青色卡片「奖励模型 RM(输出单个标量 r(x, y))」,右侧注明「主干参数从 SFT 初始化」「打分是回归任务,不再生成文本」。第二区左下是数据面板「偏好数据:三元组 (x, y_w, y_l)」:x 是提示,y_w 是人工排序中胜出的回答,y_l 是被淘汰的回答,来源是同一提示采样多个回答、标注员两两排序。第三区右下是损失推导链:① 同一 x 下分别打分 r_θ(x, y_w) 与 r_θ(x, y_l);② 分差过 sigmoid,σ(r_w − r_l) 表示「A 胜过 B」的概率;③ 最大化它等价于最小化 L = −E[ log σ(r_w − r_l) ];④ 这正是 Bradley–Terry 偏好模型。底部总结条:RM 学的是相对排序而不是绝对好坏,训好后在 PPO 阶段充当奖励信号,通常配 KL 约束防止模型刷分跑偏;最底一行小字补充工程上常对奖励做白化与裁剪。 图 2:结构改造、偏好数据格式与排序损失的推导链。

面试现场的答题结构与追问预案。

按「结构改造 → 数据格式 → 损失设计」三段推进,每段一句话结论加一层细节。常见追问三个。一,训好的 RM 怎么用:在 PPO 阶段充当奖励信号,同时配 KL 约束防止策略偏离 SFT 太远去刷分。二,RM 会出什么问题:它终究是人类偏好的近似,策略模型会钻空子拿高分不干活——奖励黑客,所以工程上常对奖励做白化和裁剪,RM 也要随策略演进持续迭代。三,为什么不直接上 PPO 训 SFT:强化学习需要可计算的奖励信号,「人类觉得好」不可微也不可计算,RM 就是把这个主观信号蒸馏成一个可微的打分函数。你在项目里做偏好数据时,标注一致性是怎么保的?欢迎聊聊。