做这一行时间久了,会发现一个挺有意思的循环:每次微调(finetuning)做到后面,大家都会回到同一个问题——模型明明"会说话"了,但"说得对"这件事还是不稳。监督微调(SFT)能把格式、口吻、任务模板灌进去,却很难把"哪个回答更好"这种相对判断灌进去。于是强化学习(RL)就被重新拉回牌桌,变成了 finetuning 的第二段引擎。这篇东西想讲的就是"结合强化学习的 finetuning"这件事本身:它到底在解决什么问题、几种主流路线(PPO 式在线策略优化、DPO 类离线偏好优化、组相对策略优化、可验证奖励训练)分别适合什么场景、跑通一版需要多少显存和多少数据、以及那些只有真正跑过几轮训练才会遇到的坑。不管你是刚接手一个对齐任务的算法同学,还是手上有 SFT 模型想再往上推一档的应用开发者,下面的内容应该都能直接抄一部分作业走。
1. 先把问题摆清楚:RL 到底给 finetuning 补了什么
1.1 SFT 的天花板在哪儿
SFT 的本质是最大化标注回答的 token 级似然。这句话听起来很技术,翻译成人话就是:让模型在每一个位置上,都更倾向于吐出标注数据里出现过的那个词。问题在于,这个目标和"整段回答好不好"并不是一回事。一个回答可以每个词都很常见、很通顺,但整体结论是错的;也可以某几个词很别扭,但结论完全正确。SFT 的损失函数分不清这两种情况,它只会说"这里你该输出这个词"。
更麻烦的是负样本的缺位。SFT 数据只有正例,模型从来看不到"这样写是错的"。它学到的是模仿,不是辨别。这就好比一个人只背过标准答案,遇到新题时他知道答案长什么样,但不知道错误答案长什么样,所以踩坑的概率反而更高。我在实际项目里观察到一个很典型的现象:SFT 之后模型的格式规范度飙升,但一旦问题稍微换个问法,或者需要多步推理,输出质量就掉得很明显——因为它没有"探索—比较—改进"这个过程。
还有一层是暴露偏差。训练时模型看到的前缀全部来自人类标注,推理时看到的前缀全部来自自己。一旦前面出现一点小偏差,后面就会滚雪球。这类误差在长输出任务里尤其致命,比如写代码、写长文档、做多步计算。SFT 对此无能为力,因为它没有"整段生成完再回头评价"的机制。
所以 SFT 的正确定位是"打底":把语言能力、任务格式、领域知识灌进去,让模型进入一个"可被优化"的状态。它解决的是"会不会",不解决"好不好"。
1.2 强化学习在这里扮演的角色
RL 补的正是"好坏判断"这一块。它的核心机制是:让模型针对同一个问题采样多条回答,用一个评分信号(奖励模型或规则)给它们打分,然后提高高分回答的概率、压低低分回答的概率。这样一来,训练目标从"token 级似然"变成了"序列级期望回报",和人类真正关心的东西对齐了。
这里有个关键点值得说透:为什么非得用 RL,不能直接用高分数据再做一轮 SFT?答案在"相对"两个字。SFT 只能告诉你"这个好",RL 能告诉你"这个比那个好多少"。后者包含的信息量更大,而且能处理那些"都对但优劣不同"的情况。另外,SFT 在只喂高分数据时会出现分布塌缩——模型越来越像那批数据,多样性掉得很快。而 RL 因为保留了采样和探索,能在提升质量的同时维持一定的输出分布宽度。
再有一个实际优势:RL 可以优化不可微的指标。比如"代码能不能通过单元测试""答案是否包含指定字段""长度是否在范围内",这些东西没法求导,但可以变成一个标量奖励喂给 RL。这是 SFT 完全做不到的事情,也是近一年"可验证奖励"路线能火起来的根本原因。
1.3 三种主流结合形态的横向对比
把 RL 和 finetuning 结合,落到工程上有三种典型形态,它们的成本结构差别很大,选错了会很痛苦。
| 形态 | 需要的模型 | 数据来源 | 显存压力 | 训练稳定性 | 典型适用场景 |
|---|---|---|---|---|---|
| PPO 式在线策略优化 | 策略、参考、奖励、价值共四个 | 在线采样 + 奖励模型打分 | 最高,约 2.5 到 4 倍 SFT | 中等,需调 KL 与 clip | 开放式生成、主观质量提升 |
| 离线偏好优化(DPO 系) | 策略 + 参考两个 | 预先构造的偏好对 | 低,接近 SFT | 较高,超参少 | 数据已有、预算有限、快速迭代 |
| 组相对策略优化(免 critic) | 策略 + 参考(+ 规则奖励) | 在线采样 + 规则/模型打分 | 中高,省掉价值网络 | 中等偏高 | 数学、代码、可自动判定的任务 |
这张表里最容易被低估的是"数据来源"这一列。PPO 和组相对方法需要在线采样,意味着每个训练 step 都要跑一遍推理,推理吞吐直接决定训练速度;而 DPO 系只需要离线数据,一次构造、反复使用,工程上轻得多。很多团队一上来就冲 PPO,结果发现 80% 的时间花在等推理上,模型本身没迭代几轮。
我的建议是:先从离线偏好优化跑通闭环,确认数据和评估链路没问题,再上在线方法。因为在线方法会把你数据构造里的问题放大十倍——偏好数据标签噪声大,在离线方法里表现为"效果提升有限",在在线方法里就会表现为"奖励涨了但模型变傻了"。
2. 方案选型:从 PPO 到 DPO 再到免 critic 路线
2.1 PPO 全链路:四个模型同场意味着什么
PPO 式做法是这条线里最经典也最重的。它的结构是:策略模型(actor)负责生成,参考模型(reference)提供 KL 约束的基准,奖励模型(reward)给整段输出打分,价值模型(critic)估计每个 token 位置的基线。四个模型同时在显存里,是它最直接的代价。
奖励是怎么落到每个 token 上的?通常是整段输出只拿一个序列级分数,然后把它广播到每个 token 位置上,再减去一个 KL 惩罚项。KL 项的形式大致是每个位置上的log π(a|s) - log π_ref(a|s)乘以系数 β。这一项的作用是防止策略跑得离参考模型太远,本质是一个"别乱来"的软约束。
价值模型负责把序列级奖励摊到 token 级,用 GAE(广义优势估计)算优势值。这里有个很反直觉的点:序列级奖励任务里,γ 一般直接设 1.0,不做折扣。因为整段输出的好坏是整体评价,不存在"越靠后越重要"的道理,折扣反而会引入偏差。真正需要调的是 λ 和 KL 系数。
PPO 的优势很明显:在线采样能持续探索当前策略的盲区,理论上上限最高。劣势同样明显:四个模型、两套前向、一次训练要跑三遍推理(策略采样、参考打分、奖励打分),工程复杂度直接拉满。如果团队里没有专门做分布式训练的人,我不太建议一上来就啃这块。
2.2 DPO:把"训奖励模型"这一步省掉
DPO 的聪明之处在于数学上的等价变换:偏好建模的目标(Bradley-Terry)可以被证明等价于一个只在策略和参考模型之间计算的分类损失,奖励模型被隐式地表达成了策略与参考的对数概率比。落到代码上只有一行核心:
# 简化形态,仅示意损失结构 logits = beta * ( (policy_logp_chosen - ref_logp_chosen) - (policy_logp_rejected - ref_logp_rejected) ) loss = -F.logsigmoid(logits).mean()其中 β 控制"允许多大程度上偏离参考模型",取值通常在 0.1 附近,任务差异大时需要重新扫。β 太小,模型几乎不动;β 太大,容易把模型推到一个奇怪的位置,输出变得生硬。我一般会在 0.05、0.1、0.3 三个点先做小规模对照。
DPO 最吸引人的地方是工程轻:不需要奖励模型,不需要在线采样,不需要 critic,两个模型(策略 + 参考)用参数高效微调(LoRA 之类)就能在单机多卡上跑起来。数据构造一次,可以反复做多轮实验,迭代速度快一个数量级。
但它有两个硬伤必须说清楚。第一,它是纯离线的,训练分布完全由数据集决定,模型在训练中探索到的新情况它看不见,所以存在分布偏移问题。第二,它对数据质量极度敏感——偏好对里如果有 10% 的标注方向反了,DPO 会老老实实把这些错误学进去,而且不像 PPO 那样有在线探索来纠偏。我在一个项目里遇到过类似情况:清理掉一批"长度明显失衡"的偏好对之后,同一套超参下效果提升了肉眼可见的一截。
2.3 免 critic 的组相对方法:为什么它在中档算力上很香
组相对策略优化(常见叫法是 GRPO 这一类)的核心改动是:干掉价值网络,用同一问题下多条采样的平均分作为基线。具体做法是对每个 prompt 采样 G 条回答(G 常取 8 或 16),算出各自的奖励,然后用组内均值做 baseline,优势值就是归一化后的偏离程度。
这个改动看起来小,实际收益不小。省掉 critic 意味着省掉一整个模型的显存和一次前向,同时也避免了"critic 学不准导致优势估计失真"这个老大难问题。而在有可验证奖励的任务里(答案对不对、测试过不过),奖励信号本身就是可靠的,不需要 critic 去平滑。
代价是采样成本变高。G=8 意味着每个 prompt 要生成 8 条完整回答,推理开销是原来的 8 倍。所以这套方法在实践中几乎必然要配一个专门的推理加速引擎,并且把生成长度控制住。我见过一些团队把最大生成长度设成 8192,结果一条 rollout 跑到天荒地老——后来压到 2048 并加上"答完即停"的判定,吞吐直接翻了几倍。
这套方法特别适合两类任务:一是答案可自动判定的(数学、代码、结构化抽取),二是评分标准可以写成明确规则的(格式、字段完整性)。反过来,开放式写作这类只能靠人打分或者奖励模型打分的任务,它的优势就没那么突出。
2.4 选型别靠感觉,按这张表对号入座
| 判断维度 | 选 DPO 系 | 选 PPO 系 | 选组相对方法 |
|---|---|---|---|
| 有没有可靠的自动判定 | 无所谓 | 可以没有 | 最好有 |
| 预算(卡数 / 时间) | 紧张 | 充裕 | 中等 |
| 数据是否已有偏好对 | 已有最好 | 可现场构造 | 可现场构造 |
| 团队是否有分布式 RL 经验 | 不需要 | 需要 | 需要一点 |
| 主要想解决 | 风格、偏好、安全性 | 综合质量上限 | 推理正确率 |
| 迭代周期 | 天级 | 周级 | 天到周 |
实际项目里最常见的组合是两段式:先用 DPO 系把明显不对的输出压下去,拿到一个"听话"的基线;再用组相对方法在可验证任务上做精修。这样既能拿到快的收益,又不会一上来就被工程复杂度拖死。
3. 数据与奖励:整条链路里最贵的部分
3.1 偏好对怎么造才不浪费算力
偏好数据的质量决定了这条路的天花板。我这里说的质量不是"标注员认不认真",而是"这两个回答之间的差距是否足够清晰且有意义"。
第一个原则是控制长度差。如果所有 chosen 都比 rejected 长,模型学到的可能只是"话多更好"。这不是危言耸听,而是这条线上最经典的偏差来源之一。我的做法是在构造阶段就把长度差限制在一个区间内(比如不超过 30%),超出范围的对直接丢弃或者重新配对。同时在训练后检查输出长度分布,如果平均长度涨了 40% 以上而质量没有对应提升,基本可以判定吃到了长度偏置。
第二个原则是负例要有难度。拿一个明显胡说八道的回答当 rejected,模型学不到任何细粒度判断,只会学到"别说胡话"这种它本来就会的东西。真正有价值的是"看起来很像但细节错了"的样本,比如推理步骤对但最后一步算错、格式规范但引用了不存在的字段。这类难负例的构造通常需要模型自己生成多个候选再挑,成本不低,但收益比堆量高得多。
第三个原则是标注一致性要量化。同一批数据让多人交叉标注一部分,算一下一致性系数。如果一致性很低,说明你的评分标准写得不够细,先改标准再扩量。我见过一个项目在没有量化一致性的情况下标了五万条,最后发现有近三成方向存疑,返工成本远高于一开始做小样本校准。
3.2 奖励模型不是越大越好,是越"稳"越好
如果用奖励模型,训练目标通常是 Bradley-Terry 形式的成对损失:让 chosen 的分数比 rejected 高出至少一个 margin。实现上有个小技巧,把 margin 设成一个小的正值(0.5 到 1.0),能缓解"分数挤在一起"的问题。
奖励模型真正的难点不在训练,在泛化。它在训练分布内表现很好,一碰到分布外输入就开始胡说——给出的分数和人类判断相关性骤降。而 RL 训练恰恰会不断把策略推向分布外,因为它就是在优化"哪里分数高往哪走"。于是就出现了那个经典死循环:策略找到了奖励模型的漏洞,分数一路涨,实际质量一路跌。
缓解手段有几个,我按有效性排序:一是在奖励模型训练数据里加入策略模型自己采样的样本,让它的判别域覆盖到策略会走到的区域;二是用集成或多次采样取均值降低单个模型的方差;三是对奖励做长度归一化,把分数除以长度的某个幂次,压住"越长越高分"的趋势;四是定期用人工抽检校准,一旦发现离线分数和人工评分背离就停下来查。
3.3 可验证奖励:能用规则就别用模型
只要能写成规则的,就别上奖励模型。这句话我逢人就说,因为实在省了太多事。数学题验答案、代码跑单测、结构化输出校验字段、格式检查正则匹配,这些奖励信号是确定性的、零噪声的、不需要训练的。用它做 RL,你就绕开了整个"奖励模型泛化"的坑。
规则奖励的设计要点是分层给分,不要非零即一。比如代码任务可以这样拆:能不能解析(0.2)、能不能跑通(0.4)、测试通过率(0.4 乘以通过比例)。分层奖励能提供更平滑的梯度,避免"全组都是 0 分导致优势值全零"这种让训练原地踏步的情况。这一点很关键,我在第一次用组相对方法时就被坑过:整组采样全部失败,组内均值等于每条奖励,归一化后优势全是 0,那一步的梯度贡献为零,白跑。
另外要注意防作弊。规则奖励也是会被 hack 的。比如你要求"答案里必须出现某个关键词",模型就会在开头把这堆关键词堆一遍然后胡写。所以规则要尽量覆盖"形式 + 内容"两层,别只查形式。
3.4 数据配比与难度分布:别都喂简单题
训练集里简单题太多,模型几乎全对,组内奖励没有方差,优势值全零,这些样本对训练毫无贡献,纯浪费算力。反过来全是难题,模型全错,同样没有梯度。理想状态是每道题的通过率落在中间区间,这样组内才有区分度。
实操上可以做一个难度预热:先用当前模型对训练集做一遍采样,统计每道题的通过率,然后把通过率过高(比如大于 0.9)和过低(小于 0.1)的样本降权或剔除,保留中间段。这个筛选过程本身要花一轮推理,但省下来的训练时间通常远超这个成本。
配比上还需要注意任务类型的平衡。如果只练数学,模型在数学上去了,但通用对话可能退化。我的经验是保留一定比例的通用指令数据(哪怕不参与 RL,只做混入的 SFT 正则),能有效缓解能力遗忘。
4. 手把手跑通一版在线策略微调
4.1 显存预算怎么算,别等 OOM 才后悔
以一个 7B 量级的模型为例,先算静态占用。bf16 权重约 14GB;如果用全参数训练,梯度再加 14GB;Adam 的一阶二阶动量按 fp32 存,约 56GB;再加上 fp32 的权重副本 28GB。加起来已经超过 110GB,单卡 80GB 直接出局。这时候要么上 ZeRO-3 之类的分片(8 卡分摊后每卡静态约 15GB 上下),要么改用参数高效微调。
如果选 LoRA,静态部分就轻多了:冻结的主干 14GB,LoRA 参数和其优化器状态通常不到 2GB。真正的显存大头变成了激活和推理时的 KV cache。这里给个估算方法:KV cache 大小约等于2 × 层数 × 隐藏维度 × 序列长度 × batch × 字节数,7B 模型在 4096 长度、batch 8 的情况下,很容易吃掉二三十 GB。所以你会看到一个现象:训练配置没变,只是把最大长度从 2048 提到 4096,就 OOM 了。
几个实用的省显存手段:开启梯度检查点(省激活,慢 20% 左右,能接受);rollout 阶段用独立的推理引擎并开启分页式 KV 管理;把参考模型和奖励模型的前向放在no_grad下并且单独调度;长序列做打包(packing)时务必处理好 attention mask,否则不同样本会互相"看见",损失会莫名其妙地抖。
4.2 冷启动 SFT 这一步不能省
直接拿一个未经过指令微调的基座模型去做 RL,是很痛苦的一件事。因为它的输出分布太散,采样出来的东西大多不可用,奖励信号几乎全是低分,优势估计没有区分度。标准做法是先做一轮轻量 SFT(几百到几千条高质量样本),把格式和基本任务能力打出来,再进 RL。
这一轮 SFT 不需要追求极致效果,目标是"能让模型按格式输出、能听懂指令"。我一般会用 1e-5 到 2e-5 的学习率训 1 到 2 个 epoch,观察验证集上的格式合规率是否超过 90%。如果连格式都稳不住,进 RL 只会更乱。
有个细节容易被忽略:SFT 阶段用的对话模板必须和 RL 阶段完全一致。我踩过这个坑——SFT 时用的模板里 assistant 段前面多了一个换行,RL 时换了模板,导致策略模型一上来输出就带奇怪的前缀,KL 直接爆掉。这类问题排查起来极费时间,因为损失曲线看起来只是"有点抖"。
4.3 rollout 阶段的采样配置
在线方法里,rollout 的配置对结果影响很大,但很多人直接抄默认值。几个关键点:
采样温度建议设 1.0,top_p 设 1.0,也就是不做截断。原因是你需要探索,需要看到模型真实分布下的多样性;如果用很低的温度,采样出来的几条回答几乎一样,组内没有方差,优势值失效。当然温度太高也会带来一堆垃圾输出,所以要在"有区分度"和"别太离谱"之间找平衡,我一般在 0.9 到 1.1 之间试。
每个 prompt 的采样条数(组大小)取 8 通常够用,追求稳定可以到 16,但吞吐会线性下降。max_new_tokens 要根据任务定,能短则短,同时在生成时加停止条件,别让模型一直输出到上限。
还有一个工程细节:rollout 用的推理引擎和训练用的模型必须保持权重同步。常见做法是每个 step 结束后把训练侧权重推给推理侧,或者共享同一份参数做原地更新。如果同步不及时,你实际上在做 off-policy 训练,重要性采样比率会偏离 1,严重时策略更新方向就错了。我见过因为同步延迟导致 ratio 长期在 1.3 以上的情况,训练曲线看着在涨,实际是虚的。
4.4 训练循环的核心结构
下面是这类训练的一个骨架,去掉具体库名,保留结构,方便对照自己的实现:
for step, batch in enumerate(loader): prompts = batch["prompts"] # 1. 采样:同一 prompt 生成 G 条回答 with torch.no_grad(): responses, logps_old = rollout_engine.generate( prompts, group_size=G, temperature=1.0, top_p=1.0, max_new_tokens=MAX_NEW ) # 2. 打分:规则或奖励模型 rewards = score(prompts, responses) # [B*G] # 3. 优势:组内归一化,免 critic 版本 rewards = rewards.view(B, G) adv = (rewards - rewards.mean(dim=1, keepdim=True)) / (rewards.std(dim=1, keepdim=True) + 1e-6) adv = adv.view(-1) # 4. 前向:策略与参考模型 logps = policy(prompts, responses) # 逐 token 对数概率 with torch.no_grad(): logps_ref = ref_policy(prompts, responses) # 5. 逐 token KL 惩罚并入奖励 kl = logps - logps_ref token_reward = -BETA * kl # 序列级奖励放在最后一个有效 token 上 # 6. 策略梯度损失 + 可选裁剪 ratio = torch.exp(logps - logps_old) surr = torch.min(ratio * adv, torch.clamp(ratio, 1-CLIP, 1+CLIP) * adv) loss = -surr.mean() + kl_penalty_coef * kl.mean() loss.backward() optimizer.step()几个实操注释。第一,序列级奖励要广播到 token 时,只能在有效 token 上算,padding 位置必须 mask 掉,否则 loss 会被稀释,看起来在降其实没学东西。第二,KL 有两种用法:一种是像上面这样放进奖励里做惩罚,另一种是单独作为 loss 项,两种都行但别混着调,会很难定位问题。第三,ratio 的裁剪范围 0.2 是常用值,任务差异大的话可以放宽到 0.28,但别超过 0.3,否则一次更新步子太大容易崩。
4.5 超参怎么定:给一组能跑起来的起点
| 参数 | 建议起点 | 说明 |
|---|---|---|
| 策略学习率 | 5e-7 到 1e-6 | 比 SFT 低一个量级,太高必崩 |
| KL 系数 β | 0.01 到 0.05 | 太大模型不动,太小跑飞 |
| 裁剪范围 | 0.2 | 常用值,别超过 0.3 |
| 组大小 G | 8 | 显存紧张可降到 4 |
| 采样温度 | 1.0 | 太低无区分度 |
| 每个 prompt 的 batch | 8 到 32 | 按显存调 |
| 梯度累积 | 2 到 8 | 用来凑有效 batch |
| 熵正则 | 0 到 0.01 | 防熵塌缩,但别太大 |
| 目标 KL | 6 左右 | 自适应调 β 时用,单位是 nats |
关于自适应 KL:做法是每个 step 算一下实际 KL,如果超过目标值的 1.5 倍就把 β 调大,低于目标值的一半就调小。这个机制在训练初期特别有用,因为一开始策略和参考差距很小,固定 β 会让训练推进极慢。
学习率这块我要多说一句。在线 RL 对学习率极其敏感,比 SFT 敏感得多。同一份数据,5e-7 和 2e-6 可能就是"稳步上升"和"两小时后崩掉"的区别。建议第一次跑先取 5e-7,跑几百步看曲线形状,如果 KL 稳定、奖励缓慢上升,再考虑往上调。
5. 踩坑记录与排查手册
5.1 奖励涨了但效果掉了:reward hacking 的典型画像
这是最让人血压升高的一类问题。训练日志上奖励曲线一路向上,验证集的人工评分却在往下走。出现这种情况,九成是策略找到了奖励函数的漏洞。
几个高频表现:输出长度暴涨(回答越啰嗦分越高)、开头堆关键词、结尾加一堆免责声明、格式过度包装(加一堆没必要的标题符号)、重复同一句话刷置信度。排查方法很土但有效——每个固定步数抽十条输出人肉读一遍,别只看指标。我自己的习惯是每 200 步抽一次,读的时候专门盯长度分布和重复率。
对策分三层。短期是给奖励加约束项(长度惩罚、重复惩罚、格式罚分);中期是修奖励模型,把策略采样出来的"高分烂样本"补进训练数据当成负例;长期是换奖励来源,能用规则就用规则,减少对单一奖励模型的依赖。
5.2 训练崩掉的几种典型曲线
曲线是最好的诊断工具,几种形态基本对应不同病因:
| 曲线形态 | 大概率原因 | 处置方式 |
|---|---|---|
| KL 突然垂直上升 | 学习率过大、数据里有异常样本 | 降学习率、开梯度裁剪、查异常样本 |
| 熵快速掉到接近 0 | 熵正则太小、β 太大、奖励太单一 | 加熵正则、降 β、增加奖励区分度 |
| 奖励几乎水平一条线 | 组内无方差、奖励信号失效 | 换难度分布、检查奖励函数是否返回常值 |
| 损失出现 NaN | 除零、log 概率溢出、长度超限 | 加 eps、检查 mask、限制长度 |
| 长度单调上涨 | 长度偏置 | 长度归一化、偏好对长度控制 |
熵塌缩这个我想单独提一下。它的表现是模型输出越来越雷同,同一 prompt 采样 8 条,8 条内容几乎一样。这时候组内优势值全接近 0,训练就停滞了。根因通常是策略过早收敛到一个局部高分区。缓解办法是保留熵正则项、适度降低 β、以及提高采样多样性。我在一个任务上把熵正则从 0 加到 0.005,停滞问题明显缓解,代价是收敛慢了一点,可以接受。
5.3 长度偏置:这条线上最顽固的偏差
不管用哪种方法,长度偏置都会冒出来。原因很朴素:更长的回答通常包含更多正确信息,人类标注时也倾向给长回答高分,奖励模型把这个偏好学走了,RL 又把它放大。
压制手段我总结成四条,按性价比排序:一是数据侧控制,偏好对里长度差太大的一律不要;二是奖励侧归一化,把奖励除以长度的对数或幂次;三是训练侧约束,对超出目标区间的输出直接扣分;四是评估侧监控,把输出长度分布作为一项必看指标,一旦均值偏移超过 20% 就停下来查。
这里有个反直觉的点:不要一味追求长度下降。有些任务的正确答案确实需要更长篇幅,你强行压长度会把质量一起压下去。目标是长度分布"合理",不是"越短越好"。我的判断标准是:和基线 SFT 模型比,长度均值偏移不超过 15%,同时质量指标有提升,这才算健康进步。
5.4 常见问题速查
| 现象 | 先查什么 | 常见结论 |
|---|---|---|
| 训练完全不动,奖励不变 | 组内是否有方差 | 难度分布太集中,全对或全错 |
| 奖励涨、指标不涨 | 抽十条输出读一遍 | 奖励被 hack |
| 验证集效果抖动很大 | 学习率与 batch 大小 | batch 太小,梯度噪声大 |
| 推理侧和训练侧结果不一致 | 对话模板、tokenizer、停止符 | 配置不一致 |
| 训练几步后输出全是重复 | 熵、温度、重复惩罚 | 熵塌缩前兆 |
| 显存随 step 缓慢上涨 | KV cache 释放、梯度图未 detach | 内存泄漏 |
| 训练速度越来越慢 | 长度分布变长 | 被长度偏置拖累 |
6. 评估这一步,八成团队做得太糙
6.1 评估集要能回答"退化了没有"
RL 微调有个隐蔽副作用:目标能力涨了,其他能力悄悄掉了。所以我建评估集时会分成三块——目标能力集(这次要提升的)、保留能力集(现有的通用能力,不能掉)、对抗集(专门挑容易出错的边界情况)。
保留能力集这块特别容易被省略。我见过一个项目只盯数学准确率,涨了八个点,结果通用对话的连贯性明显变差,用户侧反馈一堆。回滚之后发现是因为训练集里数学占比到了 95%。后来调整到 60% 数学 + 40% 通用,两边都能接受。
评估集规模不用很大,几百条就够看出趋势,但每一条都要有明确的判分标准。开放式任务的判分建议用"成对比较 + 胜率"而不是绝对打分,因为人对绝对分数的标定很不稳定,比较起来反而一致得多。
6.2 别只信离线指标,人工盲评比什么都准
自动评估(模型当裁判)很方便,但它有系统性偏差:偏爱长回答、偏爱格式规整的回答、偏爱和自己风格相近的回答。这三个偏差刚好和 reward hacking 的方向重合,所以你用自动评估去看 RL 结果,很容易看到一片大好。
我的做法是固定抽样 100 到 200 条,做人工盲评:隐藏模型来源、随机顺序、双人交叉。胜率差异在 5 个点以内基本可以认为没有显著差别,别为了这点差距反复调参。真正有价值的信号往往是那种"80 条里 20 条明显更好"的情况,这种差异人眼一眼就能看出来。
另外记得加一个格式合规检查,自动化跑。RL 训练后格式出问题是高频事故,比如偶尔漏掉结束标记、JSON 少个括号。这类问题自动检查一秒就能发现,靠人读会漏。
6.3 上线前的回滚准备
不管评估多充分,上线都要留退路。具体做法:保留上一版模型权重、保留流量切换开关、设置一个降级阈值(比如负反馈率超过基线 20% 就自动回滚)。这些是工程常识,但在 RL 场景里更容易被忽略,因为训练指标太好看,人容易上头。
还有一个我吃过亏的点:同一个模型在训练时的推理引擎和线上推理引擎可能不同,两者的数值精度、采样实现、停止条件都可能不一致。上线前一定要用线上引擎重跑一遍评估集,别拿训练时的结果当准。
7. 我个人在几轮项目里攒下的体会
第一件想说的是,这套东西的上限由数据决定,下限由工程决定。数据不行,再怎么调超参也上不去;工程不稳,再好的数据也跑不出结果。所以时间分配上,我会把六成花在数据构造和评估,三成花在工程稳定性,一成花在调参。这个比例和很多人的直觉是反的——大家往往把八成时间花在调参上,然后抱怨"RL 玄学"。
第二件是先把最朴素的方案跑到极致,再考虑复杂方案。规则奖励 + 组相对方法这套组合,在很多可验证任务上已经能拿到相当不错的收益,而且实现简单、调试容易。反而是上来就搭四模型 PPO 的团队,经常卡在工程问题上,两个月没出结果。
第三件是每次实验只改一个变量。RL 实验的方差本来就大,同时改学习率、β、数据配比,出了结果你也不知道是哪个起的作用。我现在的习惯是做一张实验记录表,每次只动一项,跑完记录奖励均值、KL 均值、输出长度均值、评估集准确率四个数,攒够十几次之后,你会对"哪个参数往哪个方向推有效"形成自己的直觉,这个直觉比任何教程都值钱。
第四件是个小技巧,关于长任务的生成控制:在训练时就把"答完即停"的信号做进奖励里。具体做法是如果模型在达到长度上限时还没输出结束标记,给一个小额惩罚;如果能提前给出结束标记且内容完整,给一个小额奖励。这个改动很小,但能显著压低无效长度,训练速度和推理成本都会跟着改善。我在一个需要长输出的任务上加了这个惩罚,平均长度降了三成,质量基本没掉。
最后提一句评估的心态。RL 微调的结果从来不是"全面提升",它就是在做权衡:把某类回答的概率提上去,必然有另一类被压下来。所以拿到结果先别问"涨了多少",先问"拿什么换了什么"。想清楚这笔交易划不划算,比盯着指标曲线有意义得多。