第 2 章 · 分词(Tokenization)

解释 LLM 为什么把文本切成子词 token,而不是字符或完整的词。

模型需要一个有限的词表,在序列长度与词表大小之间取得平衡。

7 分钟
前向传播分词嵌入查表× 24 层最终 RMSNormLM head采样
术语表 · 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 训练时正是对语料库把同样的操作做上千百万次,从而学出它的合并顺序。

BPE 合并演练 · 输入 unbelievable
第 1 / 8 步
unbelievable
从字节级开始。每个字符都是独立的片段——共 12 个。
BPE 训练会统计整个语料库中每个相邻片段对的出现次数,然后永远合并最频繁的那一对。
12 个片段

注意:这里的合并顺序仅为示意——它教的是“合并相邻对”这一模式。真实的 Qwen3 词表是从海量训练语料中学出来的,其合并在顺序和最终 token 边界上都会不同。

值得亲眼一看的分词怪癖

初学者在 LLM 成本或行为上遇到的大多数意外,都能追溯到下面这几条。每一行展示一个小输入,以及它通常分解成的 token。

刁钻的分词案例
token 字符串为合理示意;具体 id 随分词器版本而变。
前导空格
"cat"
cat

裸的 "cat" 是一个 token。与下面的 " cat" 对比——它们是两个不同的词表条目。

前导空格
" cat"
·cat

前导空格是 token 的一部分。" cat" 有自己的 id,和 "cat" 不是同一个。

缩写
"can't"
can't

BPE 把撇号拆了出来——"'t" 这个后缀足够常见,挣到了自己的 token。

展开形式
"can not"
can·not

两个完整的词,两个 token。这里两个,"can't" 也是两个——但模型看到的 id 截然不同。

URL
"https://example.com"
https://example.com

URL 按标点碎裂。在模型眼里,协议、分隔符、主机名、顶级域名是各自独立的片段。

代码
"const x = 42;"
const·x·=·42;

代码会合并常见关键字("const"),但把标点拆开。空白被吸进后面的 token。

CJK
"你好"
你好

CJK 字符通常一字一个 token,但像 "你好" 这样常见的双字组合也可能合并成一个 token——很像英文 "the" 独占一个 id。

天城文
"नमस्ते"
स्ते

少见文字会分解成比英文单词更多、更细的片段——这里 6 个字符变成 4 个 token——但每一片仍是可读的音节片段,而不是不透明的字节碎片。chars/token 比值远低于 1。

带逗号的数字
"1,000,000"
1,000,000

数字被拆成一个个数位——并没有 "000" 这个 token。逗号也单独成块,所以模型是一位一位地对数字做推理。

多语言 token 地图——一句话,六种文字
采自 Qwen3.5 的 tokenizer.json;id 已省略。
英语(English)
The cat sat on the mat
The·cat·sat·on·the·mat
6 个 token
中文
猫坐在垫子上
坐在子上
4 个 token
日语(日本語)
猫がマットの上に座った
マットの上にった
6 个 token
阿拉伯语(العربية,RTL)
جلس القط على الحصيرة
جلس·القط·على·الحصيرة
5 个 token
印地语(हिन्दी)
बिल्ली चटाई पर बैठ गई
िल्ली·चटा·पर·बैठ·गई
9 个 token
西班牙语(Español)
El gato se sentó en la alfombra
El·gato·se·sentó·en·la·alfombra
9 个 token

同一句话在每种语言里花费的 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 比值尽可能高;再找另一句,让它尽可能低。

随堂测验
1. 现代 LLM 为什么用子词 token,而不是完整的词?
2. 更高的 chars/token 比值通常说明什么?
3. 句首的 "cat" 和句中的 " cat",它们的 token id 相同吗?

动手试试

互动演示加载中……