账单异常不是一个诊断
继续同一个 coding agent 任务:“定位失败测试、修改实现、运行完整测试并交付结果”。Agent 已定位根因、修改实现,并在完整测试运行七分钟后继续交付。前一轮有大量 cache read,这一轮读取突然降到接近零,未缓存输入或 cache creation 上升,TTFT 也变长。月底账单比预期高,于是有人把它写成“Provider 驱逐了缓存”。
这个结论越过了证据。发票或单轮成本异常只是症状:输入可能真的变长;工具、模型、effort、deployment 或配置可能进入新 epoch;prune / compaction 可能重建历史;七分钟 idle gap 可能越过文档 TTL;路由或容量淘汰可能让相同前缀不可用;输出变长、失败重试也会推高总成本;Provider 还可能在模型版本升级后改变 usage 字段及其计价口径。只看总金额,既分不清输入与输出,也分不清内容变化与缓存可用性。
本例的正确诊断陈述应先写成:“请求 r-42 的 cache-read tokens 从上一可比请求的 82k 降至 0,cache-creation tokens 上升,TTFT 增加;两次模型请求相隔七分钟。”随后才检查 prefix、配置、TTL 和路由线索。没有 Provider 因果信号时,最终原因可以是 unknown。可观测性的目标不是保证每个 miss 都有漂亮答案,而是明确已经观察到什么、还能排除什么、什么仍不可知。
每个请求至少记录什么
一条可诊断记录要以 request ID 为主键,把 Agent 事件、最终请求形状、Provider usage 与结果连起来。至少记录:
- **关联身份:**Provider request ID 与客户端 request ID;task、session、active branch / head ID;重试 attempt,以及前后请求的父子关系。
- **运行范围:**Provider、模型与版本、deployment、region、账号 / workspace、effort、服务等级和网关 route;实际使用的 cache key 或 affinity hint(不含秘密)。
- 请求身份:对最终模型可见、缓存相关的表示计算带密钥、可轮换的 privacy-safe
prompt fingerprint,并分别记录 system、tools、project、history 的 component fingerprint、长度与 epoch。不要对整份 wire JSON 盲目取 hash,因为无关 metadata 和序列化 envelope 未必参与缓存;也不要默认记录 raw prompt、源码、凭据或用户秘密。 - **工具与时间:**模型可见 tool-list schema / version fingerprint;请求时间、上一可比模型请求时间与 idle interval。工具进程仍在运行不等于访问过 Provider 缓存。
- **用量与延迟:**原样保存 Provider 返回的 input、cached read、cache write / creation、uncached input、output 等字段及 API / 字段版本;记录客户端 TTFT、API latency 与端到端 latency。
- **上下文事件:**compaction、prune、模型、工具、system、project 与其他 cache-key-affecting config epoch;同时保留 Context Builder 和 tokenizer 版本。
- **结果:**HTTP / API status、retry / fallback、stop reason、工具与测试结果、任务最终成功、失败或放弃。
记录必须区分两列:observed 保存响应字段、digest、时间与实际配置;inference 保存“疑似 TTL”“疑似路由 / eviction”等解释,并附证据和置信度。cache_read=0 是观察,Provider evicted cache 通常只是推断。原始 usage 不应被报表的统一字段覆盖;以后 Provider 改口径时,团队需要重放映射,而不是让旧派生值冒充原始事实。
前缀比较也要保护隐私。默认比较 keyed digest、组件 fingerprint 和 epoch;能在受控环境确定性重渲染时,再比较本地 token digest。只有系统确实保留了可比的分块或 token 边界,才报告 first mismatch 的 component、block 或 token index;否则只报告可观察到的最细粒度。不要为了得到更具体的位置,把敏感上下文原文复制进普通日志。
这些字段还必须在发出请求时落账,不能等异常发生后从当前配置反推旧请求。工具 registry、system 模板或网关规则已经更新时,“现在看起来一样”不能证明七分钟前也一样。一次重试也要生成独立 request 记录:它可能沿用同一 session 和 prompt,却切换了 region 或 deployment;如果只保存最终成功响应,导致成本与 TTFT 增长的冷重试就会从诊断链消失。
落库可以分为三层:不可修改的原始 Provider usage 与请求配置快照;由版本化 Context Builder 产生的组件长度、epoch 和 digest;带算法版本的比率与原因分类。这样既能在字段语义更新时重新计算报表,也能限制谁有权访问更敏感的诊断层。任务级视图再通过 request ID 聚合,避免把同一个 retry 同时算进“用户一轮”和“API 两次请求”却不注明。
Hit rate 应该怎样计算
面板应同时显示单请求、累计读取比例与写读关系,公式保持简单:
request hit rate = cache-read tokens / total rendered input tokens
cumulative hit rate = Σ cache-read tokens / Σ total rendered input tokens
write-to-read ratio = Σ cache-write tokens / Σ cache-read tokens
request hit rate 与 cumulative hit rate 都是 token 加权的部分复用率,不是“这个请求 hit / miss”的二元计数。一轮可能只复用前 70%,余下后缀仍需处理;累计率按 token 加权,也不会让许多短请求淹没一轮超长 miss。分母为零、请求未达到缓存资格,或 Provider 没有暴露必要字段时,应显示 not applicable / unknown,不能填 0 假装测量完成。
total rendered input tokens 必须按每家 Provider、模型和 API 在 2026-07-26 的 usage 语义重建。按 Claude API Prompt Caching 的当日口径,Anthropic 总输入需要把 uncached input_tokens、cache_creation_input_tokens 与 cache_read_input_tokens 相加;OpenAI 的 cached_tokens 可能是总输入下的明细 / 子集,而 GPT-5.6 及后续家族另有 cache_write_tokens 和不同的写入计价。不能把字段名相似就直接相加,也不能跨 Provider 用同一个分母做朴素排名;先保存原始响应,再为每个版本写互斥桶映射,并以 OpenAI Prompt Caching 和目标 API 的当日文档核对。
例如三个请求分别渲染 2k、2k、100k token,前两个全部读取、最后一个只读取 10k。按“命中过两次、未命中一次”会得到看似不错的二元结果;token 加权累计率却是 14k / 104k,直接暴露最长请求没有充分复用。反过来,100k 输入读取 90k 也不是完整命中:剩余 10k 仍可能是正常追加后缀、一次结构变化或缓存资格之外的内容,必须结合 component length 和 first mismatch 才能解释。
高 hit rate 也不等于任务成功。它可能只是便宜地反复携带无用历史;质量、TTFT、输出、retry 和完整任务成本仍要一起看。write-to-read ratio 很高可能表示 epoch 抖动,也可能只是新版本 warmup、一次性短任务或流量尚未形成后续读取;它是调查信号,不脱离 workload、TTL 与未来轮数直接判罪。
先判断请求变没变,再判断缓存还在不在
缓存诊断要按因果可控性排序。先问应用实际发出的缓存相关表示是否相同:system 模板是否插入时间戳,工具顺序或 schema 是否变化,project 指令是否更新,history 是否 prune / compact,模型、effort、TTL 选项或 cache key scope 是否改变。若 fingerprint 或 epoch 不同,先定位第一个差异;此时没有必要先猜 worker。
若本地 digest 相同,再检查生命周期。两次 API 请求之间七分钟,是否超过该 Provider、模型、渠道和 TTL 配置的文档范围?工具运行、人工阅读或 CI 等待不会自动刷新缓存。若仍在期限内,路由、容量淘汰、后端切换才成为基础设施假设;cache key 可以帮助 OpenAI 请求路由,但截至 2026-07-26 的官方说明并不让它证明某个内部缓存为何存在或消失。
比较组件时,先按 system → tools → project → history 的应用账本找变化,再回到 Provider 实际缓存层级验证,不能把概念顺序当成所有 API 的 wire 顺序。一次 tool fingerprint 变化可能来自 schema 发布、动态排序或 eager MCP 清单抖动;history 变化可能只是预期追加,也可能是 prune、compact 或切换 branch。诊断要把“有变化”继续细分为预期 epoch 和意外回归:模型升级后的冷写入是计划成本,随机工具排序造成的周期性 miss 才是构建器缺陷。截至 2026-07-26,Claude Code 的 Prompt Caching 说明还把该产品中的模型、effort 与 compact 等变化列为具体缓存边界;这是 Claude Code 行为,不应当作所有 Claude API 客户端的统一规则。
截至同日,Anthropic Cache diagnostics(beta)提供一个产品特例。诊断链中的每个请求都必须携带 anthropic-beta: cache-diagnosis-2026-04-07 header:第一轮就显式 opt in,并设置 previous_message_id: null;后续请求再按文档把上一轮的 response / message ID 放入 diagnostics.previous_message_id。比较还要求前后请求处于同一 organization、workspace 与 API credential scope(same organization / workspace / API credential scope)。服务会用短期 request fingerprints 比较两次结构,返回首个 divergence reason,并应与 cache_read_input_tokens、cache_creation_input_tokens 合读。
这项能力只比较请求结构,不报告每个 cache breakpoint 的驻留状态。它仍是 beta 且仅支持 Claude API,不支持 Amazon Bedrock 或 Google Cloud;fingerprint 保留时间短,长会话可能超出 comparison horizon,best-effort 结果也可能是 unavailable 或在比较尚未完成时暂时为空。它很有用,但不能外推成跨 Provider 的通用因果接口。
Cache Miss 诊断顺序
下面五步必须按顺序执行:
- Did token prefix or model/cache-key-affecting config change? 比较 keyed prefix / component fingerprint、工具与配置 epoch,以及实际模型、effort、cache key scope。
- If yes, locate first mismatch. 用确定性重渲染和本地 token digest 定位首个可观察差异;只能确定到 component / block 时就停在那里,不伪造 token index。
- If no, did idle interval exceed documented TTL? 按 Provider、模型、API / 渠道和本次 TTL 配置核对,不拿另一产品的
5m、1h或30m套用。 - If no, test routing/eviction as infrastructure hypothesis. 固定内容和配置重复实验,结合 route / deployment / region / fallback 线索;除非 Provider 暴露因果字段,否则它仍是假设。
- Record
unknownwhen Provider exposes no causal signal. 可以记录“内容与配置未观察到变化、未超过文档 TTL、cache read 为零”,但不能因此宣称已经证明 eviction。

