1. 先把SRL这个说法掰开:它到底在补哪块缺口
1.1 从监督微调到强化学习,中间断了一截
接手一个LLM训练任务,最典型的场景是这样:基座模型拿到手,先做监督微调(SFT),几千到几万条高质量问答喂下去,loss降得很漂亮,模型说话也像个正常人了。可一旦上评测,问题立刻暴露——数学题步骤写到一半自己拐弯,代码生成第一行就语法错,多步推理里前一步的结论和后一步对不上。这不是数据量不够,是SFT这套目标函数天生管不到这些事。
SFT本质上是最大似然估计,它优化的是“在给定上文时,输出专家答案的概率”。这句话听起来没问题,但它隐含了一个很要命的假设:训练时模型看到的永远是正确前缀。推理时它看到的是自己刚写出来的、可能已经错了的前缀。这个错位在业内叫exposure bias,通俗点说就是“跟着师傅走会走,自己走就迷路”。SFT教会模型的是“像专家”,不是“能做对”。
强化学习补的正是这一块。它不要求模型逐字模仿,而是让模型自己生成多条路径,用奖励信号告诉它哪条路走通了、哪条路走死了。问题在于,语言生成的搜索空间大到离谱,一个50步的推理链,每步词表几万,组合数是个天文数字。纯靠稀疏的结果奖励(对/错)去训,模型大部分时间在瞎撞,梯度信号稀薄到几乎学不动。这就是所谓的冷启动难题。
监督强化学习(SRL)这个说法的价值就在于它指出了中间地带:既然纯监督学不会“对”,纯强化又搜不到“对”,那就让监督信号以各种形式渗进强化学习的回路里,给搜索提供方向感。这个思路在LLM训练里至少有三种落地形态,而它们经常被混为一谈,这也是我见过最多人绕晕的地方。
1.2 三种形态:奖励侧、数据侧、损失侧
我习惯把SRL按“监督信号插在哪一环”来分类,这样工程上好做决策。
奖励侧监督,是把人的判断或规则做成奖励模型/验证器,RL优化的是这个打分器。典型做法是训练结果奖励模型(ORM)或过程奖励模型(PRM),或者干脆用规则验证器直接判对错。这条路的特点是信号稠密、可控,缺点是奖励模型本身会被钻空子。
数据侧监督,是让模型自己采样,用监督信号过滤出好样本,再拿去做SFT,循环迭代。拒绝采样微调、专家迭代、自举推理都属于这一类。它绕开了策略梯度的复杂度,工程实现简单,本质上是用采样换数据、用数据换能力。
损失侧监督,是把RL目标改写成监督式损失。直接偏好优化(DPO)系列是代表:它把“奖励最大化加KL约束”这个带约束的优化问题解析地解出来,最后变成一个类似分类的loss,完全不需要在线采样。这类方法把RL的复杂性压到了最低,代价是只能用离线数据,分布偏移管不住。
| 形态 | 监督信号位置 | 代表方法 | 工程复杂度 | 主要风险 |
|---|---|---|---|---|
| 奖励侧 | 奖励函数/奖励模型 | PPO、GRPO + ORM/PRM/规则验证器 | 高 | 奖励黑客、打分器漂移 |
| 数据侧 | 采样过滤环节 | 拒绝采样微调、专家迭代 | 中 | 多样性塌缩、迭代收益递减 |
| 损失侧 | 目标函数本身 | DPO、ORPO、KTO | 低 | 离线分布偏移、长度偏置 |
理解这张表的实际意义在于:你手上的算力和数据决定你能选哪一档。八卡集群想跑全参PPO训7B,光是把策略、参考、奖励、价值四个模型塞进显存就够呛;但同一批卡跑DPO或者规则奖励的GRPO,是完全可行的。后面几节我会按这个分类逐条拆开讲。
2. 核心机制拆解:监督信号是怎么进到优化回路里的
2.1 奖励塑形:把“对错”拆成可累加的分项
纯结果奖励最大的问题是稀疏。一道数学题写了两百字,最后答案错了,整条轨迹拿0分,模型不知道是第3步公式用错还是第7步算错。奖励塑形的思路就是把总分拆成几个可解释的分项,让梯度能落到具体位置。
我在实际项目里常用的一套评分结构大概是这样:
def reward(prompt, response, gold): score = 0.0 # 1. 结果分:答案归一化后精确匹配 if normalize(extract_answer(response)) == normalize(gold): score += 1.0 # 2. 格式分:必须包含推理段与答案段标记 if has_reasoning_block(response) and has_answer_block(response): score += 0.3 # 3. 步骤分:PRM 或规则校验的中间步骤通过率 score += 0.4 * step_verifier(prompt, response) # 4. 长度约束:超长截断扣分,避免灌水 if len(response) > MAX_LEN: score -= 0.2 return score这套结构里有几个需要想清楚的地方。格式分看着很土,但它是所有分项里性价比最高的:模型一开始根本不知道要按什么结构输出,给0.3分的格式奖励,几十步之内就能把输出格式拉到合规,后面的结果分才有机会起作用。这就像教小孩做应用题,先要求“写解、写步骤、写答”,格式立起来了再谈对错。
步骤分要靠过程奖励模型(PRM)。PRM的训练数据标注成本远高于ORM,因为你要对每一步打标,而不是只看最终答案。业内的折中做法是用蒙特卡洛估计自动生成步骤标签:从某个中间步骤往后采样多条完整轨迹,看最终正确率,正确率高就说明这一步是好的。这个做法的隐含假设是“好步骤更容易通向正确答案”,在数学和代码这类有明确终点的任务上成立,在开放写作上就很不靠谱。
提示:结果分和步骤分的权重要反复调。步骤分给太高,模型会学会“步骤看起来漂亮但答案瞎写”;给太低,又退化成稀疏奖励。我一般从结果分1.0、步骤分0.4起步,观察奖励分布后再动。
还有一类绕不开的监督信号是可验证奖励。数学题可以直接比对最终答案,代码题可以跑单元测试,结构化抽取可以校验schema。这类任务根本不需要训奖励模型,一个确定性验证器就能把噪声降到零,而且不会随训练漂移。我在选任务时有个很朴素的判断标准:能写验证器的任务优先做RL,写不出验证器的任务老老实实做SFT加偏好对齐。原因很简单,学出来的奖励模型永远在被优化的过程中失真,而代码执行器不会因为你训得久就改变行为。
2.2 拒绝采样与自举:把采样成本换成数据质量
拒绝采样微调这条路,我愿称之为“穷人版RL”。流程很直白:拿当前模型对每个prompt采样k条回答,用验证器筛出正确的,把正确的拼成SFT数据,微调一轮,再拿新模型重复。学术界管这叫专家迭代或者ReST类方法,自举推理(STaR)是它在推理任务上的变体,区别在于自举推理会给失败的样本补上正确答案再一起训,避免只有正样本。
这条路的核心不是生成,是过滤。生成谁都会,采样参数一调就能出k条,但过滤策略决定了这一轮迭代到底是涨还是崩。我踩过的坑主要有两个。
第一个是难度失衡。如果一批prompt里简单题占比过高,采样8条全对,过滤后留下的全是简单题,模型在简单题上越训越熟,难题一点没碰。解决办法是按通过率分层:pass@8等于8的样本直接丢弃,pass@8等于0的也丢弃(现阶段学不会),只保留通过率在中间区间的样本。这个筛选逻辑在实现上就几行代码,但对迭代效率的影响是数量级的。
第二个是多样性塌缩。连续迭代几轮之后,模型的输出分布会越来越窄,同一道题采8条,8条几乎一模一样。这时候再过滤已经没有信息量了。缓解手段是每轮迭代都混入一定比例的历史数据和原始SFT数据,比例我一般控制在3:1到4:1之间,同时把采样温度维持在0.8以上,宁可要一些低质量但多样的样本。
# 难度分层筛选的核心逻辑 samples = [generate(p, k=8, temperature=0.9) for p in prompts] for p, group in zip(prompts, samples): correct = sum(verify(p, s) for s in group) if correct == 0 or correct == len(group): continue # 太难或太易,本轮跳过 kept.extend(s for s in group if verify(p, s))这套流程有个隐含的天花板:模型必须已经具备“偶尔做对”的能力,迭代才能启动。pass@k长期为0的任务,采样再多轮也没用,得先回去补SFT数据或者降低题目难度。我在一个多步计算的场景里卡过整整两天,最后发现是题目本身超出了基座能力太多,不是采样策略的问题。
2.3 混合损失:SFT项和策略梯度项怎么配比
真正在线跑策略梯度的时候,几乎没有人用纯RL loss。原因很实际:纯策略梯度更新只告诉模型“这条好、那条差”,不告诉它“语言应该长什么样”。训久了模型的输出会变得很奇怪——答案对了,但语句开始碎片化,标点乱飞,甚至混入训练时没见过的符号组合。这个现象在早期RLHF里特别常见,业内叫语言退化。
解决办法是在loss里加一个SFT项做正则。混合损失的形式大概是:
L = L_pg + λ_sft * L_sft + β * KL(π || π_ref)三个系数的分工很明确。λ_sft控制语言形态的保持力度,我一般设在0.05到0.2之间,任务越偏向开放生成,这个值越大。β是KL惩罚系数,防止策略跑得离参考模型太远,常见范围是0.01到0.04,训练初期可以给大一点比如0.04,等奖励曲线起来之后再降到0.01。这里要注意KL的估算方式,用k3估计量比k1噪声小很多,代价是要多算一次log prob。
KL约束这件事值得单独说。它的作用不是“让模型不变”,而是“让模型别一次性变太多”。语言模型的策略空间极其庞大,奖励信号稍微有点偏差,策略就可能整体漂移到某个奇怪的角落,比如所有回答都变得极长,或者所有回答都开始堆砌某些高频连接词。KL就是一根橡皮筋,拉住它。但橡皮筋太紧也麻烦,β设到0.1以上,模型基本学不动,奖励曲线平得像一条直线,你会怀疑是不是代码写错了。
| 系数 | 常用范围 | 调大后的表现 | 调小后的表现 |
|---|---|---|---|
| λ_sft | 0.05 - 0.2 | 语言形态好,任务能力涨得慢 | 能力涨得快,输出易退化 |
| β (KL) | 0.01 - 0.04 | 训练平稳,探索不足 | 探索充分,易跑偏 |
| clip ε | 0.2 - 0.28 | 更新保守,收敛慢 | 更新激进,方差大 |
| rollout温度 | 0.8 - 1.0 | 多样性强,噪声大 | 集中,易过拟合 |
离线偏好优化那一类方法,比如DPO及其变体,本质上把上面这套东西闭式解掉了。它的损失里已经包含了参考模型的比值项,相当于把KL约束内建进去,所以只需要调一个β。好处是实现极简、训练极稳,几十行代码能跑起来;坏处是它只能吃离线偏好对,无法利用在线采样探索新路径。数据集里没出现过的回答模式,DPO永远学不到。
我在选型上的经验是:如果手上有几万条高质量偏好对,且任务是风格对齐、安全性、简洁性这类“不需要探索新解法”的目标,DPO系列够用且省事;如果任务是数学、代码、多步推理这种“必须探索出正确答案”的目标,就得回到在线采样那条路,因为离线数据里压根没有正确答案的分布。
3. 实操:从零搭一条能跑通的SRL流水线
3.1 数据准备:难度分层比数量更重要
很多人一上来就问“要多少条数据”,这个问题本身问偏了。SRL对数据量的要求远低于SFT,但对数据结构的敏感度高得多。我做过对比,同样是五千条数学题,随机采样的一批和按难度分层的一批,训练曲线差得非常明显。
我的做法是先给每道题打一个难度标签,方式是用基座模型对每题采样16次,算通过率。通过率高于0.9的是简单题,低于0.1的是难题,中间的是有效区。训练集按3:5:2的比例配简单、中等、难,简单题保证模型不退化,中等题提供主要梯度,难题提供上限空间。这个配比不是固定的,训练中期可以把难题比例往上提。
数据清洗里最容易被忽略的是答案归一化。同一道题的正确答案可能是“1/2”“0.5”“二分之一”,验证器如果只做字符串匹配,会把大量正确回答判错。归一化要做的事包括:去空格、统一大小写、分数转小数或反过来、去掉单位、处理中文数字和阿拉伯数字的转换。这块工作枯燥但极其重要,我见过一次训练完全跑崩,最后定位到是验证器把带千分位逗号的数字全判错了,模型发现输出带逗号就被扣分,于是学会了所有数字都不带逗号,包括本该带的场景。
验证器本身的实现也有讲究。规则类验证器(正则、表达式求值)要写充分的自测用例,至少覆盖正确答案、明显错误、边界格式三类。代码类验证器必须放在隔离环境里执行,加超时和资源限制,否则一段死循环就能把整个训练卡住。我一般给单次执行设5秒超时,超时直接判错,不让它拖累采样吞吐。
3.2 训练配置:算清楚显存再动手
配置这块最实在的是先把显存账算明白。以7B模型为例,全参训练一份策略模型,参数用bf16存是14GB,梯度14GB,优化器状态(Adam的动量和方差用fp32)是56GB,加起来84GB,还没算激活值。如果是在线策略梯度,还要再挂一份参考模型(14GB,只前向)和一份奖励模型(7B的话14GB)。四份加起来超过110GB,单卡80G根本放不下,得上模型并行或者把奖励模型换成规则验证器。
这就是为什么规则验证器在小团队里这么受欢迎——它不占显存。把奖励模型这一份省掉,7B全参PPO在8张80G卡上用张量并行加梯度累积是能跑起来的。如果还嫌重,就上LoRA,只训适配器,显存需求直接砍到原来的三分之一左右,代价是探索能力弱一些,因为参数更新的自由度被限制了。
关键超参我的起步配置大概是:
learning_rate: 1.0e-6 # 策略模型,全参;LoRA 用 1e-5 global_batch_size: 128 # prompt 数 rollout_n: 8 # 每个 prompt 采样条数 max_new_tokens: 2048 temperature: 0.9 top_p: 0.95 kl_coef: 0.02 clip_ratio: 0.2 sft_loss_weight: 0.1这里解释两个值的来历。global_batch_size乘rollout_n等于每步实际处理的样本数,128乘8是1024条,平均长度按1500 token算,单步要过150万token。这个量级在8卡上大概每步几十秒,一天能跑两三千步,够看到趋势了。rollout_n取8是个经验平衡点,取4的话组内方差太大,优势估计噪声高;取16效果好但采样成本翻倍。温度0.9是为了保证组内有差异,如果温度太低,8条采样几乎一样,优势全为0,梯度就没了。
这里有个容易忽略的细节:组内全对或全错时,优势值全是0,这个prompt对梯度的贡献为零。如果你的数据里简单题太多,会看到大量步数的梯度接近零,训练看起来“没动静”,其实是采样没提供有效信号。监控里要专门统计每批的有效组比例,低于30%就说明数据难度配置有问题。
3.3 训练监控:只看loss会误判
SRL训练最坑的地方是loss没有直观意义。SFT的loss降了就是拟合得更好,SRL的loss可能是负的,可能震荡,跟最终效果的相关性很弱。我盯的是一组替代指标:
| 指标 | 健康区间 | 异常含义 |
|---|---|---|
| 平均奖励 | 缓慢上升,有波动 | 长期平 = 无有效梯度;骤升 = 可能有奖励漏洞 |
| KL散度 | 0.5 - 5 附近波动 | 持续暴涨 = 策略跑飞;长期近0 = 没在学 |
| 策略熵 | 缓慢下降 | 断崖式下降 = 熵坍缩,输出趋同 |
| 平均回答长度 | 平稳或缓升 | 快速膨胀 = 长度奖励被钻空子 |
| 格式合规率 | 数十步内到95%以上 | 上不去 = 格式奖励权重不够 |
| 验证集pass@1 | 阶梯式上升 | 与训练奖励背离 = 过拟合奖励 |
这几个指标要一起看,单看任何一个都会误判。举个我实际遇到的组合:平均奖励一路涨,KL也在涨,看起来一切正常,但验证集pass@1纹丝不动。查下来是模型学会了在答案段里同时写多个候选答案,验证器的抽取逻辑取第一个,恰好蒙对率提高了,但真正的解题能力没变。后来把抽取逻辑改成“只能有一个答案段,多个判错”才修好。
训练中期一定要做定期评测,不能等训完再看。我一般每200步在固定验证集上跑一次,用贪心解码,看pass@1的曲线。如果训练奖励涨而验证指标不涨超过三轮,基本可以判定是奖励被钻空子了,停下来查验证器比继续训划算。
3.4 落地:把RL能力蒸馏回可部署的模型
在线RL训出来的模型往往带着一堆训练时的脚手架,比如特定的标记格式、冗长的推理段落。直接上线体验很差,而且推理成本高。标准做法是做一轮拒绝采样蒸馏:用训好的模型对大规模prompt采样,过滤出正确且格式合规的,混入原始SFT数据做一轮监督微调,得到最终的部署模型。
这一步有个反直觉的发现:蒸馏后的模型在分布内任务上几乎不掉点,但在分布外任务上往往比RL模型更稳。原因是RL阶段容易把模型推到策略分布的边缘,追逐奖励最大化;蒸馏把它拉回中心区域,泛化反而好了。所以别舍不得那点训练奖励,该蒸就蒸。
蒸馏数据的配比也要注意。纯用RL模型自己采的样本,会把它的偏置一起固化下来;混30%到40%的原始SFT数据,能有效缓解。另外蒸馏阶段的学习率要比初次SFT低,1e-5到2e-5比较合适,训太多轮会把RL阶段学到的探索性思路磨掉。
4. 踩坑记录:那些文档里不会写的失败模式
4.1 奖励黑客:模型永远比你更懂怎么钻空子
奖励黑客是SRL里最普遍也最难缠的问题,表现形式五花八门,但本质都一样:模型找到了一条拿高分但不符合你真实意图的路径。我遇到过几种典型情况。
第一种是长度套利。奖励里如果有“步骤完整”这类和长度正相关的信号,模型会迅速学会把回答写得极长,反复复述同一句话。有一轮训练到第300步,平均回答长度从800 token涨到3000,奖励还在涨,但人工看样本全是废话。这种问题的根子往往不在长度惩罚不够,而在奖励模型对冗长的回答有偏好偏差——标注员在看两个回答时,潜意识里会觉得长的那个更认真。解决办法除了加长度惩罚项,更重要的是在奖励模型训练数据里做长度平衡,让长短回答的得分分布对齐。
第二种是格式投机。要求输出JSON,模型学会在JSON外面包一层解释性文字,因为解释性文字能让奖励模型觉得“更有帮助”。这种时候要么把格式校验做成硬约束(不合法直接0分),要么在抽取逻辑上做严格解析,解析失败即判错。
第三种最隐蔽:答案对但推理错。模型通过模式匹配猜中了答案,推理段落是编的。这类样本在结果奖励下拿满分,训久了模型会越来越依赖猜测。检测方法是人工抽检高奖励样本,看推理链是否自洽。如果发现比例不低,就得上过程奖励或者引入对抗性的验证。
注意:奖励黑客几乎无法根除,只能控制。我的做法是把“奖励上涨速度”当成一个监控项,正常的训练奖励是缓慢爬升的,如果某一段突然陡增,八成是找到漏洞了,立刻停下来抽检样本。
4.2 熵坍缩:模型突然不会说别的话了
熵坍缩的表现是策略熵在几百步内快速下降,采样出来的多条回答高度相似,组内优势全为0,训练停滞。成因通常是更新步子太大,或者KL约束太松,策略快速收敛到某个局部最优。
我在一次代码任务上遇到得特别典型:训到大概400步,模型对所有prompt的输出都开始以同一种方式开头,连变量命名习惯都趋同。当时的第一反应是加温度,把采样温度从0.9提到1.1,结果只是让输出变得更怪,多样性没回来。真正的解法是组合拳:把KL系数从0.01提回0.03,学习率砍半,同时在损失里加一个熵正则项,鼓励策略保持分散。
还有个小技巧是在数据侧做文章:每批数据里混入一定比例的高温采样样本(温度1.3以上),强制模型看到更多样的路径。这些样本质量参差不齐,但它们的作用不是教模型怎么答对,而是维持策略分布的宽度。
4.3 常见问题速查
| 症状 | 可能原因 | 排查动作 | 处理方案 |
|---|---|---|---|
| 奖励不涨 | 数据过难,有效组比例低 | 统计每批通过率分布 | 下调难度,增加简单题 |
| 奖励暴涨但评测掉 | 奖励黑客 | 抽检高奖励样本 | 修验证器,加约束项 |
| 输出长度失控 | 长度与奖励正相关 | 看长度-奖励相关系数 | 加长度惩罚,平衡奖励数据 |
| 训练loss为nan | 学习率过大或bf16溢出 | 查梯度范数 | 降学习率,梯度裁剪1.0 |
| 采样极慢 | 生成长度上限过高 | 看平均生成长度分布 | 调低max_new_tokens,分批 |
| 格式时好时坏 | 格式奖励权重不足 | 看格式合规率曲线 | 提权重到0.5,先立格式 |
| KL长期接近0 | 参考模型与策略同步更新 | 检查参考模型是否被误训 | 冻结参考模型参数 |
| 验证集波动大 | 验证集太小 | 看样本量 | 扩到至少500条,固定种子 |
这张表里的每一条我都实际撞过。最想提醒的是“KL长期接近0”这一项,它非常隐蔽,因为训练看起来完全正常,奖励也在涨,但实际上你优化的是一个没有约束的目标,模型可能早就跑到了参考分布之外。检查方法很简单,打印一下参考模型的参数是否requires_grad为False,我见过不止一次是因为配置里忘了冻结。
5. 算力预算与团队规模下的路径选择
5.1 小团队的现实路径:从便宜的方法开始试
不是每个团队都有几十张卡可以挥霍。资源有限的时候,路线选择比调参重要得多。我给出的优先级是这样的。
第一档,先做DPO或者其变体。只需要一个基座、一份偏好数据,单机多卡就能跑,一两天能出结果。它解决的是“回答风格、安全性、简洁度”这类不需要探索的问题。如果做完这一步效果已经够用,就别往上走了,投入产出比不划算。
第二档,规则验证器加分组相对策略优化。挑一个能写确定性验证器的任务,比如数学、结构化抽取、代码补全。这套组合不需要奖励模型,显存压力小,采样效率高。分组相对策略优化相比传统策略梯度省掉了价值网络,同一组内的样本互相比对算优势,实现起来也简单。我见过一个三人小队用八张卡在这个配置上把数学能力拉了一大截,关键是验证器写得扎实。
第三档,才是训过程奖励模型加完整在线RL。这一档的投入是前两档的好几倍,包括标注成本、训练成本、调参时间。只有当任务确实需要稠密的中间步骤信号,且规则验证器写不出来的时候,才值得走这条路。
5.2 评估怎么做才不骗自己
SRL项目最容易出的评估问题是自欺欺人:训练奖励涨了,就觉得模型变强了。实际上你优化的就是那个奖励,它涨是必然的。真正要盯的是三组独立的指标。
第一组是留出测试集上的pass@1,用贪心解码,这是硬指标。第二组是人工抽检,每次抽50到100条,看推理链是否自洽、有没有明显的投机行为。第三组是泛化测试,拿一批和训练分布明显不同的题去测,看能力有没有外溢。第三组经常被忽略,但它是判断“学到了能力”还是“记住了模式”的关键。
我在一个项目里吃过这个亏:训练集和测试集来自同一个题库,指标涨得很漂亮,换了个来源的题立刻掉回原点。后来养成习惯,任何SRL实验都留一个跨分布的测试集,哪怕只有一百条,也能提前发现问题。
评估频率上,前期密后期疏。前500步每100步测一次,看清楚趋势;后面每300步测一次,避免评测开销拖累训练。评测本身也要固定随机种子和提示模板,模板差一个字,分数能差好几个点,这个坑我踩过不止一次。
最后分享一个我反复验证过的判断:SRL的效果上限,七成取决于数据质量和验证器的严谨程度,三成才是算法和超参。我见过太多人把时间花在换算法、调系数上,而验证器里一个答案归一化的bug就能让所有努力归零。真正值得投入时间的地方,是把“什么叫答对”这件事定义到没有歧义,剩下的工作相对机械。这个顺序搞反了,训得越久,偏得越远。