【Claude Code-上下文实测】同一段文本,为什么不同模型算出的 token 不一样:Agent 上下文预算的隐藏误差
非线智能API经验 [Claude Code-上下文实测] 第20篇
摘要
上下文预算依赖 token 计数,但 token 不是跨模型统一的自然单位。同一段中文、英文、代码、JSON、工具结果,在不同 tokenizer、不同模型版本、不同 provider API、不同 SDK fallback 下,可能得到不同 token 数。Anthropic 文档已经说明,Opus 4.7 及之后模型使用新 tokenizer,同样固定文本的 token 数可能上升;迁移文档也提醒 /v1/messages/count_tokens 对不同 Claude 版本会返回不同结果。
对跨模型 coding agent 来说,token 计数不是成本优化小技巧,而是上下文裁剪、压缩触发、模型路由、prompt cache 和请求失败率的基础设施。不能用一个字符/token 经验值预算所有模型,也不能假设本地 tokenizer、provider count_tokens 和实际 usage.input_tokens 天然一致。
数据来源:非线智能Nonlinear 官网
证据层级
Anthropic Pricing 和 Migration Guide 可支撑 tokenizer 变化与 token 数上升的官方事实。LiteLLM Token Counting 文档可说明 SDK / 网关层存在 provider-specific token counting 和 fallback。LiteLLM GitHub issue 可作为复杂 block 支持缺口和 Bedrock-backed Anthropic 计数行为差异的案例。AWS Bedrock CountTokens 文档可支撑 Bedrock 路径也需要独立计数。
本文不写“LiteLLM token counting 不准”这种泛化判断,而写成:在多 provider、复杂 block、fallback 或 Bedrock 路由场景下,需要用真实样本验证。
token 不是字符数
tokenizer 会把文本切成模型内部单位。不同 tokenizer 对同一文本的切分可能差异很大:
| 内容类型 | 为什么误差可能大 |
|---|---|
| 中文 | 字符、词片段、标点切分差异明显 |
| 英文 | 空格、词根、常见词表影响切分 |
| 代码 | 符号、缩进、标识符、camelCase / snake_case |
| JSON | 引号、冒号、重复字段、转义字符 |
| Markdown | 表格、链接、代码块混合 |
| 日志 | 时间戳、路径、stack trace、随机 id |
| 多模态 block | provider 侧有额外序列化和计费规则 |
| tool block | 工具名、参数 schema、结果 block 不只是纯文本 |
所以“一个 token 约等于几个字符”只能用于粗略估算,不能用于硬上限判断。越接近上下文窗口边界,粗估越危险。
同一厂商内部也会变
token 计数差异不只发生在不同厂商之间。模型升级也可能更换 tokenizer。Anthropic 文档提到,Opus 4.7 及之后模型使用新 tokenizer,同样固定文本在新 tokenizer 下可能出现更多 token。迁移文档也说明,不同 Claude 版本的 count_tokens 结果可能不同。
这会影响一整套运行策略:
| 策略 | tokenizer 变化的影响 |
|---|---|
| compact 触发阈值 | 原本 80% 使用率可能变成更接近上限 |
| 文件读取预算 | 同一文件在新模型下可能超预算 |
| 工具结果截断 | 截断位置需要重新校准 |
| prompt cache 成本 | cache write/read token 统计变化 |
| 模型路由 | 同一请求在不同模型下成本不同 |
| 报警阈值 | 历史 token 监控不可直接横比 |
因此,token 校准不是一次性工作。每次模型版本、provider 路径、SDK 版本或消息格式变化,都可能让历史校准失效。
三种 token 数不能混用
跨模型 agent 至少会接触三种 token 数:
| 计数来源 | 用途 | 风险 |
|---|---|---|
| 本地估算 | 请求前快速预算、离线分析 | 可能不匹配 provider 序列化 |
provider count_tokens |
发送前精确估算 | 不一定覆盖所有 block 或所有网关 |
实际 usage.input_tokens |
计费、观测、校准真值 | 请求后才知道,且可能受缓存字段影响 |
这三者都重要,但不能互相冒充。本地估算适合快速预警;provider count 适合提交前检查;实际 usage 适合回写校准表和成本归因。
SDK 和网关层的第二层误差
很多 agent 系统不会直连单一 provider,而是通过 LiteLLM、Bedrock、OpenAI-compatible gateway、vLLM 或内部代理调用模型。网关层会带来新的差异:
| 差异来源 | 例子 |
|---|---|
| provider-specific API | 有的模型有官方 count_tokens,有的没有 |
| fallback tokenizer | SDK 找不到 provider 计数时用近似估算 |
| 消息序列化 | OpenAI-compatible messages 到 provider 原生格式的转换 |
| tool block 表达 | tool_use、tool_calls、XML tool call 的表示不同 |
| image/document block | 多模态 token 计数规则更复杂 |
| Bedrock 路由 | Bedrock 自身有 CountTokens 操作和包装格式 |
因此,“通过 LiteLLM 调 Anthropic”和“直连 Anthropic”应视为两条路径校准。同一个模型名,不同调用路径也可能产生不同预算误差。
复杂 block 比纯文本更危险
很多 token 校准只测纯文本,这对 coding agent 不够。真实请求里经常包含:
• system prompt。
• tool definitions。
• tool call / tool result。
• MCP descriptors。
• cache_control。
• image / document block。
• 长 JSON。
• stack trace。
这些内容的序列化规则可能由 provider 决定,本地 tokenizer 很难完全模拟。尤其是 tool schema 和 tool result,它们既有结构化字段,又会被模型服务转换成内部 prompt 表示。
校准样本集
建议建立固定样本集:
| 类别 | 样本 |
|---|---|
| 自然语言 | 中文、英文、中英混合 |
| 文档 | Markdown、表格、链接、长列表 |
| 代码 | TypeScript、Python、Go、SQL、YAML |
| 结构化数据 | JSON 大对象、嵌套 schema、数组 |
| 日志 | stack trace、测试输出、构建日志 |
| 工具 | tool definitions、tool call、tool result |
| MCP | 多工具 schema、MCP result、分页结果 |
| 多模态 | image block、document block |
| 长上下文 | 10K、50K、100K、500K、1M 拼接样本 |
每个样本都记录字符数、字节数、内容类型、结构类型、是否含工具 block、是否含缓存控制。
校准数据结构
一个可机器读取的校准结果可以这样表示:
{
"model": "claude-opus-4-7",
"provider_path": "anthropic_direct",
"tokenizer_version": "provider",
"measured_at": "2026-06-18",
"samples": [
{
"sample_id": "typescript_001",
"content_type": "code",
"chars": 12000,
"bytes": 12240,
"local_estimate": 4100,
"provider_count_tokens": 4520,
"actual_usage_input_tokens": 4548,
"relative_error_local_vs_usage": -0.0985,
"relative_error_count_vs_usage": -0.0062
}
],
"summary": {
"p50_abs_error": 0.03,
"p90_abs_error": 0.11,
"p99_abs_error": 0.22,
"recommended_safety_margin": 1.25
}
}
这个文件不只是报告。它应该被上下文预算器直接读取,用于决定文件读取上限、压缩触发阈值和模型路由。
预算算法
一个保守预算流程:
1、按目标模型和 provider path 选择校准系数。
2、用本地估算快速计算请求 token。
3、乘以内容类型和模型对应的 P99 safety margin。
4、如果接近阈值,调用 provider count_tokens。
5、仍接近阈值时,先压缩 dynamic tail 或裁剪工具结果。
6、请求完成后,把实际 usage.input_tokens 回写校准表。
伪代码:
type TokenBudgetDecision = {
estimatedTokens: number;
safetyAdjustedTokens: number;
shouldCallProviderCount: boolean;
shouldCompact: boolean;
shouldRejectOrSplit: boolean;
};
关键是不要在 95% 或 100% 上下文窗口才行动。token 估算有误差,compaction 自身也需要空间。
cache token 要单独统计
开启 prompt cache 后,usage 可能包含 cache write/read 相关字段。预算和成本分析要区分:
| token 类型 | 含义 |
|---|---|
| 普通 input token | 未缓存输入 |
| cache creation token | 写入缓存的输入 |
| cache read token | 从缓存读取的输入 |
| output token | 模型生成 |
如果把 cache read token 和普通 input token 混在一起,就无法判断缓存是否真的降低成本和延迟。同样,如果只看本地估算,就无法知道 provider 是否命中了缓存。
失败模式
| 失败模式 | 触发原因 | 修复策略 |
|---|---|---|
| 压缩触发太晚 | 使用旧 tokenizer 系数 | 模型版本变更后重跑校准 |
| 请求超上下文 | 本地估算低估复杂 block | provider count + P99 margin |
| 成本预估失真 | cache read/write 未分开 | 记录 provider usage 原始字段 |
| Bedrock 路径异常 | 直连校准套用到 Bedrock | 每条 provider path 单独校准 |
| 多模态超预算 | image/document block 未支持 | 单独样本和 provider count |
| 工具 schema 低估 | 本地 tokenizer 未模拟工具序列化 | 用真实 tool request 校准 |
验证清单
• 为 Anthropic、DeepSeek、Qwen、GLM、Kimi 分别建立样本集。
• 对 direct API、LiteLLM、Bedrock 或内部 gateway 路径分别测量。
• 每个样本记录本地估算、provider count、实际 usage。
• 对中文、代码、JSON、日志、tool block、多模态 block 分类别计算误差。
• 输出 P50、P90、P99 和最坏情况 safety margin。
• 模拟上下文接近上限,验证校准是否能避免请求失败。
• 模型或 SDK 升级后自动标记校准过期。
工程结论
token 预算必须模型化、路径化、版本化。跨模型 agent 不能用一个 tokenizer 预算所有请求,也不能把本地估算、provider count 和实际 usage 混为一谈。可靠做法是持续校准真实 workload,把误差分布反馈到压缩阈值、工具输出限制、prompt cache 成本和模型路由里。
参考链接
• Anthropic Pricing
• Anthropic Migration Guide: Migrating to Claude 4
• LiteLLM Docs: Token Counting
• BerriAI/litellm #20367
• BerriAI/litellm #27632
• AWS Boto3 Bedrock Runtime: count_tokens
本文由非线智能API Claude Code 行业专家整理编写