解释 LLM 为什么把文本切成子词 token,而不是字符或完整的词。
模型需要一个有限的词表,在序列长度与词表大小之间取得平衡。
术语表 · 6 个术语
- token
- Transformer 真正读取的单位——一个指向模型词表的整数 id。
- BPE
- Byte-Pair Encoding(字节对编码):从字节出发,反复合并出现频率最高的相邻片段对,直到词表达到目标大小。
- sub-word
- 比词小、比字符大的 token,例如 "ization" 或 " the"。
- vocabulary
- 模型认识的全部 token 的集合。Qwen3.5 自带约 248,320 个条目。
- special token
- 像 <|im_start|> 这样的保留 token,用来标记结构(对话轮次、文本结束),而不是字面文本。
- chars/token
- 字符数除以 token 数——衡量一段字符串与分词器词表契合程度的快捷指标。
分词(Tokenization):把文本变成模型的词表
在 Transformer 能对你的句子做任何事情之前,句子必须先被切成模型认识的片段。这些片段叫作 token(词元),而决定从哪里下刀的规则叫作分词器。token 既不是字符,也不是词——它介于两者之间,由一种叫字节对编码(Byte-Pair Encoding,BPE)的算法选出。右侧的小部件在你的浏览器里运行 Qwen3.5 的原版分词器。敲几个字,看色块实时刷新。
为什么是子词?
分词器处在一个尴尬的中间地带。如果让每个词都当一个 token,词表得有数百万个条目才能覆盖名字、错别字和生造词的长尾——嵌入矩阵(为每个词表条目存一个向量的大查找表——下一章讲)也会跟着膨胀。如果反过来让每个字符当一个 token,模型就得啃比按词计长五到十倍的序列,而每一层的开销至少与序列长度成线性关系。
一句固定的话能把这个权衡讲得很具体。拿 "The quick brown fox jumps over the lazy dog" 来说:按字符是 43 个 token;按整词是 9 个;子词分词器恰好也是 9 个——和按词一样短,但用的是一个永远不会“缺词”的词表。
BPE 找到了一条中间路线:从原始字节(或字符)出发,反复把出现频率最高的相邻片段对合并成一个新符号,直到词表达到你想要的大小。像 the 这样的常见词最终成为单个 token;像 Antidisestablishmentarianism 这样的罕见词则分解成若干更短的熟悉片段。分词器不懂词义——它只知道哪些字节序列在训练语料中统计上更常见。由于基础字母表就是 256 种可能的字节值,分词器可以表示任何输入——任何 Unicode 字符、emoji 或文字,哪怕训练时从未见过——因此不存在“未知”或词表外的 token;陌生文本只是被切成更多、更小的(字节级)token。
Qwen3.5 的具体做法
Qwen3.5 自带一个约 248,320 个条目的 BPE 分词器。有几个细节值得了解:
- 空格住在 token 里面。BPE 算法把带空格前缀的词和裸词当作两个不同的符号,所以句首的
"cat"和句中的" cat"通常是两个不同的 token。右侧的色块用一个小圆点(·)标出前导空格,让你能看见这件原本不可见的事。 - 特殊 token 标记结构。像
<|im_start|>和<|im_end|>这样的序列不是模型要生成的文本——它们是 Qwen3.5 对话模板使用的轮次边界。只要某个 token 解码后的文本里含有<|…|>,小部件就会给它画上虚线边框,方便你认出来。 - 多语言覆盖并不均匀。拉丁字母的词通常是一两个 token;CJK 字符往往一字一个 token,但罕见文字可能分解成字节级碎片。试试多语言示例,体会其中的差别。
为什么这件事重要
语言模型的每一项下游成本都是按 token 支付的:API 计费、上下文窗口上限(模型一次能容纳多少 token 的限制)、KV 缓存内存(生成期间为每个 token 保留的工作记忆——后面章节讲),以及生成的真实耗时。一段在人看来很短的提示词,如果用了罕见标点、代码,或分词器训练时见得不多的语言,对模型来说可能很长。统计栏里的每 token 字符数(chars/token)是感受这一点的最简单方式:覆盖良好的语言里的散文通常落在 4 chars/token 附近,代码接近 2-3,而满是 emoji 或罕见文字的字符串可能掉到 1 以下。
不同模型家族使用不同的分词器,同一字符串可能切出长度天差地别的结果。这就是为什么 LLM 的成本和容量都用 “token” 而不是“字符”或“词”来谈。
看着 BPE 拼出一个 token
下面的小部件在输入 "unbelievable" 上演示一个风格化的 BPE 过程。每一步把相邻的一对片段合并成一个新符号——BPE 训练时正是对语料库把同样的操作做上千百万次,从而学出它的合并顺序。
unbelievable注意:这里的合并顺序仅为示意——它教的是“合并相邻对”这一模式。真实的 Qwen3 词表是从海量训练语料中学出来的,其合并在顺序和最终 token 边界上都会不同。
值得亲眼一看的分词怪癖
初学者在 LLM 成本或行为上遇到的大多数意外,都能追溯到下面这几条。每一行展示一个小输入,以及它通常分解成的 token。
- 前导空格
- "cat"
- 前导空格
- " cat"
- 缩写
- "can't"
- 展开形式
- "can not"
- URL
- "https://example.com"
- 代码
- "const x = 42;"
- CJK
- "你好"
- 天城文
- "नमस्ते"
- 带逗号的数字
- "1,000,000"
裸的 "cat" 是一个 token。与下面的 " cat" 对比——它们是两个不同的词表条目。
前导空格是 token 的一部分。" cat" 有自己的 id,和 "cat" 不是同一个。
BPE 把撇号拆了出来——"'t" 这个后缀足够常见,挣到了自己的 token。
两个完整的词,两个 token。这里两个,"can't" 也是两个——但模型看到的 id 截然不同。
URL 按标点碎裂。在模型眼里,协议、分隔符、主机名、顶级域名是各自独立的片段。
代码会合并常见关键字("const"),但把标点拆开。空白被吸进后面的 token。
CJK 字符通常一字一个 token,但像 "你好" 这样常见的双字组合也可能合并成一个 token——很像英文 "the" 独占一个 id。
少见文字会分解成比英文单词更多、更细的片段——这里 6 个字符变成 4 个 token——但每一片仍是可读的音节片段,而不是不透明的字节碎片。chars/token 比值远低于 1。
数字被拆成一个个数位——并没有 "000" 这个 token。逗号也单独成块,所以模型是一位一位地对数字做推理。
- 英语(English)
- The cat sat on the mat
- 中文
- 猫坐在垫子上
- 日语(日本語)
- 猫がマットの上に座った
- 阿拉伯语(العربية,RTL)
- جلس القط على الحصيرة
- 印地语(हिन्दी)
- बिल्ली चटाई पर बैठ गई
- 西班牙语(Español)
- El gato se sentó en la alfombra
同一句话在每种语言里花费的 token 数都不一样——但这并不是简单的“英语最便宜”。Qwen3.5 的 248,320 条词表为高资源语言学出了整词合并,所以这里中文反而最紧凑(4 个 token),而西班牙语和印地语最贵(9 个)。token 数只在同一个分词器内部才可比。
当文本落在这些学到的合并之外——emoji,或少见的文字——分词器就退回原始字节:像 🦫 这样的 emoji 每个要 3 个 token,阿姆哈拉语大约每字符 1.7 个 token。
真实的 Qwen3.5-0.8B 分词器输出,离线采集——精确的 token 切分(省略 token id),不是在你浏览器里的实时运行。
下一章(词嵌入)我们会看到这些 token id 如何各自变成一个高维向量,让模型真正能拿来计算——这是从整数通往注意力所在的连续空间的桥梁。
- 成本随 token 数而非字符数增长:计费、KV 缓存、延迟都是按 token 计的。
- 前导空格是 token 的一部分——"cat" 和 " cat" 通常是两个不同的词表条目。
- 不同模型家族对同一字符串的分词结果不同,所以 token 数只有在同一个分词器内才可比。
用分词器演示找一句话,让 chars/token 比值尽可能高;再找另一句,让它尽可能低。