Codex CLI 接入 Bedrock 的缓存陷阱:4 天账单 1386 美元,85% 花在重复写入

2026-08-24 35 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

一次看似正常的 Codex CLI 接入,4 天产生了约 1386 美元费用,其中 85% 不是模型推理,而是 prompt cache write。这个案例提醒我们:接入支持提示词缓存的模型时,看到“缓存已开启”远远不够,还要确认缓存标识是否真正穿过客户端、provider 和云端 API,并最终转化为 cache hit。

问题不在请求次数,而在每次都重写前缀

Codex 工作时会维护 prompt_cache_key,用于标记可复用的提示词前缀。对于持续数十轮的编码会话,这类前缀通常包含系统指令、工具定义、项目上下文和历史对话,体积可能远大于用户刚输入的那一句话。

理想情况下,一轮请求写入缓存,后续请求读取同一前缀:

第 1 轮:写入 100,000 tokens
第 2 轮:读取 100,000 tokens + 推理新增内容
第 3 轮:读取 100,000 tokens + 推理新增内容

如果 Bedrock 原生 provider 没有正确传递或映射 Codex 维护的缓存身份,云端就可能把后续请求继续视为新的缓存写入:

第 1 轮:写入 100,000 tokens
第 2 轮:再次写入 105,000 tokens
第 3 轮:再次写入 110,000 tokens

随着会话增长,账单呈现的不是线性推理成本,而是不断重写长前缀的成本。来源案例中 85% 的费用落在 cache write,正是需要立即停用或降级该链路的信号。

需要注意,prompt_cache_key 是 Codex 侧的缓存标识,而 Bedrock 最终如何表达缓存检查点、缓存身份和计费指标,取决于具体 provider 实现及模型 API。仅凭配置文件里出现“cache”不能证明两端已经正确对接。

用账单指标验证缓存,而不是相信配置

排查时至少要回答四个问题:

  1. 连续请求是否使用稳定的 prompt_cache_key
  2. provider 发出的 Bedrock 请求里,是否存在对应的缓存字段或缓存检查点?
  3. 响应 usage 中,cache read 是否在第二轮后增长?
  4. cache write 与 cache read 的比例是否符合会话模式?

最直接的办法是保留每轮 usage 数据并计算费用构成。下面的脚本可以直接运行;它假设日志是 JSON Lines,每行包含 input_tokensoutput_tokenscache_write_tokenscache_read_tokens。实际字段名不同的话,修改 usage.get(...) 四处映射即可。

#!/usr/bin/env python3
import argparse
import json
from pathlib import Path


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("log", type=Path, help="JSONL usage log")
    parser.add_argument("--input-rate", type=float, required=True,
                        help="USD per 1M uncached input tokens")
    parser.add_argument("--output-rate", type=float, required=True,
                        help="USD per 1M output tokens")
    parser.add_argument("--write-rate", type=float, required=True,
                        help="USD per 1M cache-write tokens")
    parser.add_argument("--read-rate", type=float, required=True,
                        help="USD per 1M cache-read tokens")
    args = parser.parse_args()

    totals = {"input": 0, "output": 0, "write": 0, "read": 0}
    with args.log.open(encoding="utf-8") as f:
        for line in f:
            if not line.strip():
                continue
            usage = json.loads(line)
            totals["input"] += usage.get("input_tokens", 0)
            totals["output"] += usage.get("output_tokens", 0)
            totals["write"] += usage.get("cache_write_tokens", 0)
            totals["read"] += usage.get("cache_read_tokens", 0)

    rates = {
        "input": args.input_rate,
        "output": args.output_rate,
        "write": args.write_rate,
        "read": args.read_rate,
    }
    costs = {k: totals[k] / 1_000_000 * rates[k] for k in totals}
    total_cost = sum(costs.values())

    for key in ("input", "output", "write", "read"):
        share = costs[key] / total_cost * 100 if total_cost else 0
        print(f"{key:>6}: {totals[key]:>12,} tokens  ${costs[key]:>9.2f}  {share:>6.2f}%")
    print(f" total: {'':>12}         ${total_cost:>9.2f}")

    reusable = totals["write"] + totals["read"]
    hit_rate = totals["read"] / reusable * 100 if reusable else 0
    print(f"cache token hit rate: {hit_rate:.2f}%")

    if total_cost and costs["write"] / total_cost >= 0.50:
        print("WARNING: cache writes exceed 50% of estimated cost")


if __name__ == "__main__":
    main()

例如,把当前模型在 Bedrock 计价页上的每百万 token 单价填进去:

python3 audit_cache_cost.py usage.jsonl \
  --input-rate 0 \
  --output-rate 0 \
  --write-rate 0 \
  --read-rate 0

示例中的 0 必须替换成当前区域、模型和计费层级的真实价格。不要从旧账单或其他模型复制单价,因为缓存写入与读取可能采用不同费率。

抓住“第二次相同请求”这个最小证据

正式启用前,可以这样实践:建立一个隔离会话,固定系统提示词、工具列表和大段上下文,只改变末尾的一小句用户输入,连续请求两到三次。预期现象是第一次产生 cache write,后续请求产生明显的 cache read。

测试期间同时记录三层数据:

  • Codex 调试日志中的 prompt_cache_key,确认它没有每轮随机变化。
  • provider 发出的请求体,确认缓存语义没有在适配层丢失。
  • Bedrock 响应 usage 与成本指标,确认服务端确实命中缓存。

记录请求体时要先脱敏。系统提示词、源代码、工具参数和临时凭证都可能进入日志,不能为了排查成本问题而制造新的数据泄露风险。

还应测试会破坏缓存的变化,例如工具定义重新排序、系统提示词插入时间戳、工作目录元数据变化,以及把动态内容放在稳定前缀中间。即使 key 保持不变,前缀内容发生变化也可能降低复用率。

上线前给缓存链路加保险丝

这类问题最危险的地方是功能仍然可用:模型照常回答,开发者只会在数天后从账单发现异常。接入 Bedrock 原生 provider 时,建议设置以下门槛:

  • 用小额度、短会话完成 cache write/read 验收,再开放长时间代理任务。
  • 为 cache write 成本和 token 数分别设置小时级告警。
  • 按 provider、模型、区域和客户端版本拆分成本标签。
  • 升级 Codex CLI 或 provider 后重新运行缓存回归测试。
  • cache write 占比持续异常时自动停止任务,而不是等待月度预算告警。
  • 无法证明 cache hit 时,限制上下文长度、压缩工具定义,或暂时改用已验证的接入路径。

提示词缓存不是一个布尔开关,而是一条端到端协议。客户端生成稳定标识、provider 正确映射、请求前缀保持一致、服务端返回读取指标,这四个条件缺一不可。对长上下文 Agent 来说,缓存验收应该和鉴权、限流、超时测试处在同一个上线清单里。


相关推荐