跳转至

第 1 节 · Token:你和模型之间唯一的通货

模型不认识字,只认识 token。这一节要算清的是一笔账:你和模型之间唯一的通货长什么样、由谁定、到底贵在哪。

一、我的一段话,进模型前发生了什么

我随手写了一句 37 个字的话,就拿它开刀:

模型不认识字,只认识 token。你和模型之间唯一的通货,就是这一串数字。

我把它分别喂给我机器上的两个 tokenizer。第一个吐回 21 个 token,第二个吐回 19 个。

同一句话。一个字都没改。

我又去看它到底切成了什么。前 12 个 token 打印出来,是这样一堆东西:

模åŀĭ  ä¸į认è¯Ĩ  åŃĹ  ï¼Į  åıª  认è¯Ĩ  Ġ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 行是:

Ġ Ġ
ĠĠ ĠĠ
i n
Ġ t
ĠĠĠĠ ĠĠĠĠ
e r

Ġ 是一个空格。这六行在干的事,是从最碎的单位出发,把两块焊成一块,再把焊好的当新的一块继续焊。规则按顺序排,越靠前越先焊上。整份词表就是这么一层一层焊出来的。

这个工序像铸币厂:它不是一开始就有整块的硬币,它是先有一粒粒金属屑,再按"哪两粒最常挨在一起"把它们压成更大的坯子,压一次记一条规则;压到某个深度停下,那一整套坯子,就是词表。

切法不是模型聪明出来的,是词表预先定死的。

词表尾部还留着一排保留位——它们不属于上面这套"焊出来的字",是专门留出来给模型下指令的:

保留位 编号
<|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。而我自己另有一份训练日志,它数出来的又是第三个数:

Vocab: 248077, EOS: '<|endoftext|>' (id=248044)

三个数,同一份词表。差在哪,我手上没有能证明的解释,就先记成待解的一笔。词表的大小本身就不是一个数,是一串对不上的数——这件事先摆在这,我会在后面换一个地方再回来算它。

三、切法直接决定我的钱

先把数字摆出来。

  • 中文: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_length 131072;同一份模型本体配置里的 max_position_embeddings 32768
  • 本地 125B MoE(4-bit 量化)暖解码实测:975 字 / 519 token / 102.7 秒
  • 情感分类服务源码里的标签编码注释(「正面」「负面」各为单 token,置信度取首 token 概率)
  • 一次 3 轮训练的日志:Vocab: 248077, EOS: '<|endoftext|>' (id=248044)
  • 零件表与"按疼补零件"的说法,见序章

本文所有数字均来自本机模型文件与实跑记录;其中词表与配置类的数字,读者拿同一份模型文件可以复算。