代回本例:若 history fingerprint 显示完整测试结果只是尾部追加,model / tool / config epoch 未变,而 idle gap 超过实际文档 TTL,那么“TTL-compatible explanation”比“Provider 故障”更有证据;仍不能证明条目恰好在第几秒删除。若 gap 未超限且无结构差异,则进行受控重复实验,把 routing / eviction 保留为 hypothesis;没有诊断字段时,结论就是 unknown。
一份复盘记录可以保留每一步的排除结果:步骤 1 的四个组件 digest 均与父请求一致,步骤 2 不适用;步骤 3 发现七分钟间隔超过当前渠道文档 TTL;因此停止把步骤 4 写成已证实根因,并把事件分类为 ttl_exceeded。如果七分钟仍在配置 TTL 内,则记录步骤 3 已排除,继续实验。这样的顺序让未来读者知道结论来自哪一条事实,也防止“后来发现 route 改过”被悄悄改写成当时已经知道。
三个最小对照实验
A:完全相同的连续调用
**Hypothesis:**在请求满足缓存资格且状态可用时,两个 back-to-back 调用中,第二个应通过 Provider usage 显示复用。**Controls:**保持同一模型、deployment、region、effort、最终 prefix、cache key scope、breakpoint / TTL 与工具 schema,只改变 request ID;避免并发和网关 fallback。**Observed fields:**两次原始 usage、prefix / component fingerprint、cache write / read、TTFT、route 和状态。**Interpretation:**第二次 read 上升支持“当前链路可复用”;没有读取时,先核对资格与字段口径。若 Provider 不暴露原因,结果仍是 unknown,不能自动归因 eviction。
B:只改一个早期 token 或组件
**Hypothesis:**只改变早期 system token 或一个工具组件,会把 first mismatch 及其后推入 drop / rewrite。**Controls:**除这一处 mutation 外,后续 history、模型、配置、cache key scope、调用间隔与路由提示全部固定;先做一次基线 warmup。**Observed fields:**变更前后 component / token digest、实际可观察的 first mismatch、cache read / write、未缓存输入和 TTFT。**Interpretation:**读取量在该边界后下降、写入上升,支持前缀变化解释;若行为不变,可能是变更不在缓存相关渲染中或 Provider 匹配粒度不同。无产品诊断时保留 unknown,不要反推内部 block 大小。
C:围绕 TTL 的 controlled idle gap
**Hypothesis:**内容、模型与路由提示相同时,文档 TTL 内的重复调用比超过 TTL 的调用更可能读取旧缓存。**Controls:**固定最终请求、模型 / config、cache key scope、deployment / region 与网关路径;分别选择 TTL 内和 TTL 外的 controlled idle interval。**Observed fields:**精确时间、idle gap、实际 TTL 选项、原始 usage、TTFT、route / fallback。**Interpretation:**跨越 TTL 后稳定出现 read drop 支持生命周期解释,但不证明每次删除时刻;若未下降,可能有刷新或更长保留,若 TTL 内也下降则可能是其他原因。重复多轮、分开 cold / warm 样本;先在隔离环境估算流量与写入成本,不要盲目在生产制造长等待或额外请求,因果信号不足时仍记 unknown。
告警应该说事实,不要假装知道 Provider 内部
好告警描述同一时间线上的可观测变化,例如:“interactive-coding-v3 最近 15 分钟 cumulative read ratio 从基线区间下降;cache creation 上升;p95 TTFT 同步变化;其中 68% 请求发生 tool epoch 切换,另有一组 idle gap 超过当前 TTL。”也可以报告 prefix fingerprint / tool epoch 变化、模型切换、deployment / region / fallback 改变,以及重试增加。阈值和基线必须按 workload、模型和冷 / warm 队列分别建立。
坏告警写“Provider evicted cache”“路由器把请求送错 worker”,却没有直接信号。更诚实的状态机是 request_changed / ttl_exceeded / infrastructure_hypothesis / unknown,并允许一个事件只有事实没有根因。
告警窗口也要同时保留比例与绝对量。低流量时一个 100k 请求足以让比例跳变,但未必代表系统性回归;高流量时比例只降几个百分点,也可能对应大量重写。告警正文应附样本数、总 rendered input、read / creation 绝对 token、冷暖分层与最近 epoch 变更,并链接少量脱敏 request 样本。恢复告警同样只说指标回到基线,不宣称 Provider 内部状态已经“修复”。
截至 2026-07-26,Claude Code Monitoring 的 OpenTelemetry api_request 事件可提供 duration、model、request_id、input / output、cache_read、cache_creation、cost 等字段;如果采集的事件路径不含 TTFT,客户端仍需在流式边界单独测量,而不能用完整 duration 代替。相邻 trace 能力即使另有 TTFT,也要明确所用 signal 与版本。
Usage & Cost Admin API是 Claude Console / Claude Platform 组织用于聚合用量、历史趋势和账单 reconciliation 的接口,需要不同于普通 Claude API key 的 Admin API key,个人账号不可用;截至该日期,Claude Platform on AWS 也没有可编程的这组 Usage & Cost endpoint。Claude Enterprise 使用的是另一套 Analytics API 与 Analytics key,不能与前者合并成一种“Admin / Analytics 权限”。这些聚合数据适合监控和财务核对,但不是逐请求 causal trace,不能回答某次 prefix 在哪个节点失效。它们可以通过 request / task 记录和时间桶与遥测关联,职责不能互换。
Agent Context Economics Checklist
- **机制:**是否按 Provider 当日语义重建 total rendered input,并区分 uncached、cache write、cache read 与 output,而不是混加字段?
- **精确前缀:**system、tools、project、history 的最终模型可见表示是否确定性构建;keyed fingerprint 与 first mismatch 是否可比较且不记录 raw secrets?
- **工具:**tool-list schema / version、顺序、deferred / eager 加载点和授权配置是否有独立 epoch?
- **会话树:**task、session、branch / head、retry 与 request 是否一一关联;resume / fork 没有被误写成缓存证明?
- **生命周期:**是否记录 Provider、模型、deployment、region、effort、TTL、idle gap、cache key 与 route / fallback,并按同日官方文档解释?
- **上下文管理:**append、prune、compact 是否成为显式事件;编辑位置、摘要版本、证据保留与新 prefix epoch 是否可追踪?
- **经济性:**是否同时看绝对读写 token、request / cumulative hit rate、write-to-read ratio、TTFT、output、retry、task success 与完整任务成本?
- **实验:**每次模型、工具、Context Builder 或 TTL 发布是否有同请求、单点 mutation、controlled idle 三类最小实验,并分开 cold / warm?
- **预算与运维:**告警阈值是否按 workload 和任务阶段建立;新 epoch 的 warmup、写入成本、后续读取机会与回滚条件是否进入预算?
- **隐私与证据:**日志是否只保留必要的 keyed digest、长度、epoch 与 usage;密钥轮换、访问控制、保留期和原始敏感工件是否另行治理?
- **来源日期:**Provider 字段、TTL、诊断与平台可用性是否标注 2026-07-26 或接入时的复核日期;无因果信号时是否保留
unknown?
这份清单把机制、架构、经济性和运维闭合起来:先证明请求是什么,再确认缓存读取了什么,最后才解释为什么变化。一次 miss 不一定能被完全归因,但它应当能够被准确描述、最小复现,并在证据结束的地方停止猜测。
上一篇:Context Budgeting:在质量、延迟与成本之间做决策