Tokenizer 与 Context Window:模型眼里的世界
AI 系列第 10 篇。这一篇钻进 LLM 最底层的输入端——模型眼里的"字"长什么样。
0. 一个让 GPT-4 都翻车的问题
你: "strawberry 这个单词里有几个字母 r ?"
GPT-4 (早期版本): "2 个。"
你: "请数一遍。s, t, r, a, w, b, e, r, r, y..."
GPT-4: "对,3 个 r。"
你: "那你刚才为什么说 2 个?"
GPT-4: "对不起,我数错了。"
这是个让人很多人困惑的现象——这么聪明的模型,怎么连数字母都数不清?
答案不在它"笨",在它根本看不到字母。它看到的是 token。
"strawberry"
│
▼ tokenize (GPT-4 BPE)
["straw", "berry"] ← 模型实际看到的
模型看到两个 chunk:straw 和 berry。要它数 r,它得"想象"这两个 chunk 各自的字符——这就开始出错。
这就是 token 化(tokenization)的代价。今天我们把它讲清楚。
1. 为什么需要 tokenize?
我们在第 6 篇讲过 embedding。每个 token 对应一个向量。但为什么不直接以字符为单位?
方案 A:字符级(character-level)
"hello" → [h, e, l, l, o] → 5 个 token
优点:词汇表小(英文 26 字母 + 标点 ≈ 100 个 token)。 缺点:序列变长 5 倍。Transformer 是 O(n²),序列长 5 倍 = 计算量大 25 倍。太贵。
方案 B:词级(word-level)
"hello world" → ["hello", "world"] → 2 个 token
优点:序列短。 缺点:词汇表太大(英文 50 万词以上)。embedding 表会爆炸。而且没法处理新词——遇到 "TenggouwaGPT" 这种就完蛋了。
方案 C:子词级(subword)—— BPE
折中:把常见词当一个 token,罕见词切成多个常见片段。
"hello" → ["hello"] 1 token
"Tenggouwa" → ["Te", "ng", "gou", "wa"] 4 tokens
"running" → ["run", "ning"] 2 tokens
"antidisestablish" → ["anti", "dis", "establish"] 3 tokens
这就是 BPE(Byte Pair Encoding)——今天所有主流 LLM 都用的方案。
2. BPE 算法:用合并频率训练 tokenizer
BPE 起源于数据压缩领域,1994 年提出。NLP 圈在 2016 年(Sennrich 等)才把它捡起来用。
训练过程
Step 1:把所有训练文本切成单个字符。
"low low low low low" → l, o, w, ' ', l, o, w, ' ', ...
Step 2:统计相邻字符对的频率,合并最频繁的那对。
最频繁的字符对: 'l' + 'o' = "lo"
合并后: lo, w, ' ', lo, w, ...
Step 3:重复 Step 2,直到达到目标词汇表大小(如 50,000)。
迭代 N 次后:
"lo" + "w" → "low"
"low" + " " → "low "
...
最终你得到一个 50,000 大小的词汇表,包含:单字符、常见字根、完整常见词、空格-词组合等。
一个真实例子:GPT-4 tokenizer
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
enc.encode("Hello world")
# → [9906, 1917]
enc.encode("Hello, world!")
# → [9906, 11, 1917, 0]
enc.encode("strawberry")
# → [38088, 15717] ← straw + berry
enc.encode("我喜欢吃草莓")
# → [37046, 119, 11883, 119, 33002, ...] ← 中文要切碎得多
注意几个有趣的点:
- 空格通常和后面的词合并。" Hello" 是一个 token,"Hello" 是另一个。
- 同样的词大小写不同 → 不同 token。"Hello" vs "hello"。
- 中文比英文更费 token。同一个意思英文 1.5 个 token,中文可能 5 个。
3. Tokenizer 带来的几个"奇怪现象"
现象 1:LLM 数不清字符
如前面所说,模型看到的是 token chunk,不是字符。
"strawberry"
→ ["straw", "berry"]
→ 模型: "我看到两个东西。第一个有 5 个字符里 1 个 r,第二个有 5 个字符里 2 个 r..."
→ 出错率高
怎么解决?让模型先把单词"打成字符",再数:
prompt: "把 strawberry 拆成字符列表,然后数 r。"
GPT-4: "s, t, r, a, w, b, e, r, r, y → 3 个 r" ✅
现象 2:数字处理混乱
"3.14159"
→ ["3", ".", "14", "15", "9"]
→ 模型看到的是这几段,不知道这是一个完整数字
这是为什么 LLM 算术容易错——它看到的不是数字,是文本片段。新一代模型(Llama-3, GPT-4o)专门优化了数字 tokenization(每位单独 token),算术能力有提升。
现象 3:少见字符成本极高
"🤖" (emoji)
→ 可能被切成 4 个 byte-level token
如果一段文本有很多 emoji、罕见 Unicode、外语,token 数会爆炸。GPT-4 处理日语比英语贵 50%,处理中文贵 30%。
现象 4:tokenizer 不同 → 输出不同
同一段文本,不同 tokenizer 切法不同。GPT 用 cl100k_base,Claude 用自己的,Llama 用 sentencepiece。这是为什么换模型时 prompt 经常需要重新优化。
4. Context Window:模型一次能"看"多少 token?
Context window 是模型一次推理时能处理的最大 token 数。
GPT-3 (2020): 2,048
GPT-3.5 (2022): 4,096
GPT-4 (2023): 8,192 / 32,768
GPT-4 Turbo: 128,000
GPT-4o: 128,000
Claude 2: 100,000
Claude 3: 200,000
Claude 3.5/4: 200,000 (Opus 1M)
Gemini 1.5 Pro: 1,000,000 → 2,000,000
Llama 3.2: 128,000
从 2020 到 2026,context window 涨了 1000 倍。这是 LLM 应用层最重要的变化之一。
为什么 context 越长越好?
- 能塞更多上下文(一整本书、整个代码库)
- 支持更长的对话历史
- RAG 检索到的文档可以更多
- Agent 可以记住更多步骤
为什么 context 长这么难?
Self-attention 是 O(n²)。
context = 1K: 100 万次注意力计算
context = 10K: 1 亿次
context = 100K: 100 亿次
context = 1M: 1 万亿次
光是显存就放不下——存一个 1M context 的 KV cache 要几百 GB。
5. 突破 O(n²) 的几种工程招数
招 1:Flash Attention(2022)
不改变 attention 数学,但重写算子,让它对 GPU 显存层级更友好。同样的计算量,速度快 2-3 倍,显存省 5-10 倍。
Stanford 的 Tri Dao 写的。今天几乎所有大模型都用它。
招 2:Sparse Attention
不算所有 token 对的 attention,只算"近的"或"重要的"。
完整 attention: 每个 token 看所有其他 token
Sparse attention: 每个 token 只看附近 + 一些关键 anchor
代表:Longformer、BigBird、Mixtral 的 sliding window。
招 3:Linear Attention 系列
把 attention 从 O(n²) 降到 O(n),但要修改 attention 公式本身。
代表:Mamba(2023,状态空间模型)、RWKV、Linear Attention。
效果接近 Transformer,但不完全等价。目前没成为主流。
招 4:Ring Attention / Infini-Attention
把超长上下文切分到多个 GPU 上并行处理。
代表:Gemini 1.5 用的就是 ring attention 系。
招 5:KV Cache 压缩
KV cache 是 attention 的中间状态,占据大部分显存。压缩它可以省显存。
代表:H2O(2023)、StreamingLLM、各种 quantization。
6. Context window 长 ≠ 真的能用满
一个反直觉的事实——虽然模型支持 200K context,但实际上它"专注度"会下降。
研究者发现的 "needle in haystack"(草堆找针)现象:
把一句关键信息埋在 200K context 的中间位置。
问模型那句信息是什么。
结果: 大多数模型在中间位置准确率明显低于头尾。
这就是 "lost in the middle" 现象(2023 论文)。模型对开头和结尾的注意力强,对中间内容容易"遗忘"。
2024+ 的模型(Claude 3 / Gemini 1.5 / Llama 3.2)在这方面有显著改善,但还没完全解决。
一句你可以拿去吹的话: Context window 长不等于"模型记住所有"。一个 1M context 的模型实际可用部分可能只有 100K——剩下都是 marketing。
7. Context 长之后,一些新玩法
玩法 1:整个代码库丢进去
prompt = open("整个 repo 拼起来.txt").read() # 800K tokens
prompt += "\n\n请找出所有可能有 SQL 注入的地方。"
不再需要 RAG,直接全文输入。Claude Opus 4.7 的 1M context 就是这思路。
玩法 2:长 agent 链不会"忘"
agent 跑 100 步,每步几千 token,总共几十万 token。在 8K 上下文时代根本玩不了。
玩法 3:多文档分析
把 50 篇论文一起丢进去问"这些论文的共识是什么?"。原来需要复杂的 RAG pipeline,现在 prompt 一行。
玩法 4:长视频理解
Gemini 把视频每秒 1 帧 + 音频转 text,一起塞进 context。一个 30 分钟视频几十万 token。
8. Tokenizer + Context 是个被忽视的核心基础设施
这两件事都不性感。论文不爱写,公众不关心。但它们决定了:
- 模型成本:token 数 × 单价 = 你的账单
- 模型可处理任务的边界:超出 context 就没法做
- prompt engineering 的上限:token 不够再好的 prompt 也展不开
- 模型某些"愚蠢错误"的根源:数字母、算术、罕见词
理解 tokenizer 和 context,是从"会用 LLM"到"用好 LLM"的分水岭。
一句你可以拿去吹的话: 不懂 tokenizer 的人,永远在猜模型为什么犯傻;懂 tokenizer 的人,能预测它会在哪类问题上犯傻,并提前绕开。
9. 给你的小作业
- 去 platform.openai.com/tokenizer 把你的中文名输入,看它被切成几个 token。
- 为什么 GPT-4 对中文比对英文贵?给两个原因。
- 如果你做一个 RAG 系统,context 是 8K 还是 200K,对架构设计有什么本质区别?
下一篇钩子:到这里我们把"模型怎么训出来"和"模型眼里的世界"都讲完了。 但当你调用 GPT-4 时,你会发现还有一堆参数——temperature、top-p、思维链、structured output、reasoning effort…… 这些参数怎么影响输出?为什么 reasoning model(o1)和普通模型用法不一样? 下一篇我们讲推理时的全部"旋钮"。