2026年10月2日
02 分词:文本切成 token
分词器是怎么把文本切成 token 的?
目前主流的方法叫 BPE(Byte Pair Encoding,字节对编码)。它的核心思想很朴素:经常一起出现的字符组合,就合并成一个 token。
先理解为什么需要它
切分文本有两种极端做法,各有缺陷:
- 按单词切:词表会极其庞大(各种词形变化、新词、人名、错别字),而且遇到词表里没有的词就无法处理。
- 按字符切:词表很小,什么都能表示,但序列会非常长。一句话变成几十上百个 token,占满 context,计算量也大,而且单个字符承载的语义太少。
BPE 走的是中间路线:常见的词整体作为一个 token,罕见的词拆成几块,实在不认识的就拆到单个字节。
BPE 的两个阶段
第一阶段:训练分词器(构建词表)。 这一步在模型训练之前、单独进行。用一个小例子说明,假设语料中只有这几个词:
low(出现 5 次)、lower(2 次)、newest(6 次)、widest(3 次)
- 一开始,把所有词拆成单个字符,词表就是所有出现过的字符。
- 统计所有相邻字符对的出现频率。
e+s出现在 newest 和 widest 中,共 9 次,最多,于是合并成新 tokenes。 - 再统计一遍,
es+t出现 9 次,合并成est。 - 接下来
l+o出现 7 次,合并成lo;然后lo+w合并成low……
如此反复,直到词表达到预设的大小,比如 10 万个 token。最终得到的是一个词表,加上一份按顺序记录的合并规则。
第二阶段:使用分词器切分新文本。 拿到一段新文本时,先拆成最小单位,再按照训练时学到的合并规则依次合并,最后把每个 token 查表转成对应的编号(token ID)。比如 lowest 会被切成 low + est,即使训练语料中从没出现过这个词。
中文是怎么处理的
现代 LLM 大多使用 字节级 BPE:最小单位不是字符,而是 UTF-8 编码的字节。一个汉字通常占 3 个字节。这样设计的好处是,任何语言、任何符号甚至 emoji 都能被表示,永远不会遇到"不认识的字"。
在此基础上,常用汉字和词组因为在训练语料中频繁出现,会被合并成 1 个 token,比如"的""我们";而生僻字可能没被合并,会占 2~3 个 token。这也解释了第一轮提到的现象:中文的 token 效率取决于分词器训练语料中中文的比例。 早期以英文为主的模型处理中文时,同样的内容要消耗更多 token;而新一代模型扩大了词表并增加了多语言语料,中文效率明显提升。
它和前面几个概念的联系
分词器的输出是 token ID,也就是整数编号。模型内部有一张 embedding 表,大小是"词表大小 × dimension",每个 ID 对应表中的一行,查出来就是那个 token 的向量。这张表也是模型参数的一部分。
另外,词表大小也是训练前就定好的超参数。分词器一旦确定,就和模型绑定在一起,不能更换,因为模型学到的所有知识都建立在这套切分方式上。这也是不同模型的 token 数无法直接比较的原因:同一段文字,在不同模型上算出来的 token 数可能差别很大。
分词带来的一些"怪现象"
理解分词后,很多 LLM 的奇怪表现就说得通了:
- 数字和计算:像
12345可能被切成123+45,模型看到的不是逐位的数字,这是早期模型算术容易出错的原因之一。 - 数字母:问"strawberry 里有几个 r",模型看到的可能是
str+aw+berry三个 token,它并不直接"看到"每个字母。 - 空格敏感:
hello和hello(前面带空格)通常是两个不同的 token。
如果想亲手体验,可以用 OpenAI 开源的 tiktoken 库,几行 Python 代码就能看到一段文本被切成了哪些 token。不同模型的分词器不同,但原理是相通的。