一、RLHF 的困境与 RLVR 的破局

大模型对齐一直依赖 RLHF(Reinforcement Learning from Human Feedback)。这个范式有三个硬伤:

  • 标注成本高:万条人类偏好标注,每条 2-5 美元,总成本轻松破万
  • 一致性差:不同标注员对同一回复的打分方差极大,奖励模型本身就是噪声源
  • 无法规模化:每引入一个新领域(数学、代码、医学),就需要新一轮标注

2025 年初 DeepSeek-R1 引爆了一个新范式——RLVR(Reinforcement Learning with Verifiable Rewards)。核心思路简单到令人意外:既然数学题答案对错可自动判定、代码能否运行可自动验证,为什么还要花钱请人打分?

2026 年 5 月 HuggingFace 热榜论文 "You Only Need Minimal RLVR Training: Extrapolating LLMs via Rank-1 Trajectories" 进一步证实:仅需极少量的 RLVR 训练(< 100 条数据),模型就能在数学推理任务上获得显著提升。另一篇论文 "The Unlearnability Phenomenon in RLVR" 则从反面揭示了 RLVR 的边界条件。

二、RLVR 的核心原理

2.1 与 RLHF 的本质区别

RLHF 流程:
  LLM生成 → 人类打分 → 训练Reward Model → PPO优化策略

RLVR 流程:
  LLM生成 → 规则验证器打分 → GRPO直接优化策略

RLHF 需要训练一个 Reward Model(通常是一个小模型)来模拟人类偏好,然后用 PPO 优化策略。这引入了两个误差源:标注噪声 + Reward Model 近似误差。

RLVR 直接把奖励定义为一个 确定性规则函数

# RLHF: 奖励来自神经网络(有噪声、有偏)
reward_rlhf = reward_model.predict(prompt, response)  # 浮在 0~10 之间

# RLVR: 奖励来自规则引擎(无噪声、可复现)
reward_rlvr = rule_verifier(prompt, response)          # 精确的 0 或 1

2.2 可验证奖励函数设计

RLVR 的灵魂在于 rule_verifier。不同任务有不同的验证规则:

from typing import Callable
import re, ast, subprocess, math

# ──── 数学题验证器 ────
def math_verifier(ground_truth: str) -> Callable:
    """返回一个闭包,检查模型输出是否包含正确答案"""
    def verify(response: str) -> float:
        # 从模型输出中提取 \boxed{...} 内的答案
        match = re.search(r'\\boxed\{([^}]+)\}', response)
        if not match:
            return 0.0
        extracted = match.group(1).strip()
        truth = ground_truth.strip()
        # 数值近似匹配(容忍浮点误差)
        try:
            return 1.0 if abs(float(extracted) - float(truth)) < 1e-6 else 0.0
        except ValueError:
            return 1.0 if extracted == truth else 0.0
    return verify

# ──── 代码正确性验证器 ────
def code_verifier(test_cases: list[dict]) -> Callable:
    """运行测试用例,全部通过 = 奖励 1.0"""
    def verify(code: str) -> float:
        # 提取代码块
        match = re.search(r'```(?:python)?\s*\n(.*?)```', code, re.DOTALL)
        source = match.group(1) if match else code
        namespace = {}
        try:
            exec(source, namespace)
        except Exception:
            return 0.0
        # 逐一运行测试
        for tc in test_cases:
            try:
                result = eval(tc["expr"], namespace)
                if result != tc["expected"]:
                    return 0.0
            except Exception:
                return 0.0
        return 1.0
    return verify

# ──── 格式正确性验证器 ────
def format_verifier(required_tags: list[str]) -> Callable:
    """检查输出是否包含指定标签,鼓励结构化思考"""
    def verify(response: str) -> float:
        score = 0.0
        for tag in required_tags:
            if f"<{tag}>" in response and f"</{tag}>" in response:
                score += 1.0 / len(required_tags)
        return score
    return verify

# ──── 组合验证器 ────
def compose_verifiers(*verifiers, weights: list[float] = None) -> Callable:
    """加权组合多个验证器"""
    if weights is None:
        weights = [1.0 / len(verifiers)] * len(verifiers)
    def verify(response: str) -> float:
        total = 0.0
        for v, w in zip(verifiers, weights):
            total += v(response) * w
        return total
    return verify

这些验证器是纯 Python 函数,零依赖。数学验证器直接比对数值,代码验证器在沙箱中执行并比对输出,格式验证器检查结构化标签。更重要的是——它们可以在训练中 实时计算 奖励,无需预先训练 Reward Model。

三、GRPO 算法:RLVR 的优化引擎

PPO 需要 Critic 网络(一个与策略网络等大的 Value Model),显存占用翻倍。GRPO(Group Relative Policy Optimization)是 DeepSeek 提出的轻量替代方案。

