HelloAI
L4 第 2 篇 🐣 难度 🕒 14 分钟

Tokenizer 与 BPE:LLM 看到的不是字,是 token

为什么 GPT 把"strawberry"切成 4 个 token?为什么数错"strawberry 有几个 r"?这些都和 tokenizer 有关。

阿莱
2026/6/17

L0-11 词汇表里说过:LLM 不看字,看 token

那 token 到底怎么来的?这一篇打开 tokenizer 的”黑盒”,看清 LLM 输入端的真相。

🎮 建议先打开 Tokenizer Playground 可视化 玩 5 分钟——同一句话被 GPT/Claude/Llama 切成完全不同的样子。本文是它的伴读。

第一站:为什么不直接用”字”

最朴素的做法:每个字母 / 汉字 = 一个 token。

方案优点缺点
字符级词表小(英文 ~128,加中文 ~5万)序列太长(“Hello”变 5 个 token),训练慢
词级序列短词表巨大(英文几十万词),未登录词(OOV)无法处理

两个极端都不好。最终方案是”子词级”——subword tokenization

第二站:BPE 算法的故事

BPE(Byte Pair Encoding) 本来是 1994 年的数据压缩算法

2015 年研究者发现它意外地适合做 NLP 分词。今天 GPT、Llama、Mistral 都用 BPE 的变种。

BPE 训练的核心思路

  1. 初始词表 = 所有单个字符
  2. 数训练语料里”最常出现的字符对”
  3. 把它合并成一个新 token,加进词表
  4. 重复几万次,直到词表达到目标大小

让我们手动跑一遍。

语料

low low low low low
lower lower
newest newest newest
widest widest

Step 0: 初始词表

每个字符 + 空格符号:

{ 'l', 'o', 'w', 'e', 'r', 'n', 's', 't', 'i', 'd', '·' }  # · 表示词尾

每个词切成字符序列:

l·o·w·</w>   出现 5 次
l·o·w·e·r·</w>   出现 2 次
n·e·w·e·s·t·</w>   出现 3 次
w·i·d·e·s·t·</w>   出现 2 次

Step 1: 数最频繁的字符对

字符对出现次数
(l, o)5+2 = 7
(o, w)5+2 = 7
(e, s)3+2 = 5
(s, t)3+2 = 5
(n, e)3

最高的是 (l, o)(o, w)——都 7 次。假设我们合并 (l, o)

Step 2: 合并 → 新 token “lo”

更新所有词:

lo·w·</w>   出现 5 次
lo·w·e·r·</w>   出现 2 次
n·e·w·e·s·t·</w>   出现 3 次
w·i·d·e·s·t·</w>   出现 2 次

词表新增 'lo'

Step 3: 重新数

现在最频繁的是 (lo, w) = 7 次。合并 → 'low'

low·</w>   出现 5 次
low·e·r·</w>   出现 2 次
n·e·w·e·s·t·</w>   出现 3 次
w·i·d·e·s·t·</w>   出现 2 次

Step 4: 继续合并

接下来可能是 (e, s)(es, t)(low, </w>)……

每次合并都把语料中最频繁的模式打包成一个 token。

训练结束

通常合并 5 万–10 万次,得到一个 5 万–10 万大小的词表。

这就是 BPE 训练

第三站:BPE 如何切新词

训练完拿到词表后,编码新词的过程其实是”贪心匹配最长前缀”

"lower" 怎么切?

step 1: 找最长能匹配的前缀
  "lower" 在词表? 不在
  "lowe"  在词表? 不在
  "low"   在词表? ✓
  → 拿走 "low",剩 "er"
step 2:
  "er" 在词表? ✓
  → 拿走 "er",剩空

结果: ["low", "er"]   ← 2 个 token

这就是为什么”lower”在 GPT 里通常是 1-2 个 token

第四站:BPE 的几种变种

1. 字节级 BPE(GPT 用的)

不在字符上做 BPE——直接在字节上做。

为什么?因为 Unicode 太大(几万个字符),字节级只有 256 种,覆盖性更好。

代价:中文每个字 3 字节,所以”你好”在 byte-BPE 上有时被切成 6 个 token。这也是为什么中文 API 调用比英文贵 2-3 倍——L0-09 提过的现象,源头在这里。

2. SentencePiece(Llama / T5 用的)

Google 开发的库。 不需要预切词(不像 BPE 假设”词之间有空格”),适合中日韩这种没明显词边界的语言。

特点:用 (特殊下划线)表示词前空格。

