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

SFT指令数据构建

SFT数据结构(messages/chat-template/loss mask/pack边界)、Self-Instruct(175种子→52K/多样性饱和)、Evol-Instruct(进化算子:加约束/加深/具象化/增加推理步/复杂输入)、Magpie(模型自吐prompt/零种子/温度1.0/多样性极高)、三方法对比、响应生成策略(单次贪婪/采样投票/Best-of-N+RM/CoT自反思)、多样性度量(4-gram/Embedding聚类/InsTag覆盖率)、Deita关键发现(10K精选>100K粗造)

SFT Self-Instruct Evol-Instruct Magpie Deita 多样性度量

SFT 数据的灵魂是多样性,不是数量。

Self-Instruct 多样性饱和在 52K,Magpie 用零种子打破了天花板。


13.1 SFT 数据结构

13.1.1 messages 标准结构

工业级 SFT 数据统一用 messages 结构:

{
  "messages": [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "用 Python 实现快速排序"},
    {"role": "assistant", "content": "```python\ndef quicksort(arr):\n    ..."}
  ],
  "metadata": {
    "source": "magicoder",
    "task_type": "code",
    "difficulty": "medium",
    "quality_score": 0.85
  }
}

多轮对话扩展为多对 user/assistant:

{
  "messages": [
    {"role": "system", "content": "..."},
    {"role": "user", "content": "问题1"},
    {"role": "assistant", "content": "回答1"},
    {"role": "user", "content": "追问2"},
    {"role": "assistant", "content": "回答2"}
  ]
}

13.1.2 chat-template 与 loss mask

不同模型族有不同的 chat-template,训练前必须转换:

# Llama-3 chat-template
def format_llama3(messages):
    text = "<|begin_of_text|>"
    for m in messages:
        text += f"<|start_header_id|>{m['role']}<|end_header_id|>"
        text += f"{m['content']}<|eot_id|>"
    return text

# Qwen chat-template
def format_qwen(messages):
    text = ""
    for m in messages:
        text += f"<|im_start|>{m['role']}\n{m['content']}<|im_end|>\n"
    return text

loss mask 是关键:loss 只在 assistant 部分计算,user/system 部分的 token 不参与梯度。

# loss mask 计算伪代码
def compute_loss_mask(input_ids, template):
    masks = []
    for token_pos in range(len(input_ids)):
        if token_pos in assistant_region:
            masks.append(1)
        else:
            masks.append(0)
    return masks

踩坑:忘了做 loss mask,模型会学会生成 user prompt,在推理时陷入”自问自答”死循环。

13.1.3 Pack 边界处理

为提升 GPU 利用率,工业训练常用 Pack——把多条短样本拼接到一个长序列。但必须正确处理 Pack 边界:

# 错误做法:直接拼接,attention 跨样本
packed = sample1 + sample2 + sample3

# 正确做法:用 attention_mask 隔离
packed = sample1 + [EOS_PAD] + sample2 + [EOS_PAD] + sample3
attention_mask = block_diagonal(mask1, mask2, mask3)

Llama-3 引入了 <|eot_id|> 作为样本边界,配合 FlashAttention 的 document-aware mask 实现正确 Pack。


13.2 Self-Instruct:种子驱动的开山之作

13.2.1 算法核心

Self-Instruct(Wang et al., 2022)是 SFT 数据合成的开山之作,用 175 个种子指令扩展到 52K 数据。

算法流程:

1. 准备 175 个种子指令(人工编写,覆盖 35 类任务)
2. 从种子池随机采样 8 条作为 in-context examples
3. 让 LLM 生成新指令
4. 过滤:去重 / 长度 / 启发式规则
5. 将新指令加入种子池
6. 对每个指令生成响应
7. 重复 2-6 直到达到目标规模

13.2.2 关键参数

  • 种子数:175
  • 每轮 in-context 采样:8 条
  • 单次生成 temperature:0.7
  • 去重阈值:ROUGE-L < 0.7
  • 最终规模:52K

13.2.3 多样性饱和问题