3.1 GRPO 的核心思想

对同一个 prompt 采样一组 K 个回复,用组内相对排名替代 Critic 的绝对价值估计:

import torch
import torch.nn.functional as F

def grpo_loss(
    log_probs: torch.Tensor,        # 当前策略的 log π(a|s)  [K, seq_len]
    old_log_probs: torch.Tensor,    # 旧策略的 log π_old(a|s) [K, seq_len]
    rewards: torch.Tensor,          # 规则验证器给出的奖励    [K]
    advantages: torch.Tensor,       # 组内归一化优势         [K]
    clip_epsilon: float = 0.2,
    kl_beta: float = 0.01,          # KL 散度惩罚系数
    ref_log_probs: torch.Tensor = None,
) -> torch.Tensor:
    """
    GRPO 损失函数

    Args:
        log_probs: 当前策略下每条回复的对数概率
        old_log_probs: 采样时(旧策略)的对数概率
        rewards: 每条回复的可验证奖励
        advantages: 组内归一化后的优势值
        clip_epsilon: PPO-style clipping 范围
        kl_beta: KL 惩罚强度,防止策略偏离参考模型太远
        ref_log_probs: 参考模型(通常是初始 SFT 模型)的对数概率
    """
    # ── 重要性采样比率 ──
    ratio = torch.exp(log_probs - old_log_probs)

    # ── PPO-style Clipping ──
    surr1 = ratio * advantages
    surr2 = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * advantages
    policy_loss = -torch.min(surr1, surr2).mean()

    # ── KL 散度惩罚(防止 reward hacking)──
    if ref_log_probs is not None and kl_beta > 0:
        kl_div = torch.exp(ref_log_probs - log_probs) - (ref_log_probs - log_probs) - 1
        kl_penalty = kl_beta * kl_div.mean()
    else:
        kl_penalty = 0.0

    return policy_loss + kl_penalty


def compute_group_advantages(rewards: torch.Tensor) -> torch.Tensor:
    """组内归一化:将奖励转为标准化优势值"""
    mean_r = rewards.mean()
    std_r = rewards.std()
    # 避免除零
    if std_r < 1e-8:
        std_r = 1.0
    return (rewards - mean_r) / std_r

GRPO 的 trick 在于 组内对比——同一个 prompt 生成 4-8 个回复,好的回复(高奖励)获得正优势,差的回复获得负优势。这种方法绕过了 Critic 的训练,显存需求直接减半。

3.2 训练数据构造

RLVR 的训练数据极其精简。以数学推理任务为例,只需要 prompt + ground truth 对:

# 训练数据格式:不需要人类偏好排序,只需题目+答案
training_data = [
    {
        "prompt": "计算 ∫₀¹ x² dx 的值。请将答案放在 \\boxed{...} 中。",
        "ground_truth": "1/3",               # 或 "0.333333"
        "verifier": "math"
    },
    {
        "prompt": "用 Python 实现快速排序算法。",
        "ground_truth": None,                 # 不需要标准答案
        "verifier": "code",
        "test_cases": [
            {"expr": "quicksort([3,1,4,1,5,9])", "expected": [1,1,3,4,5,9]},
            {"expr": "quicksort([])", "expected": []},
            {"expr": "quicksort([1])", "expected": [1]},
        ]
    },
    # ... 只需 50-200 条这样的数据
]

对比 RLHF 需要数万条人类偏好标注,RLVR 的数据成本几乎为 0。而且验证规则一次编写、永久复用。

四、完整训练管线

下面是整合了 GRPO + 可验证奖励的完整训练脚本骨架,基于 HuggingFace TRL 的 GRPOTrainer

from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

# ──── Step 1: 加载基座模型 ────
model_name = "Qwen/Qwen2.5-7B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    attn_implementation="flash_attention_2",
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

# ──── Step 2: 定义奖励函数 ────
def reward_func(prompts, completions, **kwargs):
    """TRL GRPOTrainer 要求的接口签名"""
    rewards = []
    for prompt, completion in zip(prompts, completions):
        # 根据 prompt 中的标记选择验证器
        if "\\boxed" in prompt:
            # 数学任务:提取 ground_truth
            gt = extract_ground_truth(prompt)
            score = math_verifier(gt)(completion)
        elif "快速排序" in prompt or "def " in prompt:
            score = code_verifier([
                {"expr": "quicksort([3,1,4])", "expected": [1,3,4]}
            ])(completion)
        else:
            score = format_verifier(["think", "answer"])(completion)
        rewards.append(score)
    return rewards

def extract_ground_truth(prompt: str) -> str:
    import re
    m = re.search(r'答案[是为::]\s*([^\n]+)', prompt)
    return m.group(1) if m else ""

