第 17 章 第三部分:后训练数据工程

数据混合与配方

三轴配方(任务/领域/难度)、SFT阶段配方(Tulu-3: 通用30%+数学20%+代码15%+安全10%+指令10%+多语5%+知识5%+工具5%)、DPO阶段配方(Helpfulness 35%/Honesty 15%/Harmlessness 15%/推理20%/长度去偏10%)、RL/RLVR阶段配方(pass@8分桶: 0.8-1.0 10%/0.5-0.8 30%/0.2-0.5 40%/0-0.2 20%)、三阶段串联(Tulu-3: SFT 939K→DPO 271K→RLVR 30K)、动态配比(DoReMi for SFT)、课程化(简单→中等→难)、目标模型感知数据选择(Data-DPO one-step probing/激活差异转偏好/三重组合)、后训练动态配比(DataFlex统一三范式/DomainPilot token级域loss+Scaling Law粗优化+Mixing Law细优化/loss信号混淆噪声难度新颖性)、可复现基准(NebulaExp-8B 3.84M SFT+200K RL/五步处理栈/55.01→60.99→61.85)

数据混合 三轴配方 Tulu-3 DoReMi 课程化 动态配比 数据选择 可复现基准

数据是食材,配方是菜谱。

同样 100 万条数据,配比不同,模型能力差 30%。


17.1 三轴配方

后训练数据混合不是”一锅烩”,而是按”三轴”组织:

17.1.1 三轴定义

        难度轴
          ↑
          │
     简单 │
          │
     中等 │     ★ 甜点区
          │
     困难 │
          │
          └──────────→ 领域轴
        数学 代码 通用 安全 ...

         ↙ 任务轴
    生成 抽取 推理 对话 ...
  • 任务轴:生成、抽取、推理、对话、翻译、摘要…
  • 领域轴:数学、代码、通用、安全、多语、知识、工具…
  • 难度轴:简单、中等、困难(用 pass@K 或 LLM 评分)

17.1.2 三轴的优先级

工业实践中,三轴的优先级是:领域 > 任务 > 难度。

原因:领域失衡最致命(缺数学就是不会数学),难度可以通过课程化动态调整(见 17.6 节)。

17.1.3 三轴矩阵

每条数据在三轴上都有定位:

{
  "prompt": "证明 √2 是无理数",
  "axis": {
    "domain": "math",
    "task": "reasoning",
    "difficulty": "hard",
    "difficulty_score": 0.85  // 0-1,pass@8 = 0.15
  }
}

配方是三轴矩阵的”权重表”:

领域\难度    简单    中等    困难
数学        5%     10%     5%
代码        5%      8%     2%
通用        10%    15%     5%
...

17.2 SFT 阶段配方

17.2.1 Tulu-3 SFT 配方(标杆)

Tulu-3 的 SFT 阶段配方是工业级参考标杆:

数据源数量占比领域任务
WizardLM/Evol-Instruct280K30%通用对话
NuminaMath/GSM8K 增强190K20%数学推理
Magicoder/Evol-Code140K15%代码生成
WildGuardMix95K10%安全拒答
IFEval-like95K10%指令遵循格式
多语言(20+)45K5%多语翻译
OpenOrca 子集45K5%知识问答
xLAM/Glaive45K5%工具调用
总计939K100%

17.2.2 配方设计原则

Tulu-3 配方体现了几条原则:

1. 通用打底(30%) 通用对话数据是”底盘”,确保模型基本能用。低于 20% 模型会”偏科”。

2. 推理加强(35%) 数学 20% + 代码 15% = 35%,这是”思考能力”的核心。低于 25% 推理能力显著下降。

3. 安全底线(10%) 安全数据不能少,否则上线即翻车。但超过 20% 会过度拒答。

4. 指令遵循(10%) 指令遵循数据教模型”听懂指令”,对 IFEval 等评测至关重要。

5. 长尾覆盖(15%) 多语 + 知识 + 工具 = 15%,避免能力短板。

17.2.3 不同能力的配方敏感度

能力配方敏感度备注
数学极高数学数据从 10% → 20%,GSM8K 涨 8 分
代码高代码数据低于 10%,HumanEval 显著下降
安全高安全数据低于 5%,XSTest 拒答率不足
通用对话中通用数据 20-40% 区间影响小
指令遵循中需要专门数据,但 10% 足够
多语言低主要看绝对量,占比不是关键

17.2.4 数据选择:把”价值”从静态属性改为关系属性