Self-Instruct 的核心局限是”多样性饱和”——大约 30K-50K 后,新生成指令与已有指令的 ROUGE-L 重叠度超过 0.7,被过滤掉的比例从 20% 上升到 60%。

迭代次数 | 累计指令数 | 通过率
1        | 1K        | 95%
5        | 8K        | 80%
10       | 25K       | 60%
20       | 45K       | 35%
30       | 52K       | 10% (饱和)

根本原因:种子池的多样性上限决定生成数据的多样性上限。175 种子的任务覆盖度有限,再多迭代也是”在已知空间内打转”。

2026 年的 KITE 研究(arXiv:2607.17043, Xiaonan Luo 等, 2026-07-19)进一步揭示了更深层的问题:迭代合成中的退化不仅是”多样性饱和”,更是能力极化(Capability Polarization)——合成数据会强化模型已有的强能力,同时进一步削弱弱能力。这解释了为什么简单的数据量增加无法解决问题:需要在知识边界上针对性地生成和筛选数据。KITE 用失败引导生成 + Kernel Boundary Uncertainty 筛选知识边界上的数据,为解决能力极化提供了新方向。

13.2.4 Self-Instruct 的历史地位

Self-Instruct 第一次证明了”用 LLM 合成 SFT 数据”可行,奠定了后续所有合成方法的基础。Alpaca、Vicuna、WizardLM 早期版本都基于 Self-Instruct 范式。


13.3 Evol-Instruct:进化驱动

13.3.1 核心思想

Evol-Instruct(WizardLM, Xu et al., 2023)针对 Self-Instruct 的”多样性饱和”问题,提出”进化驱动”——不停对已有指令做变换,生成更复杂、更具体的指令。

13.3.2 五种进化算子

1. 加约束(Constraint)
   原: "写一个 Python 函数"
   进化: "写一个 Python 函数,要求时间复杂度 O(n),不使用额外空间"

2. 加深(Deepening)
   原: "解释什么是递归"
   进化: "解释什么是尾递归,以及为什么 Python 不支持尾递归优化"

3. 具象化(Concretizing)
   原: "设计一个推荐系统"
   进化: "为一个 100 万用户的电商网站设计推荐系统,冷启动阶段如何处理"

4. 增加推理步(Reasoning Step)
   原: "计算圆的面积"
   进化: "给定半径 r=5,先用积分推导面积公式,再计算具体值"

5. 复杂输入(Input Augmentation)
   原: "总结这篇文章"
   进化: "总结这篇 5000 字文章,要求包含 3 个关键论点 + 2 个反例"

13.3.3 双向进化

Evol-Instruct 实际做双向进化:

  • 深度进化(Evol-Instruct):让指令更复杂
  • 广度进化(Evol-Instruct-Breadth):在同级生成不同主题的指令

最终 WizardLM 数据集包含 250K 指令,深度进化 90%,广度进化 10%。

13.3.4 进化终止条件

进化不能无限进行——指令会变得过于复杂甚至不可解。WizardLM 的经验:

  • 单条指令最多进化 3 次
  • 进化后长度超过 500 tokens 终止
  • 检测到”无法回答”标记时回退

13.3.5 Evol-Instruct 的效果

在 Llama-7B 上 SFT 后,WizardLM 在 AlpacaEval 上胜率比 Self-Instruct 高 15%。核心原因:进化算子引入了”难度梯度”,模型学会了解决复杂问题。


13.4 Magpie:零种子突破

13.4.1 核心洞察

Magpie(Xu et al., 2024)的洞察极其简单却深刻:对齐过的模型本身就是优秀的 prompt 生成器。

直接让模型从空 prompt 开始生成:

# Magpie 核心
def magpie_generate(model, n_samples):
    results = []
    for _ in range(n_samples):
        # 空 prompt,直接触发 user 角色
        pre_query = "<|im_start|>user\n"
        response = model.generate(
            pre_query,
            temperature=1.0,  # 关键:高温
            top_p=0.95,
            max_tokens=2048,
            stop=["<|im_end|>"]
        )
        # response 是 "user prompt + assistant response"
        user_msg, assistant_msg = parse(response)
        results.append((user_msg, assistant_msg))
    return results

