15 课 · 评测与调优
LESSON 15 · 卷十五 说人话

模型不是不会答
是你没按它的规矩问

第 14 课把这个模型训到加减 99%,它认的格式是「三十四加十二等于?四十六」—— 一整句、没有角色、没有人在说话。 可你把这同一道题丢给任何一个真实的大模型,它多半会答得莫名其妙 —— 因为真实模型只认一种格式:对话。 这一课就把「一道题」改造成「一轮对话」,顺着把最关键的一件事补上:损失到底该盖在哪几个字上。

STAGE 01
三种角色
system / user / assistant
STAGE 02
掩码盖在哪
只算助手说的话
STAGE 03
两把尺子
逐字对 ≠ 整题对
STAGE 04
收官
钉在墙上

先说清楚「对话格式」到底是什么。它一点也不神秘:模型还是第 3 课那个「看前文、猜下一个字」的模型, 我们对它做的事情,只是把一次问答拼成一长串带路牌的文本 —— 路牌写着「现在是系统在说话」「现在是用户在说话」「现在轮到助手了」。 模型在这串文本上学,也在这串文本上生成。 下面这一站,我们把这道题逐字拆开给你看。

第 1 站

三种角色:一条对话被拼成什么样

真实的对话模型只有三种角色:system(系统提示,相当于「人设说明书」)、user(用户的话)、assistant(模型自己的话)。 一轮一轮拼起来,每条消息都用 <|im_start|> 开头、<|im_end|> 收尾。 下面就是「三十四加十二」这道题被拼出来的样子 —— 精确到每一个 token。

核心实验一道算术题 → 38 个 token 的对话,逐块上色
问一道
系统14 个 token
<|im_start|>system<|im_end|>
用户16 个 token
<|im_start|>user<|im_end|>
助手8 个 token
<|im_start|>assistant<|im_end|>
特殊标记(各算 1 个 token)角色名换行正文(按字切)
一共几个 token
38
9 + 11 + 3 字正文 + 15 个模板 token
模板固定吃掉
15
3 轮 × 5<|im_start|> + 角色名 + 换行 + <|im_end|> + 换行)
真正拿来训练的
4
助手那 3 个字 + 结尾的 <|im_end|> —— 只有 10.5%
模型真正看到的那一串(把标记展开、换行显出来就是它):
<|im_start|>system⏎ 你是一个算术助手。<|im_end|>⏎ <|im_start|>user⏎ 三十四加十二等于多少?<|im_end|>⏎ <|im_start|>assistant⏎ 四十六<|im_end|>⏎
这就是「对话格式」的全部秘密:把一次问答,拼成一串带角色标记的长文本。 模型学的还是第 3 课那件事 —— 看着前面的字,猜下一个字; 只不过现在「前面的字」里多了一堆 <|im_start|> 之类的路牌, 告诉它现在是轮到谁说话了
👆 每一块是一个 token:蓝色的特殊标记和角色名各自算 1 个(不是按字符数拆的)—— 这就是为什么它们被叫做「特殊」标记:它们是切词器里预先占好的格子,不参与按字切分。 换一道题看总数怎么变:变的只有正文,模板那 15 个是雷打不动的固定开销。
图 15-1 · 一条完整对话的 token 序列:模板固定 15 个,正文 23
面试常问 · 对话模板与特殊标记
  • 三种角色各管什么:system = 全局人设与规矩(每一轮都在它的笼罩下); user = 用户输入;assistant = 模型该产出的东西。 真实产品里调 prompt,主要就是在改 system 那一轮。
  • 特殊标记为什么要单独占格子<|im_start|> <|im_end|> 各算 1 个 token。它们不是「一堆普通字符」—— 切词器把它们当整体认,这样模型学到的「轮次边界」才是干净的信号, 而不是被拆碎的一串标点。
  • 各家模板不通用:Qwen 用 <|im_start|><|im_end|>、 Llama 用别的写法。喂错模板,模型会答得残缺甚至乱码 ——这是排查「模型怎么突然变笨了」时的第一嫌疑人
  • 为什么模板 token 是「固定开销」:三轮对话就是 15 个, 跟题目长短无关。所以对话越长、system 写得越啰嗦,这部分摊到有效训练上的成本就越高。
