Tokenizer
理解文本如何变成模型可消费的整数流
建议阅读:约 17 分钟
学习目标
本章关键词
| 关键词 | 解释 | ESP32 工程类比 |
|---|---|---|
| Vocabulary | token 到整数 id 的映射表。 | 像协议中的命令码表。 |
| BPE | 通过合并常见子串构建 token 的算法。 | 像把常见字节片段定义为更短的帧类型。 |
| Special token | 表示开始、结束、角色等控制含义的 token。 | 像帧头、帧尾和控制字。 |
| Chat template | 将消息列表格式化为模型训练期预期字符串的模板。 | 像不同设备各自的命令帧格式。 |
承上:回顾与定位
上一章建立了 LLM 的概率目标、发展脉络与状态术语;本章从输入端开始,固定字符、字节、token、特殊标记和消息模板的精确契约。
历史发展脉络
Tokenizer 的演进是一段在“词表有限、任何文本都要可编码、序列不能太长”之间寻找平衡的历史。它从通用压缩中的重复片段替换,逐步变成语言模型输入协议,并进一步承担对话角色与工具消息的 framing。
SentencePiece 直接从原始句子训练
SentencePiece 不要求先按空格切词,并提供语言无关的 tokenizer/detokenizer,使无空格语言和跨语言流程更一致。
SentencePiece 原始论文 ↗Chat template 把消息结构纳入 tokenizer 契约
现代聊天模型把 role/content 渲染为带控制 token 的单一序列;模板随 tokenizer 保存,应用不再适合手工猜测分隔符。
Hugging Face 官方 Chat Template 文档 ↗类比图解
跨国车站的检票与编组系统
旅客说着不同语言、带着 emoji 和代码片段来到车站。检票处先按统一规则核验证件,再把常见同行片段编成短车厢编号;站长插入“列车开始、乘客发言、列车结束”等控制车厢,模型只看到最终编号序列。
类比的边界: 类比容易让人误以为一个 token 就对应一个可读词;实际 token 可能是词、空格前缀、若干 UTF‑8 字节甚至跨字符片段。编号也没有跨 tokenizer 的通用含义,拆分结果更不直接代表模型对概念的理解程度。
本章讲解
从字符串追到字节,先固定规范化语义
视觉上相同的文本可能由不同 Unicode 码点序列组成,例如预组合字符与组合附加符;全角、换行和不可见控制字符也会改变 token。不要在应用、模板和 tokenizer 三处分别做清洗,否则训练与推理难以对齐。明确输入采用哪种规范化、是否保留空白与大小写,并用十六进制码点记录边界样本。字节级 tokenizer 虽能覆盖任意输入,也不意味着损坏的 UTF‑8 或替换字符可以被悄悄接受。
理解子词算法在词表与序列长度间的交换
BPE 从基础符号出发,按学习到的 rank 反复合并相邻片段;WordPiece 与 unigram/SentencePiece 的训练准则不同,不能只靠同一份词表复现。大词表可缩短常见文本,却增大 embedding 和输出层;小词表覆盖容易但序列更长。中文、代码、数字和 emoji 的频率结构不同,所以字符数无法预测 token 数。工程比较应使用产品语料的长度分布、编码耗时和模型质量,而非只试一句英文。
把 special token 与 chat template 当成不可拆的 ABI
聊天模型训练时看到的是 role/content 经模板渲染后的 token 序列,不是应用中的对象数组。模板决定 system、user、assistant 的边界,是否插入 BOS/EOS,以及生成提示停在哪里。先渲染字符串再 tokenize 时,通常应关闭重复的 special token 添加;切换模型必须连同 tokenizer 和模板一起切换。对用户文本还要区分普通字符与允许的控制 token,防止文本意外越过消息边界。
预算的不只是 context 上限,还有编码路径本身
请求预算应先套模板再计数,包含系统提示、历史消息、工具描述和预留输出,不能用字符数估算。端侧逐字符追加后每次重新编码整段会形成平方级重复工作;可缓存稳定前缀或使用支持增量的实现,但合并可能跨越追加边界,不能天真地把两段 token 列表直接拼接。限制超长输入时应按消息或语义块截断,并重新生成模板,避免从 UTF‑8 字节或 special token 中间切开。
用金向量验证跨语言、跨实现的一致性
建立包含中文、英文、空白、换行、emoji、组合字符、代码和疑似 special token 的小型语料,保存 tokenizer 哈希、模板版本、期望 id 与解码结果。在 Python、Host runtime 和设备实现上逐项比较,额外检查 encode→decode 是否满足声明的可逆边界。升级库后若 id 改变,即使解码文本相同也不能直接复用旧 prompt/KV cache;缓存键应包含完整 tokenizer 契约。
沿分层快照定位第一个错误 token
调试 token 差异时,不要只打印最终 ids。保存原始字节和 Unicode 码点,再快照规范化结果、模板字符串、预切分片段及各 token 的 id。按首个差异对齐:字符串不同就查换行、Jinja 空白控制和 BOS/EOS;字符串相同而片段不同,查 tokenizer.json 与 merge ranks;ids 相同而模型行为不同,再查 position 和 attention mask。这样能把问题定位到具体协议层。
动态过程演示
一段对话如何变成整数列车
逐步播放器展示人类消息在每个协议层增加或改变了什么,最后再从 token 片段流式还原文本。
组织消息 · [{role, content}, …]
保留角色、轮次和工具字段的结构
对象结构尚不是模型输入
循环 / 返回条件: 新一轮消息回到“组织消息”并重套模板;若要复用前缀,必须同时匹配模板、tokenizer 哈希、token 序列与位置。
查看静态全景图
UTF-8 text → normalize → tokenize → [BOS, 1203, 88, EOS] → embedding lookup
代码或命令示例
ids = tokenizer.encode("温度 28°C")
print(ids, len(ids))
prompt = template(system, user)
# token budget = prompt_tokens + generated_tokens动手实验
实验记录与导出
工程陷阱
核心测试
完成 3 道单选题后提交;提交前不会显示答案。
在 GitHub 上讨论本章
登录 GitHub 后提问、补充实测或分享你的实现;评论会保存在本课程的 GitHub Discussions 中。
评论区需要 JavaScript;也可以直接打开 GitHub 讨论区: 打开 GitHub 讨论区 ↗
延伸阅读
启下:下一章如何使用本章能力
下一章把 Transformer 作为完整架构单独展开,先建立 token、位置、block、FFN、残差、归一化与 logits 的全局路径。