· 预计阅读 11 分钟

Tokenizer & 样本构造:模型到底读到了什么?


本篇解决什么

本篇是「小模型训练系列」第 6 篇:从 token 切分、上下文窗口、label shift、loss mask 与对话样本格式出发,解释模型真正读到的不是文本,而是 token 序列与训练目标。

适合谁读

正在沿着「小模型训练系列」系统学习,希望把概念、工程和真实判断串起来的读者。

AI大模型训练Tokenizer样本构造Loss Mask

上一篇讲数据工程时,我们把训练数据比作一张地图。

地图决定模型要往哪里走。

但地图还不能直接塞进模型脑子里。文章、对话、代码、表格、换行、标点、角色标记,全部都要先经过一台看不见的“剪辑机”。

这台剪辑机会把人类世界里的文字,切成一格一格的 token,再排成一条胶片轨道。

模型真正看到的,不是“你好”“请解释”“这是一段答案”。

它看到的是:

[128000, 9906, 11, 220, 19182, 88923,  ...]

像不像一部动画片的底片?

银幕上是人物、对白、情绪和剧情。底片上却是一格一格的编号。训练模型也是这样:读者看到文章,标注员看到问答,训练程序看到 token id、attention mask 和 labels。

所以这一篇要拆的是训练入口处最容易被低估、也最容易出事故的一层:

模型到底读到了什么?


📌 本篇聚焦的问题

  • Tokenizer 为什么不是简单的“分词器”
  • token 切分如何改变模型看到的信息颗粒度
  • 上下文窗口为什么不是“能放多少字”的仓库
  • 一条聊天记录如何变成 input_idsattention_masklabels
  • label shift 为什么是下一词预测的关键机关
  • loss mask 为什么决定模型到底学谁、不学谁
  • prompt 部分到底该不该参与 Loss
  • 样本构造里最常见的训练事故长什么样

这一篇会稍微工程一点,但不会把读者丢进代码森林。

可以把它想象成一集训练片场的动画:Tokenizer 是剪辑师,样本构造是分镜师,loss mask 是导演手里的计分板。

模型坐在放映厅里,一帧一帧看过去,然后学会下一帧应该是什么。


🎞️ 1. 模型读的不是文字,而是 token id

人类看到这句话:

小模型训练不是背书,而是在目标函数里被塑形。

模型不会直接理解这一整句话。它会先经过 Tokenizer。

Tokenizer 会做两件事:

  1. 把文本切成 token
  2. 把每个 token 映射成词表里的数字编号

可以简化成这样:

flowchart LR
  A[原始文本] --> B[Tokenizer 切分]
  B --> C[token 序列]
  C --> D[token id]
  D --> E[Embedding 查表]
  E --> F[模型内部向量]

也就是说,文字不是直接进入模型的。

真正进入模型第一层的,是 token id 查出来的 embedding 向量。

这就解释了一个重要事实:

Tokenizer 是文本世界和向量世界之间的入口。入口怎么切,模型就怎么读。

它不是一个无关紧要的预处理工具,而是在决定模型看到世界的最小颗粒。


🧩 2. Token 不是字,也不一定是词

很多人会把 token 理解成“词”。这个说法方便,但不准确。

token 可能是一个字:

也可能是一个词:

training

也可能是一个词的一部分:

train
ing

还可能是空格、换行、标点,甚至某种特殊标记:

\n
<|user|>
<|assistant|>
<eos>

在多语言场景里,这件事会更明显。

同样一句话,在不同 tokenizer 里,可能被切成完全不同的形状。中文可能按字、词片段或混合方式切;英文可能按词根、后缀、空格和标点切;代码里的缩进、括号、引号,也可能成为非常重要的 token。

这不是细枝末节。

因为模型不是在“阅读自然语言”,而是在处理 token 序列。

如果某个领域词总是被切得很碎,模型学习它的成本就更高。
如果某种格式在 tokenizer 里很稳定,模型就更容易形成模式。
如果训练和推理用的 tokenizer 不一致,模型甚至会像拿错了字幕版本一样,台词对不上画面。


