2026年10月2日

02 分词:文本切成 token

LLM

分词器是怎么把文本切成 token 的?

目前主流的方法叫 BPE(Byte Pair Encoding,字节对编码)。它的核心思想很朴素:经常一起出现的字符组合,就合并成一个 token。

先理解为什么需要它

切分文本有两种极端做法,各有缺陷:

  • 按单词切:词表会极其庞大(各种词形变化、新词、人名、错别字),而且遇到词表里没有的词就无法处理。
  • 按字符切:词表很小,什么都能表示,但序列会非常长。一句话变成几十上百个 token,占满 context,计算量也大,而且单个字符承载的语义太少。

BPE 走的是中间路线:常见的词整体作为一个 token,罕见的词拆成几块,实在不认识的就拆到单个字节。

BPE 的两个阶段

第一阶段:训练分词器(构建词表)。 这一步在模型训练之前、单独进行。用一个小例子说明,假设语料中只有这几个词:

low(出现 5 次)、lower(2 次)、newest(6 次)、widest(3 次)

  1. 一开始,把所有词拆成单个字符,词表就是所有出现过的字符。
  2. 统计所有相邻字符对的出现频率。e+s 出现在 newest 和 widest 中,共 9 次,最多,于是合并成新 token es。
  3. 再统计一遍,es+t 出现 9 次,合并成 est。
  4. 接下来 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。不同模型的分词器不同,但原理是相通的。