一条对话里,我们写了 9 个字的系统提示(「你是一个算术助手。」)。猜一猜:训练的时候,模型要不要去「学」怎么生成这 9 个字?
第 2 站

掩码盖在哪:只算「助手说的话」

第 14 课定过一条规矩:题干屏蔽、答案算损失。 这一课它一个字都没改 —— 只是「题干」的范围变大了: 以前是「等于?」前面的题面,现在是 system 轮 + user 轮 + 助手的抬头。 下面三个开关默认都是开的(= 正确做法),试着关掉一个, 看模型会多学一些什么不该学的话。

核心实验三个掩码开关:关掉任何一个,都会让模型去学它不该说的东西
问一道
掩码开关
系统0 / 14 算损失
<|im_start|>system<|im_end|>
用户0 / 16 算损失
<|im_start|>user<|im_end|>
助手4 / 8 算损失
<|im_start|>assistant<|im_end|>
亮 = 算损失(模型要学)暗 = 被掩码盖住(不算)
算损失的 token
4
38 个 token,占比 10.5%
正确做法应该是
4
助手正文 3 字 + 结尾的 <|im_end|>
多学了几个
0
不多不少,正好
第 14 课 · 平铺样本三十四加十二等于?四十六」—— 答案 3 / 共 12
25%
第 15 课 · 对话版本助手正文 3 + im_end 1 / 共 38 个 token
10.5%
两根条是同一条刻度(满格 = 100%):同一道题,平铺格式有 25% 的 token 在训练,包装成对话之后只剩 10.5%
掩码的规矩一个字都没变过 —— 还是「只算模型该学着说出来的那部分」。 变的只是「题干」和「答案」的边界:第 14 课那条样本里, 边界是「等于?」后面那一段;这里,边界是 assistant 这一轮的正文。 剩下的全是搭台子(模板 + 系统提示 + 用户提问),一个字都不该学。
顺带一个真实现象:对话格式的固定开销很贵。 答案平均才 3.4 个字,可一条对话动辄三四十个 token —— 所以真实训练里大家会把 system 写短、把没用的空轮砍掉, 就是在省这部分「白搭的台子」。
👆 怎么玩:把上面三个开关逐个关掉,看「算损失的 token」怎么涨、看哪几个字亮起来。 亮起来的如果不该亮(系统提示、用户的问句、助手的抬头), 模型就会跑去学「怎么把人写的话复述一遍」—— 这正是我们要屏蔽掉它们的原因。
图 15-2 · 同一道题,正确掩码下只有 4 / 38 个 token 在训练(10.5%),而平铺格式是 25%
名桥 · 和第 14 课那条样本对照着看
第 14 课 · 平铺: 三十四加十二等于?四十六
 └ 算损失:四十六3 字,占 25%)

第 15 课 · 对话:
 <|im_start|>system ⏎ 你是一个算术助手。 <|im_end|> ⏎ ← 盖住
 <|im_start|>user ⏎ 三十四加十二等于多少? <|im_end|> ⏎ ← 盖住
 <|im_start|>assistant ⏎ 四十六 <|im_end|> ⏎ ← 只有这一段算损失

看出来了吗:规矩是同一条,边界是一样的 —— 「递进去的不学,要说出来的才学」。 变的只是「递进去的」从一小段题面,变成了一整圈台子。 也正因为台子变大了,有效训练的比例从 25% 掉到 10.5% —— 这是对话格式的代价, 不是它的错误。

再提醒一个容易漏掉的细节:助手那一轮的结尾的 <|im_end|> 要算损失。 因为推理时是我们把「<|im_start|>assistant ⏎」递进去让它接着写, 抬起头我们替它说了;但什么时候停下得它自己学会 —— 不然它会一直往下编。这个「学会停」的能力, 就是靠让它在 <|im_end|> 上算损失训出来的。

第 3 站

两把尺子:逐字对,不等于整题对

格式讲完了,回到这一课标题的另一半 ——评测与调优。 第 9 课说过「评估必须诚实」,第 14 课说过「要分题型看」。 这里还有最后一把最容易被骗的尺子:逐字准确率。 它总是比「整题正确率」好看得多,而用户只认后者。

