09. 指令微调 SFT:教模型按你的口径回答
前几篇讲的预训练,产出的是一个会续写文本的基础模型:给它“年假怎么申请?”,它可能接着写出三个类似的问题,而不是回答。把它变成会对话、会按格式回答的助手,第一步是指令微调(Supervised Fine-Tuning,SFT):拿一批“问题 → 理想回答”的样例继续训练。大模型专题里的 SFT 导读 讲了它适合什么场景,这一篇往下拆一层:样本在训练框架里会变成什么样子,损失到底算在哪些 token 上,打包、轮数和学习率怎样定,资料助手的数据集怎样造,以及怎样确认模型学好了而不是学坏了。
读完你应该能判断:一份 SFT 数据为什么“看起来没问题、训出来却不对”,一次训练的超参从哪里起步,以及训练前后要比较哪些指标。
从一条样本说起
资料助手已经接了检索:小林问“年假怎么申请?”,系统先从制度库里找到《员工手册》第 4.2 条,再把条款原文和问题一起交给模型。团队希望回答带条款编号;检索结果里没有答案时直接说不知道,不要编。提示词能让模型大致照做,但测试问题里总有一部分回答漏写编号,或者在片段里没有答案时自己补了一套流程。这类“口径稳定、样例好写、提示词管不住”的问题,正是 SFT 的用武之地。
训练数据通常存成 JSONL,每行一条对话,用 messages 列出各个角色说的话。和推理时一样,检索片段要放进训练样本的输入里,模型才能学会“依据片段回答”,而不是把制度内容背进参数:
json
{"messages": [
{"role": "system", "content": "你是公司资料助手。只依据【检索片段】回答,并注明条款编号;片段里没有答案时说不知道,并建议联系的部门。"},
{"role": "user", "content": "【检索片段】《员工手册》第4.2条:员工申请年假,应至少提前3个工作日在审批系统提交,由直属主管审批……\n\n问题:年假怎么申请?"},
{"role": "assistant", "content": "按《员工手册》第4.2条,年假需至少提前3个工作日在审批系统提交申请,由直属主管审批。"}
]}这条样本在训练框架里要经过两次变形:先按对话模板拼成一段带特殊标记的文本,再切成 token 并决定哪些位置计入损失。下面这张图回答“一条样本怎样变成损失”:
图里从左到右是数据、模板、token 和损失。灰色位置作为输入参与前向计算,模型要读懂它们;绿色位置才是模型要学着写出来的内容。结尾的停止标记也是绿色,下文会讲漏掉它的后果。
对话模板:写错了不会报错
模型并不认识“角色”这个概念,它只看到一串 token。对话模板(chat template)规定了怎样用特殊标记把多轮对话拼成一段文本。上面的样本按 Qwen2.5 的模板渲染后是:
text
<|im_start|>system
你是公司资料助手。只依据【检索片段】回答……<|im_end|>
<|im_start|>user
【检索片段】《员工手册》第4.2条:……
问题:年假怎么申请?<|im_end|>
<|im_start|>assistant
按《员工手册》第4.2条,年假需至少提前3个工作日在审批系统提交申请,由直属主管审批。<|im_end|>同一段对话换成 Llama 3.1 的模板,长相完全不同。它的官方模板还会在 system 消息开头自动写入知识截止日期和当天日期(未传入日期时默认是 26 Jul 2024):
text
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024
你是公司资料助手。……<|eot_id|><|start_header_id|>user<|end_header_id|>
【检索片段】……问题:年假怎么申请?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
按《员工手册》第4.2条,……<|eot_id|><|im_start|>、<|eot_id|> 这些标记在各自的分词器里是一个独立 token,模型在官方后训练阶段见过它们成千上万次,知道“看到 assistant 标记就该开始回答,写完要输出结束标记”。模板写错时,训练不会报任何错,loss 也照样下降,问题只在上线后出现。常见的几种情况:
- 拿别家的模板训练。把 Llama 的标记文字喂给 Qwen,
<|start_header_id|>在 Qwen 的分词器里不是特殊 token,会被切成十几个普通字符片段。模型学到的是一种它从没见过的格式,推理时引擎又按官方模板拼提示,训练和推理对不上。 - 手写模板少了细节。Qwen2.5 的官方模板在没有 system 消息时会自动插入一句默认 system 提示,Llama 3.1 的模板总会在 system 段写入日期,换行的位置也是固定的。自己用字符串拼接时漏掉这些,推理时的输入就和训练时差几个 token,小模型对此相当敏感。
- 结束标记不一致。Qwen2.5 的对话模型用
<|im_end|>作为回答结束,而它的基础模型默认结束符是<|endoftext|>。在基础模型上做 SFT 却不把结束符对齐,模型学会了写<|im_end|>,生成配置却在等另一个 token,结果是回答说完还继续往下编出一轮“用户”提问。TRL 的文档专门提醒过这一点(在 Qwen2.5 基础模型上训练时要设eos_token="<|im_end|>")。
所以第一条检查项是:用分词器自带的 apply_chat_template 渲染,不要手拼;训练前把渲染结果和 token 打印出来看一眼。推理端(vLLM 等)也要用同一份模板。
只对回答算损失
SFT 的损失和预训练一样,是“预测下一个 token”的交叉熵(推导见 损失与目标)。区别在于计入哪些位置。把上面那段渲染结果切成 token 后,框架会生成一个与输入等长的 labels 数组,不计损失的位置填 -100,PyTorch 的交叉熵会跳过它们(下表的位置编号是示意,实际数目取决于分词结果):
text
位置 token labels
1-3 <|im_start|> system … -100 ← 系统提示
4-60 【检索片段】…年假… -100 ← 用户输入和检索片段
61-63 <|im_start|>assistant -100 ← 回答开头的角色标记
64-95 按《员工手册》第4.2条… 计入 ← 回答正文
96 <|im_end|> 计入 ← 学会在这里停为什么不把整段都算进去?检索片段动辄几百个 token,而回答只有几十个。如果整段计损失,大部分梯度花在“学会复述制度原文和用户的问法”上,模型甚至会学会在回答里编造用户口吻的问题;只对回答计损失,梯度全部用来学“看到这样的输入,该怎样回答”。多数论文和框架默认这样做,但默认值要确认:TRL 的 SFTTrainer(v1.16)对 prompt-completion 格式默认只算 completion,对 messages 格式则需要显式设 assistant_only_loss=True,而且要求模板里带有标记回答范围的 {% generation %} 段落;LLaMA-Factory 默认只训练回答部分。设错了不会报错,只能靠打印一条样本的 labels 来确认。
多轮对话时,每一轮 assistant 的回答都计入损失,前面各轮作为上下文。少数场景会故意只训最后一轮,例如前面几轮是从线上日志里截来、质量不一的回答。
打包与填充
GPU 一次处理一批样本,同一批要拼成同样长度。最直接的办法是填充(padding):短样本后面补无意义的填充 token,再用注意力掩码把它们遮住。SFT 数据的长度差别很大,填充浪费非常可观:
打包(packing)把多条短样本首尾相接塞进同一行,几乎不留空位。代价是同一行里的样本彼此可见:B 的 token 不应该在注意力里看到 C。早期的打包实现直接拼接、不做隔断,样本之间会“串味”;现在的实现(TRL 的 packing=True 默认用 best-fit 装箱并配合 padding-free 注意力,需要 FlashAttention 2/3)会为每条样本重置位置编号、隔断注意力,效果上等价于逐条训练。样本多、长度悬殊时打包能明显缩短训练时间;数据只有几百条时,省下来的时间不值得多一个出错点。
打包还会改变“一步”的含义:一步处理的 token 数固定了,样本数却不固定。比较两次实验的步数和学习率调度时,要换算成实际看过的回答 token 数。
轮数、学习率和批大小从哪里起步
SFT 的超参没有通用最优值,但官方示例给出了大家常用的起点。下表是这些起点和它们的出处(2026 年 10 月的版本),实际取值要用验证集调:
| 超参 | 全参数微调 | LoRA | 依据 |
|---|---|---|---|
| 学习率 | 1e-5 到 2e-5 | 1e-4 左右(常见 5e-5 到 2e-4) | TRL SFTConfig 默认 2e-5,文档建议适配器用约 1e-4;LLaMA-Factory 示例全参 1e-5、LoRA 1e-4 |
| 轮数 | 1 到 3 | 1 到 3 | 两者默认都是 3 |
| 调度 | 少量预热后线性或余弦下降 | 同左 | 见 一步训练发生了什么 |
| 序列长度 | 覆盖 95% 以上样本 | 同左 | TRL 默认 1024,LLaMA-Factory 示例 2048 |
全参数微调的学习率比预训练后期还小,原因是模型已经在一个很好的位置,步子大了会把预训练学到的东西冲掉。LoRA 只训练新加的小矩阵,起点是“零更新”,需要更大的学习率才推得动,下一篇 会解释。
轮数比学习率更容易出问题。几千条样本训 3 轮,模型会开始逐字背诵训练回答:训练 loss 继续下降,验证 loss 在第 2 轮后掉头向上,回答越来越像某几条样本的模板。判断方法是每轮结束在验证集上同时看 loss 和业务指标,曲线的读法见 读懂训练曲线。验证 loss 小幅上升而业务指标还在变好的情况也常见,这时以业务指标为准。
数据质量比数量重要
SFT 教的主要是“怎样回答”,而不是“知道什么”。2023 年的 LIMA 实验把这一点推到了极端:在 65B 的 LLaMA 上只用 1,000 条精心挑选的问答做 SFT,人工评测中有 43% 的情况被认为与 GPT-4 持平或更好。同年的 QLoRA 论文也观察到,9 千条高质量的 OASST1 对话训出的聊天模型,胜过用 45 万条 FLAN v2 训出的模型。这些结论的前提是底座已经足够强、评测偏向对话风格,不能外推成“任何任务一千条就够”,但方向是清楚的:一千条一致、准确的样例,胜过一万条口径互相打架的样例。
对资料助手来说,“质量”有几个可检查的标准:同一类问题的回答结构一致(先给结论,再给条款编号,最后补充注意事项);条款编号与片段原文逐字核对过;没有把片段里没有的信息写进回答;敏感信息(工号、手机号、薪资)在入库前就清理掉。
为资料助手造一套 SFT 数据
数据集要覆盖模型上线后会遇到的几类情况,而不只是“问得到、答得出”的那一类。下面是一份示例配比,用来说明需要哪些类型,比例要按线上日志的分布调:
| 类型 | 示例输入 | 目标回答 | 示例占比 |
|---|---|---|---|
| 片段里有答案 | 片段含第 4.2 条,问年假怎么申请 | 结论 + 条款编号 | 约 60% |
| 片段里没有答案 | 片段是差旅制度,问年假天数 | 说明现有资料没有涉及,建议联系人事部 | 约 20% |
| 片段不完整或冲突 | 新旧两版制度同时被检索到 | 指出两版差异,以生效日期较新的为准并注明 | 约 10% |
| 超出职责 | 问“我这个月工资多少” | 说明助手不能查个人数据,指引到薪酬系统 | 约 10% |
“片段里没有答案”这类样本最关键,构造方法是拿一个真实问题,故意换上不相关但看起来相近的检索结果,让回答者写出“不知道”的标准说法。它教模型的不是“拒绝”,而是“回答必须以片段为依据”。
这类样本也会带来反方向的失败:比例过高时,模型学到“不知道”是一个安全的高频答案,片段里明明有第 4.2 条也回答“资料中未找到”。所以评估时要把两个方向分开统计:该答的有没有答(漏答率),不该答的有没有编(编造率)。只看其中一个,很容易用一种错误换另一种。
数据切分也要防泄漏:同一个制度条款衍生出的多条改写问法,要整体放进训练集或整体放进评估集,否则评估分数只说明模型见过这些条款的问法。
遗忘:学会了新口径,丢了旧本事
在窄领域数据上训几轮之后,模型常会在领域外变差:写代码、做翻译、讲通用常识的能力下降,或者对任何问题都套上“按《员工手册》第 X 条”的句式。这叫灾难性遗忘(catastrophic forgetting):参数朝新数据的方向移动,覆盖了原来支撑其他能力的部分。
几种常用的缓解办法:混入一部分通用指令数据,让训练分布不要太窄;轮数和学习率取下限;用 LoRA 这类只训练少量参数的方法(它“学得少,也忘得少”,下一篇会讲证据)。InstructGPT 论文在偏好训练阶段也遇到过类似的“对齐税”,部分公开基准变差,他们的做法是在训练中混入预训练数据的梯度,回退明显减轻。混多少没有固定比例,要靠评估确定。
训练前后怎样比较
SFT 是否成功,不由训练 loss 决定。训练前先固定一套评估集,在底座(或当前线上模型加提示词)上跑一遍作为基线,训练后用完全相同的输入、解码参数再跑一遍:
- 业务指标:条款编号正确率、回答与片段一致率、漏答率、编造率、格式合规率。条款编号能用程序比对,一致性可以人工抽查加 LLM 评委打分。
- 边界行为:越权问题是否正确引导,冲突片段是否指出差异。
- 通用回归:一组与领域无关的问题(写作、翻译、简单推理),看回答质量有没有明显下降。
- 换一种问法:用训练集里没出现过的口语化问法(“请假那个年假咋弄”)检查泛化。
评估集怎样构造、LLM 评委有哪些偏差,评估 一篇会展开。这里只记住一条:只有在相同输入、相同解码设置下的前后对比,才能说明训练带来了什么。
SFT 管不了的事
SFT 只给模型看“好回答长什么样”,从不告诉它两个都还行的回答里哪个更好;它会让模型更像样例,但样例没覆盖的地方,模型仍可能自信地胡说。它也不适合承载经常变化的知识:制度一改,训进参数的旧条款就成了错误答案,这类内容应该留给检索。
SFT 把对话模板、损失掩码和数据质量这三件事做对,资料助手就能稳定地按公司口径回答、带上条款编号、没有依据时说不知道;打包和超参决定的是训练快慢和稳不稳,评估决定这次训练能不能上线。8B 模型的全参数 SFT 至少要几张 80 GB 的卡(显存账见 训练显存账本),大多数团队实际用的是只训练千分之几参数的 LoRA。下一篇看它为什么只训练这么少的参数也够用:LoRA 与 QLoRA。
