---
url: /basics/tokenizer-differences.md
description: >-
  前端估算 token 最常见的错，是拿一家的词表数另一家的账。这篇讲 tiktoken 的 o200k_base 与 cl100k_base
  差在哪、Gemini 和 Claude 各自怎么数、js-tiktoken 在前端能用到什么程度、OpenAI 的 top_logprobs
  为什么最多只能取 20 个，以及我在一个多模型产品里怎么把估算和实际用量对上。
---

# 分词器不是公共资源：同一句话在三家数出三个 token 数

前端做长度限制、做费用预估、做「还剩多少额度」的进度条，都得先数 token。多数人的做法是抄一段 `js-tiktoken` 的代码，从此再也没核对过。这篇文章讲的就是这里面的坑。事实部分核对于 2026-10-28。

## 同一个字符串，三家三个数

同一个中文字符串，OpenAI、Gemini、Claude 数出来的 token 数不一样。这不是四舍五入的差别，是词表不同。

OpenAI 系列用 tiktoken，现在主流是 `o200k_base`（约 20 万词表）。更早的 `cl100k_base`（约 10 万词表）对非英文更不友好，同一个中文句子通常数出更多 token。

Gemini 用 SentencePiece（Google 的分词方案）。Claude 的分词器官方没有公开，只能调接口数。

所以三家的数字不能互相替代。拿 `o200k_base` 数出来的值去卡 Gemini 的请求，等于用一把不确定的尺子量东西。

## tiktoken 是怎么切的

BPE（Byte Pair Encoding，字节对编码）的思路是：从字节开始，反复把最常见的相邻 pair 合并成一个新 token。合并表训练完后固定下来，就是词表。

它有三个特点。第一，可逆，token 能完整还原成原文。第二，能处理任意字节序列，没见过的新词也不会报错。第三，压缩率大致稳定，英文里平均一个 token 对应约 4 个字节。

词表大小直接影响中文。`o200k_base` 的 20 万词表里，中文 token 比 `cl100k_base` 那 10 万更多，同一个句子切得更整。我拿同一段前端文档标题试过，两种词表的差值能到 15%。

数字也一样。`123456` 在有的词表里是一个 token，在另一些里是 `123` 加 `456`。你写「版本号 3.2.1」这种内容，切法完全看运气。

## 前端能用的本地方案

浏览器里能直接用的包是 `js-tiktoken`（OpenAI 官方维护，npm 上 `js-tiktoken` 最新 1.0.21）。用法和 Python 版一致：

```ts
import { getEncoding } from 'js-tiktoken';

const enc = getEncoding('o200k_base');
const n = enc.encode('你好，前端').length;
```

它把词表数据一起打包，体积在 3MB 这个量级。聊天页做「超出上下文」的实时提示够用，但它有代价：主线程首次加载要解析一大坨 JSON，白屏时间会变长。我一般把它扔进 Web Worker，或者只在上传长文档时才动态 import。

想更轻，可以用 `gpt-tokenizer`，它按模型裁剪词表，体积小得多。想跟服务端完全一致，还是调厂商的计数接口最稳。

## 各家的计数接口

本地算只是估算。要准，用厂商自己给的接口。

OpenAI 在 Responses API 上有 `top_logprobs`，取 0 到 20 的整数，控制每个位置返回多少个候选 token 及其对数概率。这个参数必须配合 `include: ["message.output_text.logprobs"]` 才有数据。

Gemini 用 `responseLogprobs`（布尔）加 `logprobs`（1 到 20 的整数），结果在 `candidates[].logprobsResult` 里，分 `topCandidates` 和 `chosenCandidates` 两份。选中的 token 不一定是概率最高的那个，这两份分开看才有意义。

Claude 的 Messages API 不支持 logprobs。要走 OpenAI 兼容层的话，`logprobs` 字段会被直接忽略。所以接 Claude 就只能按字符粗估。

注意 OpenAI 的 20 上限不是拍脑袋。它跟返回体大小有关：每个候选 token 都要带 token 字符串和 logprob，位置一多，一次长回答的响应体能涨几倍。想在客户端做「实时显示模型置信度」，这个上限决定了你能画多细。

## 我的用法：估算归估算，记账归记账

我现在的产品接了三家模型，前端只做提示，不做判决。

展示层用 `js-tiktoken` 数用户输入，给出「约 320 token」这种量级参考，并且明确写成估算值。真正决定能不能发请求的，是服务端返回的 usage 字段。

有一个坑我踩过。我按 `o200k_base` 估一个 128K 的窗口，用户贴了一段代码，本地数出来 30K，发出去却被拒。排查半天，才发现那段代码里的中文注释在 Gemini 那边数出来多了一截，而目标模型不是 OpenAI。

后来我把逻辑改成：本地估算只用于「提前警告」，超过窗口 80% 就提示，不拦截。真正卡线靠服务端错误码，再回写一个校准系数。

## 校准比估算重要

估算值一定有偏差，但偏差可以是常数。

我会在每次请求后，把本地估算值和响应里的真实 token 数做差，攒够 100 次算一个比值。这个比值会漂，模型升级、词表换代都会变，所以按周重算一次。

用法很直接：前端进度条用估算值除以校准系数。用户看到的余量从此没再骗过人。

先把校准循环建起来，再谈估算精度。没有它，你的 `js-tiktoken` 只是一个看起来精确的随机数发生器。
