[控场AI]
· 9 分钟阅读· 4,729 字

Claude提示缓存为何在Bedrock悄然失效:五大陷阱

Claude提示缓存为何在Bedrock悄然失效:五大陷阱

亚马逊 Bedrock 的 Claude 提示缓存会静默失效,五种常见陷阱让你支付全价甚至更高费用。

文章指出 Bedrock 提示缓存失效是静默的:请求照常返回 200,但会按全价重算输入 token 并支付 25% 缓存写入溢价。作者基于 CacheCanary 的排查总结出五种反复出现的失效模式:智能体循环中缓存点未随工具历史前移;宽工具扇出后未添加第二缓存点,超出约 21-22 内容块的回溯边界;将每请求变化的内容(日期、请求 ID、jsonb 属性顺序)放在缓存点之前;库升级或切换模型 ID、推理配置文件 ARN 后缓存点悄然消失;流式响应重建助手消息时改变历史结构。文章给出对应检测方法与修复方案,并建议通过 CloudWatch 的 CacheReadInputTokenCount 和 CacheWriteInputTokenCount 指标持续监控缓存读写比。

Claude prompt caching on Amazon Bedrock

引言:当缓存消失时,账单不会说谎

在 Amazon Bedrock 上运行 Claude 智能体,提示缓存(prompt caching)可能在你毫无察觉的情况下停止工作——没有报错,没有异常状态码,每个请求都返回 200。但你付出的代价是:全价重算输入 token,外加 25% 的缓存写入溢价。

据 Datadog 统计,2026 年 3 月其客户追踪中 69% 的输入 token 都是系统提示:指令、策略和工具说明被反复发送。提示缓存正是让这部分成本降至约十分之一的关键机制。然而,一个名为 CacheCanary 的工具作者在排查大量智能体框架的 Bedrock 缓存问题后发现,五种失效模式反复出现。本文将逐一拆解每种失效的原因、检测方法和修复方案。

什么是提示缓存?为什么它如此脆弱?

Bedrock 的提示缓存机制允许复用请求的起始部分——包括你的指令、工具列表和早期对话。你用缓存点(cache point)标记可复用内容的结束位置,下一次请求只有满足两个条件才能命中缓存:缓存点之前的所有内容完全一致,且 Bedrock 能找到之前的缓存条目。

任何失败都不会导致报错。请求照常完成,只是你会按全价重新支付输入 token,并额外支付 25% 的缓存写入费用。AWS 文档虽然说明了缓存规则,但对智能体影响最大的那条——“Bedrock 向前查找旧缓存条目的距离大约为 20 个内容块”——仅给出了模糊的“约 20”的说法。作者通过实测将这一边界精确到了 21 与 22 个内容块之间。

**内容块(content block)**是这里所有计数逻辑的最小单位。在 Anthropic/Bedrock Messages 格式中,一条消息的 content 是一个数组,每个元素就是一个内容块:一段文本、一次 tool_use 调用、一条 tool_result 结果、一个图像,或扩展思考产生的思考块。这与 Anthropic 自家 API 的关键差别在于计数方式——在 Anthropic API 上,助手回合中的一串连续工具调用会被当作一个逻辑位置来处理;而在 Bedrock 上,每个 tool_use 和每个 tool_result 都单独计入回溯距离。理解了这一点,就很容易明白为什么一轮并行调用十个工具就会消耗二十多个块,把旧缓存点推出可达范围。凡是排查缓存命中率,第一步都应先数清楚缓存点之后到底累积了多少个块。

陷阱一:智能体循环中缓存点未前移

在智能体循环中,大部分新增 token 来自工具调用和工具结果,而非用户消息。如果缓存点只放在最后一条用户消息上,它会停留在初始提示位置,而工具历史在其后方不断增长。

