主题
分词器不是公共资源:同一句话在三家数出三个 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 只是一个看起来精确的随机数发生器。