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

本文约 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 业务物理法则(动作空间边界)

维度 数值 说明
核心 Schema 12 张表 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.02 → 0.15 → 0.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_rate 3e-4 1e-6 ~ 5e-6 PPO 的 lr 要比 SFT 小一个数量级
vf_coef 0.5 0.01 ~ 0.1 压住 Critic,别让它带偏 Actor
clip_range 0.2 0.1 ~ 0.15 限制每次更新的幅度
target_kl 0.1 0.02 ~ 0.05 策略差异太大就熔断
n_epochs 10 2 ~ 4 同一批数据别反复学
冻结层数 0 前 10 层 物理隔离,保护语言能力
batch_size 宁大勿小 大 batch 梯度更稳定

Golden Config 表格

📖 小白看这里:这些参数都是什么意思?
参数 人话解释 生活类比
learning_rate 每次训练模型参数调整的幅度 学开车时方向盘转多大:太大容易失控,太小学得太慢
vf_coef Critic(教练)说话的分量 教练影响力太大:司机听教练的多了,自己就不敢开了
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% -
SFT 89.29% 87.50%
RL(PPO) 89.29% 89.29%

端到端 AB(执行视角):

指标 Baseline Router(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 全选 null Reward 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 下降快但效果不如 SFT beta 过低或 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多)。

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


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


posted @ 2026-01-07 21:17  遇健李的幸运  阅读(2)  评论(0)    收藏  举报