这正是 Cline 4.1.0 中发生的问题。其 Bedrock 代码将缓存点放在最后一条角色为“user”的消息上,而在其消息格式中工具结果拥有独立角色。结果缓存点从未越过开头提示。用户只能在每次会话开始时看到一次缓存写入,之后再无写入(4.0.12 版本正常)。

检测方法: 在较长的智能体会话中,缓存读取量应随每次调用逐步增长。如果读取量停止增长而输入 token 持续攀升,说明缓存点没有移动。修复方案很简单:将缓存点放在最新消息的最后一个内容块上,无论其角色是什么。

陷阱二:宽工具扇出后未添加第二个缓存点

Bedrock 在查找旧缓存条目时,只从新缓存点向前搜索固定数量的内容块。作者实测了这条边界:自旧缓存点起新增 21 个块时,下一次请求仍能命中;新增 22 个块时,每次都会落空。每个工具调用和每个工具结果都算一个块,因此一轮包含十个以上并行工具调用的回合,就可能把旧缓存条目推出可达范围。

pydantic-ai 在自己的测试中也观察到了同样的边界。10 次并行调用(21 个新块)后,下一次请求从缓存读取了 6,827 个 token;11 次调用(23 个块)后读取量为 0,并重新写入了 7,534 个 token。一位 Strands 用户则报告了 18 次调用的回合导致 0 读取和 134,082 写入的情况。同样的请求在 Anthropic 自家 API 上不会失效,因为那里一串工具调用只算一个位置——而 Bedrock 每个都单独计数。

检测方法: 查看最宽回合之后的调用。如果那里读取为零而其他位置读取正常,就是这个问题。修复方案:当一个回合新增超过约 18 个块时,保留之前的缓存点。Bedrock 允许每个请求最多四个缓存点,通常有足够空间。

Bar chart: with 19, 20 or 21 content blocks added, Bedrock read about 2,900 tokens from cache. With 22, 23 or 24, it read 0.

陷阱三:将每请求变化的内容放在缓存点之前

缓存是前缀匹配。日期、请求 ID、用户名或逐项指令只要出现在缓存点之前,就会让每个请求变得独一无二——每次调用都在写缓存,没有一次在读缓存。

一个 AWS 示例应用清晰地展示了这一点。在 28 次审查同一文档的调用中,CloudWatch 记录了 196,680 个 token 写入缓存、0 个读取,原因是缓存点被放在了每个条目自身指令之后。将其移到文档之后,第二个条目立即变成了 4,624 个 token 的读取。更隐蔽的版本也存在:在一个 Mastra 配置中,将消息存储为 Postgres jsonb 列导致属性键在读取时顺序改变,从而改变了提示字节,破坏了缓存。

检测方法: 运行两次相同的请求,比较缓存点之前的字节内容,必须完全一致。修复方案:将动态文本放在最后一个缓存点之后,并以稳定顺序序列化结构化数据。

**前缀匹配(prefix matching)**是提示缓存的工作前提:缓存键由缓存点之前的全部字节序列决定,只要其中任何一个字节不同,整段缓存就无法复用。这意味着不仅显式的动态字段(日期、请求 ID、用户名)会破坏缓存,运行时的序列化差异同样致命——字典/对象的键顺序、jsonb 读取顺序、浮点格式化、空白与换行差异都会改变字节。因此,把动态内容放在最后一个缓存点之后是基本纪律;对于结构化数据,应在序列化前固定键顺序(如显式排序字段、使用确定性编码),避免因语言或库的默认行为导致同一逻辑内容产生不同字节表示。

陷阱四:库升级后缓存点悄悄消失

大多数团队并不手动编写缓存点,而是由框架或网关添加——且通常仅对它能识别的模型 ID 生效。升级库、切换到新模型 ID 或应用推理配置文件 ARN,缓存点就可能无声消失。

