万字复盘 | 从SFT到PPO:如何用强化学习拯救大模型幻觉?(附避坑指南)

85 阅读24分钟

本文约 12000 字,⏱️ 阅读时间:约 25 分钟 🏷️ 关键词:RLHF、PPO、大模型幻觉、Text-to-SQL、工程实战。

👋 给新读者的话

如果你对"强化学习""PPO"这些词有点陌生——别担心,这篇文章专门为你准备了「前置知识」章节,用5分钟讲懂核心概念,保证你能看懂80%的内容。

如果你是开发者,可以直接跳到「一分钟速览」或「Golden Config」看实战干货。


读者导航

  • 只关心结果/配置:直接看「一分钟速览 → Golden Config → 故障排查表」
  • 想复现 RL 训练直觉:看「LunarLander 热身(可跑 demo)」
  • 想把 RLHF 真正落到业务:看「Router 方案 → 数据/环境 → 端到端评估」

本文:不讲虚的,只讲我怎么踩坑、怎么定位、怎么改进。

封面图


先讲一个真事:大模型很自信,但它“路痴”

好不容易搭了个项目系统助手 Agent,随口问一句:

"查一下海外某项目的合同变更记录"

我们接入的通用大模型(当时用的是 Qwen-72B 一类)非常自信地生成查询:直接查 contract_change_log,再用项目名过滤。

结果:空(查无数据)

为啥?因为在我们的业务数据库里,"项目"和"变更记录"并没有直接关联。这货直接跳过了 project → contract → change_log 的关联路径。

就好比你问路,它告诉你"往前走就到",但压根没提中间得先过一座桥。

大模型很聪明,但它不懂你家业务数据库的"交通规则"。 它会"幻觉"出看似合理但实际无法执行的查询路径。

这种问题,Prompt 调了几十版也没彻底解决。

失败案例对比

后来我们换了个思路:不让它直接生成查询,先让它学会"认路"。


前置知识:5分钟看懂这篇文在讲什么(小白必读)

📖 写给小白读者:如果你对"强化学习""PPO"这些词有点陌生,先看这一节。如果你是开发者,可以跳过直接看「一分钟速览」。

用一个故事讲懂什么是"强化学习"

想象你在训练一只小狗:

训练小狗强化学习训练AI
给它看动作:坐下、握手让AI尝试输出:查询路径A
做对了→给零食(奖励)路径正确→得分+10(奖励)
做错了→不给零食(惩罚)路径错误→得分-5(惩罚)
小狗学会:做动作有零食吃AI学会:输出正确路径能得分

强化学习 = 让AI在环境中试错,通过奖励和惩罚自己学会最优策略。

核心术语速查表(遇到不懂的词就回来看)

📖 点击展开:10个核心术语一图看懂
术语一句话解释生活类比
PPO一种强化学习算法,全名"近端策略优化"开车时小幅修正方向盘,别猛打
RLHF用人类反馈做强化学习训练像教练一样指导AI,而不是让它自己瞎练
SFT监督式微调,给AI看标准答案让它模仿像学生背课本,记住"这道题答案是什么"
KL散度衡量两个策略差异的数字新老策略的"距离",差太远就刹车
Actor负责做决策的模型司机,负责开车
Critic负责打分的模型教练,负责说"你刚才开得怎么样"
Value Loss预测分数和实际分数的差距你以为能考90分,实际考了60分,这差距就是Loss
Reward Hacking模型学会钻奖励函数的空子老师说"写满字就给分",学生疯狂抄作业凑字数
策略崩塌模型训练到一半突然变笨了学着学着把之前学的都忘了
vf_coef控制Critic影响力的参数教练说话的分量,太大就把司机带偏了

这篇文章在解决什么问题?

用一张图概括:

┌──────────────────────────────────────────────────────┐
│  问题:大模型写查询时"走错路"                          │
│  ─────────────────────────────────────────────────   │
│  用户问:"查项目合同变更记录"                          │
│  模型想:直接查 contract_change_log 表 ❌              │
│  实际:要 project → contract → change_log 三跳 ✅      │
└──────────────────────────────────────────────────────┘
                        ↓
