SFT:先让模型学会应该怎么回答

从监督微调的训练目标、对话数据格式和 assistant-only loss 入手,理解 SFT 如何把通用语言模型调整成更符合任务要求的模型。

SFT:先让模型学会应该怎么回答

SFT 是 Supervised Fine-Tuning,也就是监督微调。它通常是大模型进入后训练阶段的第一步:准备一批质量足够高的示例,让模型学习输入和理想输出之间的对应关系。

这一步的重点不是让模型突然获得全部知识,而是把一个通用的语言模型调整成更符合任务要求的模型。

SFT 到底在训练什么

一个最直观的训练样本是:

用户:把下面的内容总结成三句话
助手:第一句……
第二句……
第三句……

训练时,模型依次预测答案中的下一个 token,并通过交叉熵损失修正参数。大量样本叠加后,模型会逐渐学会:

  • 什么格式算是合格答案
  • 应该使用什么语气和结构
  • 哪些信息需要保留
  • 遇到类似问题时应该如何组织输出

因此,SFT 更像是把“正确示范”写进模型的行为分布里。

数据质量比数量更重要

一条 SFT 数据至少需要包含清晰的任务输入和目标输出。对于对话模型,常见格式是:

{
  "messages": [
    {"role": "user", "content": "解释什么是 JSON Schema"},
    {"role": "assistant", "content": "JSON Schema 是……"}
  ]
}

数据集里最容易被忽略的问题包括:

  1. 目标答案本身存在事实错误
  2. 同一个问题有互相冲突的写法
  3. 输入里混进了本不应该暴露给模型的答案
  4. 训练格式和推理时使用的 chat template 不一致
  5. 样本过于重复,模型只记住了表面句式

如果训练数据的标准不稳定,SFT 会把这种不稳定也一起学进去。

只计算助手答案的损失

对话数据通常不需要让模型为用户问题本身承担损失。更常见的做法是只计算 assistant 消息的 loss,让训练目标集中在“应该如何回答”。

在 TRL 中可以使用 assistantonlyloss=True。不过这个选项依赖 chat template 中存在正确的 generation 标记,不能只打开参数而不检查模板。

对于 prompt-completion 数据,也可以使用 completion-only loss。选择哪一种方式,取决于数据格式和你希望模型学习的范围。

一个最小的 TRL 训练骨架

下面的代码只是训练入口示例,真正运行前还需要准备数据、显存、评估集和模型保存策略:

from datasets import load_dataset
from trl import SFTConfig, SFTTrainer

dataset = load_dataset("json", data_files="train.jsonl", split="train")

trainer = SFTTrainer(
    model="Qwen/Qwen3-0.6B",
    train_dataset=dataset,
    args=SFTConfig(
        output_dir="./out/sft",
        assistant_only_loss=True,
        packing=True,
        max_length=2048,
    ),
)

trainer.train()

TRL 的 SFTTrainer 支持对话数据、prompt-completion 数据和 PEFT/LoRA。参数名称和默认值会随版本变化,实际训练时应该以当前版本文档为准。

SFT 能解决什么,不能解决什么

SFT 擅长改变模型的行为方式:

  • 输出格式
  • 任务流程
  • 语言风格
  • 基本指令遵循
  • 领域表达习惯

它不等于自动获得可靠的新知识,也不保证模型在训练样本之外仍然正确。一个模型可能在训练集上表现得很像专家,换一种问法就失去稳定性。

所以 SFT 之后必须准备独立评估集,至少检查:

格式是否正确
内容是否正确
换一种表达能否完成任务
长输入是否仍然稳定
拒答和边界行为是否符合预期

我对 SFT 的理解

SFT 是让模型先知道“什么样的答案被认为是好答案”。它建立的是行为基础,后面的偏好优化和强化学习,才是在这个基础上继续调整“哪些答案更值得被选择”。

因此,一套糟糕的 SFT 数据会给后面的 PPO 或 GRPO 留下很差的起点。后训练不是越往后越能补救,最开始的示范质量仍然决定了上限。

参考:Hugging Face TRL SFT Trainer 文档