为什么 AI 数不清 strawberry 里有几个 r:它压根没看见字母
问大模型 strawberry 里有几个 r,它答两个;问 9.11 和 9.9 哪个大,它也常错。同一个模型却能写出能跑的代码。用分词器实测一遍就明白:模型收到的不是字,是一串编号,strawberry 在句子里是一个整块,字母从来没在它面前展开过。顺着这条线还能解释三件事:多位数加法为什么错、中文为什么比英文贵一倍多、prompt 里一个空格为什么会影响输出。
去年有个梗传了很久:问 ChatGPT “strawberry 里有几个 r”,它答两个。正确答案是三个。很多人拿这个证明大模型连小学生都不如。可同一个模型能写出能跑的 Python,能翻译合同条款。这种能力错位,光用”它笨”解释不通。
我自己第一次认真想这个问题,是在算一份文档的费用时:一篇中文文章和它的英文翻译,篇幅差不多,账单上中文却贵了将近一倍。两件事看起来不相干,根子是同一个:模型从来没有”看见”过字,它看见的是编号。
切块与编号
大模型读文本之前有一道预处理工序,叫分词(tokenization)。它拿一张固定的词表,把输入的字符串切成一块一块,每块换成一个整数编号,模型收到的就是这串整数。
词表怎么来的:在训练之前,用一个叫 BPE(字节对编码)的算法在海量语料上统计,哪两个相邻片段最常一起出现就合并成一个新条目,反复合并,直到词表达到预设大小。常见的词表规模是十万到二十万条。
我用 GPT-4 那代分词器实际切了一下(工具是 OpenAI 开源的 tiktoken,谁都能跑):
- 单独的 strawberry 被切成三块:str、aw、berry,编号 496、675、15717。
- 句子里的 strawberry,也就是前面带一个空格的形式,是一整块,编号 73700。
第二条比第一条更说明问题。在”I like strawberry”这句话里,模型收到的是一个整数 73700。它当然知道 73700 大致对应草莓,训练里见过无数次。但”这个编号里有几个字母 r”这件事,从来没在它面前展开过。就像你记得朋友的电话号码,却没法从号码里数出他名字有几画。

这也解释了它为什么错得那么有规律。它不是随机瞎猜,是凭着对拼写的模糊记忆估。berry 里的两个 r 很显眼,str 尾巴上那个容易漏,估成两个,是很合理的错法。
补一句现状:现在的推理模型多半能答对这题。看它的思考过程会发现,它先把单词一个字母一个字母拆开写出来,s-t-r-a-w-b-e-r-r-y,然后数。这一步等于把一个整块编号重新展开成十个单字母编号,是绕开分词的办法,不是分词变了。
数字也被剪错了位
同一个道理放到数字上,后果更直观。
GPT-4 那代分词器对数字的规矩是从左边起三位一切。“1234567”切成 123、456、7。念数字时还凑合,做加法就麻烦了。“12345 + 67890”切出来是 123、45、+、678、90。人做竖式,个位对个位,进位从右往左走;模型拿到的块,123 对着 678、45 对着 90,从左边开始分组,跟右对齐的进位规则正好拧着。它算错多位数加法,很大一部分是这个原因。
再看那个被嘲笑得更多的问题:“9.11 和 9.9 哪个大”。切完是 9、.、11 和 9、.、9。小数点后面,一边是编号 11,一边是编号 9。模型见过太多”11 比 9 大”的上下文,章节号、版本号、日期,要它在这里想起来”小数点后的 11 其实是 0.11”,需要额外一步推理。答错不奇怪。

所以让模型做算术,最稳的办法不是换更强的模型,是让它写代码算,或者接一个计算器工具。前天讲 tool calling 那篇说的”把模型不擅长的事交给工具”,算术是最典型的例子。
中文为什么更贵
回到我的账单。
同样的算法,词表里收录什么,取决于语料里什么多。英文语料多,常见英文单词几乎都是一个整块,前面带空格的形式也单独收录了。中文语料相对少,很多汉字没进词表。没进词表的字怎么办:退回到字节。一个汉字在 UTF-8 里占三个字节,就切成三块。
还是 GPT-4 那代分词器,实测:
| 文本 | token 数 |
|---|---|
| The quick brown fox jumps over the lazy dog | 9 |
| 敏捷的棕色狐狸跳过了懒狗 | 24 |
| Please translate this passage into English and explain how each word is used. | 14 |
| 请把这段话翻译成英文,并解释每个词的用法。 | 25 |
同样的意思,中文要一倍多的 token。token 是按量计费的单位,也是上下文窗口的单位,所以中文用户在同一个模型上,付得更多、装得更少。

好消息是这个差距在缩小。GPT-4o 那代换了二十万条目的新词表,中文词收得多了:狐狸那句从 24 降到 13,“人工智能”从 5 块变成 2 块,就是”人工”和”智能”。各家模型的分词器不一样,Claude 的没有开源,但趋势相同:语料越均衡,非英文越便宜。
一个实用的推论。之前讲上下文窗口那篇说窗口是按 token 算的,你要估一篇中文文档能不能塞进去,别拿英文”四个字符一个 token”的经验套,按一个汉字一到两个 token 估更保险,要准数就用官方的 token 计数接口算。
几个顺手的推论
空格和大小写不是小事。hello、Hello、前面带空格的 hello,是三个不同的编号。模型当然学会了它们是同一个词,但进模型的是三个不同的输入。prompt 里多一个空格、换个大小写,喂进去的东西就变了。大多数时候没影响,偶尔影响输出的稳定性,就是从这来的。
词表里有幽灵。2023 年有人发现 GPT 系列对 SolidGoldMagikarp 这类字符串反应异常,问它是什么,它答非所问,甚至复读。后来查出这些字符串是 Reddit 一个计数板块里高频出现的用户名,建词表的语料里有它们,进了词表;训练模型的语料多半把这部分清掉了。于是这个编号存在,模型却从没学过它是什么。词表和模型是两次分别训练的产物,中间能对不上。
术语拆得很碎。Kubernetes 被切成 K 和 ubernetes;一条 kubectl get pods 命令六七块。这不影响理解,但意味着技术文档、代码、日志这类文本 token 密度高,同样的字符数比自然语言更占窗口。
max_tokens 不是字数。调 API 时限制的是输出 token 数。让模型写中文,一个 token 对应不到一个汉字,按字数套会被截断。
我的看法
分词是整条链路里最”土”的一环:不是神经网络,就是一张查找表加一个贪心合并算法,训练前定死,之后再也不动。它决定了模型看世界的最小颗粒。很多被当成”模型智力问题”的现象,往下追一层,是颗粒度的问题:字母、数字的个位、汉字的一半,在它眼里根本不是独立存在的东西。
知道这一层,用起来会踏实很多。让它数字母、做算术,别跟它较劲,给它工具或者让它先展开;估中文成本,别套英文经验;prompt 里的空格和大小写,别随手改。剩下的事,它是真强。
评论