主题
SFT 数据工程:微调效果的上限在数据——指令数据构造、清洗与配比
提出问题
LoRA 参数怎么设、学习率多少、训练几个 epoch——这些调参问题在微调群里天天有人问。但大部分人忽略了一个更本质的事实:微调效果的上限在数据,不在模型参数。
一个 7B 模型用 1 万条高质量指令数据微调,效果大概率超过用 10 万条低质量数据的结果。数据质量决定效果天花板,训练配置只是决定你离天花板有多近。但现实是,很多团队拿到开源指令集直接跑训练,跑完发现模型跟基座差距不大,或者出现了灾难性遗忘。根源绝大多数在数据,不在模型。
数据工程才是微调投入最大的环节。从数据来源到格式设计、清洗管线、配比策略,每个环节都踩过坑。这篇就讲清楚 SFT 数据到底怎么造。
数据来源:四路渠道与各自坑
SFT 指令数据通常来自四个渠道,各有优劣。
1. 开源指令集
最省力的方式——Hugging Face 上有大量开源指令集,比如 Alpaca(52k 英文)、ShareGPT(对话类)、Belle(中文)、Firefly(中文多任务)。但你拿来直接用,大概率踩坑:
- 领域不匹配:通用指令集里没有你们公司业务领域的问题,微调后模型对领域知识毫无提升
- 质量参差不齐:很多指令集是蒸馏生成的(GPT 输出 → 其他模型微调 → 再输出),存在「教师模型幻觉」的传递问题
- 格式不统一:有人用 Alpaca 格式(instruction/input/output),有人用 ShareGPT 格式(多轮对话 JSON),混着用会导致训练不稳定
对策:只把开源指令集当作种子数据(seed data)或通用能力保留数据,不要全量依赖。建议从开源集里人工筛选 500-1000 条高质量样本,其他丢掉。
2. 真实业务日志蒸馏
最高质量的数据来源,没有之一。你线上跑着 LLM 应用,每次请求的 prompt + response 就是天然训练数据,但需要处理:
- 日志格式清洗:线上日志里混着各种 debug 信息、trace_id、时间戳,需要正则去掉
- 隐私脱敏:用户信息(手机号、身份证、邮箱)必须脱敏,这一步不能省。出过真实的合规事故
- 质量筛选:不是所有线上请求都值得训练。用 NLL(负对数似然)或人工抽检,过滤掉失败兜底、拒绝回答、低分回答的样本
原始日志示例:
[2026-09-15 10:23:45] [INFO] [trace_id=a1b2c3] request: "帮我查一下本月话费"
[2026-09-15 10:23:45] [INFO] [trace_id=a1b2c3] response: "您好,您本月已使用话费 128.50 元,剩余..."
清洗后:
{
"instruction": "帮我查一下本月话费",
"output": "您本月已使用话费 128.50 元,剩余流量 5.2GB"
}关键:线上日志蒸馏出来的数据,是模型当前能力的反映,不是目标能力。如果模型当前回答质量一般,蒸馏出来的数据只能让模型学会「保持这个水平」。需要结合人工修正或强模型重写来提升质量天花板。
3. 人工标注
效果最好的方式,也是最贵的。人工标注不是简单让人写答案,你需要设计标注规范:
- 标注指南:每条指令的「好回答」标准是什么?格式要求、长度限制、引用规范
- 质量抽检:标注员之间的一致性(inter-annotator agreement)要定期检查,kappa 系数低于 0.6 的标注员需要重新培训
- 成本控制:一条复杂指令的标注成本可能在 5-10 元,简单指令 1-2 元。1 万条高质量标注数据大约需要 2-5 万元
大部分团队扛不住这个成本,所以人工标注通常只作为黄金标准集,用来验证其他渠道的数据质量,而不是全量数据来源。
4. 难例挖掘
从现有模型错误中挖掘训练数据,ROI 极高。方法:
- 上线一个 SFT 基线模型
- 收集用户反馈/打分/纠错行为——哪些回答用户不满意
- 对这些回答做错误分类(hallucination/拒绝/格式错误/逻辑错误)
- 针对每类错误构造修正数据,让新模型学会改正
难例挖掘的直觉是:模型在 top-1% 最难的问题上犯错,修正这 1% 的问题就能带来 80% 的用户体验提升。这个帕累托原理在微调数据工程中非常适用。
指令样本格式与多轮对话构造
标准格式
不同训练框架(LLaMA-Factory、ChatML、OpenAI 格式)的格式略有差异,但核心结构一致。
json
{
"system": "你是一个客服助手,请用友好专业的语气回答用户问题。",
"instruction": "如何查询本月未出账单?",
"input": "用户是 VIP 客户",
"output": "您好,查询本月未出账单可以通过以下方式:\n1. 打开 App 首页→账单→未出账单\n2. 发送短信 'CXWD' 到 10086\n3. 拨打 10086 按 1 转人工\n请问您需要我帮您查询吗?"
}- system:系统指令,不是必须的。如果训练数据没有 system 字段,模型在推理时加 system prompt 效果会不稳定。建议 80% 样本带 system 字段,20% 不带,让模型学会两种场景。
- input:可选。用于补充用户输入之外的上下文(用户画像、前文等)。如果 input 恒为空,建议去掉这个字段,减少 token 浪费。
- output:核心字段。控制长度分布——训练数据里 output 的平均长度决定了模型生成时的平均长度偏好。
多轮对话构造
多轮对话比单轮难造,因为需要保证对话一致性:第二轮的回答不能忽略第一轮交代的事实。
{"messages": [
{"role": "system", "content": "你是一个智能客服。"},
{"role": "user", "content": "我要办理宽带续费"},
{"role": "assistant", "content": "好的,请问您的宽带账号是多少?"},
{"role": "user", "content": "账号是 1008610086"},
{"role": "assistant", "content": "已查到您的宽带套餐为 500M 融合套餐,月费 199 元,到期日为 2026-10-15。是否现在续费?"}
]}多轮构造注意事项:
- 轮次分布:不要全用 1 轮对话。建议 1 轮 40%、2-3 轮 40%、4+ 轮 20%,让模型学会多轮能力
- 上下文长度:超出上下文窗口的样本直接截断或丢弃,训练时做自动截断会破坏对话边界
- 角色扮演一致性:assistant 不能在同一条数据里出现两次以上的「角色切换」,否则模型会学会在单个回答里切换人格
数据清洗管线:不是越多越好,是越干净越好
SFT 数据清洗应该比预训练数据清洗更严格,因为每条样本都会被模型「记住」。
清洗管线流程
原始数据
│
▼
├─ 1. 去重(MinHash / SimHash)
│ └─ 近似去重:相同语义的指令只保留一条
│ └─ 精确去重:一模一样的 prompt 只保留一条
│
├─ 2. PII 脱敏
│ └─ 正则匹配手机号/身份证/邮箱/银行卡号
│ └─ 替换为占位符 [PHONE] / [ID] / [EMAIL]
│ └─ 注意:不能用同一占位符替换所有手机号,否则模型会学到「所有手机号都一样」
│
├─ 3. 长度过滤
│ └─ instruction 少于 5 个 token → 丢弃(无意义)
│ └─ output 少于 3 个 token → 丢弃(嗯、好的、我不知道)
│ └─ output 超过 2048 token → 截断(太长影响训练效率)
│
├─ 4. 语言质量评分
│ └─ 用预训练语言模型(如 bert-score)打分
│ └─ 低于阈值(如 0.3)的样本丢弃
│
├─ 5. 拒绝有害样本
│ └─ 关键词匹配:色情/暴力/歧视内容
│ └─ 或者用一个小模型做分类器
│
└─ 6. 格式校验
└─ JSON 格式是否正确
└─ 必须字段是否非空
└─ 特殊字符是否转义去重为什么重要
假设你的训练集里有一条样本「帮我总结以下内容:...」,出现了 100 次(日志采集重复)。模型会在这个样本上过度拟合,导致两个问题:
- 推理时见到「帮我总结」就输出固定回答,不会泛化
- 其他样本的梯度被淹没,等价于 1:100 的不平衡训练
去重不是简单的字符串相等。用 MinHash 做近似去重,阈值设为 0.85,基本能消除语义重复的样本。
一条样本的清洗示例
python
import re
import json
def clean_sample(sample: dict) -> dict | None:
"""清洗单条 SFT 样本"""
# 检查必要字段
if not sample.get("instruction") or not sample.get("output"):
return None
# 长度过滤
if len(sample["instruction"]) < 5 or len(sample["output"]) < 3:
return None
# PII 脱敏
sample["instruction"] = re.sub(
r"1[3-9]\d{9}", "[PHONE]", sample["instruction"]
)
sample["output"] = re.sub(
r"\d{17}[\dXx]", "[ID]", sample["output"]
)
# 有害内容过滤(简单关键词)
harmful_keywords = ["暴力", "色情", "反动", "毒品"]
for field in ["instruction", "input", "output"]:
if field in sample and any(k in sample[field] for k in harmful_keywords):
return None
return sample配比策略:任务类型比例与灾难性遗忘
混合配比
SFT 数据通常包含多个任务类型。配比决定了模型在各类任务上的能力分布:
| 任务类型 | 推荐比例 | 说明 |
|---|---|---|
| 通用对话 | 20-30% | 做人不做机器,维持对话流畅性 |
| 领域问答 | 30-40% | 核心能力,提高领域准确率 |
| 指令遵循 | 15-20% | 格式、约束、角色扮演 |
| 推理/逻辑 | 10-15% | CoT、数学、逻辑推导 |
| 安全/拒绝 | 5-10% | 学会拒绝不安全请求 |
配比实验:如果领域问答占 70%,模型在通用对话上会退化。反过来,如果领域问答只占 10%,模型在业务场景上表现不够好。建议先用小模型(比如 1B)做配比实验,确定最优配比再训大模型,节省资源。
灾难性遗忘的应对
灾难性遗忘(Catastrophic Forgetting)是 SFT 的最大敌人。模型在领域数据上微调后,忘记了你之前学过的通用能力。
标准做法是保留 5-10% 的通用能力 replay 数据。这些数据可以是:
- 基座模型原始训练数据中的通用 QA(但通常拿不到)
- 开源指令集里通用能力强的样本(如 Alpaca 的高质量子集)
- 模型在通用评测集(MMLU、C-Eval)上的正确答案
这些 replay 数据混在训练集里,不需要单独训练阶段。关键是不要集中放在训练末尾,而是均匀混入每个 batch。
数量直觉
很多团队问的第一个问题是:需要多少条数据?
- 领域适配(教会模型一个特定格式/场景):1k-5k 高质量数据够用
- 能力扩展(教会模型做新事,比如 SQL 生成):5k-20k
- 全面对齐(通用能力 + 领域能力 + 安全对齐):20k-100k
关键结论:1k 条精心构造的高质量数据 > 100k 条蒸馏噪声数据。先手工造 500 条验证数据管线,训一个过拟合检查(overfit sanity check)。如果模型在训练集上 loss 降不到 0.1 以下,说明数据有问题。不用急着加数据量,先修数据质量。
验收方法:训前训后怎么判断数据好不好
训前:过拟合检查
取 50 条数据训 1 个 epoch,观察 loss 曲线:
- loss 能降到 0.1 以下 → 数据质量基本合格
- loss 收敛到 0.5 以上就上不去 → 数据里有冲突(同一条指令对应不同回答)或格式问题
- loss 震荡严重 → 数据配比失衡,某些任务类型样本太少
训后:评测集对比
准备一个 200-500 条的评测集(人工标注),对比基座模型和 SFT 模型的得分:
- 领域任务准确率提升 > 20% → 数据有效
- 通用能力(MMLU 子集)下降 < 5% → 灾难性遗忘控制得当
- 如果通用能力下降 > 10%,加 replay 数据比例
决策树
要解决业务问题
│
├─ 现有 prompt/RAG 能解决?
│ └─ 是 → 别微调,先调 prompt 或 RAG
│
├─ 需要模型学会新格式/新行为?
│ └─ 是 → 准备 500-1k 条高质量数据,LoRA 微调验证
│
├─ 需要领域知识注入?
│ └─ 先试 RAG,RAG 不够 → 微调
│
└─ 需要模型推理能力增强?
└─ 准备 CoT 数据 + 领域数据混合,LoRA 微调不要为了微调而微调。很多场景下,RAG + 好 prompt 就能解决 80% 的问题。SFT 数据工程是投入最大的环节——如果 prompt 和 RAG 已经够用,别冲动上微调。如果决定微调,把预算的 70% 花在数据工程上,30% 花在训练调参上。
— 📚 小杰