┌──────────────────────────────────────────────────────┐
│  方案:加一个"认路层"Router)                        │
│  ─────────────────────────────────────────────────   │
│  第一步:Router 先输出"从哪张表,经过哪些表,到哪张表"   │
│  第二步:Generator 按这个路径生成具体查询               │
└──────────────────────────────────────────────────────┘
                        ↓
┌──────────────────────────────────────────────────────┐
│  训练:用强化学习让 Router 学会认路                    │
│  ─────────────────────────────────────────────────   │
│  SFT(背课本):先教会它"这道题答案是什么"              │
│  PPO(练实战):让它在环境中试错,学会"为什么走这条路"   │
└──────────────────────────────────────────────────────┘

💡 读完这一节,你已经掌握了看懂这篇文章的80%密码。接下来可以按需阅读:

  • 赶时间?看「一分钟速览」
  • 想玩代码?看「LunarLander 热身」
  • 想学原理?跟着章节顺序读

一分钟速览(赶时间看这一段就够了)

关键点一句话
问题本质Text-to-MQL 里最难的不是写语法,是选对"多跳路径"
方案加一个 Router:先输出 (Anchor, Target, Via),再交给生成器按路写查询
冷启动先 SFT 再 PPO,否则前 ~20k steps 基本在瞎蒙,正奖励几乎拿不到
稳定性PPO 必须保守:低 lr + 低 vf_coef + 严 KL 熔断 +(必要时)冻结 backbone
奖励设计不做反作弊,模型会钻空子:动态加权 + 条件发放

最终效果:

  • 路由准确率:35% → 89%(提升 53%)
  • 端到端执行成功率:80% → 90%

如果你也在做 RL 微调,或者被 PPO 训练崩溃折磨过,往下看。


一、问题拆解:为什么大模型会"走错路"?

我们做的是 Text-to-MQL(自然语言转mongodb数据库查询)。

大模型直接生成查询时,常见翻车可以粗暴分三类(这里是我们线上的高频占比观测):

失败类型例子占比
表选错该查项目表,它去查合同表~40%
路径断裂跳过中间关联表,直接查目标表~35%
语法对但结果空运行没毛病,但业务上查不到东西~25%

这类问题不是"模型不会写查询",而是它不懂你业务 Schema 的物理法则

SFT(监督微调)能教会它"这道题的答案是什么",但教不会它"为什么必须走这条路"。

技术假设

我们的技术假设

SFT 擅长"模仿分布",但在强约束逻辑任务存在上限。引入环境反馈(RL Reward)后,模型可通过试错优化不可微目标:合法路径、执行成功率、低空结果率。

📖 小白看这里:什么是"不可微目标"?

可微目标 = 可以用数学公式求导的目标(比如预测准确率,模型可以直接计算梯度来优化)

不可微目标 = 不能直接求导的目标(比如"查询能不能成功执行",只有试了才知道,没法直接算梯度)

强化学习的作用:让AI通过试错来优化那些没法直接算梯度的问题


二、我们的方案:先"认路",再"开车"

与其让大模型一步到位生成查询,不如拆成两步:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────┐
│  用户问题       │ ──▶ │  Router 模型     │ ──▶ │  Generator  │ ──▶ 查询结果
│  + Schema 信息  │     │  输出路径三元组  │     │  生成 MQL   │
└─────────────────┘     └─────────────────┘     └─────────────┘

Router 只做一件事:告诉后面的生成器"走哪条路"。

输出就三个字段:

  • Anchor:从哪张表出发
  • Target:最终查哪张表
  • Via:中间要经过哪些表(可以为空)

早期验证:路由层真的有用吗?

在正式开干之前,我们先做了个小规模 A/B 测试:

方案$lookup 场景准确率空查询数
Baseline(直接生成)80/100 (80%)20
注入路由约束95/100 (95%)5

