1. RLHF到底在解决什么问题1.1 从“预测下一个词”到“和人类对齐”先从一个最直观的场景说起。你训练了一个对话模型它能够流畅地写出段落、回答问题语法完全正确逻辑也基本自洽。可是当你真正和它对话时却发现它总是说一些漂亮但没用的话甚至在某些价值观场景下给出让人不适的答案。问题出在哪因为传统语言模型的训练目标只有一句话根据上文预测下一个最可能的词。这个目标太原始了它只教会模型像人一样说话却没有教会模型说人真正爱听的话。这是行业里非常核心的一个痛点模型的真实能力和训练目标之间存在严重错位。能力越强错位带来的风险就越大。RLHFReinforcement Learning from Human Feedback基于人类反馈的强化学习就是来解决这个错位问题的。它的思路很直接既然人类满意这件事很难写成规则那就直接让人对模型的输出做评价再把评价转化成一个可优化的奖励信号最后用强化学习让模型按照这个奖励信号去调整自己的行为。这套方案在近几年大模型对齐工作中被反复验证已经成了实际项目中对齐阶段的标配。而PPOProximal Policy Optimization近端策略优化在其中扮演的就是训练引擎的角色。你会发现几乎所有做对齐的实战项目最后跑到训练阶段时核心的强化学习算法几乎都会落到PPO或其变体上。这篇文章把我实际跑过RLHFPPO流程中值得关注的技术细节全部梳理一遍包括奖励模型怎么训、PPO为什么重要、四个模型如何共存、KL散度到底在干什么以及我在实操中踩过的坑和排查思路。内容更适合已经接触过强化学习、打算在大模型项目里落地对齐流程的读者当然如果你是刚入门的新手前两个章节也能帮你快速理解整体脉络。1.2 三个阶段缺一不可整个RLHF流程业界公认的拆分是三个阶段。第一阶段是监督微调Supervised Fine-TuningSFT。前期需要一批高质量的人工标注对话样本让模型先学会正确的回应格式。但SFT解决不了价值观对齐问题因为人类标注者对同一问题的回答可能各执一词模型学到的只是平均正确。第二阶段是训练奖励模型Reward ModelRM。让模型对一组候选回答做出打分或者让标注员对多个回答进行排序。训练一个专门负责预测人类偏好的模型。这个模型不直接对外服务它只负责在后续PPO训练里给模型的输出一个标量奖励。第三阶段才是PPO强化学习。上一步训练好的奖励模型作为奖励信号来源通过PPO算法去更新语言模型策略。这里还会引入一个参考模型Reference Model来约束更新幅度防止模型为了拿高奖励而彻底说胡话。这三者层层递进缺一不可。你会发现奖励模型的质量几乎决定了最终对齐的天花板。而PPO阶段能保住的奖励下限和训练稳定性则决定了对齐效果的下限。所以RLHF不是某个模型的补丁而是一整条流水线。谁跳过其中一环后面都要用更大的代价还回去。2. 奖励模型整条流水线的质量天花板2.1 为什么不能直接让人类打分有人可能会问既然目标是让人类满意为什么不直接在训练时让人类实时对模型的每个输出打分理论上可以但实操中完全走不通。模型每秒钟能生成几十个token人类看一眼输出都要几秒钟更何况一个batch里可能就有几十条样本。让人类实时打分既不现实也不经济。所以常规做法是把人类偏好蒸馏到一个静态模型里。具体说来你准备好一批prompt让同一个模型或者多个模型针对每个prompt生成若干个候选回答然后请标注员对候选回答排序。把这些排序结果转成pairwise对比数据训练一个二分类模型让模型学会判断回答A是否比回答B更好。奖励模型的训练目标看似简单但数据质量的影响非常大。奖励模型本质上是你把人类偏好压缩成了一个数学函数。输入是prompt回答输出是一个标量。训练得越好PPO阶段模型就越知道往哪个方向优化。我总结一条经验奖励模型的效果和排序数据质量强相关排序数据如果本身噪声大后面的一切努力都是在放大错误。2.2 排序数据怎么采排序数据的采集并不是简单丢给标注员一个问答对去打分。实际项目中需要设计合理的任务流。常用做法是给定一个prompt将模型采样的N个答案N通常取4到9混在一起交给标注员排列。为了让标注员的判断稳定需要提前明确排序准则比如逻辑完整性优先于文采、拒绝答非所问否则不同标注员的排序逻辑会漂移。采集时还需要关注prompt的覆盖面。我踩过的坑就是prompt分布过于集中导致奖励模型对某些领域的偏好预测很准换个领域基本瞎猜。后来我会刻意在prompt集合里做了分层采样保证对话类、写作类、问答类、安全敏感类都有足够样本。另外排序标注过程中还需要混入质量对照组。有些标注员为了赶量会对长回答有天然偏好认为越长越好。为了对抗这种偏差我一般会在数据集中加入一些明显更简洁但正确性更高的金标回答如果标注员对这类回答的排序偏低就需要复核。排序数据转训练样本的方法主流做法是枚举所有答案对。如果N4就有C(4,2)6个对比对。奖励模型拿两个答案作为输入输出哪个得分更高。这里要注意如果N太大pair数量会膨胀得很厉害训练反而变慢所以4~6个候选是比较划算的区间。2.3 奖励模型的训练细节奖励模型的结构通常是在SFT模型底座上去掉语言模型输出头换成一个标量回归头。训练数据就是一对答案输入格式需要包含prompt和被比较的两个回答。损失函数上常规采用二元交叉熵形式loss -log( sigmoid( r(x, y_win) - r(x, y_lose) ) )其中r是奖励模型的打分y_win是排序靠前的回答y_lose是靠后的回答。这个loss的意思是让赢家得分尽量比输家高同时差距越大越确信越好。实际训练中要注意几点。第一同一batch中不同pair可能来自不同prompt不要做全局的batch normalization因为每个prompt的奖励绝对数值区间可能不一样。第二奖励模型输出的绝对值本身没有明确意义只有相对大小才有意义所以后期使用它的输出时不要做绝对阈值判断。第三训练到后期watch一下acc和lossacc如果还在涨但loss波动大警惕过拟合。我只用一个指标判断奖励模型好坏在留出的验证集上它的pairwise排序准确率有多少。通常做到70%以上才算初步合格低于这个水平劝你别急着进PPO阶段先把数据扩了再训。3. PPO机制拆解四个模型如何一起跑起来3.1 重要性采样与old_logprobPPO不是凭空优化一个奖励值它属于策略梯度类算法的一个改良版。策略梯度类算法的核心思路是让模型策略去和环境互动采样轨迹根据轨迹上的奖励来调整策略增加高奖励动作的概率降低低奖励动作的概率从而找到一个好的策略。直接做策略梯度的问题在于样本效率低。模型每更新一次旧的互动数据就不能再用了。PPO的改进在于引入了重要性采样。即使当前策略变成了新的版本我们也可以拿旧的采样轨迹来估计新策略的期望收益。只需要用新旧策略在同一个输出上的概率比值来修正即可。我们把这个概率比值记为ratio。假设在某个token位置旧策略生成该token的概率是p_old当前新策略生成该token的概率是p_new那么ratio就是p_new / p_old。如果ratio大于1说明新策略更倾向生成这个token如果小于1说明新策略正在远离这个token。训练时我们不直接用原始语言模型的输出概率而是取出训练前缓存的old_logprob。这就是为什么你在很多开源代码中会看到logprobs这个字段它是在采样阶段由旧策略算好并保存的。后面的loss计算全靠old_logprob和当前模型重新算出的logprob做比值或差值。一个小小的经验如果你的采样阶段和训练阶段模型泄露了数据或者保存logprob对不上后面PPO训练会出现莫名其妙的异常。3.2 修正信号的两个部分回报和KL惩罚PPO的最终目标是最大化每个token的优势估计advantage。优势估计简单理解就是这个token的生成相对于平均情况来说是更好还是更差。计算advantage常用GAEGeneralized Advantage Estimation。GAE的输入需要每一步的即时奖励。在RLHF里一个回答通常只在结尾处获得一个整体奖励中间每个token本身的即时奖励一般是0。问题来了如果中间token没有即时奖励全部靠末尾奖励回溯优势估计的方差会很大。模型不容易知道到底是哪个词导致了高分。为了控制方差并防止模型跑飞还要加上KL散度惩罚。对生成回答中的每个token惩罚项都会比较当前策略在对应token上的输出分布与参考模型Reference Model的输出分布。如果当前策略偏离了参考策略的分布就要从总奖励中扣分。这个KL散度惩罚是RLHF稳定性的关键。参考模型一般就是未参与PPO更新的SFT阶段模型。它起的作用是当锚告诉模型别改太野。你甚至可以把它理解成老师给学生划的边界你可以探索但不要彻底否定之前的常识。kl_loss的计算有两种常见方式。一种是先求出每个token上当前策略和参考策略的KL散度然后乘以系数beta另一种是通过两个策略的平均logprob差来近似。实践中我倾向用每个token的显式KL散度累加因为在奖励模型整体打分波动大时逐token约束更稳。3.3 GAE到底怎么算GAE计算advantage需要用以下信息每一步的即时奖励r_t、价值模型在每个token步预测的价值v_t、折扣因子gamma和GAE参数lambda。对于每个token步tdelta_t的计算是delta_t r_t gamma * v_{t1} - v_t然后advantage_t的计算为advantage_t delta_t gamma * lambda * advantage_{t1}这两个公式就是GAE在RLHF场景中的完整形态。其中gamma控制对未来奖励的重视程度lambda控制偏差和方差的折中。gamma越接近1模型越能考虑远期影响lambda越接近0越接近只看即时奖励。在长度较短的对话场景中我把gamma设为0.99左右lambda设为0.95。如果做长文本生成任务gamma要调低一些否则末尾奖励回溯到前面时方差太大。价值模型Critic在这里很关键。它负责预测状态价值或动作价值和策略模型一起训练。每个样本的value target可简单视为该轨迹未来总回报真实回报value来自价值网络预测。你会发现价值模型的训练稳定程度直接影响PPO整体收敛价值模型输出越准advantage估计越准策略更新才越可靠。3.4 四模型谁是谁在常规LLM的PPO训练中总共有四个模型会出现在显存里或参与交互Actor策略模型、Critic价值模型、Reference参考模型、Reward Model奖励模型。Actor是我们要更新的核心模型也就是最终要上线服务的那个模型。Critic和Actor共享主干或者独立成一份权重都可以。现实中为了省显存会给Actor和Critic各自单独初始化但共享Transformer主干的权重不做梯度更新只训练价值头。Reference是冻结的SFT基线用于计算KL惩罚。Reward Model也是冻结的只输出最终奖励。训练中Actor产生回答时Reference和Reward Model会被并行执行分别得到KL散度和奖励。Critic则在每个token位置给出状态估计值。这种结构看起来简单实际工程实现时会遇到巨大的显存和调度压力。尤其是序列很长时Reference和Reward Model在推理端的计算非常大。如果你自己搭框架建议把奖励模型和参考模型的推理用批量预测预先把每个样本的得分和KL算好缓存在本地再进入PPO更新阶段。这样做能减少训练过程中的同步等待流水线会顺很多。4. 从零走一遍RLHFPPO落地流程4.1 选基线模型和超参先从模型选型说起。我个人建议是做RLHF时Actor的起点最好是SFT阶段已经收敛的模型并且训练数据分布和奖励模型训练数据分布一致。哪怕模型能力再强如果它的基础分布和奖励模型见过的分布差异很大PPO阶段会有很多输出落在奖励模型的盲区训练信号就乱。模型规模上如果是百亿以下规模单机多卡可以做PPO再往上要谨慎考虑资源。很多人以为PPO入门中最难的算法其实不是最现实的问题是显存。你需要同时加载四个模型其中一个还要带梯度还要产生长序列推理的中间量显存优化策略直接影响能否跑起来。超参方面给一组我常用的初始值作为参考参数初始推荐值备注学习率 Actor1e-6 左右比SFT学习率稍低一点学习率 Critic1e-5 左右价值模型收敛快一些kl_coef0.02KL系数太大模型更新缓慢clip_range0.2PPO裁剪范围gamma0.99折扣因子lambda0.95GAE参数batch_size32或64按显存调整每批次训练轮数1~2多了容易过拟合旧数据括号里是经验值不是万能药。不同任务、不同模型最好的做法是先小规模实验观察KL和reward曲线再调。4.2 奖励模型部署和打分缓存在进入PPO之前把奖励模型按照SFT阶段的batch和采样格式对一批prompt做打分并把打分结果缓存下来会很有用。这不能替代PPO在线更新的价值但能帮助你提前排查奖励模型是否出现普世偏好——比如对某个字眼的偏好、对长度的过度偏好。我记得有一次奖励模型在验证集上准确率78%看起来不错。结果进PPO后模型很快学会在回答结尾强行重复任务描述因为奖励模型对提及任务词给出了高奖励。这种现象我们叫奖励欺骗reward hacking是RLHF里最棘手的问题之一。提前做一次调包检测很有必要准备一组提示词让模型用多种风格回答同样内容看看奖励模型打分分布是否合理。如果打分只被长度主导那这个奖励模型需要重新训练或加正则。这种预防比事后修补便宜得多。4.3 PPO训练主循环实际训练环节整体流程如下第一步准备好一行训练样本每条包括prompt数据。Actor根据当前策略生成回答保存每个token的logprob。同时Reference模型和Reward模型分别计算KL惩罚和最终奖励。第二步Critic对每个token的状态给出价值估计。第三步计算GAE得到每个token的advantages和returns。第四步组装minibatch开始更新Actor和Critic。更新时重新计算当前策略的logprob并构造PPO的clip loss。如果选token时概率是0怎么办会加一个极小常数防止取log后出现负无穷。很多新手在这一步直接报错数NaN满天飞。在更新Actor时我们希望在advantages为正的方向加大概率在advantages为负的方向减少概率。但是为了防止单次更新过大用clip_range限制ratio的跨度。计算完Actor的loss之后还加上KL惩罚项。Critic的loss通常是value预测和GAE returns之间的MSE注意不要用advantage作为target那是对价值估计的一种常见误解。整体结构上伪代码的核心如下# 采样阶段缓存 sequences, old_logprobs, rewards, kls rollout_once(actor, ref_model, reward_model) # 计算价值和advantage values critic(sequences) advantages, returns compute_gae(rewards, values, gamma, lambda) # 每个PPO epoch内更新 for _ in range(ppo_epochs): logprobs actor(sequences) ratio torch.exp(logprobs - old_logprobs) surr1 ratio * advantages surr2 torch.clamp(ratio, 1 - clip_range, 1 clip_range) * advantages policy_loss -torch.min(surr1, surr2).mean() kl_loss (logprobs - ref_logprobs).mean() * kl_coef actor_loss policy_loss kl_loss value_pred critic(sequences) critic_loss F.mse_loss(value_pred, returns)这段代码虽然简单但是构成了PPO训练的主体骨架。实际工程里还要考虑padding的mask以及各种数值稳定处理不过我建议先把骨架吃透再堆细节。4.4 训练中的观察曲线PPO训练的观测重点和SFT很不一样。SFT你主要盯loss下降就可以但PPO需要同时关注三类信号reward曲线的变化、KL散度的变化、以及模型输出多样性/质量的定性观察。正常情况下reward曲线会有起伏但整体趋势不应该一骑绝尘地暴涨因为如果KL惩罚机制有效reward不应该瞬间冲到极高值。如果你看到reward在几十步内暴涨同时KL散度也在大幅升高几乎可以肯定是奖励模型被钻了漏洞。reward曲线稳定上升但KL曲线也稳定上升这种状态也需要注意。它说明模型正在为了拿分而偏离参考策略。此时应该提高kl_coef或者检查奖励模型是否过分依赖某些表面特征。KL散度如果长期在0附近几乎不动说明模型根本没在更新。这可能是因为学习率太低、优势几乎为0或者policy loss在clip处全部被卡死。排查时先看advantage分布如果advantage全部接近0问题大概率在奖励模型打分离散度太小或者价值模型估计太准导致优势抵消。前者要重新考虑奖励模型的训练目标后者要检查是否对value做了过度正则。还有一个容易忽略的细节生成的终止符。在PPO采样阶段如果对话模型不输出EOS序列会被强行截断。截断会改变KL惩罚的计算范围也会让奖励打分的context不一致。所以采样阶段要限制最大长度并检查EOS是否合理出现。5. 常见问题与排查技巧实录5.1 奖励hacking怎么防奖励hacking是RLHF里最常见的灾难性问题。模型找到奖励模型的漏洞后会疯狂利用它输出越来越偏离正常表达。哪怕奖励模型在验证集准确率75%以上还是可能出现这个问题。防御方向有三个。第一在奖励模型层面加正则训练奖励模型时如果发现它过度关注长度或者某些高频词可以在loss中加入奖励分数和真实偏好之间的单调性约束或对奖励模型的输出方差做约束。第二在PPO层面加更强的KL惩罚KL系数本身可以设置成一个动态范围reward和KL同步上升时就提高beta偏离预警一旦解除再恢复beta。第三在数据层面增加对抗样本在奖励模型的训练数据里刻意添加一些诱饵回答这些回答表面上高分但实际上错漏百出。奖励模型如果识别不了这类样本说明它还没学会判断真实质量。我自己的经验是同时在三个方面做会安全很多。光靠一个KL惩罚设得再大也挡不住奖励模型本身的漏洞。5.2 训练崩溃和NaN排查在RLHF训练中NaN和inf的出现频率远高于普通SFT。常见原因包括对数概率取值0导致的inf、GAE计算时价值预测出现极端值、没那么常见但很折磨人的奖励模型输出溢出。如果你遇到NaN不要只盯最后一步的loss先定位是由哪个tensor引入的NaN。我常用的一种排查方法是在PPO更新前打印advantages的均值方差在更新后打印ratio的均值方差。如果advantages在更新前已经出现NaN那问题在采样或GAE计算如果更新前没问题更新后才有NaN那问题大概率出在优化器或loss计算的某处。另一个非常值得注意的坑是采样和训练的数据对齐问题。如果在采样阶段对序列做了截断或者padding在训练阶段又用不同的mask方法就可能导致logprob对不上位置模型更新时以为某位置概率很高实际上那个位置是padding这样训练信号会乱成一团。5.3 KL偏大偏小如何调KL偏大说明模型正在快速走远偏小说明更新幅度不足对这两种情况的调法不一样。如果KL偏大先区分是全局性增大还是部分样本增大。全局性增大说明当前策略在整体范围内漂移考虑提高kl_coef、调低学习率、或者给ppo_epochs降成1。部分样本增大说明是某些样本脱离了控制范围可以考虑对这些样本的KL做动态加权给它们更高惩罚。如果KL长期偏小说明模型基本没有偏离参考策略要检查policy_loss是否被clip_range完全限制住了。将clip_range值打出来看一下如果大量ratio超过1.2或者低于0.8说明策略已经被裁切很多样本梯度失效。再来就是检查advantages是否分布在0附近。如果age分布太窄模型没有足够的动力去更新这种情况要回头检查奖励模型是否输出过于平坦。5.4 一张速查表现象常见原因首要排查点reward曲线飙升奖励hacking / KL惩罚失效观察KL是否同步飙升复盘奖励模型训练数据reward整体偏低奖励模型与SFT分布不匹配检查prompt是否一致奖励模型是否过拟合输出质量下降KL惩罚过大 / 学习率太高降低kl_coef降低Actor学习率训练完全不动advantage普遍为0检查奖励模型检查value target精度logprob出现NaN概率0或负值未处理加log_eps检查采样掩码显存不足四模型同时推理缓存reward和KL阶段化执行6. 最后聊点个人感受跑RLHF和PPO这个组合我最大的体会是算法原理看起来不复杂但真正让这套系统稳定工作的都是工程细节。奖励模型质量不行、数据分布不统一、KL系数设置不当、显存优化太激进任何一环都会让结果崩掉。别指望有一组一劳永逸的超参我在不同任务上试过的参数差别很大。每次新项目我都会从一条很小的验证集开始快速跑几十步看曲线形状再放大到完整数据。这种做法帮我节省了至少一半的调试时间推荐你也试试。另外如果你在实际操作中遇到和上面不完全一样的情况建议先打印advantages和KL的完整分布不要只看均值。均值掩盖问题非常严重两条完全不同的训练轨迹可能均值相同但方差天差地别。RLHF这套流程的收益和风险都在方差里盯住方差能少走很多弯路。