【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_usetool_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 行业专家整理编写