🔍 3. Tokenizer 会改变信息的颗粒度

想象一个动画片场。

一句台词本来是:

低成本微调

剪辑师可能把它切成三格:

低成本 / 微 / 调

也可能切成四格:

低 / 成本 / 微调

也可能切成更多碎片。

表面上意思没变,但模型看到的训练路径变了。

每一个 token 都是一次预测位置。序列越碎,模型要走的步子越多;某个概念被拆得越零散,模型越需要从更多上下文里重新拼回它。

这就是为什么 tokenizer 会影响:

影响位置具体表现
序列长度同样内容切得越碎,占用 token 越多
训练成本token 越多,计算和显存压力越高
概念学习专有词被拆太碎,模型更难稳定记住
格式学习角色标记、换行、代码缩进会影响输出习惯
上下文容量token 变多后,能装进去的原文变少

所以,“这个模型支持 8k 上下文”并不等于“能放 8000 个汉字”。

它的意思是:最多能放 8000 个 token。

而 token 和字数之间没有一个永远固定的换算关系。


🪟 4. 上下文窗口不是仓库,而是一条胶片轨道

很多人听到上下文窗口,会把它想成一个仓库:

这个模型有 32k,上下文更大,能塞更多资料。

这个理解有一半对。

窗口确实限制了最多能放多少 token,但它不是一个安静堆东西的仓库,更像一条正在播放的胶片轨道。

模型沿着这条轨道,从左往右看。

[system] [user] [assistant] [user] [assistant] ... [当前要预测的位置]

当前位置前面的 token,都会成为预测下一个 token 的条件。

这带来几个现实问题:

  1. 不是塞进去就一定被充分利用
  2. 长上下文会增加计算成本
  3. 重要信息放在不同位置,效果可能不一样
  4. 截断策略会决定哪些信息被保留,哪些被切掉
  5. 训练时见过的长度分布,会影响推理时的稳定性

上下文窗口不是“越大越自动聪明”。

它更像舞台长度变长了:能站更多角色,但导演、灯光、走位也更难控制。


🎬 5. 一条聊天记录如何变成训练样本

现在进入片场。

假设有一条最普通的聊天记录:

用户:什么是 Loss?
助手:Loss 是模型预测和目标答案之间的差距,用来指导参数更新。

人类看起来很自然。

但训练程序不能直接吃这段文字。它要把这条对话变成一个固定格式的样本。

通常会经历四步:

flowchart TB
  A[原始聊天记录] --> B[套入聊天模板]
  B --> C[Tokenizer 编码]
  C --> D[构造 input_ids / attention_mask]
  D --> E[构造 labels / loss mask]

套入模板后,可能变成类似这样:

<bos>
<|user|>
什么是 Loss?
<|assistant|>
Loss 是模型预测和目标答案之间的差距,用来指导参数更新。
<eos>

然后 Tokenizer 把它变成一串 token id:

input_ids:
[BOS, USER, 21603, 3922, 11, ASSISTANT, 8210, 318, ... , EOS]

还会有 attention mask:

attention_mask:
[1, 1, 1, 1, 1, 1, 1, 1, ... , 1]

如果有 padding,还会出现:

attention_mask:
[1, 1, 1, 1, 1, 1, 0, 0, 0]

其中 1 表示真实内容,0 表示填充出来的空位,不应该被模型当成有效上下文。

但真正决定训练目标的,是 labels。


🧠 6. Labels 不是答案原文,而是“下一格该是什么”

语言模型的核心训练方式通常是下一 token 预测。

它不是一次性猜完整答案,而是在每个位置猜下一格。

比如:

今 天 天 气 很 好

训练时更像这样:

当前看到要预测
今 天
今 天 天
今 天 天 气
今 天 天 气 很

这就是 causal language model 的基本节奏。

模型像一个逐帧播放的动画师:每看完前面的画面,就要猜下一帧是什么。

所以 labels 往往和 input_ids 很像,但在训练内部会做一个位移。

