跳到主要内容

你的工具结果缓存是一份你从未签署过的过期数据契约

· 阅读需 12 分钟
Tian Pan
Software Engineer

追踪记录看起来很干净。Agent 调用了 get_inventory_status,工具返回了 {"available": 142, "warehouse": "SEA-3"},模型将这些信息编织成了一个自信的回答。客户下单了。仓库却说该商品自上午 9 点以来一直缺货。缓存的行数据是四小时前的。团队中没人决定过四小时是可接受的 —— 这只是平台团队连接包装器(wrapper)时,缓存框架默认的设置。

这种失效模式经常被误归类为幻觉。模型并没有在胡编乱造;它是在忠实地根据一个过期的工具结果进行推理,而没人费心将该结果标记为过期。追踪记录显示的是一次干净的调用和干净的响应,评估集(eval set)从未见过过期缓存的情况,而这种退化在每一个撞上相同 TTL 窗口的客户身上悄无声息地累积。

位于工具调用之前的缓存,其性质与位于 JSON 端点之前的 CDN 这种优化方式截然不同。CDN 负载的消费者是渲染它的浏览器;而工具结果的消费者是一个模型,模型会对它进行 规划,将其编织进多步链条中,并在下游产生一个自信的回答。在你 API 缓存中隐含的新鲜度契约(freshness contract),在模型成为读取者的那一刻,就变成了一个事关正确性的表面(correctness surface)。

默认 TTL 是你未曾阅读的契约

每个缓存框架都有一个默认值。Redis 风格的代理默认为平台团队配置的任何值。LiteLLM 和类似的中间件暴露了一个 ttl 参数,如果未设置,则回退到集成的默认值 —— 对于响应缓存通常是一小时。像最近的 ToolCacheAgent 这样用于工具缓存的框架明确地对每个工具的缓存能力和过期规则进行建模,但大多数生产团队尚未采用它们;他们仍在使用某个员工在部署缓存层当天选择的全局 TTL。

结果就是,一套统一的新鲜度策略被覆盖在了波动性(volatility)极不均匀的数据之上。库存水平每隔几分钟就会变化。客户画像数据在客户登录更新时变化。定价在财务部门推送新的 SKU 表时变化。知识库文章根据编辑团队的进度变化。一小时的 TTL 对其中之一是合理的折中,而对其他则是正确性 Bug —— 团队选择了这种折中,却没有说明它是针对哪些数据进行的妥协。

微软的 Cache-Aside 模式文档中埋着一句话,更多的团队应该把它纹在值班电脑上:缓存策略应该被视为一种产品契约,明确定义哪些地方可以接受过期读取,哪些地方需要严格的新鲜度。大多数 Agent 平台尚未编写这份契约。契约确实存在 —— 它只是隐含地存在于框架默认的任何设置中。

为什么过期在追踪记录中会变成幻觉

过期缓存故障被模式匹配为幻觉的原因是,追踪记录看起来是一样的。一次干净的工具调用,一次干净的响应,一个自信的下游回答。评估集(如果存在的话)是使用新鲜数据构建的;它没有“当 Agent 获取的库存是四小时前的时候该怎么办?”这样的条目。因此,评估通过率保持绿色,而用户感知的准确性却在下降。

这种模式出现在各种缓存工具中。在源文档编辑前构建的向量索引(embedding index)上进行 RAG 检索,返回错误片段的速度只会更快,而不会更慢;Agent 自信地引用了在实时文档中已不存在的段落。缓存的价格表告诉 Agent 折扣是 15%,而该折扣在午夜已过期。缓存的可用性检查显示日历是开放的,而它刚刚被预订。

Streamkap 对此的撰文 —— 即过期数据比没有数据更糟糕,因为没有数据会提示模型承认无知,而过期数据会提示它自信地给出错误答案 —— 是正确的切入点。模型无法访问它未被告知的新鲜度信号。如果你的工具无论底层数据是两秒钟前还是两天前,都返回相同形状的负载,模型就会以同样的自信度对待两者,因为 Schema 就是这么告诉它的。

源自数据波动性的每工具 TTL

必须落地的第一项准则,是为 Agent 可以调用的每个工具命名波动性类别,并据此推导 TTL,而不是使用缓存框架的默认值。

波动性类别不需要非常精确。一套包含四个桶的分类方案就能覆盖大多数生产环境中的 Agent 栈:

  • 实时关键型 (Real-time critical)。 下单时的库存、支付授权、日历预订。缓存 TTL 为零或接近零,或者在关键路径上完全跳过缓存。
  • 分钟级新鲜度 (Minutes-fresh)。 搜索结果数、最近活动流、进行中的订单状态。TTL 为 30 到 300 秒,并带有随机抖动(jitter)以防止过期时的缓存击穿。
  • 小时级新鲜度 (Hours-fresh)。 产品目录元数据、账户设置、知识库文章。根据源内容的编辑频率,TTL 为 1 到 12 小时。
  • 天级新鲜度或静态 (Days-fresh or static)。 参考分类法、货币列表、国家代码、极少更改的文档。长 TTL 是合适的;这里的失效模式是当罕见的更新发生时忘记清理缓存。

分类并不难。难点在于目前团队中没有人负责这个问题,所以分类从未完成。平台团队负责缓存框架。功能团队负责工具包装器。产品团队负责用户体验。新鲜度契约就落在三个负责人的间隙中,结果就是通用的默认设置被发布并一直保留。

一个简单的干预措施:要求每个工具注册时必须声明一个 volatility 字段,并让缓存层从该字段读取其 TTL,而不是从框架默认值中读取。当功能团队第一次必须为库存工具填入 volatility: "real-time",为目录工具填入 volatility: "hours-fresh" 时,关于正确数值的讨论就从理论变成了具体。

