同一段语义,中文消耗的 token 曾是英文的近两倍——这不是编码 bug,而是词表里语言配额分配的直接后果。理解 token 膨胀的量化规律,是中文场景下成本估算、上下文规划与词表选型的前提。
token 膨胀的量化
拿五组语义等价的中英句对实测(密度越高越省;单条数值随题材波动大,只有区间与中位数有意义):
| 词表 | 中文密度 | 英文密度 | 同义 token 比(中/英) |
|---|---|---|---|
cl100k_base | 0.73–1.07 字/token(中位 0.88) | 4.6–6.8 字符/token | 1.44–2.08(中位 1.64) |
o200k_base | 1.16–1.58 字/token(中位 1.26) | 4.6–6.8 字符/token | 1.06–1.25(中位 1.14) |
最反直觉的一行是 cl100k 下中文普遍不足 1 字/token:平均每个汉字要花掉一个多 token。汉字在 UTF-8 里占 3 字节,而 cl100k 只把极少数常见汉字收进词表,其余全靠 字节级 兜底拆成 2–3 个字节片段。o200k 把密度拉到 1.26、把中英差距压到近乎持平,靠的是把大量汉字与双字词补进 merge 表。
复现(换成自己的语料再测——题材对结果的影响大于直觉):
import tiktoken, statistics as st
pairs = [
("深度学习模型的训练需要大量算力和高质量数据。",
"Training deep learning models requires substantial compute and high-quality data."),
("根据合同第三条约定,甲方应于交付后三十日内付款。",
"Per Article 3 of the contract, Party A shall pay within thirty days after delivery."),
("这家餐厅的红烧肉做得非常地道,值得再来一次。",
"The braised pork at this restaurant is very authentic and worth a second visit."),
("人工智能正在重塑软件工程的每一个环节,从需求分析到代码生成。",
"Artificial intelligence is reshaping every stage of software engineering, from "
"requirements analysis to code generation."),
("检索增强生成通过外部知识库缓解大模型的幻觉问题,瓶颈往往在检索质量而非生成端。",
"Retrieval-augmented generation mitigates hallucination via an external knowledge "
"base; the bottleneck usually lies in retrieval quality rather than generation."),
]
for name in ["cl100k_base", "o200k_base"]:
e = tiktoken.get_encoding(name)
# 中文按「字」计密度,英文按「字符」计——两者不可直接比,可比的是同义 token 比
dens = [len(zh) / len(e.encode(zh)) for zh, _ in pairs]
ratio = [len(e.encode(zh)) / len(e.encode(en)) for zh, en in pairs]
print(f"{name}: {st.median(dens):.2f} 字/token, 中/英 token 比中位 {st.median(ratio):.2f}")成本与延迟
token 比直接等比例放大三项开销:
- API 计费:按 token 计价,中文同义内容在 cl100k 时代贵约 60%。换到 o200k 系模型,溢价降到 15% 以内——换模型往往比压缩 prompt 更有效
- 有效上下文:128k 窗口在 cl100k 下约装 11 万汉字,o200k 下约 16 万。同一份长文档可能在旧词表下溢出、新词表下装得下
- 推理时延:prefill 与 decode 均按 token 计。输出端更敏感——生成 1000 汉字,cl100k 约需吐 1140 个 token,o200k 约 790 个,在相同 tok/s 下中文的字级生成速度天然慢于英文
这种不对称已被系统性讨论为公平性问题(Petrov et al., Language Model Tokenizers Introduce Unfairness Between Languages, 2023):低资源语言用户为相同语义支付数倍费用,且拿到更短的有效上下文。
词表的语言配额
词表容量是零和的。按 token 解码后的首字符所属 Unicode 区块统计(byte_frag 指无法单独解码的字节片段):
| 类别 | cl100k (100k) | o200k (200k) |
|---|---|---|
| ASCII/拉丁 | 95.0% | 64.4% |
| CJK 汉字 | 0.8% (840) | 3.8% (7532) |
| 西里尔 | 0.7% | 7.1% |
| 阿拉伯 | 0.1% | 4.0% |
| 天城体 | 0.02% | 2.0% |
| 日文假名 | 0.2% | 0.4% |
cl100k 给 CJK 的名额只有 840 个 token——不足常用汉字数量(3500)的四分之一,绝大多数汉字根本没有专属 token。o200k 把汉字配额提到 7532(9 倍),拉丁占比从 95% 压到 64%,腾出的名额均摊给非拉丁语系。
配额的分配依据是分词器训练语料的语言混比,与主模型的训练语料无关,也无法事后调整。这正是中文垂域场景做词表扩充或裁剪的动机所在,权衡见 词表设计与裁剪。
繁简、标点与混排:哪些真的贵
繁体成本远高于简体,同一语义的两种字形 token 数差近一倍:
| 文本 | cl100k | o200k |
|---|---|---|
| 软件工程的实现(7 字) | 7 | 4 |
| 軟體工程的實現(7 字) | 13 | 7 |
| 计算机网络(5 字) | 4 | 3 |
| 電腦網路(4 字) | 9 | 5 |
原因是训练语料以简体为主,繁体字多数没进词表,退化为逐字节编码。工程含义很直接:繁体输入应在计费与上下文预算里单独估算;若下游允许,简繁归一化(OpenCC)是立即见效的成本优化。
标点则是个常见的误判:全角标点确实一符一 token、且从不与汉字合并,但 ASCII 标点同样是一符一 token(well. = well + .)。标点密集的中文文本按字符算密度几乎不变(实测下降仅 2%),它不是中文特有的惩罚项,不必为此做全角转半角。
中英混排也常被当成膨胀源,实测同样不成立。单个英文技术术语普遍是 1 token,其中文译名要 2–4 个:
e = tiktoken.get_encoding("o200k_base")
# 英文带前导空格,模拟句中真实位置(GPT 系词表把空格并入词首 token)
for en, zh in [(" embedding", "嵌入"), (" tokenizer", "分词器"),
(" attention", "注意力"), (" gradient", "梯度")]:
print(f"{en.strip():10s} {len(e.encode(en))}tok vs {zh} {len(e.encode(zh))}tok")
# embedding 1tok vs 嵌入 4tok
# tokenizer 1tok vs 分词器 3tok
# attention 1tok vs 注意力 2tok
# gradient 1tok vs 梯度 2tok
# 「嵌」这种绝非生僻的字连专属 token 都没有,直接退化成 3 个字节片段
ids = e.encode("嵌入模型")
print(len(ids), [e.decode_single_token_bytes(i).decode("utf-8", "replace") for i in ids])
# 5 ['�', '�', '�', '入', '模型']整句层面混排大致收支平衡(中文虚词本身很省),但结论足够明确:技术文档保留英文术语不会增加 token 成本,不必为省钱强行全译。
CJK 无空格的预切分
预切分正则是 token 膨胀的结构性源头。cl100k 的字母分支是 [^\r\n\p{L}\p{N}]?+\p{L}++——贪婪吞掉连续字母。对空格分隔的语言,这等价于「一词一块」;对 CJK,整句话是一个块:
import regex, tiktoken
pat = tiktoken.get_encoding("cl100k_base")._pat_str
print(regex.findall(pat, "深度学习模型 needs compute"))
# ['深度学习模型', ' needs', ' compute'] —— CJK 整段成一块,英文按空格切开后果是 merge 在 CJK 内部没有任何边界约束,token 划分完全由语料频次决定,与语言学词边界系统性错位:
| 文本 | o200k 切分 |
|---|---|
| 中华人民共和国 | 中华人民共和国(7 字 1 token) |
| 人工智能 | 人工 + 智能 |
| 深度学习 | 深 + 度 + 学习(把词切穿) |
| 没关系 | 没 + 关系 |
高频专名压成单 token,而「深度学习」这类词却被切穿——不要假设 token 边界等于词边界,任何依赖 token 对齐做实体标注或 logit bias 的逻辑都会踩坑,相关陷阱见 Tokenizer 对推理的影响。
自建中文分词器的两条主流路线:
-
纯统计,不做语言学预切分:SentencePiece 的默认做法。关键参数是
--character_coverage(中文取 0.9995 而非拉丁语的 1.0,尾部生僻字交给--byte_fallback)与--max_sentencepiece_length(不限制会学出十几字的长串,泛化差)spm_train --input=zh.txt --model_prefix=zh_bpe --model_type=bpe \ --vocab_size=64000 --character_coverage=0.9995 \ --byte_fallback=true --max_sentencepiece_length=6 \ --split_by_unicode_script=true # 中英数字边界强制切开,避免跨语种 merge -
jieba/LAC 预切分后再训子词:把词边界作为硬约束灌进 merge 训练。token 与词对齐、可解释性好,但引入外部分词器的错误传播,且推理时必须携带同一套词典,多语言混排场景反而更脆
主流中文大模型(Qwen、DeepSeek、GLM)选的都是第一条路线:纯统计 + 字节兜底,把汉字与双字词的配额提到 3 万以上,chars/token 普遍达到 1.5–1.7,明显优于通用词表。这也说明中文的 token 效率是词表配额问题,不是算法问题——算法层面的取舍参见 Tokenizer 概述,OpenAI 侧词表的具体实现见 Tiktoken。