13.4.2 关键参数

  • temperature:1.0(必须高,否则多样性坍塌)
  • top_p:0.95
  • 采样数量:100K-1M
  • 后过滤:去重(4-gram 重叠率 < 0.3)+ 质量过滤

13.4.3 多样性对比

方法4-gram 多样性InsTag 覆盖率任务类型数
Self-Instruct0.4232055
Evol-Instruct0.5558075
Magpie0.711240120+

Magpie 的多样性显著优于前两者,根本原因是它不依赖种子池,直接采样自模型的”指令分布”。

13.4.4 Magpie 的局限

  1. 依赖强模型:用弱模型做 Magpie 生成的 prompt 质量差
  2. 偏置明显:模型偏好生成”它擅长回答”的 prompt,导致能力分布失衡
  3. 安全数据缺失:模型不会主动生成越狱 prompt,需要补充

13.4.5 Magpie 的工业实践

实际使用 Magpie 的推荐流程:

1. 用 Llama-3-70B / Qwen-2.5-72B 做生成器
2. 生成 1M 原始数据
3. 去重(MinHash + 0.7 阈值)→ 700K
4. 质量过滤(RM 评分 > 0.5)→ 400K
5. 多样性采样(Embedding 聚类,每簇 5 条)→ 200K
6. 与其他来源混合

13.5 三大方法对比

维度Self-InstructEvol-InstructMagpie
种子需求1751750
上限52K250K1M+
4-gram 多样性0.420.550.71
难度可控否是否
任务覆盖55 类75 类120+ 类
算力成本低中中
实现难度简单中等简单
推荐场景快速原型难度梯度大规模底盘

工业级组合:用 Magpie 生成 70% 底盘 + Evol-Instruct 补 20% 难度梯度 + 人工 10% 质量锚。


13.6 响应生成策略

有了 prompt 后,还要生成高质量响应。4 种主流策略:

13.6.1 单次贪婪

response = model.generate(prompt, temperature=0, max_tokens=2048)

优点:成本低,可复现 缺点:质量方差大,对 prompt 敏感

13.6.2 采样投票

responses = [model.generate(prompt, temperature=0.7) for _ in range(5)]
best = select_by_consistency(responses)  # 选多数派

优点:质量稳定 缺点:成本 5 倍

13.6.3 Best-of-N + Reward Model

responses = [model.generate(prompt, temperature=0.7) for _ in range(8)]
scores = [reward_model(prompt, r) for r in responses]
best = responses[argmax(scores)]

优点:质量最高 缺点:依赖 RM,成本 N 倍

13.6.4 CoT 自反思

# 第一轮生成
initial = model.generate(prompt, temperature=0.7)
# 第二轮反思
critique = model.generate(f"反思这个回答的问题:{initial}")
# 第三轮修正
final = model.generate(f"基于反思改进:{critique}\n原回答:{initial}")

优点:推理类任务质量最高 缺点:成本 3 倍,速度慢

工业选择:通用任务用 Best-of-N+RM,推理任务用 CoT 自反思,快速实验用单次贪婪。


13.7 多样性度量

合成数据必须做多样性度量,三大指标:

13.7.1 4-gram 多样性

from collections import Counter
def ngram_diversity(texts, n=4):
    all_ngrams = []
    for text in texts:
        tokens = text.split()
        all_ngrams.extend([' '.join(tokens[i:i+n]) for i in range(len(tokens)-n+1)])
    unique = len(set(all_ngrams))
    total = len(all_ngrams)
    return unique / total  # 0-1,越高越好

经验阈值:

  • ≥ 0.6:优秀
  • 0.4-0.6:合格
  • < 0.4:不合格,需要去重

13.7.2 Embedding 聚类

from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans

embeddings = model.encode(prompts)
kmeans = KMeans(n_clusters=100)
labels = kmeans.fit_predict(embeddings)

# 计算每个簇的样本数
cluster_counts = Counter(labels)
max_cluster_ratio = max(cluster_counts.values()) / len(prompts)
# 最大簇占比 < 5% 才算合格

13.7.3 InsTag 覆盖率