结论很清楚:路由决策层对执行质量有直接贡献。

这给了我们信心:方向对了,值得继续投入。


三、RL 热身:用 LunarLander 建立直觉(附可跑代码)

在正式上强化学习训练落地讲解之前,我强烈建议先用游戏环境练练手。不是为了炫技,是为了建立直觉——理解 RL 的反馈循环到底是怎么回事。

3.1 为什么推荐 LunarLander?

这是 HuggingFace Deep RL Course 的入门环境,优点是:

  • 状态空间小、训练快(几分钟就能看到效果)
  • 奖励信号直观(落地成功 +100,坠毁 -100)
  • 方便你亲手调 Reward,体会"奖励设计"的威力

3.2 动手实践指南

Step 1:跑通官方 Demo

👉 HuggingFace 官方教程:你可以直接用的 HuggingFace 官方 hands-on

这个 Notebook 可以直接在 Colab 跑,10 分钟内你就能看到一个小飞船学会降落。

Step 2:尝试自己改 Reward

官方 Demo 用的是环境默认奖励。但真正的 RL 工程,核心就是设计你自己的奖励函数

我写了一个可配置的 Reward Wrapper,你可以用它来做对比实验:

📦 点击展开:可配置奖励的 LunarLander 代码
# 可配置奖励包装器
from dataclasses import dataclass
import numpy as np
import gymnasium as gym

@dataclass
class RewardConfig:
    # 势能型稠密项(基于状态)
    w_distance: float = 0.0   # 距离着陆区的负权(越近越好)
    w_velocity: float = 0.0   # 速度幅值的负权(越慢越好)
    w_angle: float = 0.0      # 姿态角度的负权(越正越好)
    w_legs: float = 0.0       # 腿接触正项(每条腿 +1)

    # 推进器代价(离散动作:0无操作,1左侧推,2主推,3右侧推)
    penalty_main: float = 0.0
    penalty_side: float = 0.0
    # 是否替换原始 reward
    replace_reward: bool = False
    scale: float = 1.0


class RewardShapingWrapper(gym.Wrapper):
    """
    记录奖励分量到 info['reward_components']
    可选地以自定义加权合成为新的 reward
    """
    def __init__(self, env: gym.Env, config: RewardConfig):
        super().__init__(env)
        self.cfg = config

    def _decompose(self, obs: np.ndarray, action) -> dict:
        x, y, vx, vy, angle, v_angle, l_leg, r_leg = obs[:8]
        return {
            "distance": -float(np.sqrt(x*x + y*y)),
            "velocity": -float(np.sqrt(vx*vx + vy*vy)),
            "angle": -float(abs(angle)),
            "legs": float((l_leg > 0.5) + (r_leg > 0.5)),
            "pen_main": float(action == 2),
            "pen_side": float(action in [1, 3]),
        }

    def step(self, action):
        obs, reward, terminated, truncated, info = self.env.step(action)
        comps = self._decompose(obs, action)
        
        shaped = (
            self.cfg.w_distance * comps["distance"]
            + self.cfg.w_velocity * comps["velocity"]
            + self.cfg.w_angle * comps["angle"]
            + self.cfg.w_legs * comps["legs"]
            - self.cfg.penalty_main * comps["pen_main"]
            - self.cfg.penalty_side * comps["pen_side"]
        ) * self.cfg.scale

        info["reward_components"] = {**comps, "env_reward": reward, "shaped": shaped}
        
        if self.cfg.replace_reward:
            reward = shaped
        return obs, reward, terminated, truncated, info

# 配置1:仅记录,不改变原始奖励
log_only = RewardConfig()

# 配置2:自定义奖励(鼓励稳、慢、省油)
custom_cfg = RewardConfig(
    w_distance=100.0,
    w_velocity=150.0,
    w_angle=50.0,
    w_legs=10.0,
    penalty_main=0.3,
    penalty_side=0.03,
    replace_reward=True,
)

# 分别训练,对比 GIF 效果

