如何统计 Token 数量并估算 LLM API 成本
使用 OpenAI、Claude、Gemini 和 Llama 的 tokenizer 精确计算 LLM token,并估算 API 成本、上下文窗口和计费。
要准确统计 token 数量,需要将完整的请求体交给你实际调用的那个模型的 tokenizer 处理,然后按照 (input_tokens ÷ 1,000,000) × input_rate + (output_tokens ÷ 1,000,000) × output_rate 估算成本,其中的费率取自服务商定价页面上的当前数值。
没有人会提前把这件事算清楚。它往往出现在账单到账的那个早晨,或者某段长对话开始向真实用户抛出上下文窗口错误的那个下午——突然之间,“这到底是多少个 token?“成了唯一重要的问题。尴尬之处在于:token 不等于单词,计数结果取决于模型,而且你被计费的内容中有一半根本不会出现在你的 prompt 字符串里。
本文提供一套可复用的方法:什么时候粗略估算就够用、如何针对各家服务商获得精确计数、到底哪些内容会被计费,以及如何把计数转化为一份能经受价格变动考验的成本预测。
核心要点
- 使用你所调用模型对应的 tokenizer 来统计 token:OpenAI 用 tiktoken,Claude 用
messages.countTokens,Gemini 用countTokens,Llama 则用该模型在 Hugging Face 上发布的 tokenizer。 - 诸如「字符数 ÷ 4」这类启发式方法可用于容量规划,但绝不可用于计费;面对代码、JSON、非英语文本和 emoji 时它们都会失效。
- 被计费的 prompt 是完整的请求体,包含 system prompt、角色框架、工具 schema 以及重复发送的对话历史,而不仅仅是用户的那条消息。
- 输入 token 数是确定性的,输出 token 数则不是:采样 50–200 个真实请求,按平均输出长度规划成本,按 p95 设置
max_tokens。 - 单次请求的预估成本为 (input_tokens ÷ 1M) × input_rate + (output_tokens ÷ 1M) × output_rate,费率需实时从服务商定价页面读取。
为什么 Token 不等于单词?
token 是由子词 tokenizer 生成的、与具体模型绑定的文本单位,它既不对应单词也不对应字符。基于 byte pair encoding 构建的 tokenizer(如 OpenAI 的 tiktoken)会把高频出现的字符序列合并为单个 token,并把罕见词切分成若干片段。单词 “idempotency” 在 cl100k_base(GPT-4 时代的编码)下被编码为四个 token(“id”、“emp”、“ot”、“ency”),而在 o200k_base(当前 OpenAI 模型使用的编码)下则是三个(“id”、“empot”、“ency”)。
最后这一点对计费至关重要:切分方式因模型而异。同一句话在 GPT、Claude、Gemini 和 Llama 的 tokenizer 下会得到不同的计数,因为每个 tokenizer 都是在不同数据上、以不同词表训练出来的。用错误的 tokenizer 得到的任何计数都只是猜测。
什么时候粗略估算就够用?
对于英语散文,字符数 ÷ 4 或单词数 × 1.33 已经足够接近,可以用来确定数据库字段大小或勾勒容量规划。启发式方法适用于容量规划,绝不适用于计费或上下文窗口的判断。
这些启发式方法恰恰在生产流量所在之处失效:代码、JSON、非英语文本和 emoji。结构化载荷会在标点和空白模式上被切分,而字符计数完全忽略了这一点;单个 emoji 可能扩展成数个 token,因此「字符数 ÷ 4」会严重低估富含 emoji 的字符串。在英语散文上尚且温和的跨 tokenizer 差异,在代码和结构化数据上会显著放大——而这恰恰是摘要器或 agent 所发送的内容类型。
哪个 LLM Token 计数器能给出精确结果?
原则一句话即可概括:使用你所调用模型对应的那个 tokenizer 来计数。各服务商的路径如下:
| 服务商 | 精确计数途径 |
|---|---|
| OpenAI | tiktoken,或在 Node 与 edge 运行时中使用 js-tiktoken |
| Anthropic | count-tokens 端点,TypeScript SDK 中为 client.messages.countTokens() |
| Gemini | @google/genai SDK 中的 ai.models.countTokens() |
| Llama 及其他开源模型 | 该模型在 Hugging Face 上发布的自有 tokenizer |
在 JavaScript 中,js-tiktoken 是纯 JS 移植版本,因此无需加载 WASM 二进制文件,也无需手动释放内存,而且你可以只引入单个编码而非整套编码,从而保持较小的打包体积:
import { Tiktoken } from "js-tiktoken/lite";
import o200k_base from "js-tiktoken/ranks/o200k_base";
const enc = new Tiktoken(o200k_base);
const count = enc.encode("Summarise this ticket thread for support.").length;
Anthropic 的端点可免费调用,仅受其自身的速率限制约束,因此没有理由出于成本考虑而用其他服务商的 tokenizer 去近似 Claude 的计数。但应把它的结果视为权威的预检计数,而非精确值:Anthropic 在文档中将其定义为估算值,实际计费数字来自响应中的 usage 字段。此外,即便在同一家服务商内部,tokenizer 也会随模型代际更迭而变化。Anthropic 的 token 计数文档指出,Claude 4.7 及之后的版本采用了更新的 tokenizer,同样的文本会被切分出比早期 Claude 模型多约 30% 的 token,具体差距取决于你的内容。旧的计数结果无法迁移;请针对你实际调用的模型重新计数。而当你只是想拿到数字、不愿接入 SDK 时,可以把 prompt 粘贴进覆盖 GPT、Claude、Gemini 和 Llama 的 LLM token 计数器。
为什么我的计数和账单对不上?
被计费的 prompt 是完整的请求体,而不是你写下的那串字符。角色框架、system prompt、工具与函数 schema,以及每条消息之间的分隔符都会增加 token,这就是为什么只统计用户消息总会低估。单个工具定义就可能给每个携带它的请求增加数百个输入 token。
对话历史则是放大器。聊天功能在每一轮都会重新发送完整历史,因此每一轮的输入都包含此前所有轮次,单次会话的成本会随对话长度呈超线性增长。计数层面的解决办法很简单:把你即将发送的 messages 数组、system prompt 和 tools 完整组装好,然后对其计数。Anthropic 的 count-tokens 端点接收的正是你本来要用于创建消息的同一份载荷(包括工具定义),因此你可以把组装好的请求直接传给它。
如何把 Token 计数转化为成本估算?
单次请求的预估成本就是一行算术,保持符号化表达:
cost = (input_tokens / 1_000_000) * input_rate + (output_tokens / 1_000_000) * output_rate
在服务商支持 prompt 缓存的情况下,被缓存的输入 token 会按一个单独且更低的 cached_input_rate 计费。各模型价格在数周内就可能变动,因此这里不列出任何具体费率。请把费率当作代码中注入的配置项,从服务商定价页面读取当前数值,并使用 LLM 成本计算器 横向比较各模型的最新数字。
有两个事实会影响每一次估算。第一,在主流服务商处,输出 token 的费率通常明显高于输入 token,因此响应长度往往主导成本。第二,输入计数是确定性的,而输出计数不是:同一个请求发出去时计数始终相同,但返回内容会随采样而变化。请以实测方式衡量输出:跑 50–200 个有代表性的请求,按平均输出长度规划成本,并以第 95 百分位设置 max_tokens,这样既不会截断正常响应,又能给失控生成设上限。
如何确认 Prompt 能装进上下文窗口?
输入 token 加上预期输出 token 必须能装进模型的上下文窗口,否则调用会直接失败或响应被截断。这项预检应放在你的请求封装层中:在 system 上下文、对话历史和输出余量之间分配窗口预算,对组装好的请求计数,并在发送前裁剪历史,而不是等报错之后再处理。上下文窗口检查器 能告诉你某个 prompt 是否适配某个模型,而无需记忆那些每次发版都在变的窗口大小。
之所以强调封装层的位置,是因为超限对用户是可见的:答案被截断,或者流式输出中途报错,而用户的本能反应是重试——于是一个 token 预算的 bug 会被计费两次。面向 LLM 功能的会话回放能精准暴露这类重试循环,远早于它出现在每月才审阅一次的账单上。
生产环境中该记录什么
即使价格在变,方法依然稳定:用调用模型自带的 tokenizer 对组装好的请求计数,采样真实流量以掌握你的输出分布,并把费率作为可从定价页面刷新的配置项来维护。然后在生产环境中形成闭环。所有主流服务商都会在响应的 usage 字段中返回实际 token 数,例如 Anthropic 的 usage.input_tokens 和 Gemini 的 usageMetadata;不过 Gemini 较新的 Interactions API(仍处于 Beta 阶段)返回的是包含 total_input_tokens 和 total_output_tokens 的 usage。从第一天起就按请求记录这些字段;记录它们非常简单,而在收到令人意外的账单之后再去还原它们就没那么简单了。
常见问题
我能用 tiktoken 来统计 Claude 或 Gemini 模型的 token 吗?
不能。每家服务商的 tokenizer 都有自己的词表,因此 tiktoken 的计数只对 OpenAI 模型有效,对 Claude 或 Gemini 而言,同样的输入可能得出相差甚远的结果。Claude 请使用 Anthropic 的 count-tokens 端点(可免费调用),Gemini 请使用 @google/genai SDK 中的 countTokens 方法,而 Llama 这类开源模型请使用其在 Hugging Face 上发布的 tokenizer。
tiktoken 和 js-tiktoken 这两个 npm 包有什么区别?
tiktoken 是 WASM 绑定:它会加载一个编译后的二进制文件,并要求你在用完后调用 free() 来释放编码器占用的内存。js-tiktoken 是纯 JavaScript 移植版本,方法名为 camelCase(getEncoding、encodingForModel),没有 WASM 二进制文件,也无需手动管理内存,因此在 edge 和 serverless 运行时中是更稳妥的选择。只导入单个编码 rank 文件可以让打包体积保持较小。
流式响应还会报告 token 用量吗?
会,但并非在所有场景下都默认开启。对于 OpenAI Chat Completions,将 stream_options 中的 include_usage 设为 true,API 就会额外流式返回最后一个数据块,其 usage 字段覆盖整个请求,而 choices 数组为空。Anthropic 会自动流式返回用量:message_start 事件携带 input_tokens,message_delta 事件携带累计的 output_tokens。请记录这些字段,而不要自己去统计流式数据块。
不同的 OpenAI 模型该用哪种 tiktoken 编码?
对于 gpt-4o 及之后的当前 OpenAI 模型使用 o200k_base,仅对 GPT-4 时代的模型使用 cl100k_base。这两种编码切分文本的方式不同,因此在其中一种下得到的计数无法迁移到另一种。给定一个模型 ID,js-tiktoken 中的 encodingForModel 会为你选出匹配的编码,从而避免在模型更迭时锁定了错误的编码。