有一次我随手在 OpenAI的tokenizer演示页面 敲了个中文名字“张三”,点完“Tokenize”之后,我愣住了——它被切成了两个token:“张”和“三”。一个完整的名字,在 GPT-4 眼里,就是两个毫无关联的碎片。更夸张的是,如果你叫“欧阳娜娜”,它可能会切成“欧” “阳” “娜” “娜”四个 token,像拆积木一样把你的身份拆得稀碎。那一刻我才意识到,我们平时跟大模型说话,表面上是在输入汉字,底层其实是在玩一个乐高游戏——而中文,恰好是那个特别容易被掰碎的零件。

这背后就是 Token,大模型世界里最小的意义单元。你问 ChatGPT 一个问题,它看到的不是句子,而是一串 token 编号。一个 token 可以是一个完整的词(比如“cat”),也可以是一个词的一部分(比如“un”和“able”),甚至只是一个标点符号。在英文里,一个 token 平均对应 0.75 个单词,效率很高;但在中文里,一个 token 可能只对应一个汉字,甚至半个生僻字,同样的信息量需要多消耗 2-3 倍的 token。这就是为什么你用中文跟 GPT-4 聊天,总感觉回复得比英文“慢半拍”、成本也更高——因为你的名字在它眼里已经被切成三块了。
为了搞清楚这件事,我翻了 Sennrich 等人那篇奠定现代分词基础的论文 Neural Machine Translation of Rare Words with Subword Units,又去啃了 HuggingFace 的 Tokenizer 课程,再把 OpenAI 的 官方 token 说明 反复琢磨了几遍,慢慢才拼出完整的图景。这篇文章就是我消化后的认知过程——从 token 是什么,到它为什么这样设计,再到中文为什么总是“吃亏”,最后说说我们该怎么跟这种“歧视”和平共处。
Token 不是字,是大模型吃进去的“最小食物颗粒”
你可能会想,模型直接读汉字不就行了?干嘛非要切成 token?这就要回到一个根本矛盾:词汇量爆炸。如果模型把每个汉字都当作一个独立的输入单元,中文常用字四千多个,看起来还行。但英文呢?英文单词是拼音文字,光是“run”“running”“runner”就应该算三个词吗?如果每个词形变化都单独存一个字,词典很快就会膨胀到几百万,模型根本记不住,也学不会词汇之间的关联。
所以 NLP 工程师们想了一个折中办法:把词再拆成更小的、可复用的子词(subword)。这样,“running”可以被拆成“runn”和“ing”,模型同时学会了“runn”和“ing”之后,自己就能拼出“running”的意思,而不需要为每个时态都死记一个词。这就是 BPE(Byte Pair Encoding) 的核心思想——它也是 GPT 系列、LLaMA 等主流大模型采用的分词算法。
BPE:那个把词语大卸八块的算法,竟然是从压缩技术里偷来的
BPE 最初是 1994 年一篇数据压缩论文里提出的方法,后来被 Sennrich 等人搬到了 NLP 领域。它的工作逻辑很像一个“贪吃的合并游戏”:
- 先把训练语料里所有词拆成最小单位——比如英文字母或汉字,甚至更细的 Unicode 字节。
- 统计所有相邻字符对的出现频率,比如“e”和“r”老是一起出现,“t”和“h”也经常贴在一起。
- 把频率最高的那一对合并成一个新的符号,比如“er”或“th”。
- 重复步骤 2 和 3,一直合并,直到达到预设的词汇表大小(比如 GPT-4 的词汇表有 10 万个 token)。
这个过程就像把一堆乐高散件,按照最常见的组合方式粘成一个个小部件。英文里“the”是高频词,可能直接变成一个 token;而“unbelievable”这种低频词,会被拆成“un”“believe”“able”三个 token。HuggingFace 的课程里有一个很形象的比喻:BPE 是在用最少的零件拼出最多的单词。
但这里有一个关键点:BPE 是基于统计的,它只关心字节对出现的频率,不关心语言学上的词边界。这就为中文的悲剧埋下了伏笔。
中文为什么特别吃亏?因为 BPE 根本不懂什么叫“词”
英文单词天然有空格分隔,BPE 在一个单词内部进行合并,哪怕拆成“un” “believe” “able”,至少每个片段还是有一定意义的词根或词缀。但中文没有空格,BPE 必须从一串连续的汉字开始合并。问题是,BPE 是靠频率决策的,它会把“我们”合并成一个 token,因为“我”和“们”经常挨着;但“张三”这种低频人名,在训练语料里出现的次数远不如“张”和“三”分别与其他字组合的次数多,所以 BPE 很可能根本不会把“张三”合并成一个 token,而是保留“张”和“三”两个独立 token。
这就导致了一个反直觉的现象:越是特别、越有信息量的词,越容易被切碎。人名、地名、专业术语、生僻成语,统统被 BPE 当成了散装零件。而模型在阅读这些散装零件时,需要多消耗几步注意力,才能拼凑出原意——就像你读“张” “三”两个字,还得在脑子里再组合成“张三”这个人,比直接看到一个“张三” token 多费了一层功夫。
我用 OpenAI 的 tokenizer 做了一个简单对比,结果放在一张表里,一目了然:
| 输入文本 | 英文 token 数 | 中文 token 数 | 中文 / 英文比例 |
|---|---|---|---|
| “你好” | (“Hello”) 1 | 2 | 2× |
| “人工智能” | (“Artificial Intelligence”) 2 | 4 | 2× |
| “张三” | (“John”) 1 | 2 | 2× |
| “欧阳娜娜” | (“Olivia”) 1 | 4 | 4× |
同样的语义,中文消耗的 token 数量是英文的 2-4 倍。这意味着什么?你的 API 账单会变贵,上下文窗口会被更快填满,模型的“记忆力”会变差。因为 GPT-4 是按 token 计费的,而且一次能处理的 token 总数有上限(比如 8K 或 32K),中文用户无形中就被“惩罚”了。
三个 token 引发的惨案:名字被切碎后,大模型可能认不出你
更麻烦的是,token 碎片化会直接影响模型的理解和生成质量。我最早以为,只要模型足够大,就一定能学会把“张”和“三”自动关联起来。但后来我发现,这是一个“注意力稀释”问题。Transformer 的注意力机制需要每个 token 和其他 token 都交互一次,如果你的名字被切成三个 token,模型就需要在三个位置分别计算它们与上下文的关联,然后再通过多层网络隐式地“合成”一个人名。这种合成不是每次都成功,尤其当上下文很长、其他干扰信息很多时,模型很容易只记住了“张”而忘了“三”,或者把“张”和隔壁的某个字错误地绑定。
有一个经典的实验可以说明这个现象:你让 GPT-4 翻译“张三昨天去了北京”,它可能翻成“Zhang San went to Beijing yesterday”,没问题。但如果你让它翻译一篇小说,里面出现了十几个不同的人名,而且每个人名都只出现一两次,你会发现模型开始张冠李戴,把张三的事安到李四头上。这并不是模型“笨”,而是因为名字被切碎后,模型失去了一个稳定的身份锚点。在英文里,“John”是一个 token,注意力一抓一个准;在中文里,“张三”是散装的,注意力要在两个 token 之间来回跳,出错概率自然就高了。
这件事让我困惑了很久,直到我看到一篇分析 OpenAI tokenizer 的博客,才突然想通:Token 不仅是模型读取文本的方式,更是模型“理解”世界的颗粒度。如果颗粒度太粗,你会丢失细节;如果颗粒度太细(比如中文人名被拆成单字),你会丢失结构。一个理想的 tokenizer 应该让高频概念变为粗颗粒 token,低频概念保持细颗粒但可组合,这样模型才能高效学习。但 BPE 纯粹看频率,它根本不知道“张三”是一个概念单元,它只知道“张”和“三”各自跟别的字组队的频率更高。
我踩过的坑:以为换了中文模型就万事大吉
有一段时间,我特别迷信“中文优化”的大模型,觉得既然用中文语料训练,总该把中文词合并得更好了吧?比如用 Chinese-BPE 或者 SentencePiece 在中文上单独训练一个 tokenizer。确实,这样能把“人工智能”合并成一个 token,“张三”也可能被合并,但我很快发现了一个新问题:这种模型在跨语言任务上会变笨。因为它的词汇表被中文高频组合占满了,留给英文和代码的 token 空间就少了,导致它在写代码、翻译或用英文推理时表现下降。
这就回到了 BPE 的本质:词汇表大小是固定的,你多分给中文一些 token,英文和代码就少一些。这是一个零和博弈。所以现在的通用大模型(比如 GPT-4、LLaMA 3)都采用多语言联合训练的 BPE 词汇表,试图在几十种语言之间找一个平衡点。结果就是,中文人名这类低频专有名词,无论用什么模型,都逃不掉被切碎的命运。
还有一个坑:我尝试过在 prompt 里用拼音代替中文名字,比如写“Zhang San”而不是“张三”,以为能减少 token 消耗。实验了一下,确实 token 变少了(“Zhang San”可能只占 2-3 个 token),但模型对拼音的理解准确度直线下降,因为它训练数据里拼音远少于汉字,反而更容易出错。这让我意识到,你不能用 hack 的方式绕过 tokenization,因为模型的行为是训练数据分布决定的,不是 token 数量决定的。
怎么办?四招帮你和“散装中文”和平共处
虽然我们没法改变大模型的 tokenizer,但知道底层原理后,至少可以聪明地避开一些坑。下面是我总结的几条实用策略,它们不是魔法,但能帮你省钱、少出错、让模型更听话。
- 关键信息前置,别让名字被长上下文淹没:如果你需要模型准确记住“张三”这个人,最好在对话开头、或者一段段落的起始位置就明确写出“张三”,并且尽量用短句、让它周围没有太多干扰 token。这样模型更容易通过注意力机制把“张”和“三”固定成一组。
- 用英文别名或代号,但仅在内部使用:如果你在用 API 做批量处理,而且不要求中文输出,可以考虑把中文人名临时替换成英文缩写(如“ZS”),但记住,这只在不需要名字语义的某些任务中可行,比如数据分类。对于需要理解人物关系的任务,别这么干,会丢掉上下文。
- 选择 token 效率更高的模型:不同模型的分词器对中文友好度不同。比如,OpenAI 的 GPT-4o 和 Claude 3.5 在中文 token 消耗上略有差异,你可以通过它们的 tokenizer 工具测试一下常用文本,选择那个把你的常用词切得更少的模型。几毛钱的成本差异,乘以每天几十万 token,就是一笔不小的开支。
- 接受现实,把 token 成本计入系统设计:如果你的产品面向中文用户,在估算推理成本时,请把 token 消耗乘以 1.5-2 倍。不要用英文的基准去预估中文,否则账单会让你怀疑人生。同时,在设置上下文窗口时,中文能放进去的有效信息量天然少于英文,需要提前规划摘要策略。
几个常见误解,一次说清
“中文一个字就是一个 token,英文一个词是一个 token,所以中文总是比英文多 token?”
不是绝对的。高频中文词也会被合并成一个 token,比如“我们”“中国”“人工智能”都可能是一个 token;而英文长词也会被拆分。只是整体上,中文因为单字表意单元多,合并效果不如英文,所以平均 token 数更高。
“如果我用 SentencePiece 训练一个纯中文 tokenizer,人名就不会被切碎了?”
如果训练语料中“张三”频繁出现,它确实会被合并成一个 token。但低频人名(比如“欧阳若曦”)依然会被切碎,因为 BPE 是基于频率的,不可能覆盖所有姓名。而且,纯中文 tokenizer 会牺牲英文和代码的 token 效率,不适合通用模型。
“Token 碎片化只是增加了成本,对模型性能没影响?”
有影响。碎片化会让模型更难捕捉长距离依赖,因为注意力需要跨更多 token 传播信息。在人名、地名、专业术语等实体识别任务上,碎片化会导致准确率下降,这在一些Tokenizer 评测中已经得到验证。
“我只要在 prompt 里告诉模型‘张三是一个人’,它就能自动把两个 token 绑定了?”
模型会在训练中学习到这种共现关系,但不如直接一个 token 来得稳固。长上下文下,这种隐含绑定很容易被其他信息干扰,所以你会发现,越长的对话,模型越容易把人名搞混。
“未来会有更好的 tokenizer 解决中文问题吗?”
研究者们正在探索字节级别的模型(如 ByT5、MegaByte),试图直接处理原始字节,从而彻底消除 tokenization 带来的语言偏见。但这会使序列长度暴增,计算成本极高,目前还无法大规模应用。短期内,BPE 及其变体仍将是主流,中文的“散装”问题也会持续存在。
说到底,token 就像大模型戴的一副眼镜,这副眼镜的镜片是用英文的屈光度磨出来的。中文透过它,难免会有点散光。我们能做的,不是砸碎眼镜,而是认清散光的度数,然后调整自己的站位。下次再看到 API 账单上多出来的 token 消耗,你就知道,那不是你浪费了,而是你的名字被切成了三块,每一块都在向模型索要一点注意力——这很无奈,但也很公平,因为它对全世界所有低频名字都一视同仁。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/148.html