模型读的不是文本,而是整数。
在你的字符串和模型之间,隔着一个分词器。它悄悄决定了你要花多少钱、上下文窗口能装下多少内容,以及为什么模型数不清字母个数。
我一直把它当作预处理环节的细枝末节,直到我开始动手算账。
分词器到底做了什么
它把文本切分成小片段,这些片段来自一个固定词表——通常有3万到20万个条目——再把每个片段映射成一个id。常见词对应一个token,生僻内容则会被拆成更小的片段。
用tiktoken可以直接看到这个过程:
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
for w in ("tokenization", " tokenization", "Pneumonoultramicroscopicsilicovolcanoconiosis"):
print(repr(w), [enc.decode([t]) for t in enc.encode(w)])
'tokenization' ['token', 'ization']
' tokenization' [' token', 'ization']
'Pneumonoultramicroscopicsilicovolcanoconiosis'
['P', 'ne', 'um', 'on', 'oul', 'tram', 'icro', 'sc', 'op', 'ics', 'il', 'ic', 'ov', 'ol', 'cano', 'con', 'iosis']
常见词可能只有两个token,生僻词却可能有十七个。还有个细节:开头的空格会改变token。事情虽小,却解释了为什么提示词里空格不同,效果可能大不相同。
词表是怎么构建的
现代大模型大多采用BPE(byte pair encoding,字节对编码)的各种变体。思路简单到近乎无趣:从单个字符开始,找出出现频率最高的相邻字符对,把它们合并成一个新符号,然后不断重复。
我在一个人工编造的小语料上跑了一遍(low ×5、lower ×2、newest ×6、widest ×3):
1 merge s+t (9x) n e w e st
2 merge e+st (9x) n e w est
3 merge o+w (7x) l ow
4 merge l+ow (7x) low
5 merge w+est (6x) n e west
6 merge n+e (6x) ne west
在海量语料上重复这个操作数万次合并,就得到了一份词表。训练文本中常见的内容会变成短小、廉价的 token;不常见的内容则相反。
最后这句话就是整篇文章的核心。
各流派速览
- BPE:合并高频字符对,GPT-2 采用的方式。
- Byte-level BPE:直接在原始字节上跑 BPE,因此不存在“未知”字符。任何内容都能编码,只是可能需要很多 token。GPT-2 和 OpenAI 的 tiktoken 编码都采用这种方式。
- WordPiece:类似 BPE,但按似然而非原始频率来选择合并。BERT 采用这种方式。
- SentencePiece:直接处理含空格的原始文本(不按空白预切分),因此能很好地处理无空格语言。T5 和许多其他模型都采用这种方式。
于是我做了一轮统计
同样的字符串,用四种分词器处理:GPT-2、cl100k_base(GPT-4 时代)、o200k_base(GPT-4o 时代)以及 T5 的 SentencePiece。结果统一换算成每 100 字符的 token 数,这样长短样本之间也可以直接对比。
原始数据如下:
| 文本 | 字符数 | GPT-2 | cl100k | o200k | T5 |
|---|---|---|---|---|---|
| 英语句子 | 84 | 18 | 18 | 18 | 22 |
| Python 代码片段 | 176 | 83 | 43 | 44 | 61 |
| JSON 工具 schema | 610 | 329 | 160 | 161 | 183 |
| 一句印地语 | 74 | 111 | 78 | 24 | 31 |
| Emoji 字符串 | 12 | 28 | 32 | 18 | 8 |
(这些只是我挑选的个别样本,并非基准测试,你的文本结果会有所不同。另外 T5 和其他分词器的词表本身不同,所以这不是纯粹的“谁更好”排名。)
有几个现象很扎眼。
英语是一路坦途。各家表现几乎一样,因为每份词表主要都是基于英语构建的。
代码和 JSON 的 token 数减半。我猜是因为新版词表训练时用了多得多的代码,所以连续空格和常见的 JSON 标点都被合并成单个 token。我没有深入查过词表文件来验证这一点。
印地语才是重灾区。同一句话,GPT-2 要 111 个 token,cl100k 要 78 个,o200k 只要 24 个。用旧版编码,成本和上下文占用都是 4 倍以上,我猜生成质量也会更差,不过没有实测过。当一个字节级分词器几乎没见过某种文字时,它就会退化到逐字符切分:
'मेरा' -> ['म', 'े', 'र', 'ा'] # one token per character/mark
Emoji到处都是。
这对Agent为什么重要
- 工具schema每次调用都要付费。我那段610字符的JSON schema大约占160个token。二十个这样的工具,在循环的每一步都会产生3000多个token的开销。缩短描述、减少每次调用的工具数量、保持稳定的排序(这样提示词缓存才能生效,参见KV cache那篇)都有帮助。
-
格式化的JSON比紧凑JSON更贵。缩进就是token。试试在工具结果上用
json.dumps(obj, separators=(",", ":"))。 - 先计数,别瞎猜。用模型实际的分词器。字符数除以4对英文还算靠谱,对印地语、代码或emoji就完全失准了。
- 非英语用户付出更多——既费钱也费上下文。如果你的产品服务这类用户,请用他们的文本来测算,而不是你自己的。
- 模型的离奇失误,比如数"strawberry"里有几个r、对长数字做算术,部分原因就在分词:模型从没见过单个字母,只见过分块。
坦白说说局限
每个类别我只测了一个样本,用了四种分词器。这只是演示这种效应,不是严谨研究。另外我只展示了token数量;模型在某种语言上表现如何,取决于训练数据,不只是文本切分方式。
想试试的话:换成你自己的字符串、你自己的语言、你自己的工具schema。花十分钟,你很可能会发现意想不到的东西。我就是。