# ──── Step 3: 准备数据集 ────
dataset = Dataset.from_list([
    {
        "prompt": [
            {"role": "system", "content": "你是一个数学助手。先思考再作答,将最终答案放在 \\boxed{...} 中。"},
            {"role": "user", "content": "计算 ∫₀¹ x² dx。答案: 1/3"}
        ]
    },
    {
        "prompt": [
            {"role": "system", "content": "你是一个编程助手,输出可运行的 Python 代码。"},
            {"role": "user", "content": "用 Python 实现快速排序算法。"}
        ]
    },
    # ... 更多数据
])

# ──── Step 4: GRPO 训练配置 ────
training_args = GRPOConfig(
    output_dir="./rlvr_checkpoints",
    num_train_epochs=1,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=5e-6,
    warmup_ratio=0.1,
    logging_steps=5,

    # ── GRPO 核心参数 ──
    num_generations=4,              # 每个 prompt 采样 K=4 个回复
    max_completion_length=512,      # 回复最大长度
    temperature=0.9,                # 采样温度(需要一定随机性以探索)

    # ── KL 约束 ──
    beta=0.01,                      # KL 散度惩罚系数

    # 显存优化
    gradient_checkpointing=True,
    bf16=True,
    max_grad_norm=0.3,
)

# ──── Step 5: 启动训练 ────
trainer = GRPOTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    reward_funcs=[reward_func],     # 规则验证器列表
    tokenizer=tokenizer,
)

trainer.train()
trainer.save_model("./rlvr_qwen_math_final")

五、训练效果与调参经验

在实际训练中,RLVR 有几个值得注意的特性:

1. 极速收敛:RLVR 通常 50-200 步就能看到显著提升。因为规则奖励的信号极其清晰(0 或 1),不存在 RLHF Reward Model 的那种模糊区间。论文 Minimal RLVR Training 报告仅需 rank-1 trajectory(最优的那条采样轨迹)即可实现有效外推。

2. 对 KL 惩罚敏感beta 太小 → 模型快速学会"作弊"(如输出超长文本增加碰对概率);beta 太大 → 策略不更新。建议 beta 从 0.01 开始,观察 KL 散度曲线动态调整。

3. 温度是关键超参temperature=0.9 是经验起点。太低(< 0.6),K=4 个回复几乎一样,组内对比失效;太高(> 1.2),回复质量下降导致奖励方差过大。

4. 冷启动建议:直接用 Base Model + RLVR 难以收敛。建议先在少量 SFT 数据上 warmup 1-2 epoch,再用 RLVR 强化。

# 快速调参脚本:对关键超参做网格搜索
configs = [
    {"lr": 5e-6, "beta": 0.01, "temperature": 0.9},
    {"lr": 5e-6, "beta": 0.02, "temperature": 0.9},
    {"lr": 1e-5, "beta": 0.01, "temperature": 0.7},
]
for cfg in configs:
    print(f"\n=== Testing {cfg} ===")
    # 在验证集上评估 pass@1 和 pass@4
    # ...

六、RLVR 的边界:什么时候不 Work?

The Unlearnability Phenomenon in RLVR 这篇论文揭示了一个关键限制:当奖励信号过于稀疏时,RLVR 可能完全学不动

# 奖励稠密 → RLVR 有效
def dense_reward(response: str) -> float:
    score = 0.0
    for step in extract_steps(response):   # 每个中间步骤都有部分奖励
        if is_partial_correct(step):
            score += 0.2
    return min(score, 1.0)

# 奖励稀疏 → RLVR 难以学习
def sparse_reward(response: str) -> float:
    return 1.0 if final_answer_correct(response) else 0.0
    # 长推理链中只有最后一步有信号,前面全是 0

对策:为长推理链拆解中间验证点。例如数学题可以验证中间公式推导,代码任务可以分步骤验证(语法 → 类型检查 → 测试通过)。

七、RLVR 的未来与工程建议

RLVR 正在从学术论文走向工业落地。DeepSeek-R1-Zero 证明了纯 RLVR(零人类偏好标注)就能训练出强推理模型,各路开源项目也在快速跟进。

工程落地建议:

  • 从小任务开始:先为单一任务(如数学计算)构建验证器,跑通流程后再泛化
  • 验证器可组合:用上文 compose_verifiers 的模式,数学+格式+安全三个验证器加权组合,覆盖多个维度
  • 监控 KL 散度:如果 KL 散度持续飙升,说明模型在"作弊"——降低学习率或增大 beta
  • 数据质量 > 数据数量:50 条精心构造的 (prompt, verifier) 对比 50 条随意拼凑的数据,效果天差地别

RLVR 的终极吸引力在于:它让大模型训练从"有多少预算请人标注"变成了"有多少人能把规则写好"。这是一个工程师友好型范式——写代码比做标注靠谱得多。

更多推荐