核心实验同一份模型:逐字率一个数、整题率另一个数,中间隔着一道幂运算
逐字率90%
答案长4
逐字快捷档 答案长度 
逐字答对率
90%
模型每吐一个字,对的机会
4 个字的答案全对 = 90% 4
整题正确率
65.6%
答案 4 个字,每个都要对
0%25%50%75%100%12345678整题正确率答案长度(字)90%^4 = 65.6%
👆 这条蓝线就是「整题率 = 逐字率答案长度」:拖「逐字率」,整条线跟着上下动; 拖「答案长度」,琥珀色那个点顺着曲线往右走 —— 走得越远掉得越快,它不是一条直线,是往下加速掉的。答案长度拖到 8 个字、逐字率只有 80% 时, 整题率就只剩 16.8% 了。
逐字答对率看起来的样子
90%
整题正确率用户看到的样子
65.6%
同一根刻度(满格 = 100%):逐字 90% 的字答对率, 放到 4 个字的答案上只剩 65.6% —— 因为错一个字,整题就算错。
答案越长,这把尺子差得越狠:4 个字要连乘 4 次。 这就是为什么「逐字准确率 90%」这种说法几乎没有信息量 —— 它没说答案多长。评测报数的时候,必须报整题正确率(或者明确说是逐字)
现在回到我们那个模型。它考出来的 整题 正确率是这几行, 倒推回去,逐字 大约是多少(假设每个字独立地错):
题型整题正确率(实测)倒推的逐字正确率
加法99.3%99.8%
减法98.7%99.6%
乘法30.7%70.6%
除法37.3%74.8%
整体66.5%88.7%
倒推的算法只有一步:答案平均 3.39 个字, 整题正确率 e 就意味着每个字大约 e1/3.39 对。 验算一下乘法:70.6% 的 3.39 次方 = 30.7%,回到原值。
看乘法那一行:整题 30.7%,逐字却有 70.6% —— 差了将近 40 个百分点。 如果你只看逐字准确率,会以为「乘法已经七成了,快好了」; 可用户拿到手里的是「十道乘法错七道」。两把尺子都要量,但报出去、以及决定下一步做什么的,必须是整题那一把。
还有一个诚实的提醒:这条倒推的前提是「每个字的错误互相独立」。 真实情况没那么干净(模型往往错在同一个位置上,比如进位的那个字), 所以它只是个量级估算 —— 但它足以说明两把尺子不是一回事。
👆 表怎么读:中间那列是第 8 / 12 / 13 课那份实测考卷的原数, 右边那列是按「一个字一个字独立」倒推出来的估算。 加法减法两行几乎贴在一起(99% 附近,两个数本来就难分), 乘法除法两行差得很开 —— 越是没学会的题,两把尺子差得越大,因为它的错误更多。
图 15-3 · 整题正确率 = 逐字正确率答案长度 —— 乘法那一行两把尺子差了 39.9 个百分点
你要向老板汇报进度,只有两个数可选: A.「逐字准确率 70.6%」; B.「整题正确率 30.7%」。哪个更该说?为什么?

还有一层:「逐字准确率」也不能乱平均。 我们那个模型整体整题率 66.5%,可它是由加减的 99% 和乘除的 30% 混出来的 —— 一平均,两边的极端都被抹平了。这和第 9 课那句 「只看整体分数,你永远发现不了加减已经 99%、乘除还在 30%」是同一件事, 只是在「对话格式」这个大背景下又强调了一遍:报数之前,先问清楚这个数是在什么粒度上量的、在哪一批题上量的。

