12k
All articles

如何统计 Token 数量并估算 LLM API 成本

使用 OpenAI、Claude、Gemini 和 Llama 的 tokenizer 精确计算 LLM token,并估算 API 成本、上下文窗口和计费。

OpenReplay Team
OpenReplay Team
如何统计 Token 数量并估算 LLM 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 来计数。各服务商的路径如下:

服务商精确计数途径
OpenAItiktoken,或在 Node 与 edge 运行时中使用 js-tiktoken
Anthropiccount-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_tokenstotal_output_tokensusage。从第一天起就按请求记录这些字段;记录它们非常简单,而在收到令人意外的账单之后再去还原它们就没那么简单了。

常见问题

我能用 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 会为你选出匹配的编码,从而避免在模型更迭时锁定了错误的编码。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.