import imageio.v2 as imageio
from stable_baselines3 import PPO

ENV_ID = "LunarLander-v2"
TOTAL_STEPS = 200_000
SEED = 42

def make_env(cfg, render_mode=None):
    env = gym.make(ENV_ID, render_mode=render_mode)
    return RewardShapingWrapper(env, cfg)

def train_and_save(cfg, model_path):
    env = make_env(cfg)
    model = PPO("MlpPolicy", env, verbose=0, seed=SEED)
    model.learn(total_timesteps=TOTAL_STEPS)
    model.save(model_path)
    env.close()

def record_gif(cfg, model_path, gif_path, max_steps=1000, fps=30):
    env = make_env(cfg, render_mode="rgb_array")
    model = PPO.load(model_path, env=env)
    obs, _ = env.reset(seed=SEED)
    frames = [env.render()]
    for _ in range(max_steps):
        action, _ = model.predict(obs, deterministic=True)
        obs, reward, terminated, truncated, info = env.step(action)
        frames.append(env.render())
        if terminated or truncated:
            break
    env.close()
    imageio.mimsave(gif_path, frames, fps=fps)

# 方案A:默认奖励(仅记录)
train_and_save(log_only, "ppo_default")
record_gif(log_only, "ppo_default", "ppo_default.gif")

# 方案B:自定义奖励
train_and_save(custom_cfg, "ppo_custom")
record_gif(custom_cfg, "ppo_custom", "ppo_custom.gif")

用上面的代码,你就可以对比"默认奖励" vs "自定义奖励"两种奖励策略下小飞船的强化学习训练效果了。

3.3 这一步的核心收获

通过这个热身,你会建立几个关键直觉:

直觉说明
Reward 决定行为你设计什么样的奖励,模型就往什么方向优化
稠密 vs 稀疏只给终点奖励(稀疏)学得慢,过程奖励(稠密)学得快但容易被 hack
反馈循环Env.step() → Reward → Update → Env.step()... 这个循环是 RL 的核心

📌 Key Takeaway:RL 的难点不是写模型,是写环境与奖励

3.4 不同RL算法下lunarlander的效果对比

用上面的代码,我们还对比了四种不同的RL算法训练策略在相同训练steps下lunarlander的训练效果。:

Demo 结果

直观感受:不同的强化学习训练算法,训练出来的模型风格也是完全不同。


四、数据与环境:我们构建的"物理世界"

RL 训练离不开一个靠谱的环境。我们花了不少精力在这一块。

4.1 业务物理法则(动作空间边界)

维度数值说明
核心 Schema12 张表Project/Contract/Delivery/Construction…
合法路径42 条脚本穷举,Router 动作空间上界

为什么要穷举?减少无效探索,把 Schema 约束变成可学习信号。

4.2 数据流水线

种子生成(Gemini)  →  语义增强(Qwen-72B)  →  实体填充(Qwen-72B)  →  566条样本
       ↓                    ↓                   ↓
  42路径×3种子问题        按难度分级采样        60%真实/40%模糊

AI合成数据生成管线

4.3 数据分布(直接影响 Reward 策略)

类别分布优先级分布
类别优先级

4.4 环境建模

💡环境基于真实业务逻辑构建,包含以下三个核心组件:

组件描述
Schema 信息12 张表的结构定义与外键关系
路径规则42 条合法路径的校验逻辑
执行反馈路径匹配度、语法正确性、结果有效性

五、两阶段训练:SFT 冷启动 + PPO 强化

5.1 为什么需要 SFT 冷启动?

这是我踩的第一个大坑。

一开始我想:既然最终要用 RL,能不能直接 RL 起手?

结果是:前 2 万步,模型基本在"瞎蒙",几乎拿不到有效的正奖励。

冷启动前后对比

原因很简单:动作空间虽然不大(42 条合法路径),但随机探索命中正确答案的概率太低了,尤其是多跳场景。

