什么是 token
语言模型不直接处理字符,也不直接处理单词。它处理的是 token——一段文本被切分后得到的片段序列,每个片段在词表里对应一个整数 ID。模型真正读到的是这串整数。
所以"这句话有多长"对模型而言不是字数问题,而是 token 数问题。上下文窗口按 token 计算,API 计费按 token 计算,生成速度也按 token 计算。
什么是 tokenizer
tokenizer 是执行切分和还原的那套程序。它做两件事:把文本变成 token ID 序列(encode),以及把 ID 序列变回文本(decode)。
这里需要区分三个经常被混用的词:
- tokenizer:执行编码与解码的组件或过程。
- encoding:具体的切分规则、正则表达式、词表和合并数据。上面演示中的
o200k_base和cl100k_base都是 encoding 的名字。 - model:消费 token ID 的模型本身。
一个 encoding 可以被多个模型使用,一个模型在不同版本里也可能换用不同的 encoding。因此本页面以 encoding 名称为准来标注结果,而不是承诺某个模型别名永远对应某套切分规则。
token 为什么不等于单词
上面的演示用的是 BPE(byte pair encoding)系列方法。它的词表不是人工整理的词典,而是从大量文本里统计出来的:越常见的片段越可能被合并成一个 token。这带来几个直觉之外的结果。
切分位置不看词义边界。 英文里 token 可能是一个 token,而 tokenization 会被拆成几段。中文里两个字的词未必被切在一起,反而可能和前后的字组合。
空格通常属于后面的词。 英文文本里 " the"(带前导空格)往往是一个 token,而不是空格和 the 分开。这就是为什么演示中很多 token 以 ␠ 开头。
一个字符可能横跨多个 token。 这是最反直觉的一点。BPE 在 UTF-8 字节上工作,不是在字符上。一个汉字占 3 个字节,一个 Emoji 常占 4 个字节;如果词表里没有正好覆盖这些字节的条目,切分就会落在字符内部。这时单独一个 token 不对应任何完整字符——演示中这类 token 用虚线框显示它的十六进制字节,而不是显示一个乱码方块,因为那个方块并不是真实内容。
Emoji 经常不止一个 token。 像 👨👩👧👦 这样的序列由多个码位和连接符组成,通常会被切成好几个 token。
两套 encoding 为什么会不同
打开「对比 cl100k_base」开关,同一段文本会同时按两套 encoding 切分。它们的差异来自几个层面。
词表规模不同。 cl100k_base 大约有 10 万个条目,o200k_base 大约有 20 万个。更大的词表能为更多常见片段各留一个位置,因此往往用更少的 token 表示同一段文本——尤其是中文、日文这类在旧词表里覆盖较少的文字。
预切分规则不同。 在 BPE 合并之前,两套 encoding 各自用一段正则把文本先切成大块,对数字、缩写、连续空格的处理并不一样。这一步的差异会一路传递到最终结果。
ID 不能跨 encoding 比较。 这是最容易犯的错。token ID 只是这套词表里的下标,12345 在两套词表里指向完全不同的片段。两个 ID 之间比大小、算差值、判断"谁更靠前",都没有意义。演示里两栏各自标注 encoding 名称、各自有独立的详情面板,就是为了不制造这种误解——两栏的第 N 个 token 之间也没有对应关系。
token 数相同不代表切分相同。 两套 encoding 完全可能对同一段文本给出一样的数量,却把边界落在不同的位置。演示会区分这两种情况:数量相同但边界不同时,会明确说出来,而不是简单显示"差值为 0"。
怎么读差值
差值只回答一个问题:这段文本在这两套词表下,token 数相差多少。它不回答哪套 encoding 更好。
换一段文本,结论完全可能反过来:对中文更紧凑的词表,未必对代码或某种欧洲语言也更紧凑。所以这里不显示"赢家",也不用颜色或箭头暗示某个方向是好结果。
怎么读这些数字
演示把指标分成两层。上面一行是文本本身的属性,两套 encoding 完全相同:
- 字符数:用户感知的字符数,用
Intl.Segmenter统计,Emoji 组合序列算作一个。 - UTF-8 字节:文本编码成 UTF-8 后的字节总数。
每一栏里则是这套 encoding 的结果:
- token 数:这套 encoding 切出的片段数量。
- 字节 / token:UTF-8 字节数除以 token 数,衡量这段文本在这套 encoding 下的编码紧凑程度。
最后一项最容易被误读。它描述的是"这套词表对这段文本的压缩效率",不是语言的表达能力、信息密度或优劣。同一种语言换一段文本、换一套 encoding,数值都会变。
如何阅读多语言图表
页面底部那张图用的是一组固定的、人工校对的平行语料:五个语义单元,每个都用简体中文、英文、日文和西班牙文表达同一件事。它和你在上面输入的文本没有任何关系,也不会因为你改动输入而变化。
翻译时没有刻意让四种语言的字符数对齐。强行对齐会写出别扭的句子,那样图表量到的就是翻译腔,而不是 tokenizer 的行为。你可以展开「查看这组语料的全部句子」核对每一句——看不到被测量的文本,数字就无法验证。
先看纵向,别急着横向比。 每种语言下面有两条柱子,分别是两套 encoding。真正有信息量的是同一种语言的两条柱子之间的落差:换到 cl100k_base 之后,中文多了约 48%、日文多了约 33%,而英文几乎没变(123 → 124)。这说明新词表把大量容量投在了 CJK 文字上。
从左到右比较不同语言当然也能读出数字,但那是最容易被误读的方向。日文的数值最高,原因是假名本身占用更多 UTF-8 字节,而且旧词表对 CJK 的覆盖较少——这是词表设计的结果,不是日语的性质。同一批句子换一套针对日文优化的 encoding,排序立刻会变。
柱子只画了总数。逐句的 token 数和字节/token 在下方的数据表里,那张表也是屏幕阅读器读到的完整数据来源。
这个演示不能推出什么结论
- 不能推出某种语言"更高效"或"更适合大模型"。这里只有一段你自己输入的文本和两套具体的 encoding。
- 不能推出哪套 encoding"更好"。差值只描述这段文本上的差异,换一段文本可能就是另一个方向。
- 不能跨 encoding 比较 token ID 的大小。ID 只是词表里的下标,
12345在两套词表里指向完全不同的片段,比较大小没有意义。 - 不能用 token 数推断模型的理解能力。切分方式和模型效果是两件事。
- 不能把这里的数字当作计费依据。真实调用还包含系统提示、消息结构和特殊标记的开销。
顺带一提:如果你在输入框里打出 <|endoftext|> 这样的字面文本,这里会把它当作普通文字切分。而在真实的 API 调用中,同样的字符可能被当作控制标记处理,这是两种不同的行为。
隐私与本地处理
你输入的文本不会离开这个页面。词表是一次性下载到浏览器的静态文件,切分全部在你的设备上完成,运行在一个独立的 Web Worker 里。
cl100k_base 的词表只在你打开对比开关时才会下载,之后缓存在内存中,反复开关不会重复请求。默认进入页面只下载 o200k_base 一套。
页面不会上传、不会保存、不会写入任何分析事件,也不会把文本放进 URL、localStorage 或 Cookie。刷新页面即回到默认状态。
实现与来源
- 切分实现:js-tiktoken 1.0.21,纯 JavaScript,随页面自托管,不从 CDN 加载;按 MIT 许可使用。
- 词表数据:
o200k_base与cl100k_base,来自上述依赖并在构建前离线导出为本站静态文件。 - 参考实现:OpenAI 的 tiktoken。
- token 的字节视图直接由词表数据构造,因此每个 token 显示的字节拼接起来严格等于原文的 UTF-8 编码。
- 多语言图表的数字由离线脚本预先算好并随页面一起发布,因此它在词表下载完成前就能显示。一条测试确保这些数字与页面自己的 tokenizer 对同一组语料的实时输出完全一致。
- 发布前另用 OpenAI
tiktoken0.13.0 独立核对 20 个样本和两套 encoding,共 40 组 token ID,结果全部一致。
当前为 prototype 状态:功能已经完整,但尚未在各类真实设备和屏幕阅读器上逐项验证过。