InsTag(Lu et al., 2023)是一个指令标签系统,标注每条指令的”意图标签”(如”代码生成”、“数学计算”、“创意写作”等)。

# InsTag 标签覆盖
tags = ins_tag_annotate(prompts)
unique_tags = len(set(tags))
# 工业级要求 unique_tags ≥ 1000

13.8 Deita 的关键发现:10K 精选 > 100K 粗造

13.8.1 实验设计

Deita(Liu et al., 2023)做了一个反直觉实验:

  • 用 1M 条 OpenHermes 数据训练 Llama-7B
  • 用 10K 条精选数据训练同模型
  • 对比 MT-Bench、AlpacaEval

13.8.2 实验结果

数据规模MT-BenchAlpacaEval 胜率
1M 粗造6.855%
100K 粗造7.060%
10K 精选7.575%
1K 精选7.268%

核心发现:10K 精选数据训练效果,显著优于 1M 粗造数据。

13.8.3 Deita Selector

Deita 的精选方法叫 Selector,核心是两个维度评分:

  1. Complexity(复杂度):用 LLM 评分 1-6,越高越复杂
  2. Quality(质量):用 Reward Model 评分 0-1,越高越好
def deita_select(data, target_size=10000):
    # 1. 评分
    for d in data:
        d['complexity'] = llm_score_complexity(d)  # 1-6
        d['quality'] = reward_model(d)  # 0-1
    
    # 2. 计算 complexity × quality
    for d in data:
        d['score'] = d['complexity'] * d['quality']
    
    # 3. 按分数排序取 top
    sorted_data = sorted(data, key=lambda x: -x['score'])
    
    # 4. Embedding 距离去冗余
    selected = []
    for d in sorted_data:
        if all(emb_distance(d, s) > 0.3 for s in selected):
            selected.append(d)
        if len(selected) >= target_size:
            break
    return selected

13.8.4 Deita 的启示

  1. 数据规模不是越大越好:精选 10K 胜过粗造 1M
  2. Complexity × Quality 是核心:复杂且高质量的数据最有价值
  3. 多样性要去冗余:即使分数高,与已有数据太相似也要剔除

13.8.5 目标函数层:PoFT 补齐 SFT 的第三层

本章至此覆盖了 SFT 数据改进的两层:数据合成(Self-Instruct / Evol-Instruct / Magpie)与数据选择(Deita)。但 SFT 改进实际上有三层结构——合成 → 选择 → 目标函数,第三层常被忽略。

PoFT(Preference-Oriented SFT, arXiv:2412.12865) 在 SFT 阶段引入偏好信号:用 Bradley-Terry(BT)偏好退化的交叉熵(CE)目标,为每个样本赋予逐样本权重 τ。核心价值在于:对噪声标注鲁棒,且与 IFD / Instag / Deita 等”数据选择”方法正交——Deita 改的是”选哪些数据”,PoFT 改的是”怎么用这些数据训练”。实验显示 PoFT+DPO 优于 SFT+DPO。

CLEAR(模型置信度过滤) 则是”模型驱动的数据清洗”邻接路线:用模型自身置信度作为 SFT 数据过滤信号,与 Deita 的 IFD 复杂度评分形成”复杂度评分 vs 置信度过滤”的对照。二者机制不同,可作为 Deita 的补充而非替代。


13.9 SFT 数据构建的工业 SOP

综合本章内容,工业级 SFT 数据构建 SOP:

1. 需求定义:要补什么能力
2. 来源规划:
   - 70% Magpie 合成(底盘)
   - 20% Evol-Instruct(难度梯度)
   - 10% 人工(质量锚)
3. Prompt 生成:Magpie 1M + Evol 200K
4. 响应生成:Best-of-N + RM
5. 质量评分:Complexity + Quality
6. Deita Selector 精选:100K
7. 多样性度量:4-gram ≥ 0.6, InsTag ≥ 1000
8. 去污染:13-gram 匹配 20+ 评测集
9. 配方混合:按任务/领域/难度三轴
10. 训练 + 评估 + 闭环

第 14 章将深入偏好数据构建,重点拆解 DPO vs RLHF 路线选择与 RLAIF 实践。