正确姿势

  1. Phase 1(SFT):先把准确率拉到一个可用起点(比如接近 80%)
  2. Phase 2(PPO):在 SFT 基础上做策略优化

两阶段训练

用一个比喻来说:

先让实习生背熟操作手册,再让他在模拟环境中实战。

换个角度理解

学习阶段SFT(背课本)PPO(练实战)
像什么像学生背公式像做实验题
学什么记住标准答案理解为什么这么做
能应对做过的题型没见过的新题型
局限题目变了就懵需要大量试错

💡 Trick:如果 Base-RL 效果想更进一步,可以先用 base-RL 拒绝采样一批样本,对 Base 模型做简单冷启动微调,再继续 RL。

5.2 SFT vs RL:工程视角对比

SFT vs RL:工程视角对比

SFT 和 RL 的本质区别

场景SFT 局限RL 优势
对齐人类偏好难以标注"什么是好回答"只需打分即可训练
优化不可微指标BLEU/ROUGE 无法反向传播任意指标可作为 Reward
探索能力只能模仿训练数据能发现训练集没有的好策略

5.3 模型架构:三头分类 + 一个 Value Head

策略模型的架构

关键设计决策:

决策理由
部分冻结 Backbone防止 RL 初期梯度破坏预训练特征
三头独立与 SFT 结构一致,可直接加载权重
共享 Backbone减少参数,但 Critic 梯度会回传(⚠️ 坑点)

💡 Trick:初始化 PPO 时,Critic 模型的权重应该从 SFT 模型加载,而不是随机初始化。随机初始化的 Critic 是策略崩塌的主要元凶之一。

5.4 基座模型选择

阶段基座模型效果备注
初期chinese-roberta-wwm-ext✅ 可行中文语义理解能力强
后期Qwen-0.5B-Instruct✅ 更优指令遵循能力更强

微调建议:推荐使用 LoRA 方式进行微调,仅更新少量参数(~1%)即可注入领域知识。


六、策略崩塌:PPO 训练的噩梦

6.1 现象描述

训练到中后期,你会看到这些信号同时出现:

准确率:97% → 15% → 8%
KL 散度:0.020.150.35 (飙升)
Value Loss:剧烈震荡

模型"学傻了"。

📖 小白看这里:KL散度和Value Loss是什么意思?

KL散度 = 新策略和旧策略之间的"距离"

  • 如果KL散度很小(0.02):说明策略变化不大,还在可控范围
  • 如果KL散度飙升(0.35):说明策略变化太剧烈,模型可能"学歪了"
  • 类比:就像开车时,小幅修正方向盘没问题,但猛打方向盘就会失控

Value Loss = Critic(打分教练)预测的分数和实际分数的差距

  • Loss低:教练预测准,说明训练稳定
  • Loss剧烈震荡:教练自己都搞不清楚状况,训练肯定出问题
  • 类比:就像考试前你估分90分,实际考了30分,说明你对题目理解有偏差

PPO 灾难性遗忘

6.2 根因分析

PPO 训练时有四个关键组件:

┌─────────────────────────────────────────────────────────┐
│                    PPO 四模型架构                        │
├─────────────────┬─────────────────┬─────────────────────┤
│  Actor (新策略)  │  Actor (旧策略)  │  用于计算 ratio     │
├─────────────────┼─────────────────┼─────────────────────┤
│  Critic (价值)   │  Reward (奖励)   │  用于计算 Advantage │
└─────────────────┴─────────────────┴─────────────────────┘

问题出在 Actor 和 Critic 共享 Backbone

Actor = SFT 预训练"高手"
Critic = 随机初始化"新手"
        ↓
共享 Backbone 下,Critic 为拟合 value 产生大梯度
        ↓
污染语言特征 → 触发灾难性遗忘

简单说:新手 Critic 把老司机 Actor 带沟里了。

用生活场景理解

想象一个老司机(Actor)带一个新手教练(Critic)练车:

┌─────────────────────────────────────────────┐
│  正常情况(Critic 权重低):                   │
│  ────────────────────────────────────────   │
│  老司机开车 → 新手教练打分 → 老司机参考调整   │
│  结果:老司机主导,新手教练慢慢学会打分       │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│  崩溃情况(Critic 权重过高):                 │
│  ────────────────────────────────────────   │
│  老司机开车 → 新手教练瞎指挥 → 老司机被带偏   │
│  结果:老司机的经验被污染,两个人一起迷路     │
└─────────────────────────────────────────────┘

6.3 Golden Config:稳定训练的配方

经过无数次实验,我总结出一套"保守优先"的配置:

参数激进配置(会崩)保守配置(稳定)为啥这么改
learning_rate3e-41e-6 ~ 5e-6PPO 的 lr 要比 SFT 小一个数量级
vf_coef0.50.01 ~ 0.1压住 Critic,别让它带偏 Actor
clip_range0.20.1 ~ 0.15限制每次更新的幅度
target_kl0.10.02 ~ 0.05策略差异太大就熔断
n_epochs102 ~ 4同一批数据别反复学
冻结层数0前 10 层物理隔离,保护语言能力
batch_size宁大勿小大 batch 梯度更稳定

Golden Config 表格

📖 小白看这里:这些参数都是什么意思?
参数人话解释生活类比
learning_rate每次训练模型参数调整的幅度学开车时方向盘转多大:太大容易失控,太小学得太慢
vf_coefCritic(教练)说话的分量教练影响力太大:司机听教练的多了,自己就不敢开了
clip_range限制策略更新的幅度跑步时每次步伐别太大,不然容易摔倒
target_kl策略变化的警戒线就像车速限制,超过这个速度就自动刹车
n_epochs同一批数据重复学几次同一道题做太多遍容易背答案,而不是真正理解
冻结层数固定住的神经网络层数把房子的地基固定住,只装修上面的楼层
batch_size一次训练用多少样本做饭时一次炒多少菜:太少费火,太多容易炒不熟

核心思想

PPO 在预训练模型上,不是用来"猛涨分"的,是用来"稳稳变好"的。

PPO + 预训练模型 = 必须保守

💡 Trick

  • 学习率建议用余弦衰减,避免固定学习率导致后期震荡
  • Critic 的学习率可以比 Actor 高(如 Actor 1e-6,Critic 5e-6),因为 Critic 需要更快拟合奖励值
  • 显存不够时,优先用 Gradient Accumulation 等效扩大 batch size

调优后,训练曲线明显更平稳,具备自我恢复能力:

新旧参数对比Via 多跳优化结果
对比Via优化

七、Reward Engineering:把业务约束写进奖励

7.1 三条通用原则

在讲具体做法之前,先说三条通用原则,这是解决一切 RL 问题的基石:

原则说明
奖励模型是天花板RM 质量直接决定 RLHF 上限。如果 reward 信号本身有噪声,后续再怎么调也白搭
KL 散度是缰绳既要学新偏好,又不能偏离原模型太远。KL 就是控制这个距离的"缰绳"
深度学习经验通用RLHF 本质是深度学习,调参经验大多通用

7.2 奖励机制本质

⚠️ 重要澄清:在当前任务中奖励是可验证的规则函数,而非训练出来的 RM。

方式描述适用场景
规则函数(当前)根据路径匹配度、语法正确性综合评分逻辑明确、可控
奖励模型(RM)训练一个模型来打分任务复杂度高、规则难以穷举
混合方案规则为主 + LLM 判别器辅助复杂生成任务

7.3 分层奖励设计

层级类型奖励值目的
L1组件级(Dense)对 +1.0 / 错 -0.5密集信号,避免早期迷失
L2合法性约束合法 +0.2 / 非法 -2.0注入 Schema 规则
L3完全匹配(Sparse)全对 +10.0引导追求完美
📖 小白看这里:什么是Dense和Sparse奖励?