17.2.1–17.2.3 讨论的是”选多少”(配比),但在此之前还有一步”选哪些”(数据选择)。主流选择方法——包括第 16 章的 IFD、Deita——都隐含一个假设:数据价值是数据自身的属性。

Data-DPO(Peng Sun 等 10 人)质疑的正是这一点。它的诊断是:现有方法”通常把数据价值当作相对静态的属性,很少关注数据与目标模型能力分布的兼容性”。

也就是说,同一批数据对一个 3B 模型是高价值的,对一个 70B 模型可能全是已掌握的内容。价值不是一个数,而是数据与模型之间的关系。

方法三步:

  1. one-step probing:用目标模型对候选样本做一步探测,观察它在不同样本上的局部训练反馈
  2. 转化为偏好:把样本之间的激活差异(activation differences)转成 pairwise data preferences
  3. 训练轻量 reward model:学习目标模型感知的数据偏好

最终选择阶段不是只看这个 reward,而是三者结合:目标模型偏好 + 外部质量分数 + 边际多样性。

为什么第三步的组合很重要:纯目标模型偏好选择会有一个系统性风险——模型会偏好”自己已经会的”或”最容易产生激活变化的”样本,前者导致无效训练,后者可能放大噪声。叠加外部质量分数(第 16 章那套)与边际多样性后,才构成可用的选择信号。这个”三重组合”本身就是一个可以直接复用的工程模板。

⚠️ 命名易混淆:Data-DPO 的名字里虽然有 DPO,但它不是用来选择 DPO 训练数据的方法,而是借用 DPO 的偏好建模思想来做 SFT 数据选择。论文摘要第一句即为 “Data selection in supervised fine-tuning…”。如果你在找”DPO 阶段的数据选择”,本章 17.3 才是对应位置,而该方向目前缺少同类的目标模型感知工作。

⚠️ 成本提示:one-step probing 需要对候选样本跑目标模型,成本随候选池规模线性增长。论文未给出在百万级候选池上的完整成本评估。落地时建议先用小规模候选池(如 100K)验证收益,再决定是否全量展开。


17.3 DPO 阶段配方

17.3.1 DPO 配方的特殊性

DPO 配方不是按”领域”分,而是按”偏好维度”分:

维度占比数据来源
Helpfulness(有帮助)35%UltraFeedback
Honesty(诚实)15%TruthfulQA 增强
Harmlessness(无害)15%WildGuardMix
推理(Reasoning)20%数学/代码偏好对
长度去偏15%人工构造短/长对
总计100%

17.3.2 长度去偏数据的构造

def construct_length_debias_data(prompts):
    """构造"短而精"胜过"长而水"的偏好对"""
    data = []
    for p in prompts:
        # 让 GPT-4 生成简洁版与冗长版
        concise = gpt4.generate(p, style="concise")
        verbose = gpt4.generate(p, style="verbose")
        
        # 让 judge 评分
        score_concise = judge(p, concise)
        score_verbose = judge(p, verbose)
        
        # 如果简洁版更好,作为 (chosen, rejected)
        if score_concise > score_verbose:
            data.append({
                'prompt': p,
                'chosen': concise,
                'rejected': verbose
            })
    return data

17.3.3 DPO 配方的常见错误

错误 1:Helpfulness 占比过高(>50%) 导致模型变得”阿谀奉承”,Sycophancy 评测掉分严重。

错误 2:缺推理维度 推理类偏好对缺失,模型推理能力会回退。

错误 3:长度去偏缺位 不加长度去偏数据,DPO 训练后输出长度漂移 +30%。


17.4 RL/RLVR 阶段配方

17.4.1 pass@8 难度分桶配方

RL/RLVR 阶段的配方核心是难度分桶:

pass@8难度推荐占比训练价值
0.8-1.0太简单10%低(模型已会)
0.5-0.8简单30%中(巩固)
0.2-0.5中等40%高(学习边界)
0-0.2困难20%中(突破)

核心洞察:pass@8 在 0.2-0.5 的 prompt 训练价值最高——模型有时能解、有时不能解,处于”最近发展区”。

17.4.2 任务配比

RLVR 阶段按可验证任务类型配比:

任务类型占比Verifier备注
数学50%sympy推理核心
代码30%单测工程能力
指令遵循20%规则格式能力

17.4.3 RLVR 规模

工业级 RLVR 的 prompt 池规模:

  • 最小可用:1 万
  • 典型:5 万-10 万
  • 上限:50 万

每个 prompt 训练时采样 N=8-64 次,实际算力消耗巨大。


