第 1 节 · Token:你和模型之间唯一的通货¶
模型不认识字,只认识 token。这一节要算清的是一笔账:你和模型之间唯一的通货长什么样、由谁定、到底贵在哪。
一、我的一段话,进模型前发生了什么¶
我随手写了一句 37 个字的话,就拿它开刀:
模型不认识字,只认识 token。你和模型之间唯一的通货,就是这一串数字。
我把它分别喂给我机器上的两个 tokenizer。第一个吐回 21 个 token,第二个吐回 19 个。
同一句话。一个字都没改。
我又去看它到底切成了什么。前 12 个 token 打印出来,是这样一堆东西:
看着像乱码。它没坏——那是字节。我那句里的"模型",UTF-8 编码是六个字节(一个字三个字节),tokenizer 把它们一个字节一个字节地打印出来,就成了 模åŀĭ。
这里有个我一开始没想通的事:token 不是"字"。 token 是"几个字节被粘成的一块"。中文一个字通常占三个字节,所以一块 token 里有时候是一个字,有时候是好几个字。两个最直白的例子都在上面那一串里:åŃĹ 这一个 token,装的是"字"这一个字;模åŀĭ 这一个 token,装的是"模型"两个字。
图 A-1-1(待出):一句 37 字中文,进两个 tokenizer,出来 21 / 19 个 token(真实切分 + id 序列)
二、切法不是我定的:词表和 BPE¶
那"怎么切"是谁定的?答案在一个文件里,不在模型里。
我打开机器上那份词表文件,数它的条目:
| 模型 | 词表文件条目 |
|---|---|
| Qwen2.5-0.5B | 151643 |
| Qwen3.5-0.8B | 248044 |
另一件很容易看漏的事,是这份词表从第 0 条开始是什么。Qwen2.5 那份的前 30 条,是 ! " # $ % & ' ( ) * + , - . / 加十个数字,再加 : ; < = >——全是单字符。一份十五万条目的词表,开头排着的是一张字符表。后面那十五万条,全都是在这张表上长出来的。
我原先以为"新一代词表 = 更大 = 更强",看到 248044 对 151643 才反应过来:它更像同一张表被焊得更深了一层,不是换了一批零件。
它们是怎么长出来的,看合并规则那张表最清楚。我打开 Qwen2.5 那份合并规则表,前 6 行是:
Ġ 是一个空格。这六行在干的事,是从最碎的单位出发,把两块焊成一块,再把焊好的当新的一块继续焊。规则按顺序排,越靠前越先焊上。整份词表就是这么一层一层焊出来的。
这个工序像铸币厂:它不是一开始就有整块的硬币,它是先有一粒粒金属屑,再按"哪两粒最常挨在一起"把它们压成更大的坯子,压一次记一条规则;压到某个深度停下,那一整套坯子,就是词表。
切法不是模型聪明出来的,是词表预先定死的。
词表尾部还留着一排保留位——它们不属于上面这套"焊出来的字",是专门留出来给模型下指令的:
| 保留位 | 编号 |
|---|---|
<|endoftext|> |
151643 |
<|im_start|> |
151644 |
<|im_end|> |
151645 |
<tool_call> |
151657 |
第一个保留位的编号 151643,正好等于 Qwen2.5 那份词表文件的条目数。也就是说:前面全是给读者看的字,从这一格开始,是给模型下的令。这份配置里还写着一个总数:model_max_length 131072。这是 tokenizer 侧声明的输入上限,不等于这个模型本体的上下文长度——而模型本体那份配置里,max_position_embeddings 只有 32768。
图 A-1-2(待出):BPE 怎么长词表——从单字节出发,一层层把碎块焊成大块
再说回第一节那两句实验:同一段中文,Qwen2.5 切 21 个,Qwen3.5 切 19 个;同一段英文,两代都是 23 个。
前后差的那两个 token 差在哪,我看到了其中一处:你和 这两个字,Qwen2.5 切成两块(ä½ł、åĴĮ),Qwen3.5 把它们焊成了一块(ä½łåĴĮ)。新一代词表把上一代的碎块又往上焊了一层。我没有逐个 token 对齐完,这只是我看到的一处,不冒充全部。
还有一件我到现在没弄明白的事,得如实记下来:同一份词表,"大小"不止一个数。 词表文件里是 151643 条,配置文件里声明的 vocab_size 是 151936;Qwen3.5 那边是 248044 对 248320。而我自己另有一份训练日志,它数出来的又是第三个数:
三个数,同一份词表。差在哪,我手上没有能证明的解释,就先记成待解的一笔。词表的大小本身就不是一个数,是一串对不上的数——这件事先摆在这,我会在后面换一个地方再回来算它。
三、切法直接决定我的钱¶
先把数字摆出来。
- 中文:37 个字 → 21 个 token(约 1.76 字/token);换了词表是 19 个 token(约 1.95 字/token)
- 英文:105 个字符 → 23 个 token(4.57 字符/token),两个 tokenizer 完全一样
- 中英混合:33 个字符 → 19 个 vs 15 个
翻译成一句能用的:一个 token 里,中文能塞 1.76 到 1.95 个字,英文能塞 4.6 个字符。反过来按字符算,每写一个字符,中文要付的 token 大约是英文的 2.3–2.6 倍(Qwen3.5 约 2.34、Qwen2.5 约 2.59)。
同一段意思,用中文写,账天生比英文厚。
但这个"2.3 倍"要往下再挖一层,不然会得出一个错的结论。词表是从字节长出来的,所以真正对齐的单位本来就是字节,不是字。中文一个字在 UTF-8 里占三个字节,英文的 ASCII 字符占一个字节:
| 口径 | 中文 | 英文 |
|---|---|---|
| 一个 token 装多少 | 约 1.9 个字 ≈ 5.7 字节 | 4.57 个字符 = 4.57 字节 |
| 一个字节要几个 token | 约 0.18 个 | 约 0.22 个 |
按字节算,中文不但不贵,还略便宜。中文显得贵,不是因为词表歧视中文,是因为中文的一个"字"天生就是三个字节。 而人报数的时候是按"字"报的,账单是按 token 报的——两份口径错位,错觉就出来了。
生成方向还有另一笔实测。我本地那个 125B 的 MoE 模型(4-bit 量化)跑一次中文写作,实测是 975 字 / 519 token / 102.7 秒,也就是大约 5.1 token/s——折算下来 1.88 字/token,正落在上面那段对照实验的 1.76 到 1.95 字/token 区间内。
这个数字口径要说清楚:它是生成侧的账,不是"输入被压缩了多少"。同一个词表,读进来和写出去都按 token 数,但两边的数不该混着用。
还有一个更极端的现场,来自我自己写的一个情感分类服务。它的词表里,「正面」和「负面」这两个标签各占 1 个 token。所以整个分类任务,机器只要读生成出来的第一个 token 的概率,就能收工。两个字,一枚硬币。
这一条最值得记住:当任务窄到一定程度,token 的粒度可以窄到"一个词就是一块"。 切法不是只能由词表说了算,它也可以按任务来定。
那这些 token 怎么变成钱?分两条路,性质不一样:
- 挂在 API 后面:token 直接就是计价单位。上面那个约 2.34 倍(按 Qwen3.5 那一代算),会一比一落到账单上。
- 跑在自己机器上:token 不当场变成钱,它变成时间(5.1 token/s)和内存。
两条路是同一件事的两面:token 是系统里唯一被计价的单位。 你写了多少字不算,你的意思有多重要不算,只有它被切出来的块数算。
贵不贵,不取决于你写了多少字,取决于你的字被切成了几块。
图 A-1-3(待出):中文 vs 英文的 token 密度对照——同一个意思,两种语言的账厚薄不同
四、为什么一个 AI System 需要它¶
回到序章那张"按疼补的零件表",第一行是 Small Model / Model Pool:一个模型不够用,按档放好几个。
这一节讲的东西,正好是那张表的分母。
我池子里放谁,表面看是"谁更强"的问题。但真正要先算的一列,是"同一份活,谁的 token 账更薄":
- 同一段中文,换一代词表,从 21 个 token 降到 19 个——约省 9.5%,一个字没改
- 同一段英文,两代都是 23 个——在这儿,换词表一个 token 都没省
- 一个窄任务(情感二分类),可以被压成 1 个 token
这三行的结论是同一个:token 账不是模型能力的副产品,它是词表、语言、任务三者一起定出来的。所以选型的时候,它必须是一列独立算的数,不能顺手带过。
延迟按它算(5.1 token/s),账单按它算,一个 token 能装多少内容还是按它算。它是所有成本共同的分母——分母错了,后面每一个决策都跟着错。
池子里放谁,不是"谁更强",是"谁更划算"。 而"划算"这两个字,得先落到 token 上,才有一个能算的数。
这一节就走到这里:一段话被切成了几块、谁切的、为什么贵。第 2 节,一个 token 进来之后,它要变成一个向量——那是它第一次有"意思"。
图 A-1-4(待出):同一段话 → 两个 tokenizer → 两份 token 账 → Model Pool 选型时怎么比
数据与出处¶
- 跨模型同段对照:一句 37 字中文、一句 105 字符英文、一句 33 字符中英混合,各在两个 tokenizer 上切一次,本机实跑、当日落盘
- 两份词表文件(vocab.json)的条目数(151643 / 248044)与前 30 条内容,以及两份模型配置里声明的
vocab_size(151936 / 248320) - Qwen2.5 的合并规则表(merges.txt)前 6 行
- Qwen2.5 的 tokenizer 配置:四个保留位的编号,以及
model_max_length131072;同一份模型本体配置里的max_position_embeddings32768 - 本地 125B MoE(4-bit 量化)暖解码实测:975 字 / 519 token / 102.7 秒
- 情感分类服务源码里的标签编码注释(「正面」「负面」各为单 token,置信度取首 token 概率)
- 一次 3 轮训练的日志:
Vocab: 248077, EOS: '<|endoftext|>' (id=248044) - 零件表与"按疼补零件"的说法,见序章
本文所有数字均来自本机模型文件与实跑记录;其中词表与配置类的数字,读者拿同一份模型文件可以复算。