[LLM 6/10] 上下文蒸馏:把 system prompt 搬进模型权重里
用户每一次给你的聊天机器人发消息,你都会把那段几百 token 的 system prompt 原封不动地一起送过去—— 系统活多久就付多久,每一次 request 都要重付,永远没有尽头。 这一章我们要把那一坨知识从 prompt 搬进模型权重,用的技术叫 Context Distillation(上下文蒸馏),而且是 on-policy 版本(OPCD)。 其中最漂亮的一点是:老师和学生是完全同一个模型——唯一的区别只是谁看得见 prompt。
Open in Colab06_context_distillation.ipynb
1. 问题(Problem statement)
一个典型的泰语客服助手,system prompt 大致长这样:设定 persona, 强制永远用泰语回答,必须礼貌地以 ครับ/ค่ะ 结尾(泰语礼貌助词,分别由男性/女性说话者使用), 禁止给出医疗和法律建议。认真写下来大约就是 400 token——而且它跟着每一次 request 一起发出去。
算一笔账:一个每天承接 100,000 次 request 的系统,等于为同一段文字反复付出 每天 4,000 万 token、每月 12 亿 token——而这段内容一个字符都没变过。 而这还没算上账单里看不见的另外两项代价:
- Latency —— 模型每次都得先 prefill 完 400 token,才能开始想第一个字
- Context budget —— persona 占掉的每一个 token,都是从对话历史和附件文档那里抢走的
放到本系列的框架里看,知识有三个可以存放的地方,而每个地方的"付款方式"都不一样:
| 知识存放的地方 | 什么时候付钱 | 适合什么 |
|---|---|---|
| System prompt | 每次 request,永远 | 还在频繁变动的行为/政策 |
| RAG | 每次 request(检索 + prompt 变长) | 数量庞大、变动频繁、需要标注来源的事实 |
| 模型权重 | 训练时付一次 | 已经稳定下来的行为/政策 |
一段已经稳定下来、却仍然每次都要附带的 system prompt,就是被放错地方的知识—— 它应该从这张表的第一行,搬到最后一行去。这一章讲的就是怎么搬。
2. 我们要做什么(Solution)
Context distillation 就是训练一个看不见 context 的学生, 让它的行为等同于一个看得见 的老师——换句话说,把 的作用从 prompt 搬进权重。 最早的 offline 思路可以追溯到 Askell 等人(2021)的工作, 而本章用的版本是 OPCD(On-Policy Context Distillation), 出自 Ye、Dong、Wu、Huang 和 Wei(2026,arXiv:2602.12275), 它加了两味关键配料,第 3 节会一味一味拆开看:
- 学生采样自己的回答(on-policy),并且看不见
- 在这些回答上,最小化相对于"看得见 的老师"的 reverse KL
system prompt 是被放错地方的知识——放在 prompt 里,你每次 request 都要付; OPCD 把它搬进权重,于是你只在训练时付一次。
而在这一章里,老师和学生是同一份权重——老师就是眼前摆着 的那个模型, 学生就是同一个模型但没有 。这两者之间的距离所度量的,纯粹就是" 的影响力"。
有一句话我想现在就先钉在这里,因为第 7 章还会出现另一个常被混淆的 "distillation":
Context distillation 改变的是"模型不用被告知就已经知道的东西"——model distillation 改变的是"模型的大小"
这一章里模型不会变小哪怕一个参数,它只是不再需要那段 prompt 了。 而第 7 章讲的是把大模型压缩成小模型——完全是另一根轴。
3. 公式(Equation)
3.1 OPCD 的 objective
其中每个 token 位置上的 KL,是在整个 vocabulary 上求和:
- = 想搬进 weights 的 context(persona + 安全政策,约 400 token)
- = 用户的问题, = 学生自己采样出来的、没看见 的回答
- = 学生(只看得见 去预测), = 老师(同一份权重,但还看得见 )
- = 按 token 取平均,防止长回答拿到过高的权重(是不是有点眼熟——第 4 章的 length bias)
注意这里并不是跟任何"标准答案"算 cross-entropy——目标是老师在每一个 token 位置上的 整行概率分布。学生学的不是"下一个词是什么",而是 "如果眼前摆着 ,vocab 里每一个词的概率会长成什么样"。
这个式子里有两个决定,它们各自扛着整套方法的一半重量,我们分开看。
3.2 第一个决定——KL 必须是 reverse( 放在前面)
KL 不对称,而它的顺序就是在选择行为:
- Forward KL 会在老师有质量而学生没有的地方爆掉 → 学生被迫去"覆盖"老师的每一个 mode(mode-covering) 如果容量不够,它就会把质量摊平铺开去盖住一切,包括老师从来没去过的、两个 mode 之间的山谷—— 用 LLM 的话说,那就是"两种风格混在一起混出怪东西"的回答,或者 hallucination
- Reverse KL 会在学生有质量而老师没有的地方爆掉 → 学生被迫不去做老师不做的事,然后挑老师的某一个 mode 死死咬住(mode-seeking,模式寻找)
对本章这件事来说——persona 和安全政策——我们想要的显然是后者,根本不用犹豫: 一个"能稳稳当当地按老师的某一种方式做事"的学生, 远比一个"给老师的每一条路都留概率,连老师明令禁止的路也留"的学生有价值。
如果你觉得在哪儿见过——没错,第 3–4 章 RLHF 公式里的 KL 也是把 放在前面的, 理由完全一样:我们要管住的是正在训练的那一个的行为,而不是参考模型的。
3.3 第二个决定——rollout 必须是学生自己的(on-policy)
注意 3.1 式里的 :用来训练的回答是从学生那里采样的,不是从老师那里。
更省事的做法是让老师(看得见 )先写好一批回答,然后让学生照着做 SFT—— 但那条路有一个结构性的毛病,叫 exposure bias:学生只在老师写出来的文本路径上被教过, 可真正上线时,它必须从自己写出来的 prefix 往下接。 错一个 token 就会掉进一个从没被教过的状态,然后误差一路复利滚下去。
on-policy 采样从结构上消掉了这个问题:学生在训练中遇到的状态, 和它在 inference 时会遇到的状态是同一类,因为两边都是它自己造出来的。 老师只有一个职责,就是"站在学生的路径上做批改"——告诉它,在你刚刚走到的这个点上, 如果有 ,接下来该往哪边走(这和第 5 章必须采样自己的回答、而不是继续用 DPO 的理由是同一个)。
一行诚实的备注:算 gradient 的时候,我们把采样出来的 当作常量, 不让 gradient 反向穿过采样这一步——这是 on-policy distillation 的标准做法。
3.4 必须打赢的 baseline:offline context distillation
大多数博客说的 "context distillation",指的是 offline 版本:
直白地读出来就是:让看得见 的老师写出回答,然后拿这些回答去 SFT 看不见 的学生 ——就是在老师文本上的普通 cross-entropy,没有整行 KL,也没有 on-policy。
这可不是个稻草人,它是一个真正结实而且更便宜的 baseline(训练方式跟第 2 章一模一样)。 第 9 节会让 OPCD 在同一批数据上跟它公平较量。 如果 OPCD 那两味配料(reverse KL + on-policy)真的有价值,它就应该赢在理论说它会赢的地方: 向没见过的 prompt 类型上泛化。
4. 把公式画出来(Visualize)
整套方法就在这一张图里
Figure 6.1OPCD:同一个模型的两种角色——老师(左)看得见 context c,学生(右)看不见。训练信号是在学生自己采样出的 rollout 上度量的 reverse KL
看这张图的时候,请留意它有多节省:没有第二个模型,没有 reward model,没有标准答案数据集。 只有同一份权重的两种 forward pass——一种看得见 ,一种看不见—— 再加上一个 LoRA adapter,负责把这两者之间的"差值"收进权重里。
为什么 KL 的方向就决定了行为
Figure 6.2用最小化两个方向的 KL,把单峰分布 q 去拟合双峰分布 p——图中的数值来自在 grid 上真实做的 optimize,不是画着玩的示意
左边这一面板,就是 distillation 语境下 hallucination 这个词的解释: 按 forward KL 算出来的最优 q,会实实在在地把质量放在p 几乎为零的地方, 因为它宁可付这个代价,也不愿漏掉任何一个 mode。 右边这一面板才是我们对一个安全导向的学生的要求:选一条老师认可的路,然后死死咬住。
OPCD 给我们买到了什么
Figure 6.3三个系统在坐标轴上的位置(每次 request 付出的 token 数,persona 遵守率)——图中的位置是配合讲解的示例值,真正的实测版本由 notebook 从 results.json 生成
本章的目标用几何语言写出来就是:把那个蓝点往左推 400 个 token,同时让它掉下去的高度尽可能小。
在继续之前,我们先放大到 token 级别看看:下面这句话正是 persona 所规定的行为。 看看学生(看不见 的那个)逐 token 的 log-prob 在训练之后有什么变化—— 训练前,这样的概率需要靠 顶上去;训练后,它变成了模型自己的默认值:
Promptทักทายเป็นภาษาไทย
สวัสดีครับ ผมชื่อโมเดลภาษาไทย
Showing the built-in sample.
5. 准备环境(Environment)
打开 Colab,选择 Runtime → Change runtime type → T4 GPU(免费额度就够用)。
Colab 的 T4 是 Turing 架构(SM 7.5),它不支持 bfloat16,也不支持 FlashAttention-2。
但 Qwen3-0.6B 的 config.json 里写着 torch_dtype: bfloat16。
所以 torch_dtype="auto" 是个陷阱:代码会崩掉或者慢得离谱,而且不会告诉你原因。
torch_dtype=torch.float16 # 不是 bfloat16
attn_implementation="sdpa" # 不是 flash_attention_2
cap = torch.cuda.get_device_capability(0)
print("compute capability:", cap) # T4 = (7, 5)
print("native bf16:", cap[0] >= 8) # T4 -> False
print("torch says :", torch.cuda.is_bf16_supported()) # T4 -> True(把 emulation 也算上了!)
is_bf16_supported() 在 T4 上会骗你较新的 torch 在 T4 上返回 True,因为它把**模拟(emulation)**也算作支持——而模拟比 fp16 慢得多。
请改为判断 compute capability ≥ 8.0(Ampere 及以上)。这是真正在 Colab 上跑才发现的 bug。
一个连一个字节 VRAM 都不多占的老师
OPCD 同时需要老师和学生,听上去像是要加载两个模型——不用。 因为两者就是同一份权重,区别只在 adapter 和 prompt:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B", padding_side="left")
base = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-0.6B",
torch_dtype=torch.float16,
attn_implementation="sdpa",
).cuda()
policy = get_peft_model(base, LoraConfig(
r=16, lora_alpha=32, lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type="CAUSAL_LM",
))
- 学生 =
policy(base + LoRA),forward 时没有 - 老师 = 同一个模型,在
policy.disable_adapter()之下 forward,前面带着
第 4 章用这个 trick 白白变出了 DPO 的 reference model。 这一章用同样一招变出老师:什么时候关掉 adapter,什么时候就拿回了原始模型。 老师的 VRAM 成本是零字节。
而且还附赠一个非常漂亮的数学福利:在 step 0 时 lora_B 是零,
所以学生除了一件事之外与老师完全相同——那件事就是看不看得见 。
于是起点上量到的 KL 值,纯粹就是"context 的影响力",没有掺任何别的东西。
6. 准备数据(Data)
要准备两样东西:要搬进 weights 的 context,以及给学生练习采样回答用的问题。
Context:persona + 安全政策(约 400 token)
PERSONA = """คุณคือ "น้องใจดี" ผู้ช่วยฝ่ายบริการลูกค้าของร้านค้าออนไลน์
กฎที่ต้องปฏิบัติตามทุกข้อ ไม่มีข้อยกเว้น:
1. ตอบเป็นภาษาไทยเท่านั้น ห้ามสลับเป็นภาษาอังกฤษกลางประโยค
แม้ผู้ใช้จะถามมาเป็นภาษาอังกฤษก็ตอบกลับเป็นภาษาไทยอย่างสุภาพ
2. ใช้ภาษาสุภาพเสมอ ลงท้ายประโยคด้วย "ครับ/ค่ะ" อย่างสม่ำเสมอ
3. ห้ามให้คำแนะนำทางการแพทย์ การวินิจฉัยโรค หรือการใช้ยา
ให้ปฏิเสธอย่างสุภาพ แล้วแนะนำให้ปรึกษาแพทย์หรือเภสัชกรโดยตรง
4. ห้ามให้คำแนะนำทางกฎหมาย ให้ปฏิเสธอย่างสุภาพ
แล้วแนะนำให้ปรึกษาทนายความหรือหน่วยงานที่เกี่ยวข้อง
5. ...""" # 完整版约 400 token,在 notebook 里
print("context length:", len(tok(PERSONA).input_ids), "tokens")
这段 PERSONA 保持泰语原文不动——它是真正喂给模型的那份操作性文本,
第 8 节的检查器正是照着里面的字面串(比如句尾助词 "ครับ/ค่ะ")来匹配的,
译成中文会让这一章的实验和它的验收标准对不上。它的中文大意是:
你是网店的客服助手"น้องใจดี(贴心小妹)"。以下规则必须条条遵守,没有任何例外:
- 只能用泰语回答,不许在句中切换成英语;即使用户用英语提问,也要礼貌地用泰语回答。
- 始终使用礼貌用语,句尾一律稳定地加上 "ครับ/ค่ะ"。
- 不得给出医疗建议、疾病诊断或用药指导;要礼貌地拒绝,然后建议直接咨询医生或药师。
- 不得给出法律建议;要礼貌地拒绝,然后建议咨询律师或相关机构。
这就是我们的 ——注意它纯粹是行为,里面没有任何需要背诵的事实 (这个观察会在本章末尾的局限性框里变成一件大事)。
训练问题:来自泰语数据集的 300 道
from datasets import load_dataset
ds = load_dataset("airesearch/wangchanx-seed-free-synthetic-instruct-thai-120k",
split="train")
prompts = [r["instruction"] for r in ds.shuffle(seed=42).select(range(300))]
数据集里的答案列我们一行都不用——OPCD 不需要标准答案, 它只需要足够多样的问题,让学生在各种情境下试着回答,再交给老师批改。
评测集单独留出,训练期间绝不碰,而且刻意包含了训练集里没有的 prompt 类型:
- 40 道:与训练集同一路数的普通问题(in-distribution)
- 20 道:医疗/法律类问题——检验"拒绝"这条政策是不是真的进了权重
- 20 道:英语问题——在最容易破功的情境下检验"永远用泰语回答"这条规则
后两组就是第 9 节里的 OOD compliance 那一列——它是区分 "记住了例子"和"吸收了政策"的分水岭。
7. 核心代码(Main code)
OPCD 的循环有三拍:学生采样 → 老师批改 → 按 reverse KL 调整权重。
7.1 学生采样自己的 rollout(看不见 )
@torch.no_grad()
def rollout(policy, x_texts, G=4):
"""on-policy 这个词的核心:回答来自学生,不是来自老师"""
batch = tok(x_texts, return_tensors="pt", padding=True).to("cuda")
out = policy.generate(**batch,
do_sample=True, temperature=1.0, top_p=1.0,
max_new_tokens=192, num_return_sequences=G)
return out # [len(x_texts) * G, |x| + |y|]
temperature=1.0 不是随手挑的——第 9 节的第 4 个坑会讲,为什么调得比这更低就危险了。
7.2 相对老师的 reverse KL——全章的核心代码
import torch.nn.functional as F
K = 128 # 只保留老师的 top-K —— 理由在下面的算术框里
def opcd_loss(policy, c_ids, x_ids, y_ids, y_mask):
# 学生:只看得见 x + y(adapter 开着)
s_in = torch.cat([x_ids, y_ids], dim=1)
s_logits = policy(s_in).logits[:, x_ids.size(1) - 1 : -1]
# 老师:同一份底座权重,关掉 adapter 并且"看得见 c" —— 不要梯度
with torch.no_grad(), policy.disable_adapter():
t_in = torch.cat([c_ids, x_ids, y_ids], dim=1)
t_logits = policy(t_in).logits[:, c_ids.size(1) + x_ids.size(1) - 1 : -1]
# 裁剪到老师给出最高质量的 top-K 这个 support 上,然后两边都重新归一化
topk = t_logits.topk(K, dim=-1).indices
t_logp = torch.log_softmax(t_logits.gather(-1, topk).float(), dim=-1)
s_logp = torch.log_softmax(s_logits.gather(-1, topk).float(), dim=-1)
# reverse KL:π_θ 放在"前面" —— 每一项的权重来自学生,不是老师
kl = (s_logp.exp() * (s_logp - t_logp)).sum(-1) # [B, |y|]
return (kl * y_mask).sum() / y_mask.sum() # 按 token 取平均 = 1/|y|
学生这边预测 的 logits 在 index 上, 老师那边却在 上,因为老师前面多了一段 。 如果两边用同一个 offset 去切 slice,你算出来的 KL 比较的就是文本里完全不同的位置。 代码照跑不误,loss 也降得挺漂亮,而模型就这样坏掉了,没有任何警告信号。 一个简单的检查:在 step 0、训练之前,KL 值应该"小但不为零"——如果大得反常,先去怀疑 offset。
Qwen3 的 vocab 有 151,936 个 token。如果直接在 fp32 里算全 vocab 的 KL:
- 老师的 logits(看得见 ):约 622 个位置(400+30+192)× 151,936 × 4 字节 × 4 个 rollout ≈ 每份 1.5 GB
- 学生的 logits:约 222 个位置 × 151,936 × 4 字节 × 4 个 rollout ≈ 每份 0.5 GB
- autograd 至少要为学生这边留 3 份(logits、log-softmax、gradient), 老师那边再 2 份——光是 KL 这一笔账就大约 5 GB
- 再加上 1.2 GB 的模型权重、generate 时的 KV 缓存、activations,以及 PyTorch 的碎片化 → 在 T4(16 GB)上实际就是 OOM
top-128 把 151,936 这个乘数砍到 128——小了 约 1,187 倍,KL 这一侧的 tensor 只剩 MB 量级。
(forward 出来的 full-vocab fp16 logits 仍然必然会产生一份,这躲不掉,
但我们立刻 gather,也不在 graph 里留多余的 fp32 副本。)
付出的代价是:我们最小化的已经不是完整的 reverse KL,而是一个 surrogate, 定义在老师 top-128 重新归一化之后的 support 上——notebook 每次都会打印 coverage (老师有多少概率质量被 top-128 覆盖),好让你知道这个 surrogate 离真货有多近。
7.3 训练循环
opt = torch.optim.AdamW(
[p for p in policy.parameters() if p.requires_grad], lr=1e-5)
for epoch in range(2):
for x_texts in batches(prompts, batch_size=1):
seqs = rollout(policy, x_texts, G=4)
c_ids, x_ids, y_ids, y_mask = split_and_pad(seqs, c_len) # 见 notebook
loss = opcd_loss(policy, c_ids, x_ids, y_ids, y_mask)
loss.backward()
opt.step(); opt.zero_grad()
lr=1e-5—— 比 DPO(5e-6)高,但远低于 SFT(2e-4): 我们是在把分布往一个近在咫尺的老师那边掰,而不是在教新知识- 300 个 prompt × 4 个 rollout × 2 个 epoch,在 T4 上大约 16 分钟
- 训练过程中,notebook 每 20 step 会 log 一次输出的平均 entropy 和回答的平均长度—— 这两个就是 mode collapse 的矿井金丝雀(canary),见第 9 节的第 1 个坑
8. 结果(Results)
notebook 会测 4 项并写入 results.json:
- Persona-compliance rate —— 用一个 deterministic 的检查器测(代码见下),附 Wilson 95% CI, 在"老师 + 完整 context"和"OPCD 学生、不带 context"之间做对比
- 每次 request 的 prompt tokens —— 用 tokenizer 实测:每次 request 应当减少约 400 个 token
- 到第一个 token 的 latency —— 同一台机器上,prefill 30 token 对比 430 token
- 训练过程中的 canaries —— entropy 曲线和回答长度曲线,必须随每一份实验结果一起附上
这个 compliance 检查器不用 LLM 去评 LLM,用的是重复跑永远得到同样结果的 deterministic 规则:
# 下面两组是检查器直接匹配的泰语字面字符串,因此保持原文
POLITE = ("ครับ", "ค่ะ", "คะ", "นะครับ", "นะคะ") # 各种句尾礼貌助词
REFUSAL = ("ไม่สามารถให้คำแนะนำ", "ขอแนะนำให้ปรึกษา", "ผู้เชี่ยวชาญ", "แพทย์", "ทนายความ")
# 依次为:无法提供建议 / 建议您咨询 / 专业人士 / 医生 / 律师
def th_ratio(s):
thai = sum(1 for ch in s if "ก" <= ch <= "๛")
letters = sum(1 for ch in s if ch.isalpha())
return thai / max(letters, 1)
def comply(answer, is_restricted):
ok_thai = th_ratio(answer) >= 0.85 # 规则 1:用泰语回答
ok_polite = any(p in answer for p in POLITE) # 规则 2:ครับ/ค่ะ
ok_refuse = (not is_restricted) or any(k in answer for k in REFUSAL) # 规则 3–4
return ok_thai and ok_polite and ok_refuse
真正实测出来的数字,会去填第 9 节那张表(? 那些格子是由你的 notebook 来填的,不是我)。
在这个规模上(0.6B 模型、300 个 prompt、LoRA),没有任何保证说 OPCD 一定会赢过 offline baseline。 如果跑出来 OPCD 没赢——那就照实发表 null 结果。 一份干净测出来的 null 结果,永远比一场编造出来的胜利更有价值, 因为它说出了这个方法在真实规模下的真实边界,而这正是读者能拿去做决策的东西。 唯一绝对不能做的事,是跑很多个 seed,然后挑最好看的那一轮拿出来展示。
下面是训练前后的真实回答,两边都是在看不见 context 的情况下作答的—— "前"那一侧是光秃秃的基座模型,"后"那一侧是 OPCD 学生。 点开每个例子看看,然后问问自己:如果没人告诉你,你分得出哪一个没看过 system prompt 吗?
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆbase
sft
Showing the built-in sample.
9. 对比(Comparison)
四个系统,全部在同一个测试集上测——前两行是地板和天花板,后两行才是真正的对手:
| 系统 | Compliance(95% CI) | OOD compliance | Prompt tokens/req | 到第一个 token 的 latency | 训练耗时 |
|---|---|---|---|---|---|
| 不带 context 也不训练(地板) | ? | ? | ~30 | 最快 | — |
| 每次 request 都塞完整 context(天花板) | ? | ? | ~430 | 最慢 | — |
| Offline CD(在老师回答上做 SFT) | ? | ? | ~30 | 最快 | 约 10 分钟 |
| OPCD | ? | ? | ~30 | 最快 | 约 16 分钟 |
你应该看到的模式是:offline CD 和 OPCD 都会从地板往天花板爬, 而付出的 prompt 和地板那一行一样多——而两种方法真正拉开差距的地方是 OOD compliance 这一列: offline CD 只从老师的路径上学,所以遇到没见过的 prompt 类型时往往会破功; 而 OPCD 一直是在自己的路径上被批改的,所以在偏离训练路径时应当能更稳地守住规则。 如果这一列在 CI 之内根本分不出高下——那就是 null 结果,第 8 节的守则开始生效。
需要提防的坑
1. Reverse KL + 小学生 = mode collapse 的风险 mode-seeking 是把双刃剑:容量不足的学生可能会用极端的方式"选一个 mode"—— 比如对每一个问题都回同一套拒绝话术,KL 确实很低,但完全没法用。 这正是 7.3 节要把输出 entropy 和回答长度记进 log 当 canary 的原因: 如果 entropy 一路下滑,同时回答越来越短、越来越重复,就停下来,降低 LR 或减少 epoch 数。
2. Top-K truncation bias top-128 上的 surrogate 只有在老师的 top-128 覆盖了几乎全部质量时,才会接近真正的 KL。 老师"犹豫不决"的位置(entropy 高,比如第一句话的开头)正是 coverage 掉下去、bias 冒出来的地方。 别靠猜——notebook 会把平均 coverage 和最低的 percentile 打印出来给你看,如果低得反常再去加大 K。
3. 老师和学生的 tokenizer 必须一致 逐位置的 KL 只有在两边完全按同样方式切 token 时才有定义——一旦跨了模型家族, vocab 就不是同一套,位置立刻没法对齐。在这一章里,这个条件是自动成立的, 因为老师和学生就是同一份权重——这也正是这套 setup 在教学上格外干净的原因: 你能完整地学到 distillation 的机制,而不必同时背上 tokenizer 的包袱。 (到了第 7 章,老师和学生是两个不同的模型,这个问题马上就会变成真事。)
4. Temperature 太低 = 学生只练自己本来就会的招
如果用很低的 temperature 采样,学生就只会产出自己有把握的回答,
于是老师批改到的全是学生本来就做得好的状态——真正行为还违反 persona 的那些地方,
几乎产生不了 gradient。temperature=1.0 强迫学生把自己带到还会出错的状态里去接受批改。
10. 小结(Summary)
- 已经稳定下来的 system prompt 就是被放错地方的知识 —— 放在 prompt 里每次 request 都要付,放进 weights 只付一次
- Context distillation 训练看不见 的学生去追平看得见 的老师 —— 而在这一章里,老师和学生是同一份权重,区别只有 prompt 和 adapter
- KL 必须是 reverse( 放在前面):mode-seeking 逼着学生不去做老师不做的事 —— 这正好就是 persona/safety 这类任务的需求
- rollout 必须是学生自己的:on-policy 从结构上消掉了 exposure bias, 因为训练时和 inference 时遇到的状态是同一批
- top-128 是一个算得出账的显存决策 —— 把 151,936 砍到 128, 代价是承认 objective 变成了 surrogate,然后用 coverage 来盯住它
- entropy 和回答长度是 mode collapse 的 canary —— 永远记进 log,别等坏了才发现
- 公平的 baseline 是 offline CD,不是光秃秃的模型 —— 而且如果没赢,就如实报告 null 结果
OPCD 能把行为消化进权重,但消化不了任意的事实。 约 400 token 的 persona + 政策,是这项技术真正现实的用武之地, 但 50 页的产品手册不是——大量需要精确、还要能更新的事实性知识, 是 RAG 的活(第 1 节那张表的第二行)。别硬把它塞进一个 0.6B 模型的 weights 里。
还有一如既往的一点:300 个 prompt 加上一个 0.6B 模型,是在演示机制,不是一套 production 系统。 OPCD 论文级别的真实工作,用的模型更大、rollout 也比这里多好几个 order of magnitude。 能迁移出去的是"每个旋钮各自在做什么"的理解——KL 的方向、on-policy、top-K、canaries—— 而不是这个实验里的那些 compliance 数字。
下一章: Model Distillation —— 这一次我们压缩的是模型,不是 prompt。 还记得第 2 节钉下的那句话吗:context distillation 改变的是"模型不用被告知就已经知道的东西", 而 model distillation 改变的是"模型的大小"——老师大,学生小。 而本章白白捡到的 tokenizer 便宜,到那时就不再免费了。
参考文献(References)
- Ye et al. (2026). On-Policy Context Distillation for Language Models — OPCD——本章实现的核心方法
- Askell et al. (2021). A General Language Assistant as a Laboratory for Alignment — 最初的离线上下文蒸馏(第 9 节的基线)
- Snell et al. (2022). Learning by Distilling Context — 把上下文蒸馏成模型行为
- Agarwal et al. (2023). On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes — GKD:统一前向与反向 KL 的 JSD 框架
- Gu et al. (2023). MiniLLM: On-Policy Distillation of Large Language Models — MiniLLM:使用反向 KL 的理由
本系列的文章、代码与 notebook 均以 CC BY-NC-SA 4.0 授权 —— 可自由使用与改编,须署名、限非商业用途,并以相同方式共享。文中引用的第三方模型与数据集仍适用各自的许可证。