动手实验同一份模型,只换考卷的题型配比:整体分自己会动
加法考卷里 150 道 整题率 99.3%(实测,改不了)
99.3%
减法考卷里 150 道 整题率 98.7%(实测,改不了)
98.7%
乘法考卷里 150 道 整题率 30.7%(实测,改不了)
30.7%
除法考卷里 150 道 整题率 37.3%(实测,改不了)
37.3%
整体(四类混起来)加权平均 —— 这才是用户看到的那个数
66.5%
默认(四类各 150 道)= 66.5% → 现在 = 66.5%(还没动过)
加法150
减法150
乘法150
除法150
加减在整体里的权重
50.0%
加法 150 道 + 减法 150 道 = 300
加起来 300 道 —— 全是 99% 上下的题
乘除在整体里的权重
50.0%
乘法 150 道 + 除法 150 道 = 300
加起来 300 道 —— 只有 30% 上下的题
整体 = (150 × 99.3% + 150 × 98.7% + 150 × 30.7% + 150 × 37.3%)÷ 600 = 39900 ÷ 600 = 66.5%
加减占 50.0%、乘除占 50.0% —— 权重全押在「题数」上,跟模型本身没关系。
四类各自的整题率一个字没动(加减还是 99.3% / 98.7%, 乘除还是 30.7% / 37.3%),只有配比变了, 整体就从 66.5% 走到 66.5%整体分是「题型配比」混出来的 —— 换一批题,它就能虚高。
👆 先拖四根滑块里任意一根(加法 / 减法 / 乘法 / 除法):你拖走的题会按现在的比例补到另外三类上,总数永远是 600 道;也可以直接点「把乘法题砍一半」。 再看那根「整体」条和下面两张权重卡片 —— 乘法题一少,加减的权重就涨, 整体条立刻变长:四类各 150 道时是 66.5%, 把乘法砍一半就成了 72.5%,可四类分数一个字都没改。
图 15-4 · 整体整题率 = 四类按题数加权平均:四类各 150 道时 66.5%,把乘法砍到 75 道(省下的补给另外三类)就是 72.5% —— 同一份模型,换一批题分数就虚高了
第 4 站 · 收官

把这一课钉在墙上

本课核心 · TAKEAWAY

「对话格式」就是往一次问答里塞进一串角色路牌。一条 system / user / assistant 的对话,每条消息用 <|im_start|><|im_end|> 包住, 这两个标记各自只算 1 个 token;一轮固定 5 个模板 token。 模型要学的,从头到尾只有 assistant 那一轮的正文加结尾的 <|im_end|>—— 第 14 课那条「题干屏蔽、答案算损失」的规矩一个字没改, 只是「题干」从一段题面变成了一整圈台子,有效训练比例也从 25% 收到 10.5%。 而评测上,永远记住两把尺子:整题正确率 = 逐字正确率答案长度, 乘法那两把尺子差了 39.9 个百分点 ——报出去的、以及决定下一步改什么的,必须是整题那一把。

这一课你亲手做完了

  • 把一道题拆成了对话:「三十四加十二」→ 38 个 token, 三种角色一目了然;模板固定吃掉 15 个, 其余 23 个是正文。
  • 数清了掩码38 个 token 里只有 4 个算损失 (10.5%)= 助手正文 3 字 + 一个 <|im_end|>。关掉任意一个开关都会让它去学不该学的话。
  • 认清了模板 token<|im_start|> / <|im_end|> 各算 1 个 —— 它们是切词器里的特殊格子,不按字符拆。
  • 对上了两把尺子:乘法整题 30.7%、倒推逐字 70.6%(差 39.9 个百分点); 整体整题 66.5%、逐字约 88.7%。
  • 看清了台子的代价:平铺格式 25% 的 token 在训练, 对话版 10.5% —— 所以提示词要写短、空轮要砍掉。

学习小测验

已完成 0 / 60.0%答对 0
还没提交过 —— 每题先选一个选项,再点「提交」,答完就能看到诊断。
Q1把一道算术题喂给真实的大模型,为什么要先「包装成对话」?
Q2对话模板里,一条消息固定带几个 token 的「台子」?
Q3损失掩码要盖住哪些部分?
Q4为什么助手那一轮结尾的「<|im_end|>」要算损失,而抬头不算?
Q5评测时报「逐字准确率 90%」有什么问题?
Q6对话格式让「有效训练比例」从 25% 掉到 10.5%。这说明什么?
NEXT · 第 16 课

量化与 LoRA

这一课我们把题改造成了对话,也知道该往乘法上使劲了。 可还剩最后一个拦路虎没解决:钱包。 完整版 2,677,632 个参数还能在免费显卡上跑, 可真到了 7B 那种规模,光是装下权重就要几十 GB —— 更别说微调时还要存梯度、优化器状态(第 12 课那笔账)。 下一课就解决这件事:量化(把每个参数从 16 位压到 4 位) 和 LoRA(只训练一小撮「外挂」参数)。 这一课那条 38 个 token 的对话, 到了下一课会变成显存账单上的一行数字。

从零手册 —— 下一课:量化与 LoRA