从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 40 分钟 页内实验 ×5
C12 后训练:从续写到听话、推理、做事 第 1 课

预训练模型只会续写,怎么让它回答问题?

SFT:从续写到回答
预训练模型看到问题只会接着往下编。SFT 用的还是「预测下一个 token」,只是换成「指令 → 回答」数据、只在回答上算 loss。这一课用 Qwen3.8 的真聊天模板数清一条样本里哪些 token 算 loss,再看我们自己的小模型在 SFT 前后怎么从「停不下来」变成「答完就停」。
❓

本课一问:一个预训练好的模型,你问它「「小明把苹果给了小王。」苹果现在在谁手里?」,它会接着往下编,而不是回答。怎么让它学会回答?

这一课先不碰:怎样的回答「更好」(第 2 课起);推理、思考模式(第 6 课);工具调用(第 8 课)。这一课只解决格式和行为:看到问题就作答,答完就停。

从已有出发

C0–C11 训出来的是一个「预测下一个 token」的模型。贯穿本章的提问,用 Qwen3.8 的真 tokenizer 切出 13 个 token;对预训练模型来说,这 13 个 token 和语料里任何一段文字没有区别——它的全部本事就是猜「后面最可能接什么」。网页上一个问句后面,常接的是另一个问句、一段评论、一篇文章的下一段,不一定是答案。

从已有出发 · 后训练流水线 点已学的站看数据、损失与模型数

后训练流水线的第一站就是这一课。先看它在整章演进里的位置:

演进图 · 每一代都是被上一代的失败逼出来的本课高亮
  1. 第一代 SFT:用示范教格式 FLAN 2021 · LIMA 2023 你在这里
    解决了同一个交叉熵,只在回答 token 上算:Qwen3.8 模板里一条 87 token 的样本只有 24 个算 loss;LIMA 只用 1,000 条示范。
    卡在只有正例:教学世界里只示范 A,SFT 对另一个好回答 B 和拒答 F 的梯度逐位相同,两者概率永远相等(第 2 课浏览器实算)。
  2. 第二代 RLHF:奖励模型 + PPO Christiano 2017 → Ziegler 2019 → InstructGPT 2022
  3. 第三代 · 离线 DPO:不训奖励模型 Rafailov et al. 2023(Stanford)
  4. 第三代 · 在线 GRPO 系:去掉 value model DeepSeekMath 2024-02 → DAPO / Dr. GRPO / GSPO 2025
  5. 奖励来源 RLVR:可验证的奖励 Tülu 3 2024-11 命名 · DeepSeek-R1 2025-01 规模化
  6. 岔路 RL 扩展了能力,还是重排? Yue 2025 vs ProRL 2025
  7. 第四代 思考模型:想更久,也能控 o1 2024-09 · R1 2025-01 · Qwen3 2025-05
  8. 第五代 蒸馏:小模型直接继承 Hinton 2015 → GKD 2023 → Qwen3 2025
  9. 第六代 Agentic RL:在环境里做事 SWE-RL 2025-02 · Kimi K2 · GLM-4.5 slime · DeepSeek-V3.2

先自己设计:原样把问题交给预训练模型

最省事的办法:什么都不训,直接把问题喂进去,顶多在前面放几个「问:… 答:…」的示例(few-shot)。我们自己从 0 训练的 3.7M 模型(《三國志演義》语料,C0–C11 的项目)正好可以试:给它一个「問:…說了什麼?答:」格式的提示,贪心生成 64 个 token。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案原样把提示交给预训练模型

两边的差距来自同一个事实:预训练数据里从来没有「这里该作答、这里该停」的信号 。few-shot 示例能让大模型「看出」格式,但它随示例变化、会跑偏,而且每次都要占上下文;想让模型稳定地按格式回答,得把这个行为训进参数里。

改进一:SFT——同一个目标函数,换一种数据

监督微调(SFT)不发明新的训练方法:优化器、学习率调度、混合精度,和预训练一模一样。变的只有两件事——数据换成「指令 → 回答」,loss 只在回答上求和:

SFT 损失(对比预训练:变的只有求和范围)
= − log
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
所以「SFT 教的是行为不是知识」在公式层面也成立:目标函数没变、引擎没变,变的只是在哪些 token 上学。

「只在回答上求和」这件事是可以严格证明的等价:把 −100 位置的项从 Σ 里拿掉,和给这些项乘 0 完全相同,梯度里也就没有它们 。需要多少数据?LIMA 只用 1,000 条精心挑选的示范微调 65B 的 LLaMA,在人工评测里对 GPT-4 有 43% 持平或更好、对 Bard 58%、对 DaVinci003 65%;同样 65B、在 52,000 条数据上训练的 Alpaca 复现反而不如它 。

💡

SFT 教的是「表层行为」,不是知识。知识几乎全部在预训练阶段学好了;SFT 只教用什么格式、以什么角色、是回答还是续写。这一条同时解释了三件事:为什么预训练模型只会续写、为什么 1,000 条就够、以及为什么 SFT 有天花板(改不了知识和推理上限,所以后面还需要 RL)。

改进二:对话要有结构——聊天模板