input_ids:  [A, B, C, D, E]
labels:     [A, B, C, D, E]

训练时实际比较:
模型在 A 后预测 B
模型在 B 后预测 C
模型在 C 后预测 D
模型在 D 后预测 E

这个“错一格就全片跑偏”的机关,就是 label shift。

如果 shift 逻辑错了,模型可能在错误的位置被惩罚。它看起来还在训练,Loss 也可能在下降,但学到的东西会变形。

这类事故特别隐蔽,因为训练日志不会大喊:“你的电影字幕提前了一秒。”

它只会安静地给出一个数字,然后让模型往奇怪的方向收敛。


🎯 7. Loss mask:导演说哪里算分,哪里不算分

现在来到最关键的一幕。

同一条聊天样本里,哪些 token 应该参与 Loss?

还是刚才这条:

<|user|>
什么是 Loss?
<|assistant|>
Loss 是模型预测和目标答案之间的差距,用来指导参数更新。

如果目标是训练一个助手,通常希望模型学会回答,而不是学会复读用户的问题。

那么一个常见做法是:

用户部分:不计算 Loss
助手部分:计算 Loss

在 labels 里,经常会用一个特殊值,比如 -100,表示这个位置不参与 Loss。

概念上可以写成:

tokens:
[USER, 什么, 是, Loss, ASSISTANT, Loss, 是, 模型, 预测, ...]

labels:
[-100, -100, -100, -100, -100, Loss, 是, 模型, 预测, ...]

这就是 loss mask。

它像导演在片场举起计分板:

这段只是铺垫,不算分。
从这里开始,是演员正式表演,算分。

模型会在算分的位置被奖励或惩罚,在不算分的位置只把它们当作上下文。

所以 loss mask 决定的不是小细节,而是训练责任如何分配。


🧭 8. Prompt 部分到底该不该参与 Loss?

这是一个非常容易争论的问题。

答案不是永远“该”或“不该”,而是看训练目标。

如果是在预训练阶段,目标是让模型学习通用文本分布,那么很多连续文本都会参与下一 token 预测。模型要学习语言、知识、风格、结构。

如果是在指令微调阶段,目标通常是:

给定 system / user / context,让模型学会生成 assistant 答案。

这时 prompt 更像题目,assistant answer 更像标准答案。

题目本身通常不应该被当成模型要生成的目标。

所以常见策略是:

场景Prompt 是否算 Loss原因
通用预训练通常会算学习连续文本分布
指令微调 SFT通常不算用户部分让模型学习回答,而不是复读问题
多轮对话微调常只算 assistant 部分保持角色边界
格式续写任务可能部分计算取决于训练目标
特定模板学习可能计算模板后的目标段让模型稳定输出格式

一句话:

Prompt 是否参与 Loss,不是道德问题,而是目标函数设计问题。

如果目标是让模型学会“看到问题后回答”,就不要把问题本身也当成答案来惩罚。
如果目标是让模型续写某种完整文本分布,prompt 和 answer 的边界就没那么绝对。

训练不是让模型“理解你的意图”,而是把意图翻译成一个可计算的目标。

loss mask 就是这份翻译里最锋利的一支笔。


🚧 9. 样本构造事故剧场

这一层最有意思,也最危险。

因为很多训练事故不是模型结构错了,而是样本构造这场动画片剪坏了。

事故一:模型学会复读用户

如果用户问题也参与 Loss,模型可能会把用户内容也当成要生成的目标。

结果就是:

用户:帮我总结这段文字
模型:帮我总结这段文字……

它不是故意敷衍,而是在训练时被教过:用户说的话也是“正确输出”的一部分。

事故二:模型停不下来

如果训练样本没有稳定的结束标记,比如 EOS,模型可能不知道什么时候该收尾。

它会像一集没有片尾曲的动画,一直往下演。

表现为:

回答已经完整了,但模型继续补充、重复、绕圈。

事故三:角色边界混乱

如果 chat template 不稳定,有时写 User:,有时写 <|user|>,有时又没有角色标记,模型就很难学会谁在说话。

