Token 计数入门:从终端估算到核对 API 用量
先分清字符、token 和账单
接入 LLM API 时,我会先做一个小实验:保存输入,发送请求,再检查 usage,而不是凭回答长度猜费用。Token 是分词器处理文本的单位,不等于一个汉字、一个单词或一个字节;输入内容和模型生成内容都需要计数,输出用量也不一定只包含屏幕上可见的文字。([developers.openai.com](https://developers.openai.com/api/docs/guides/token-counting?utm_source=openai))
下面使用 Bash、Python 3、curl 和 jq。所有命令在同一个终端执行;先用纯文本、非流式请求把计数跑通,再接入编辑器里的工作流。
第一步:保存一份固定输入
创建实验目录和虚拟环境,避免把依赖装进项目的全局环境:
mkdir -p token-lab; cd token-lab; python3 -m venv .venv; source .venv/bin/activate; python -m pip install tiktoken
写入一段中英文混合输入,后续本地估算和真实请求都读取同一个文件:
printf '%s' '请解释这个函数:def add(a, b): return a + b' > prompt.txt
先查看字符数和 UTF-8 字节数,给自己建立一个对照:
python -c 'from pathlib import Path; s=Path("prompt.txt").read_text(encoding="utf-8"); print({"characters":len(s),"utf8_bytes":len(s.encode("utf-8"))})'
第二步:运行分词器,但不把估算当账单
使用 tiktoken 的一个明确编码,打印 token 数量和编号:
python -c 'from pathlib import Path; import tiktoken; s=Path("prompt.txt").read_text(encoding="utf-8"); e=tiktoken.get_encoding("o200k_base"); ids=e.encode(s); print({"encoding":e.name,"tokens":len(ids),"ids":ids})'
这里选择 o200k_base 只是演示分词,不代表它适用于所有模型。不要根据模型名称相似就认定编码相同:tiktoken 的名称映射甚至可能匹配不存在的模型名称。没有确认目标模型的分词器之前,这个结果只能用于实验,不能作为准确预算。([github.com](https://github.com/openai/tiktoken/blob/main/tiktoken/model.py?utm_source=openai))
第三步:配置 Fluxo 并选择真实模型 ID
Fluxo 用一个密钥和一个基础地址 https://api.fluxorelay.com 接入多个模型系列,也可用于 Claude Code、Codex、Gemini CLI 和 OpenAI SDK。这里先用 curl,方便直接保留原始响应。如果你想跟着实际跑一次,可以创建一个 Fluxo 账户。
交互式读取密钥,避免把它直接写进 shell 历史:
export FLUXO_BASE_URL='https://api.fluxorelay.com'; read -r -s -p '输入 Fluxo API 密钥:' FLUXO_API_KEY; printf '\n'; export FLUXO_API_KEY
读取模型列表,并把结果保存下来:
curl --fail-with-body -sS "$FLUXO_BASE_URL/v1/models" -H "Authorization: Bearer $FLUXO_API_KEY" -o models.json; jq '.data' models.json
本次可以选择 Claude Haiku 4.5。展示名称不一定就是 API 标识;以返回结果中的实际 id 为准,不要把猜测的名称直接粘进请求:
read -r -p '粘贴选中模型的实际 id:' MODEL; export MODEL
第四步:发送请求,保存完整 usage
用 jq 构造 JSON,避免手动拼接中文、引号和换行。这个最小请求不设置输出长度参数,先减少不同模型之间的参数兼容问题:
jq -n --arg model "$MODEL" --rawfile prompt prompt.txt '{model:$model,messages:[{role:"user",content:$prompt}],stream:false}' > request.json
curl --fail-with-body -sS "$FLUXO_BASE_URL/v1/chat/completions" -H "Authorization: Bearer $FLUXO_API_KEY" -H 'Content-Type: application/json' --data-binary @request.json -o response.json
jq '{model,usage,finish_reason:.choices[0].finish_reason}' response.json — 在 Chat Completions 的标准返回结构中,prompt_tokens 表示输入用量,completion_tokens 表示生成用量,total_tokens 表示合计。请求包含消息结构,而不仅是 prompt.txt 里的文字,所以不能要求本地文本计数与服务端结果完全一致。若接口返回错误或没有 usage,先检查 response.json,不要把缺失值记成零。([developers.openai.com](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create?utm_source=openai))
第五步:缓存 token 不要重复相加
查看缓存和生成细分字段: — jq '.usage | {prompt_tokens,completion_tokens,total_tokens,prompt_tokens_details,completion_tokens_details}' response.json
在 OpenAI 的 Chat Completions 用量结构里,prompt_tokens_details.cached_tokens 是输入总量中的缓存命中部分,不是额外输入。计算未缓存输入时,应从 prompt_tokens 中扣除它,而不是再加一次。([developers.openai.com](https://developers.openai.com/cookbook/examples/prompt_caching_201?trk=article-ssr-frontend-pulse_little-text-block&utm_source=openai))
Claude 原生结构采用另一种拆分:input_tokens、cache_creation_input_tokens、cache_read_input_tokens 分别表示普通输入、缓存写入和缓存读取,完整输入需要三者相加。使用中转接口时,先确认实际返回的字段语义,不能把两套规则混用。([github.com](https://github.com/anthropics/skills/blob/main/skills/claude-api/shared/prompt-caching.md?utm_source=openai))
Fluxo 按真实 token 用量计费,包括缓存读取和写入,没有月费。但 token 总数不是费用:应分别用普通输入、输出、缓存读取、缓存写入数量乘以对应单价,再按单价标注的计量单位换算。
第六步:把计数接进日常调试
- 每次修改 提示词 之后,保存 请求内容、模型标识、响应文件 和 usage,让 下一次 对比 有据可查。
- 每次比较 两个版本 时,固定 模型、输入文件 和 调用次数,避免 把 随机输出长度 当成 优化成果。
- 接入 编辑器代理 之后,检查 系统指令、历史消息、工具定义 和 工具返回,不要 只统计 最后一条 用户消息。
- 记录 缓存读取 与 写入 时,保留 原始字段 和 计费分类,避免 重复相加 或 错用 单价。
团队预算也保持同样边界:Fluxo 的邀请机制会在受邀用户付费时产生分成,但它属于独立收入记录,不应改写请求的 token 用量。先让每次调用可复查,再谈节省多少,这比盯着一条总数可靠。