Dense(稠密)奖励 = 每一步都有反馈

  • 类比:学开车时,教练每个动作都点评("方向盘打得不错""刹车有点急")
  • 优点:学得快,知道自己哪里做对了
  • 缺点:容易被"hack",模型学会钻空子

Sparse(稀疏)奖励 = 只有最后才有结果

  • 类比:考试只有最后出分数,中间不知道对错
  • 优点:目标明确,不会钻空子
  • 缺点:学得慢,前期像"瞎蒙"

最佳实践:Dense + Sparse 混合,既有过程引导,又有最终目标。

💡 Trick

  • 复杂任务的奖励函数不要太单一,否则很容易 Reward Hacking
  • Reward Clipping:建议把奖励输出限制在 [-2, 2] 范围内,防止异常高的奖励主导梯度
  • 对 reward 或 advantage 做归一化(减均值、除标准差),能显著提升稳定性

7.4 发现的 Reward Hacking

训练过程中,我发现模型学会了"作弊":

坑 1:Via 字段 80% 是 null,模型无脑预测 null 也能得高分。

解决:动态加权,非 null 的 Via 给予 10 倍权重。

坑 2:即使 Anchor 错了,Via 碰巧对了也能得分。

解决:条件发放,Anchor 错则 Via 不得分。

📖 小白看这里:什么是Reward Hacking?

Reward Hacking = 模型学会钻奖励函数的空子,而不是真正学会任务

经典案例

场景奖励函数模型学会的"作弊"方式
考试评分"写满字就给分"疯狂抄作业凑字数
赛艇训练"只要往前游就有分"转圈游(无限得分)
本文案例"Via对就给分"无脑预测null(80%概率对)

解决思路

  1. 动态加权:难的对的给高分
  2. 条件发放:前置条件错了,后面对了也不算
  3. 加惩罚项:发现作弊就扣分
# 条件发放示例
if anchor_correct:
    reward += via_reward * dynamic_weight
else:
    reward += 0  # Anchor 错了,Via 分不给

Reward 设计

💡 Trick:遇到 Reward Hacking,解决方案通常是:

  1. 在奖励函数中加入惩罚项
  2. 调低某个 reward 的权重系数
  3. 把作弊样本作为负例,重新训练奖励模型

八、评估:别只看训练曲线,要证明"确实学到了"

我们做了统一评估模块,让三种方法同台:

方法描述模型架构
Baseline通用大模型的原生能力LLM + 正则提取
SFT模仿学习的上限BERT + 3 分类头
RL (PPO)自我探索与优化的成果PPO + SFT 预训练

核心指标

方法完全匹配Via 准确率
Baseline(通用 LLM)35.71%-
SFT89.29%87.50%
RL(PPO)89.29%89.29%

端到端 AB(执行视角):

指标BaselineRouter(RL)变化
执行成功率80%90%🔼 +10%
查空/报错率20%10%🔽 -50%

执行效果

我们观察到一个有意思的点:

  • SFT 和 PPO 的 Full Match 在某些测试集上差不多
  • 但 PPO 更容易在多跳、长尾问法上稳住,而且执行端指标更好

这也是我最后觉得"PPO 值得做"的原因:它不是为了把一个数字从 80 提到 90,而是为了让模型在真实环境里更不容易翻车。


九、故障排查手册(15 种典型问题)

分享一个我总结的排障手册。核心原则:先止血,再找病因。

9.1 快速止损表

现象大概率原因怎么救
approx_kl > 0.1更新步幅太大降 lr / 降 clip_range / 严 target_kl
Reward 长期不涨SFT 权重没加载检查初始化,确认从高起点开始
Via 全选 nullReward Hacking开启动态加权
Value Loss 剧烈震荡Critic 在捣乱降 vf_coef / 冻结更多层
训练越久越差灾难性遗忘Early Stop / 减少 n_epochs

9.2 详细现象排查