上线后可能出现:

用户:给我一个建议
模型:用户:当然可以。助手:我建议……

这不是模型调皮,是片场的分镜牌乱了。

事故四:Padding 被算进 Loss

批量训练时,不同样本长度不一样。为了拼成 batch,短样本会被补齐。

如果 padding 位置没有正确 mask 掉,模型会被迫学习“空白”。

这会把训练信号稀释掉,甚至制造奇怪的输出偏好。

事故五:截断切掉了答案

如果样本太长,超过最大长度,就要截断。

最糟糕的情况是:题目保留了,答案被切掉了;或者答案只剩半截,还被当成完整目标训练。

模型会学到一种很危险的模式:

复杂问题 → 半截答案

如果评估时只看 Loss,甚至不一定马上发现。

因为它确实在认真学习,只是学习对象已经坏了。


🧪 10. 一个训练样本的“论文级”拆解

把所有东西合在一起,一条样本可以拆成五层:

flowchart TB
  A[人类语义层: 问题与答案] --> B[模板层: system / user / assistant]
  B --> C[Token 层: token 切分]
  C --> D[张量层: input_ids / attention_mask]
  D --> E[目标层: labels / loss mask]
  E --> F[优化层: Loss / 梯度 / 参数更新]

这五层对应五个判断:

层级要问的问题
人类语义层这条样本是否真的高质量
模板层角色边界是否清楚
Token 层切分后是否过长、过碎、异常
张量层padding、attention、长度是否正确
目标层哪些位置参与 Loss,是否符合训练目标

真正严肃的训练工程,不会只问“数据集有多少条”。

它会问:

每一条数据最后变成了什么训练目标?

因为模型不会对原始文本负责。

模型只对它实际看到的 token、实际比较的 labels、实际计算的 Loss 负责。


🧱 11. 样本构造的三个核心原则

第一,模板要稳定

同一种任务,最好使用一致的 chat template。

角色标记、换行位置、结束符、system prompt 的位置,都应该尽量稳定。

稳定模板不是为了好看,而是为了让模型把格式当成可靠信号。

第二,目标要干净

不要让模型在不该学习的位置计算 Loss。

指令微调时,通常重点是 assistant 输出。用户问题、系统提示、检索上下文,多数时候应该作为条件存在,而不是作为生成目标。

第三,截断要可控

长样本不是不能用,但要知道它怎么被截断。

尤其要避免:

  • 只保留问题,切掉答案
  • 切掉关键角色标记
  • 切掉 EOS
  • 把一个多轮对话从中间切开,却还当成完整样本
  • 训练和推理的模板不一致

样本构造最怕“看起来差不多”。

模型看到的不是差不多,它看到的是严格的一串 token。


🧾 12. 训练前的样本构造检查表

如果要真的微调一个小模型,训练前至少做一次这张表。

检查项为什么重要
Tokenizer 是否和底座模型一致不一致可能导致整套输入空间错位
Chat template 是否固定决定角色边界和对话格式
System / user / assistant 是否清楚防止模型混淆谁在说话
是否添加 EOS影响模型是否学会停止
Prompt 是否 mask决定模型学回答还是学复读
Padding 是否 mask防止空白 token 污染 Loss
Label shift 是否正确防止预测目标错位
最大长度是否合理防止有效答案被截断
截断策略是否检查过样例防止半截样本进入训练
训练和推理模板是否一致防止上线时“字幕版本”不同

这张表看起来普通,但它能拦住很多训练事故。

训练系统里最贵的错误,不一定是报错。

有时报错反而是好事,因为它及时拦住了你。

更可怕的是:程序不报错,Loss 还在下降,模型也产出了东西,但你训练出来的是一个被错误目标塑形的版本。


🌱 13. 小模型为什么更需要认真做样本构造

大模型容量大,见过的分布广,有时能靠底座能力把一些脏格式扛过去。

小模型不一样。

小模型容量更紧,容错空间更少。每一条高质量样本都很宝贵,每一种格式混乱也更容易留下痕迹。