纯文本没有「谁在说话」。对话需要标出系统提示、用户、助手的边界,每家模型都用一套自己的特殊 token 来标。下面是锚点模型 Qwen3.8 的官方聊天模板真实渲染出来的样子——不是示意字符串,而是用它的 tokenizer 和 chat_template.jinja 调 apply_chat_template 算出来的:

同一串模板字符交给两个词表(<|im_start|>assistant↵): Qwen3.8 切成 3 个 token,GPT-2 切成 10 个: <|im_start|>assistant↵。 特殊 token 是在词表里显式注册的:没注册过的词表只会把它当成一串普通字符。

🎯

试三件事:① 先看「裸文本」,再点「训练样本 · 思考模式」:同一句问题被包进了 <|im_start|>、<|im_end|>、<think> 这些粗框 token,每个都是词表里显式注册的一个 id(<|im_start|> 是 248045);② 看组件最下面:同一串 <|im_start|>assistant↵,GPT-2 的词表切成 10 块——它从没注册过这个 token,只能当普通字符拆;③ 点「推理提示 · 思考(xhigh)」,开头那段英文是模板自动插进 system 的推理力度指令(37 个 token),你在对话里根本没写它。

每家的模板都不一样,也没有统一标准。用错模板不会报错:模型照样生成,只是悄悄变笨——训练时见过的边界 token 不在了,它得靠猜。

改进三:只在回答上算 loss——先看朴素做法在哪失败

一条样本里有系统指令、用户问题、模板头,也有助手的回答。最省事的做法是整段都算 loss,和预训练一样:

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案整段都算 loss(和预训练一样)

先点上方的「整段都算 loss(朴素)」,看绿色下划线落在哪些 token 上。

为什么不在用户的话上学?模型的容量有限,把梯度花在「预测用户会问什么」「背一段固定的系统指令」上,是在浪费;要教的是怎么回答。Gottesman 在 Universal-NER 上的对照实验里,屏蔽用户 token 的版本验证 loss 更好。

⚠

常见误解一:「只在回答上算 loss 一定更好。」

这是实验选择,不是定理。Shi et al. 2024 发现,当训练数据的指令很长、回答很短,或者样本很少(正好是 LIMA 那种设定)时,把指令部分也算进 loss 反而更好 。我们项目的提示(上文 + 「某某說了什麼?」)比回答长得多、样本也只有 6,750 条,这正是章首页项目决策 2 要你拍板的地方;项目里的对照结果(回答 loss 在重跑噪声以内,预训练 loss 涨得更少)见实验课。

常见误解二:「loss mask 把用户的话遮住了,模型看不见。」

遮的是 loss,不是注意力。第 t 个位置预测时,前面的系统指令和用户问题全都在上下文里;只是这些位置自己不当预测目标。

一个新的静默失败:掩码是谁算的?

「只算助手」要由训练代码知道哪些 token 属于助手。HF 的做法是让聊天模板用 {% generation %} 标记把助手输出包起来,apply_chat_template(..., return_assistant_tokens_mask=True) 据此返回掩码。回到上面的组件,点「官方模板 + return_assistant_tokens_mask」:Qwen3.8 的官方模板没有这个标记,transformers 4.51.3 只打一行日志,返回的掩码是 0 / 87——如果训练代码直接信任它,这条样本等于没训练。

截至 2026-09,TRL 的 SFTTrainer 已经补上了这个坑:assistant_only_loss=True 而模板缺标记时,自动换上它自带的训练版模板(本页「只算助手」按钮就是用这份模板现算的,commit 31bfab1829);掩码全 0 时直接报错;结束符不在掩码里时警告「model may not learn to stop」——训练 loss 看着正常,推理时才发现停不下来 。

🔍 一个经典陷阱:base 模型停不下来

拿 base 模型做 SFT 时,如果结束符没有对齐成模板里的那个 token(Qwen 系是 <|im_end|>),模型不知道什么时候该停,会一直生成下去。我们的项目用分词器自带、预训练语料里从没出现过的 <|endoftext|>(id 2047)当结束符,并让它进 loss:SFT 前结束符的 loss 是 16.84,300 步后 0.37(2026-09-26 导出,见实验课)。旧版课程说「模板缺 generation 关键字时 mask 静默失效」:对手写的训练循环仍然成立,TRL 这一侧已经改成报错或自动换模板。

再深一层:能不能不动整个模型?LoRA

SFT 的数据不多、要改的只是行为,那能不能不更新全部参数?全量微调要给每个参数存梯度和优化器状态(C9:每参数 16 字节),Qwen3.8 这个规模就是几百 GB。LoRA 的洞察是:微调时的权重更新是低秩的——不必动整个 d×d 的大矩阵,用两个瘦长矩阵 B、A 的乘积去逼近就够了:

LoRA 的前向计算(对每个被适配的权重矩阵)
= +
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
可训练参数 = B(d·r) + A(r·d) = 2rd,对比全量的 d²。下面的计算器把这笔账算给你看。
W₀
d×d
❄️ 冻结
原权重(不训)
+
B
d×r · 训
×
A
r×d · 训
全量微调
每层要训整个 W(d×d)
LoRA