"Hello world" → ["▁Hello", "▁world"]
"你好世界"   → ["▁你好", "世界"]  (SentencePiece)

3. WordPiece(BERT 用的)

类似 BPE,但合并准则不一样——它优化的是语言模型的对数似然而不是单纯”最频繁”。

特点:用 ## 表示非词首子词。

"unhappy" → ["un", "##happy"]

第五站:用代码做一遍

# 用 HuggingFace tokenizers 训一个简单的 BPE
from tokenizers import Tokenizer, models, trainers, pre_tokenizers

tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()

trainer = trainers.BpeTrainer(
    vocab_size=5000,
    special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]"]
)

# 训练
tokenizer.train(files=["my_corpus.txt"], trainer=trainer)

# 用它编码
encoded = tokenizer.encode("Hello, AI world!")
print(encoded.tokens)  # ['Hello', ',', 'AI', 'world', '!']
print(encoded.ids)     # [4567, 89, 1234, 567, 90]

或者直接调商用 tokenizer:

# tiktoken 是 OpenAI 开源的 GPT tokenizer
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")  # GPT-4 用的
tokens = enc.encode("Hello, AI world! 你好!🌍")
print(tokens)          # token IDs
print(len(tokens))     # 总 token 数
print([enc.decode([t]) for t in tokens])  # 还原每个 token

第六站:为什么 GPT 数不清字母

L0-04 我们说 LLM 会幻觉。有些幻觉是因为 tokenizer——

问 GPT-4:“strawberry 里有几个 r?” 它经常答错。

原因:

"strawberry" → ["st", "raw", "ber", "ry"]   # 4 个 token

GPT 看不到单独的字母——它看到的是 4 个 token 组成的”块”。它没有”字母级”的认知——只有”token 级”。

这也解释了:

  • 为什么 GPT 算字符数容易错
  • 为什么处理 anagram 类问题不行
  • 为什么提示词错别字会让它”理解错”——一个错字可能让整个 token 序列完全不同
⚠️ 一个反直觉

LLM 不”读”句子——它读 token 序列。对人类显而易见的”字母组成”,对 LLM 来说是隐藏在 token 后面的内部细节。

第七站:tokenizer 影响一切

维度tokenizer 怎么影响
API 费用按 token 收费——切得越细越贵
上下文长度”128k 上下文”是 128k token,不是字
训练效率token 越少,前向计算越快
多语言能力词表里有多少中文、阿拉伯文 token 决定了模型在那门语言上的表现
生成多样性tokenizer 决定了模型能”说”出哪些组合

一个关键发现:Llama 系列在中文上的能力,被它的 tokenizer 严重拖累。它的词表 90% 是英文 token,中文的字几乎都是 byte 级切分——意味着它”读”中文比”读”英文费力 3 倍。Llama 3 后来扩了多语言词表,但仍然不如专门为中文优化的 Qwen。

第八站:高级话题

Tokenizer 是固定的吗?

训练时定,之后不能改

如果你想给模型加新的”专有名词 token”(比如某个公司名),必须重训模型——这是个工程难题。

能不能不用 tokenizer?

有研究在尝试”无 tokenizer” LLM(如 ByT5,纯字节级)——但代价是序列变长,计算量增加 10 倍。

未来可能有混合方案:粗粒度 + 字节级冗余。

Tokenizer 也是攻击面

恶意攻击者可以用”特殊 token 字符串”让模型行为异常。 例如某些 token 在训练时几乎没见过——它们触发的模型行为完全不可预测。

一句话总结

Tokenizer = LLM 的输入端。

它决定了模型怎么看世界、为什么算不清字母、为什么中文贵。

改变 tokenizer,就是改变 LLM 的基础设施。

想”看见”它

👀 Tokenizer Playground 可视化 —— 输入文字,看 GPT/Claude/Llama 怎么切,对比 token 数差异,估算 API 费用。

🔬 L4 路径下一站

你已经懂了 tokenizer(输入端)。接下来要懂”位置编码”(让 Transformer 知道 token 顺序)和”采样”(输出端的概率分布怎么选词)。

  • L4-03 · 位置编码(RoPE / ALiBi)
  • L4-04 · 采样策略(结合 LLM 采样可视化)
📬

读到这里说明你认真在学 🎯

订阅每周精选 —— 下一篇新文章 / 新可视化第一时间送到邮箱。

💬

讨论区

· 用 GitHub 账号登录评论
⚠️ Giscus 评论未配置 —— 在 src/components/Comments.astro 顶部填入 仓库 ID 和分类 ID(见组件注释里的配置步骤)。