在小模型训练里,样本构造像给一间小房子布置家具。

空间大时,沙发歪一点还能忍;空间小时,桌子放错位置,门都打不开。

所以小模型微调的关键不是“把数据塞进去”,而是:

让每一个 token 都尽量有意义。
让每一次 Loss 都尽量算在该算的位置。
让每一次梯度更新都尽量朝着目标能力移动。

这也是小模型训练最有趣的地方。

它不只是堆算力,而是在做一种非常精细的能力编排。


🧠 14. 这一篇的核心结论

  1. 模型不直接读文字,它读 token id 对应的向量序列
  2. Tokenizer 决定文本进入模型时的颗粒度
  3. 上下文窗口是 token 序列长度,不是字数仓库
  4. 聊天记录必须先变成模板化样本,才能进入训练
  5. Label shift 决定模型在每个位置预测哪一个 token
  6. Loss mask 决定哪些位置负责学习,哪些位置只做上下文
  7. Prompt 是否参与 Loss,取决于训练目标
  8. 样本构造错误会让模型稳定地学到错误行为
  9. 小模型更依赖干净、稳定、目标明确的样本格式

如果把上一篇的数据工程看成“选择什么剧情”,那么这一篇讲的就是“剧情怎样被剪成模型能学习的分镜”。

剧情再好,如果剪辑错位、字幕错位、计分位置错位,最后训练出来的也会是一部奇怪的片子。

所以训练入口处的真正问题不是:

数据有没有放进去?

而是:

模型实际看到了什么,又被要求在哪些地方学会什么?


📌 下一篇预告

《LoRA / QLoRA:为什么低成本微调可行?》

下一篇会进入很多人真正关心的部分:显卡不大、预算有限、模型又想变成自己的,怎么办?

会拆开这些问题:

  • 全参微调为什么贵
  • LoRA 为什么只训练一小部分参数也能改变模型行为
  • rank、alpha、dropout 到底在调什么
  • QLoRA 为什么能把显存压力继续压低
  • 显存账本应该怎么算,哪些地方最容易爆
  • 小模型微调时,怎样在成本、效果和稳定性之间做选择

核心观点是:低成本微调不是“少训练一点”,而是把可训练能力放在更聪明的位置上。


🗺️ 后续章节路线

顺序章节方向要解决的问题
第 7 篇LoRA / QLoRA:为什么低成本微调可行?全参训练、参数高效微调、rank、adapter 与显存账本
第 8 篇评估体系:模型到底有没有真的变好?training loss、validation loss、任务评估、回归集与人工验收
第 9 篇RAG vs 微调:知识应该放进检索,还是写进参数?知识更新频率、事实性、成本、召回边界与能力边界
第 10 篇推理与部署:模型能力如何稳定跑起来?量化、KV Cache、吞吐、并发、延迟与部署成本
第 11 篇工具调用与 Agent:模型如何从会说话变成会做事?Function Calling、工具参数、权限边界、日志与人工确认
第 12 篇Memory 与工作流:模型如何承接长期任务?对话记忆、任务状态、上下文压缩、恢复现场与流程编排
第 13 篇行业模型与业务闭环:训练如何进入真实场景?领域数据、流程反馈、人工审核、持续评估与灰度改进
第 14 篇系列总结:从数据到 Agent 的小模型工程地图把训练、微调、评估、RAG、部署和 Agent 串成完整路线

到这里,小模型训练系列已经走过了一个完整入口:

从数据如何决定方向,到 token 如何切分世界,再到样本如何定义训练目标。

后面会继续往工程实践里走:怎么低成本改模型、怎么评估是否真的变好、怎么部署到真实场景里稳定运行。

持续学习,持续记录,持续筛选

如果这篇对你有用,可以继续沿着主题读下去。

福星家和会长期记录 AI 技术、生活成长、真实好物与正向文化观察。产品体验、人物故事、教育内容、生活方式、文化作品和内容共创都欢迎邮件沟通,合作内容会清楚标注。