将新鲜度元数据织入结果

第二条准则是模型应当能够在必要时对过时性(staleness)进行推理,而目前它还做不到,因为过时信息在缓存边界就被丢弃了。

修复方法是用模型可见的显式新鲜度元数据包装缓存的工具结果。与其返回:

{"available": 142, "warehouse": "SEA-3"}

不如返回:

{"available": 142, "warehouse": "SEA-3", "as_of": "2026-04-28T09:14:00Z", "data_age_seconds": 14400, "source": "cached"}

或者,对于那些无法很好地处理任意元数据字段的提示词工程驱动的智能体(prompt-engineered agents),更务实的做法是添加一行前导语:“此库存快照来自 4 小时前,可能无法反映当前库存。” 成本仅为几个 token。而收益是模型现在可以选择标记过时性、询问用户是否重新检查,或升级为获取最新数据的调用——如果过时性对模型不可见,它就无法完成其中的任何操作。

这并非理论上的模式。最近关于工具调用追踪(tool-call tracing)的可观测性报告强调,来源新鲜度和最后更新时间戳应属于每一次检索和工具调用的元数据 Schema。模型可以使用它们;追踪(trace)可以使用它们;评估(eval)也可以使用它们。在缓存边界剥离这些信息的团队实际上已经认定模型不需要知道这些——而这个决定就是一份没人写下的契约。

次生收益:当新鲜度元数据存在于 trace 中时,你可以运行一个 eval,专门询问智能体如何处理过时结果。以前你无法运行这种评估,因为 trace 并没有携带过时信号。现在,评估集可以包含缓存值已存在 X 分钟的场景,模型必须决定是使用它、刷新它还是披露它。

针对高风险调用的缓存绕过层级

有些工具调用根本不可缓存,它们的新鲜度契约是“始终绕过”。下单、支付授权、账户变更,以及任何其下游效应是依赖于读取操作的写入操作的调用,都应走绕过层级——接受延迟成本以换取正确性保证。

这种模式是在工具调用中采用每调用一次就设定一种新鲜度模式:freshness: "strict" 强制缓存失效;freshness: "best-effort" 接受工具 TTL 内的缓存值;freshness: "stale-ok" 在数据源无法连接时接受已过期的缓存值。智能体的规划器(planner)——或工具包装器本身——根据该调用在链条中的角色来选择模式。

这与经典系统设计中的标准 Cache-aside、Read-through 和 Write-through 模式具有相同的形态,只是让模型作为规划器参与其中,而不是单纯作为缓存读取器。经典模式假设程序员在编写代码时选择了策略。智能体系统需要策略能够按调用进行选择,因为同一个智能体在同一个会话中可以以两种不同的角色调用同一个工具:一次是为了为对话式回答提供依据(“尽力而为”即可),另一次是为了授权交易(则必须“严格”执行)。

显式评估过时缓存场景

最后一条准则是最容易被忽略但杠杆率最高的。如果你的评估集(eval set)是在一个闲暇的下午从生产系统中构建的,那么其中包含的过时缓存场景必然为零。评估期间的缓存命中率被人为抬高了;缓存寿命被人为缩短了;评估变成了一种系统性地低估了你最需要捕获的故障模式的测量工具。

修复方法是增加一个过时缓存评估切片(stale-cache eval slice)。合成一些缓存工具结果已有 N 分钟、小时或天数的场景,并且自缓存预热以来,真相来源(source-of-truth)已经发生了变化。从三个维度对智能体进行评分:

  • 它是否产生了正确的答案(使用实时数据)?
  • 如果它使用了过时缓存,它是否向用户披露了过时情况?
  • 当过时程度超过提示词中指定的阈值时,它是否选择了刷新?

第一次运行这种评估切片的团队通常会发现他们的智能体表现得极其糟糕——因为智能体没有收到数据已过时的信号,没有关于数据过时后该做什么的指令,也没有动机(在评估所衡量的损失函数中)去披露。这三点都是可以修复的,但前提是评估必须先让问题暴露出来。2026 年的 RAG 评估工作正因如此开始加入过时检索率指标:将过时和新鲜的案例平均在一起,会掩盖掉那些本应作为独立指标报告的故障模式。

在下一次事故发生前该做什么

这项工作的性质并不光鲜。它不是模型升级,不是新工具,也不是一个华丽的演示。它是一份新鲜度契约文档,命名了智能体可以调用的每一个工具、其底层数据的波动类别、由此推导出的 TTL、织入结果中的新鲜度元数据、高风险调用的绕过层级,以及衡量过时缓存行为的评估切片。

大多数在生产环境中遇到过过时缓存事故的智能体团队,都在事故发生后的那一周写下了这份文档的一个版本——在复盘的压力下,且已经背负了客户信任度下降的代价。而那些尚未遇到事故的团队,正运行在缓存框架开启那天为他们自动生成的契约上,那个契约通常是:“一个 TTL 适配所有场景,没有新鲜度元数据,没有绕过层级,没有过时场景评估。” 那个契约在每个采用它的团队中都扮演着同样的角色:它一直运行良好,直到那天的到来,失败看起来就像幻觉,接下来的整个季度都被用来调试模型,而实际上 Bug 存在于模型无法看到的缓存层。

必须达成的架构认知是:缓存工具结果与缓存 API 响应并非同一种优化。消费者是一个规划器。计划的新鲜度取决于数值的新鲜度。而数值的新鲜度则取决于那个团队未曾视作正确性层面的缓存。明确新鮮度契约——显式地、针对每个工具、在代码中、在元数据中、在评估中——才能将缓存从一个悄悄累积的 Bug 转化为一个经过深思熟虑管理的权衡。

在事故为你写下契约之前,先亲手写下它。

References:Let's stay in touch and Follow me for more thoughts and updates