放到锚点模型上算一次(anchor-model.json 现算,覆盖全部线性层:16 个全注意力层的 q/k/v/o、64 层 FFN 的 3 个矩阵、48 个线性注意力层的 3 个投影):这些矩阵共有 24,350,556,160 个参数;r = 16 时 LoRA 只加 108,863,488 个可训练参数,是全模型 27,781,427,952 个参数的 0.39%;r = 64 时 435,453,952(1.57%)。

关于 rank r 的取舍,有一段值得看的认知演进:2024 年的结论是「LoRA 学得少、也忘得少」(学新知识不如全量微调,但更能保住原能力);2025 年 John Schulman 团队复现后修正为「配置对了,LoRA 能追平全量微调」——关键是用在所有权重矩阵上,而不只是注意力层,并且用更高的学习率 。同一个问题的认知在两年里演进,比背一个静态答案有价值。

🔍 QLoRA 与 NF4:再省一半

QLoRA 把冻结的 W₀ 用 4 位的 NF4 格式存(一种按正态分布分位数设计的 4 位数),只在计算时临时反量化;LoRA 的 B、A 仍用高精度训练。再加「双重量化」(量化常数本身也量化)和分页优化器,单卡能微调更大的模型。Raschka 的实测:比普通 LoRA 省约 33% 显存、慢约 39%。4 位格式本身怎么设计,见 C9 的岔路课。

再深一层:SFT 会不会伤到它没见过的 token?

2026 年 5 月,MiniMax 排查了一个现象:模型明明知道某位艺人是谁,却就是输出不了他名字里的「嘉祺」(词表里单独的一个 token)。官方博客的结论是:后训练数据里含这个 token 的样本不足 5 条;SFT 前后 vocab embedding 几乎没变,lm_head 对应的那一行却显著偏移——全词表 lm_head 与预训练版的余弦相似度平均 0.9837、最低 0.3290,4.9% 的 token 低于 0.95(日语 token 高达 29.7%);修复办法是混入覆盖全词表的「复读」合成数据,最低余弦回到 0.9711 。

机制可以用交叉熵的梯度直接推出来:对 lm_head 第 j 行,梯度是 (pj − 𝟙[j = 目标])·h。一个 token 只要不是目标,它那一行每一步都收到「把我的 logit 压低」的推力;输入 embedding 却只在它出现在输入里时才有梯度 。Qwen3.8 的 tie_word_embeddings = false,正是不共享的情形。下面在 5 个 token 的玩具模型上真跑一遍:

「嘉祺」的 lm_head 行 vs 预训练(余弦)「嘉祺」的输入 embedding 行 vs 预训练(余弦)在该说「嘉祺」的上下文里,它的概率

玩具模型:词表 5 个 token(曹操 / 孔明 / 说 / 曰 / 嘉祺),「预训练」权重由种子 1 的随机数生成(不是任何真模型的权重);隐藏向量共享一个公共方向 mu;SFT 数据 12 对,只含前 4 个 token;学习率 0.5 的 SGD,lm_head 与 embedding 一起训。所有数由浏览器现算。

拖到第 300 步:「嘉祺」的 lm_head 行与预训练时的余弦从 1 掉到约 0.17,在该说它的上下文里它的概率从 0.506 掉到 0.001,输入 embedding 行一动不动(余弦 1.0000);勾上「混入全词表复读数据」,余弦停在 0.89 左右,概率反而升到 0.72。玩具模型只演示机制的方向,不复现 MiniMax 的数值;五道口纳什的 E10(B 站 BV1qZRZBcEiH,需联网)从梯度视角讲了同一件事。

证据三问

问本课的回答
① 它解决什么问题?预训练模型只会续写,不知道「这里该作答、答完就停」;SFT 把这个行为训进参数
② 数学上为什么可能有效?SFT 就是在新数据上的条件交叉熵,只改求和范围 ;「知识来自预训练,SFT 只教格式」是 LIMA 提出的假设
③ 实验上为什么选了它?FLAN 20 / 25、LIMA 1,000 条;我们的小模型 300 步后留出提示里 98% 在 64 token 内停下(SFT 前是 0%)

一句话

📌

SFT 用的还是「预测下一个 token」,只是换了数据、只在回答上算 loss:它教的是格式和行为,知识来自预训练。

🚦

先停在这里:记住「同一个交叉熵、换数据、只算回答、结束符要进 loss」,就可以去下一课。
再深一层:读「SFT 会不会伤到它没见过的 token」一节并拖一遍玩具模型,展开「base 模型停不下来」,再去实验课看我们模型的完整 SFT 曲线。

下一个缺口

SFT 只能模仿:示范里有的就学,没有的就不会;而且它从来不知道「这个回答不好」。好回答又很难写——写得最好的也只是标注者本人的水平。可人能比较两个回答哪个更好。好回答很难写出来,但人能比较两个回答——怎么把「比较」变成训练信号? → 第 2 课:RLHF

回扣:思考块 <think> 为什么出现在训练样本里、推理力度那段英文从哪来,第 6 课兑现;工具调用的轨迹怎么算 loss,第 8 课兑现。

你可能会问