17.5 三阶段串联:Tulu-3 完整配方

17.5.1 Tulu-3 三阶段串联

SFT (939K) → DPO (271K) → RLVR (30K)

每阶段的数据量递减,但单条价值递增:

阶段数据量单条价值训练目标
SFT939K低教格式
DPO271K中教品味
RLVR30K高教思考

17.5.2 为什么是递减

  • SFT 教基础能力,需要大量数据覆盖各场景
  • DPO 教偏好,数据量过大会过拟合
  • RLVR 训练时每条要采样 N 次,30K × 16 = 480K 次生成,算力消耗大

17.5.3 阶段间的衔接

SFT → DPO:

  • DPO 的 reference model = SFT 训练完的模型
  • DPO 数据中的 prompt 应该与 SFT 数据分布一致

DPO → RLVR:

  • RLVR 的初始 policy = DPO 训练完的模型
  • RLVR 的 prompt 池应该是 DPO 模型 pass@8 在 0-0.8 区间的
def prepare_rlvr_pool(dpo_model, candidate_prompts, verifier):
    """用 DPO 模型评估候选 prompt 的 pass@8"""
    pool = []
    for p in candidate_prompts:
        pass_at_8 = compute_pass_at_k(dpo_model, p, verifier, k=8)
        if 0.0 < pass_at_8 < 0.8:  # 有学习空间
            p['pass_at_8'] = pass_at_8
            pool.append(p)
    return pool

17.5.4 串联的失败模式

失败 1:跳过 SFT 直接 DPO 模型没学会对话格式,DPO 训练时输出乱码。

失败 2:跳过 DPO 直接 RLVR 模型没有”偏好”基础,RLVR 训练时探索方向失控。

失败 3:三阶段都用同一批 prompt 会导致模型在某类 prompt 上过拟合,多样性下降。


17.6 课程化

17.6.1 课程化的核心

课程化(Curriculum Learning)的核心:从简单到困难逐步训练。

Epoch 1-10:  pass@8 0.5-0.8 的 prompt(简单)
Epoch 10-30: pass@8 0.2-0.5 的 prompt(中等)
Epoch 30-50: pass@8 0-0.2 的 prompt(困难)

17.6.2 RLVR 的课程化重采

RLVR 训练中,模型的 pass@8 会变化——原本困难的 prompt 变简单。需要动态调整:

def curriculum_rlvr(model, prompt_pool, verifier, total_epochs=50):
    for epoch in range(total_epochs):
        # 1. 每 5 epoch 重新评估 pass@8
        if epoch % 5 == 0:
            for p in prompt_pool:
                p['pass_at_8'] = compute_pass_at_k(model, p, verifier, k=8)
        
        # 2. 根据当前 epoch 选难度区间
        if epoch < total_epochs * 0.2:
            target_range = (0.5, 0.8)  # 简单
        elif epoch < total_epochs * 0.6:
            target_range = (0.2, 0.5)  # 中等
        else:
            target_range = (0.0, 0.2)  # 困难
        
        # 3. 采样训练
        batch = [p for p in prompt_pool if target_range[0] <= p['pass_at_8'] < target_range[1]]
        train_step(model, batch, verifier)

17.6.3 SFT 的课程化

SFT 阶段也可以课程化,但效果不如 RLVR 明显:

def sft_curriculum(data, epochs=3):
    # 1. 按难度排序
    sorted_data = sorted(data, key=lambda x: x['difficulty_score'])
    
    # 2. 分 3 档
    n = len(sorted_data)
    easy = sorted_data[:n//3]
    medium = sorted_data[n//3:2*n//3]
    hard = sorted_data[2*n//3:]
    
    # 3. 逐档训练
    for epoch in range(epochs):
        if epoch == 0:
            train(easy)
        elif epoch == 1:
            train(easy + medium)
        else:
            train(easy + medium + hard)

踩坑:SFT 课程化容易导致”灾难性遗忘”——后学的内容覆盖前学。需要在每 epoch 混入 20% 旧数据。


17.7 动态配比:DoReMi

17.7.1 DoReMi 算法

DoReMi(Xie et al., 2023)是动态配比的代表方法。核心思想:用小模型预测各领域的”最优配比”,再用到大模型上。

1. 训练一个小参考模型(1B),用初始配比
2. 用 DoReMi 算法调整配比,最大化参考模型的 loss 下降
3. 用调整后的配比训练大模型(7B/70B)

17.7.2 DoReMi 配比调整

def doremi_adjust(domain_weights, reference_model, train_data_by_domain):
    # 1. 当前配比下计算各域 loss
    domain_losses = {}
    for domain, weight in domain_weights.items():
        data = sample(train_data_by_domain[domain], int(weight * batch_size))
        loss = compute_loss(reference_model, data)
        domain_losses[domain] = loss
    
    # 2. 用 DoReMi 更新规则调整权重
    total_loss = sum(domain_losses.values())
    for domain in domain_weights:
        # loss 高的域加权重(说明该域学得差,需要更多数据)
        domain_weights[domain] *= (domain_losses[domain] / total_loss) * len(domain_weights)
    
    # 3. 归一化
    total = sum(domain_weights.values())
    for domain in domain_weights:
        domain_weights[domain] /= total
    
    return domain_weights

17.7.3 DoReMi 的效果

在 Pile 数据集上,DoReMi 优化后的配比训练 Llama-7B:

  • 平均 loss 下降 4%
  • 在下游任务上提升 3-6%

适用场景:预训练 + SFT 都可用,但 RLVR 阶段不太适用(RLVR 配比主要看 pass@8)。

17.7.4 后训练配比动态化:从”静态超参”到”训练中的第一公民”

DoReMi 解决的是预训练阶段的配比。后训练阶段的配方在工业实践中仍以静态三轴为主(17.1),原因不是不想动态化,而是三个现实阻碍。2026 年的两个工作分别从工程整合与信号质量两个方向推进。

DataFlex:先把接口统一了(arXiv:2603.26164, 2026-03-27)

DataFlex(Hao Liang 等 25 人)的诊断很朴素但切中要害:数据选择、配比优化、样本重加权这三类方法,各自长在互相独立的代码库里,接口不一致。结果是三件事都做不成——可复现性差、公平对比做不了、实际集成成本高。

它的做法是把三者统一进一个框架(基于 LLaMA-Factory 构建),支持 sample selection、domain mixture adjustment、sample reweighting 三种范式,同时保持与原有训练流程完全兼容,可作为 drop-in 替换。

DomainPilot:直指 loss 信号的根本缺陷(arXiv:2607.22769, 2026-07-24)

DomainPilot(He Zhang)列出了工业级落地的三条硬阻碍,其中第三条是对 DoReMi 路线的直接质疑:

阻碍具体问题后果
选择成本数据选择在 TB 级语料上是 O(N) 成本全量选择不可行
配比优化现有方案引入严重 I/O 瓶颈,或需要训练辅助参考模型训练被拖慢 / 额外开销
信号混淆样本级重加权依赖的 loss 信号混淆了噪声、难度与新颖性配比调整方向本身是错的

第三条值得单独展开,因为它直接动摇了 17.7.2 那套更新规则的前提。DoReMi 的规则是”loss 高的域加权重”,隐含假设是高 loss = 学得差 = 需要更多数据。但在真实数据里,一个高 loss 的样本至少有三种可能:

  • 噪声:标注错误或格式损坏的脏数据 → 应该降权甚至删除
  • 难度:真正有价值但模型还没学会 → 应该加权重
  • 新颖性:模型没见过的新分布 → 应该谨慎加权,并观察是否破坏已有能力

样本级重加权把这三种成分当作同一个东西处理,这正是很多”loss 引导动态配比”在干净 benchmark 上有效、在真实数据上抖动的原因。

DomainPilot 的两阶段应对:引入 token 级 domain loss 监控,在训练过程中捕获各域学习动态,不中断数据管线(针对阻碍 2 的 I/O 问题);在此基础上做两级优化:

阶段引导信号做什么针对的问题
粗优化(coarse)Scaling Law 引导拟合各域的收敛曲线,导出配比调整的先验阻碍 3——拟合的是趋势,天然平滑掉单点噪声
细优化(fine)Mixing Law 引导通过受控 sweep 实验建模跨域交互效应,精调配比单域 Scaling Law 看不到的域间耦合

整套机制通过 patch-based 架构实现——把 domain-aware loss 计算注入既有训练框架,不必替换训练栈。这一点决定了它的落地成本:不是”换一套训练系统”,而是”给你的训练系统打一个补丁”。

工程判据:如果你的 loss 引导配比在真实数据上不稳定,先不要调学习率或权重,先做一个廉价诊断——把高 loss 样本抽样出来人工看 50 条。如果其中噪声占比超过 20%,说明你遇到的是信号混淆问题,需要的是噪声过滤或域级聚合(DomainPilot 的思路),而不是继续在样本级调权重。

⚠️ 注意适用范围:DomainPilot 的论文标题与实验设定是 Language Model Fine-Tuning(微调),并非预训练。把它当作”后训练配比的动态化方案”使用是合理的,但不应用其结论替换预训练阶段的 DoReMi。


17.8 配方验证

17.8.1 消融实验

任何配方调整都要做消融:

ablation_groups = [
    ("baseline", default_recipe),
    ("+5% math", modified_recipe_math_plus),
    ("-5% general", modified_recipe_general_minus),
    ("+5% code", modified_recipe_code_plus),
]

for name, recipe in ablation_groups:
    model = train_sft(recipe)
    scores = evaluate(model, benchmarks=['MMLU', 'GSM8K', 'HumanEval', ...])
    print(f"{name}: {scores}")

17.8.2 帕累托前沿

不同配方在不同评测上表现不同,要找”帕累托最优”——没有其他配方在所有维度都不差。

配方 A: MMLU=70, GSM8K=85, HumanEval=60
配方 B: MMLU=72, GSM8K=80, HumanEval=65
配方 C: MMLU=68, GSM8K=88, HumanEval=58

A 不是 Pareto(被 B 在 MMLU/HumanEval 上超过)
B 不是 Pareto(被 A 在 GSM8K 上超过)
C 不是 Pareto(被 A 在 MMLU/HumanEval 上超过)
→ 三者都是 Pareto 最优

最终选择根据业务优先级——如果数学最重要,选 C。

17.8.3 可复现基准:NebulaExp-8B

消融实验与帕累托前沿解决的是”怎么比”,但还有一个更前置的问题:跟谁比。

本章引用的配方数字(Tulu-3 等)来自不同论文,每篇只披露自己关心的那部分。这带来一个实践困境——你想复现别人的配方,但缺少数据构造细节、过滤规则与训练超参,消融就变成了”从零猜”。NebulaExp 的作者在论文里直接把这一点列为动机:

现有工作大多不公开详细的数据构造、过滤规则与训练配方,这阻碍了社区的可复现性与轻量级模型优化。

NebulaExp 的回应是一个全透明、消融驱动的后训练管线(基于 Qwen3-8B-base),其披露的颗粒度可以作为一个基准线:

维度披露内容
基座Qwen3-8B-base
分支设计两个正交分支:通用 instruct 模型 / 复杂推理专精模型
数据规模3.84M 多源 SFT 样本 + 200K 可验证 RL 候选池
处理栈response distillation → multi-dimensional cross-verification filtering → fine-grained difficulty grading → task classification → diversity-aware sampling
SFT 方案Instruct 分支的三阶段优化 SFT(NebulaExp-Ins-SFT)
实测轨迹Qwen3-8B-nothink 基线 55.01 → Ins-SFT 60.99 → GRPO 61.85(平均分)

怎么用这个基准:

  1. 对齐你的处理栈:上表第三行的五步处理栈可以直接对照自查——你的管线缺了哪一步,就是与公开基准的差距所在。特别是 “multi-dimensional cross-verification filtering”(多维度交叉验证过滤),这是很多团队只做单维度质量分时的常见缺口。
  2. 校准量级预期:3.84M SFT + 200K RL 候选池是一个可参照的量级锚点。如果你的 SFT 数据在 100K 量级却期望同等效果,差距在数据规模而非配方法。
  3. 作为消融的对照组:做 17.8.1 的消融时,用 NebulaExp 的完整配置作为 baseline,比用自研配方更有说服力——评审者可以复现你的 baseline。

⚠️ 引用边界:本节的规模数字来自论文披露,但论文未给出各阶段配比的具体百分比——“全透明”指的是管线结构与处理步骤透明,不等于配方数值全部公开。若需要具体配比数字,仍需查阅论文正文表格或开源仓库。此外该工作基于单一基座(Qwen3-8B-base),跨基座的迁移性未经验证。


17.9 配方工业 SOP

1. 三轴标注:每条数据标 domain/task/difficulty
2. 初始配方:参考 Tulu-3 标杆
3. 小模型消融:1B 模型快速验证
4. DoReMi 优化:用 1B 模型找最优配比
5. 大模型验证:7B 模型做最终验证
6. 课程化设计:SFT 3 epoch / RLVR 50 epoch
7. 三阶段串联:SFT → DPO → RLVR
8. 帕累托分析:多配方对比
9. 业务选择:按优先级选 Pareto 配方
10. 上线 + A/B 测试 + 反馈

第 18 章将深入多轮对话与 Agent 数据——这是后训练最前沿的难点。