现象可能原因解决方案
Reward 上升 + KL 爆炸kl_penalty 系数过低或没加增加 KL 惩罚项,从 0.001 开始调
KL 很低 + Reward 不涨kl_penalty 太强,模型被束缚调低系数,同时检查学习率
初期输出重复/无意义学习率过高,参数更新过猛降到 1e-6 ~ 1e-5,加 warmup
响应长度异常(过长/过短)RM 有 length bias在 RL 阶段加入长度惩罚/奖励
训练不稳定,loss 剧烈波动batch_size 太小 / reward 没归一化扩大 batch / 对 reward 做 norm 和 clip
后期质量下降过拟合 RM / KL 约束失效Early Stop,检查 KL 是否在合理范围
Critic Value Loss 波动reward 方差过大对 reward 或 advantage 做归一化
策略熵快速下降,输出同质化entropy_coef 过低,探索不足增大熵系数
梯度范数爆炸学习率过高 / 没有梯度裁剪降 lr,启用 gradient clipping
Reward 上涨但人工评估差RM 过拟合或偏好数据有偏拆分多维度 reward,分别标注加权
测试集好但部署效果差训练数据与真实场景分布差异扩充领域/风格数据,提升泛化
DPO 的 chosen/rejected 概率差增长慢beta 值过高,更新太保守调低 beta
DPO loss 下降快但效果不如 SFTbeta 过低或 lr 过高调高 beta,降低学习率

RL训练trick总结

最重要的一条

📌 如果你只想盯一个指标,盯 approx_kl。超过 0.1 立刻停下来检查。


十、三条核心教训

三条教训

经验记忆口诀
必须 SFT 冷启动"先背书,再做题"
PPO 必须保守更新"低 lr + 低 vf + 严 KL"
奖励设计防作弊"动态加权 + 条件发放"

写在最后

做完这个项目,我最大的感受是:

RL 的难点不是写模型,是写环境和奖励。

代码可能只占 20% 的工作量,剩下 80% 都在:

  • 设计环境和奖励
  • 调参、debug
  • 理解模型为什么"学歪了"

换个角度想,RLHF 本质上就是一个自动化的"集成测试"循环:模型输出 → 环境打分 → 模型调整 → 再输出。

只不过这个"测试用例"是你设计的 Reward 函数。

希望这篇文章能帮你少踩几个坑。如果你也在做类似的事情,欢迎留言交流。


Q&A(把评论区高频问题先回答掉)

Q1:为什么 SFT 和 RL 在简单测试集上结果一样?
A:简单单跳样本 SFT 已经接近满分。RL 的优势主要在长尾复杂样本、多跳与执行端稳定性,数据越复杂差异越明显。

Q2:训练初期 reward 不涨怎么办?
A:优先检查 SFT 权重是否正确加载。RL 应该从一个高起点开始(比如 Acc ~80%/90%),而不是从零瞎蒙。如果确认加载了还是不涨,用训练前的模型针对一些 case rollout 多个回复,看这些回复的奖励是不是都特别低——如果是,说明基模能力上限就这样,换模型或优化 SFT。

Q3:Baseline 评估为什么这么慢?
A:因为要调用大模型 API,单条请求耗时很常见在秒级(我们当时约 10 秒/条量级)。

Q4:奖励函数是训练出来的吗?
A:当前不是。我们用可验证规则函数做 reward,因为逻辑明确、可控。若任务复杂到规则难穷举,可以引入 LLM Judge 做软评分,规则做硬约束。

Q5:表结构变更需要重新训练吗?
A:不一定。Router 更依赖"表之间怎么连",不强依赖字段细节;但新增表/新增关系通常需要补数据再训一版适配。

Q6:模型保存有什么建议?
A:RLHF 最好每隔一定 step 保存优化器参数,这样可以随时恢复训练。尤其是多机多卡场景,容易出现通信问题导致训练中断。


📚 参考资料

👤 关于作者

专注AI与算法工程落地的一线开发者。踩过的坑比写过的代码还多(因为vibe code多)。

如果你也在做大模型相关的工程化工作,欢迎关注交流。


⚠️ 原创声明:本文为原创内容,转载请联系作者获取授权。