这正是前面提到的 LiteLLM 事件:一次升级改变了系统提示在 Bedrock 请求中的位置,Claude Code 流量的缓存命中率从约 90% 骤降至 25-45%,每日支出增长了 2-3 倍,持续六天才被定位。这不是孤例——MLflow 的 AI Gateway 将 Bedrock 聊天消息转为纯字符串,从不发送缓存点;LangChain 的 openwiki 发现其缓存中间件跳过了应用推理配置文件 ARN。

检测方法: 如果缓存读取和缓存写入都为零,说明没有缓存点到达 Bedrock。每次库升级和模型变更后都应检查这一点。

**推理配置文件 ARN(inference profile ARN)**是 Bedrock 中用于跨区域路由或指定特定模型变体的标识符。很多框架的缓存逻辑只根据它能识别的模型 ID 字符串来决定是否插入缓存点,当请求的模型字段变成推理配置文件 ARN 时,匹配失败,缓存点就静默消失。由于请求仍然返回 200,没有异常日志,问题只能通过指标暴露:CacheReadInputTokenCount 与 CacheWriteInputTokenCount 同时归零。因此建议在任何库升级、切换模型 ID、或改用推理配置文件之后,都做一次基线对比,将缓存读写比作为发布检查项之一。

陷阱五:流式响应重建消息时改变历史结构

当你流式接收响应并将其保存回对话时,库会从各个块重建助手消息。如果重建方式与单一文本片段不同,历史结构就会改变。

在 LangChain.js 中,在 Bedrock 上启用扩展思考的流式传输会丢失文本块索引,导致一个回答被保存为数百个独立文本块。这把下一个缓存点远远推过了回溯限制。

检测方法: 统计已保存助手消息中的内容块数量。一条回复应该是一个文本块,而不是数百个。

如何在你的账户中自查

AWS/Bedrock 命名空间中的两个 CloudWatch 指标能告诉你大部分信息:CacheReadInputTokenCount 和 CacheWriteInputTokenCount,按模型 ID 拆分。在稳定流量中,读取量应是写入量的多倍。写入量高而读取量接近零,意味着缓存已开启但持续落空——这比完全不缓存更贵。两者都接近零则说明根本没有发送缓存点。

如果命中率在繁忙时段下降而提示保持不变,请检查跨区域推理。AWS 指出,在高需求时段其路由“可能导致缓存写入增加”,因为请求可能落在不持有缓存副本的区域。

想了解原因,可以开启 Bedrock 模型调用日志。日志会记录每次调用的缓存计数以及调用方身份。

**跨区域推理(cross-region inference)**会在多个区域之间路由请求以应对容量或延迟。缓存本身是有区域属性的:缓存副本存放在处理该请求的区域,若后续请求被路由到另一区域,即便提示完全一致也可能找不到原缓存。AWS 文档也提示,高需求时段的这类路由“可能导致缓存写入增加”。因此当命中率在繁忙时段下滑而提示不变时,除了检查缓存点位置和内容块数量,也应将路由行为纳入排查范围,必要时固定区域或观察缓存读写比在时段上的分布。

还缺什么

两件事。一是 Bedrock 的缓存诊断功能:Anthropic 的 Console 现在能解释请求为何未命中缓存,而 Bedrock 不能。二是 AWS 文档中给出精确的回溯数字,而非“约 20”——因为多一个或少一个工具调用,就决定了智能体为其历史支付 0.1 倍还是 1.25 倍的价格。

结语

提示缓存的失效是静默的。它不会抛出异常,不会返回错误码,只会在账单上留下痕迹。上述五个陷阱覆盖了从缓存点位置、工具扇出、提示结构、库行为到流式重建的完整链路。核心建议只有一条:在稳定流量中持续监控缓存读写比,并将缓存点视为需要主动管理的运行时资源,而非一次性配置。

#Claude 提示缓存#Amazon Bedrock#提示缓存失效#Bedrock 缓存点#Claude 智能体#LLM 成本优化#prompt caching
分享:

相关推荐