← 返回网页版

Tokenizer Explorer

在浏览器里并排对比 o200k_base 与 cl100k_base 如何切分中文、英文、Emoji 和代码,查看每个 token 的 ID 与 UTF-8 字节,并用平行语料观察四种语言的差异。

2026-07-28Lingming约 2 分钟 https://lingming.blog/labs/tokenizer-explorer/

什么是 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 tiktoken 0.13.0 独立核对 20 个样本和两套 encoding,共 40 组 token ID,结果全部一致。

当前为 prototype 状态:功能已经完整,但尚未在各类真实设备和屏幕阅读器上逐项验证过。

延伸阅读

  • How to count tokens with tiktoken
  • tiktoken 仓库
Lingming 灵明 · https://lingming.blog/labs/tokenizer-explorer/