端侧 AI · 15 天工程路线
目录首页 English
DAY 08

Tokenizer

理解文本如何变成模型可消费的整数流

建议阅读:约 17 分钟

学习目标

能解释 vocabulary、merges、special tokens、padding 与 token budget,并定位端侧分词开销。

本章关键词

关键词解释ESP32 工程类比
Vocabularytoken 到整数 id 的映射表。像协议中的命令码表。
BPE通过合并常见子串构建 token 的算法。像把常见字节片段定义为更短的帧类型。
Special token表示开始、结束、角色等控制含义的 token。像帧头、帧尾和控制字。
Chat template将消息列表格式化为模型训练期预期字符串的模板。像不同设备各自的命令帧格式。

承上:回顾与定位

上一章建立了 LLM 的概率目标、发展脉络与状态术语;本章从输入端开始,固定字符、字节、token、特殊标记和消息模板的精确契约。

从源头看今天

历史发展脉络

Tokenizer 的演进是一段在“词表有限、任何文本都要可编码、序列不能太长”之间寻找平衡的历史。它从通用压缩中的重复片段替换,逐步变成语言模型输入协议,并进一步承担对话角色与工具消息的 framing。

1994

BPE 从反复替换高频字节对开始

Philip Gage 描述的 Byte Pair Encoding 反复以新符号替换最高频相邻字节对,原本目标是用简单替换表压缩数据。

BPE 原始论文 ↗
2015

BPE 被改造成神经翻译的子词算法

Sennrich 等人用子词序列表示稀有词和未登录词,让固定词表不再把所有未知形式压成同一个 UNK。

Subword NMT 原始论文 ↗
2016

WordPiece 进入大规模翻译系统

GNMT 将词切成有限的 common sub-word units,在受控词表规模下覆盖开放文本,子词分词成为神经语言系统的关键部件。

GNMT 原始论文 ↗
2018

SentencePiece 直接从原始句子训练

SentencePiece 不要求先按空格切词,并提供语言无关的 tokenizer/detokenizer,使无空格语言和跨语言流程更一致。

SentencePiece 原始论文 ↗
2019

GPT-2 采用字节级 BPE 覆盖任意文本

GPT-2 报告使用 byte-level BPE,在字节覆盖与可复用子词之间折中,减少传统词级词表面对异常字符的盲区。

GPT-2 原始技术报告 ↗
2023–现在

Chat template 把消息结构纳入 tokenizer 契约

现代聊天模型把 role/content 渲染为带控制 token 的单一序列;模板随 tokenizer 保存,应用不再适合手工猜测分隔符。

Hugging Face 官方 Chat Template 文档 ↗
今天为什么仍然重要: 因此,tokenizer 不是可随意替换的文本工具,而是模型 ABI 的一部分。词表、merge/rank、规范化、特殊 token 和模板中的一个字节发生变化,都可能改变后续全部 token id、位置与 KV cache。
借熟悉的系统建立直觉

类比图解

跨国车站的检票与编组系统

旅客说着不同语言、带着 emoji 和代码片段来到车站。检票处先按统一规则核验证件,再把常见同行片段编成短车厢编号;站长插入“列车开始、乘客发言、列车结束”等控制车厢,模型只看到最终编号序列。

证件核验 Unicode 处理、规范化与原始字节边界
车厢编组 BPE merge、WordPiece 或 SentencePiece 子词切分
编号车票 vocabulary 中稳定的 token id
站长控制车厢 BOS/EOS、角色 token 与 chat template

类比的边界: 类比容易让人误以为一个 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 片段流式还原文本。

步骤 1 / 6

组织消息 · [{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

动手实验

选同一句中英文混合文本,比较不同 tokenizer 的 token 数;测量逐 token 编码与整段编码的差异。
实验记录与导出

工程陷阱

避免误判: 把“同一句文本”直接拼接给不同聊天模型,或重复插入 BOS/EOS,常会造成模型角色混乱、首 token 异常或上下文预算失真。

核心测试

完成 3 道单选题后提交;提交前不会显示答案。

1. LLM 的上下文窗口通常限制什么?
2. 为什么应使用模型自带 chat template?
3. 先 apply_chat_template(tokenize=False) 再 tokenize 时通常要注意?

一起把本章讲清楚

在 GitHub 上讨论本章

登录 GitHub 后提问、补充实测或分享你的实现;评论会保存在本课程的 GitHub Discussions 中。

评论区需要 JavaScript;也可以直接打开 GitHub 讨论区: 打开 GitHub 讨论区 ↗

延伸阅读

启下:下一章如何使用本章能力

下一章把 Transformer 作为完整架构单独展开,先建立 token、位置、block、FFN、残差、归一化与 logits 的全局路径。

下一天: Day 9 · Transformer 架构