一、先分清:微调和对齐是两条轴
很多人把这两个词混着用,结果配置全写错了方向。它们的区别是:
| 微调(Fine-tuning) | 对齐(Alignment) | |
|---|---|---|
| 改什么 | 会不会做(能力、格式、领域适配) | 愿不愿意做、怎么做(偏好、风格、安全) |
| 数据形态 | 输入 → 期望输出(一对一) | 好答案 vs 差答案(成对比较) |
| 典型方法 | SFT、LoRA、QLoRA | DPO、RLHF、GRPO/RLVR |
| 失败的信号 | 做不对、格式不对、不覆盖领域 | 会做但不好好做:啰嗦、谄媚、绕开限制 |
按”你要解决什么”对号入座:
- 想让模型按固定格式输出、懂你的领域术语 → SFT
- 想让回答更简洁、更直接、更少谄媚 → 对齐(DPO 起步)
- 想提升数学/代码这类可验证能力 → RLVR(GRPO)
- 想让模型知道新知识/新文档 → 先考虑 RAG,不是微调(微调擅长”改变行为”,不擅长”注入事实”)
二、SFT 实操
数据:先管质量,再管数量
起步规模:1k~10k 条。这个量级足够让模型学会一种输出风格;再多就是边际递减,而一条脏数据的代价可能超过一百条好数据。
造数据的四条路子,按成本排序:
- 从真实日志里挖:把线上问答、客服记录、用户提问整理成
指令 → 好回答(最贴合真实分布) - 人工写少量高质量示范:几百条统一的格式模板,效果常常超过一万条杂数据
- 用强模型批量生成 + 人工抽检:注意生成的结果要人工过一遍,否则会把强模型的坏习惯一起学进来
- 公开数据集:注意许可证和领域匹配度
数据检查清单(发布前逐条过):
- 格式统一吗(同一个域内不要混多种模板)
- 有没有”正确但冗长””错误但漂亮”的样本
- 覆盖了该覆盖的吗(简单/边界/拒答三类都要有)
- 有没有把测试集混进训练集(会直接让评测失效)
模板(template)是比参数更容易踩的坑:示例配置里的 template: qwen3_nothink 不是随便填的——它决定训练时怎么把数据拼成”模型见过的那个格式”。填错有两种典型表现:训练 loss 正常下降但推理时行为异常(比如把系统提示当成用户说的话),或者多轮对话里角色错位。规则很简单:template 必须和目标模型的官方 chat 模板一致,而且同一模型的不同版本模板也可能不同。
一个多轮样本(带 system 和工具调用)长这样:
{
"conversations": [
{"from": "system", "value": "你是一个严谨的技术助手,不确定时直接说不确定。"},
{"from": "human", "value": "帮我看下这个报错"},
{"from": "function_call", "value": "{\"name\":\"read_log\",\"arguments\":{}}"},
{"from": "observation", "value": "java.lang.NullPointerException ..."},
{"from": "gpt", "value": "空指针发生在第 42 行的 userService,先确认 userId 是否可能为 null。"}
]
}
注意角色顺序,以及”哪些内容会被学”——只有 gpt 和 function 的内容计入 loss,human 和 observation 不计。
数据够不够,看两个信号:验证集 loss 是否还在降(不降了说明这批数据学到头了);任务评测是否还在涨(不涨说明缺的不是数据量,而是别的东西)。
参数:照这个抄
下面是 LLaMA-Factory 示例配置(examples/train_lora/qwen3_lora_sft.yaml)里的真实取值,可以直接作为起点:
### 方法
finetuning_type: lora
lora_rank: 8
lora_target: all
### 数据
template: qwen3_nothink
cutoff_len: 2048 # 截断长度:按你的数据实际长度设,别默认往大里开
max_samples: 1000
### 训练
per_device_train_batch_size: 1
gradient_accumulation_steps: 8 # 等效 batch = 1 × 8 × 卡数
learning_rate: 1.0e-4
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
几个经验值的来历和调整方向:
- 学习率 1e-4:LoRA 微调的常用档位。全参微调要低一个量级(1e-5 ~ 2e-5)
- epoch 3:这是”跑几遍数据”的上限;如果你的数据小(几百条),可以到 3
5,数据大则 12 - rank 8:LoRA 的秩,官方示例就用 8——先别急着往 64 上堆(「高效微调」那篇解释过 rank 太大会失去意义)
- cutoff_len:这个参数最容易埋雷。设得太小,长样本会被截断(还可能在截断处学出”半句话就结束”的习惯);设得太大,显存又爆
- batch 1 + 梯度累积 8:显存不够时的标准做法,等效 batch 一样,只是慢一点
三个高频坑
- loss 掩码搞错:只应训练”回答”部分,指令是条件。掩码写错会让模型学着模仿用户的语气
- 格式过拟合:模型把模板的腔调学会了(动不动分点、堆小标题),但能力没涨——评测分高、真实使用僵
- 忘了留验证集:官方示例里 eval 段默认是注释掉的,别真按默认来——把
val_size: 0.1打开,否则你没有任何信号判断”该停在第几个 epoch”
训练时看什么
| 现象 | 判读 | 动作 |
|---|---|---|
| 训练 loss 降,验证 loss 也降 | 健康 | 继续 |
| 训练 loss 降,验证 loss 先降后升 | 过拟合 | 回退到验证 loss 最低的 checkpoint |
| 训练 loss 几乎不动 | 学习率太小,或数据格式/模板不对 | 先查格式与模板,再调 lr |
| loss 突然 NaN 或爆掉 | 学习率太大、bf16 环境混入 fp16 数据、异常长样本 | 降 lr + 检查数据长度分布 |
三、DPO 实操
DPO 是当前对齐的默认起点:比 RLHF 简单、比 SFT 更能改”偏好”。前提是先有一个 SFT 过的模型(对齐是在会做事的基础上调偏好)。
数据:成对,而且要成体系
规模上 5k~50k 对是常见区间,但比数量更重要的是成对的构造质量——每一对的 chosen 和 rejected 应该只差在想改的那个维度上:
想让回答更简洁 → chosen 是简洁版,rejected 是啰嗦版(内容都对)
想让模型不谄媚 → chosen 是礼貌纠正,rejected 是附和用户
想加安全边界 → chosen 是解释后拒答,rejected 是无条件配合
如果 chosen 和 rejected 的差别五花八门,模型学到的偏好就是混乱的。
同样列出组合的检查清单:
- 长度分布:统计 chosen 和 rejected 的长度——如果 chosen 普遍更长,你会训出一个话痨
- 多样性:每个维度的样本不要来自同一个模板或同一个生成模型
- 留一批”没见过的维度”:用于验证对齐没有把通用能力带坏
参数
成对数据从哪来(按成本排序):
- 人工标注:质量最高、成本最高,适合几千对但极关键的维度
- 强模型生成:给强模型明确的”分别写一个好回答和一个坏回答”的指令,再人工抽检
- 规则扰动:拿现成的好回答做可控破坏——抽掉关键信息、替换成错误事实、把简洁版扩写成啰嗦版。成本最低,而且天然满足”只差一个维度”这个要求
reference model 是谁:就是做 DPO 之前那个 SFT 模型的副本(权重冻结,不参与训练)。它给 beta 提供参照系——偏离它越多,惩罚越大。
DPO 的 loss 怎么看:正常是缓慢下降。如果很快降到接近 0,通常是 beta 太小或数据太单一——模型已经学会用极端表达刷分(reward hacking 的 DPO 版)。这时该做的不是接着训,而是回头把数据做多样。
顺手记住两个变体(都在往”去参考模型”方向走):ORPO(2403.07691)把 SFT 和对齐合成一步、不需要参考模型;SimPO(2405.14734,NeurIPS 2024)用序列的平均对数概率当隐式奖励,同样去掉了参考模型。
同样来自官方示例(qwen3_lora_dpo.yaml):
pref_beta: 0.1
pref_loss: sigmoid # 可选 sigmoid(dpo) / orpo / simpo
learning_rate: 1.0e-4 # 与 SFT 同档;若在 SFT 模型上继续训,可再降一半
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
- beta = 0.1:控制”允许偏离参考模型多远”。调大更保守(学得慢但不容易跑偏),调小更激进
- loss 可选 sigmoid / orpo / simpo:ORPO 和 SimPO 是后续的变体,思路是减少对参考模型的依赖;先从 sigmoid(标准 DPO)开始
- DPO 的坑集中在 beta:beta 太小 + 数据单一 = 学会极端表达(同一句话反复加强),这是 reward hacking 在 DPO 上的表现形式
四、RLHF 与 RLVR:什么时候值得
RLHF(PPO):要同时伺候策略、参考、奖励、价值四个模型,工程和显存成本都高。现在很少是首选——除非你要的偏好很难用成对数据表达(例如”整体对话质量”这种模糊标准)。如果真要做,先用一档:用 reward 模型(示例配置里 stage: rm,超参与 SFT 同档:lora_rank 8、lr 1e-4、epoch 3)把奖励模型训出来,再决定要不要上 PPO。
RLVR(可验证奖励):只有一类任务适用——答案能被自动验证(数学题、代码题、结构化的抽取任务)。它的数据不是”输入 → 输出”,而是”题目 + 标准答案”,奖励由规则算出来:
def reward(question, answer):
score = 0.0
if has_required_format(answer): # 格式分:逼出稳定的输出结构
score += 0.2
if normalize(answer.final) == normalize(ground_truth[question]):
score += 1.0 # 正确性分:占主导
return score
三个实操要点:
- 奖励要分层:格式分给一点权重让训练稳定,正确性分占主导,别让两者量级混淆
- 题目难度要有分布:全是难题 → 全对或全错 → 没有梯度信号;全简单题 → 学不到东西。官方工作里会把难易混合
- 别指望它改风格:RLVR 只在”能判对错”的维度上生效,写作、咨询这类没有标准答案的任务写不出奖励函数
LLaMA-Factory 的示例配置覆盖 SFT / DPO / KTO / reward 这几档;更完整的 RL 算法需要用支持强化学习流程的框架来做。
五、评测:怎么知道有没有效果
这是最容易被跳过、也最影响成本的一步。没有评测,你就是在盲训。
三件套,缺一不可:
1. 任务指标:留一个训练里没出现过的测试集(200~500 条足够),比较微调前后的输出质量。评测时温度设 0、跑两三次看方差——跑一次的结果不可信。
2. 通用能力回归:同一个通用问题集(几十条就够),对比微调前后的表现。如果明显下降,说明学得太狠或数据太窄。这一步专门用来抓”灾难性遗忘”。
3. 人工盲评(最有效也最容易被忽略):把 base 和微调后的输出打乱顺序放在一起,让人挑更好的那个。50~100 条就能看出趋势。要点:
- 一定要盲(不告诉评审哪个是新模型)
- 一定要打乱(人会对第一个回答有偏好)
- 对齐任务的盲评要专门盯三个指标:长度、拒答率、是否附和
用强模型当裁判(LLM-as-judge):没有标准答案的任务,常见做法是让一个强模型按评分标准打分——便宜、可复现,但它有三个已知偏差,必须校准:
| 偏差 | 表现 | 对策 |
|---|---|---|
| 位置偏差 | 更偏爱排在前面的回答 | 两个回答对调顺序各评一次,只取两次一致的结论 |
| 长度偏差 | 更偏爱长回答 | 评分标准里显式声明”简洁不加分”,或先按长度分层再比 |
| 自我偏好 | 偏爱同族模型的输出 | 裁判模型不要用被评测模型同一族的 |
人工盲评的最小可行做法:2 个评审 × 60 条 × 打乱顺序,先标”明显更好”和”分不出”,只统计”明显更好”的比例——60 条足够看出 20 个百分点以上的差异,不必追求统计学显著。
对齐专项检查(这三项能提前抓住 reward hacking):
| 指标 | 怎么看 | 异常信号 |
|---|---|---|
| 平均回答长度 | 对齐前后对比 | 明显变长 = 学成了话痨 |
| 拒答率 | 抽样统计 | 该答的也不答 / 该拒的全接 |
| 附和倾向 | 构造”用户说错”的样本 | 一味认同用户 |
六、完整路径
把上面的东西串起来,从零到上线的顺序是:
- 明确目标 → 判断是”能力问题”还是”偏好问题”(决定走 SFT 还是对齐)
- SFT → 造 1k~10k 条数据 → 官方示例参数起步 → 看验证集损失决定停在哪
- 用评测三件套验收 SFT → 不达标先修数据,别急着上对齐
- 需要改偏好 → 造 DPO 成对数据 → beta 0.1 起步 → 再跑一遍评测三件套
- 需要提可验证能力 → 设计分层奖励 → 上 RLVR
- 上线前:对齐专项检查(长度/拒答/附和)+ 一次盲评
每一档之间都要评测——不然出了问题无法判断是哪一步引入的。
七、汇总记录
| 阶段 | 坑 | 表现 |
|---|---|---|
| 数据(全程) | 测试集污染 | 评测分虚高,上线就现原形 |
| SFT | loss 掩码错 | 学会了用户的语气 |
| SFT | cutoff 太小 | 长回答被腰斩,学出”半句话”习惯 |
| SFT | 没留验证集 | 不知道该停在第几个 epoch |
| SFT | 数据太窄 | 通用能力掉、任务能力涨 |
| DPO | chosen 都比 rejected 长 | 训出话痨 |
| DPO | beta 太小 + 数据单一 | 极端表达(reward hacking) |
| RLVR | 奖励不分层 | 训练震荡、没有稳定梯度 |
| RLVR | 难度全在同一档 | 全对或全错,学不到东西 |
| 评测 | 只看任务指标 | 不知道通用能力掉没掉 |
| 评测 | 不盲评 | 被自己的预期骗了 |
八、高频追问
微调能让模型学会新知识吗?
能记住一点,但很不可靠,还容易和已有知识打架。注入事实用 RAG,微调用来改行为和格式。 如果非要用微调记住,同一事实要在数据里以多种问法反复出现。
数据是不是越多越好?
不是。1k 条干净数据常常打得过 10 万条杂数据。顺序是:先扩多样性,再扩数量。
能不能跳过 SFT 直接 DPO?
技术上可以,实践上不建议。DPO 的参考模型通常就是 SFT 模型;没有它,模型连”该怎么回答”都还没建立,偏好学习就失去了稳固起点。
LoRA 和全参微调差多少?
大多数任务上差距很小;差在”学新知识、学新语言”这类底层能力时。先上 LoRA,确实不够再动全参。
对齐会不会把模型变笨?
会——如果学得太狠(灾难性遗忘)。对策:控制 epoch、混入一部分通用数据,并且用”通用能力回归”一直盯着。
怎么判断是不是 reward hacking 了?
三个信号:回答明显变长、开始附和用户的错误观点、输出高度模板化。出现就先回去检查偏好数据和 beta,而不是继续训。
总结
微调改”会不会”,对齐改”愿不愿意”;数据质量比数量重要一个量级;参数照官方示例起步就好;而评测——尤其是盲评和通用能力回归——是把上面所有努力变现的那一步。