跳到主要内容

语义缓存是安全隐患,而非性能提升

· 阅读需 14 分钟
Tian Pan
Software Engineer

语义缓存命中是唯一一种能在不到一毫秒的时间内,将错误答案发送给错误用户的 LLM 优化方式。SQL 缓存之所以会返回你或他人的数据行,是因为有人写错了 join —— 这种故障模式属于查询 bug。而语义缓存返回另一个租户的响应,是因为两个 embedding 在 0.03 的余弦距离内落到了一起,这正是系统完全按设计运行的结果。缓存完成了它的工作,问题在于这份工作本身。

大多数团队将语义缓存作为一种成本方案来推行 —— 每个 AI 工程 Slack 频道里都流传着一份“削减 70% 账单”的 PPT —— 并且像对待 Redis TTL 一样审查缓存键(cache key):完全不审。这种审查通常交由性能团队负责。安全团队永远看不到设计文档,因为没有人会为“我们增加了一条更快的路径”提交安全审查。六个月后,某人的合规审计发现,“我无法登录我的账户,我的电子邮件是 [email protected]”和“我无法登录我的账户,我的电子邮件是 [email protected]”在向量化后都处于“我无法登录我的账户”的阈值内,于是缓存愉快地向 Bob 提供了原本为 Jane 生成的响应,其中包含了她账户请求的密码重置链接。

这篇文章将讨论为什么语义缓存值得拥有与 SQL 谓词相同的审查严谨性、如何通过缓存键设计从结构上防止跨用户泄露,以及你需要什么样的审计追踪来区分“缓存命中提供了正确答案”与“缓存命中在亚毫秒级延迟下提供了他人的答案”。

为什么 Embedding 相似性是错误的等价类

传统的缓存键是一个哈希值。两个输入只有在哈希值完全相同时才是“相同的”,而优秀的哈希函数保证了细微的输入变化会产生不相关的输出。这种雪崩效应(avalanche property)正是基于哈希的缓存机制安全的原因:除非两个查询在字节级别完全一致,否则缓存查找机制不会混淆两个语义不同的查询。

语义缓存刻意抛弃了这一特性。它的核心意义就在于,让“我该如何重置密码?”和“我忘记密码了”命中同一个缓存条目。查找机制奖励局部性 —— 细微的输入变化产生细微的 embedding 变化 —— 并通过相似性阈值(通常为 0.85–0.95 的余弦相似度)来决定什么算作“足够接近”。这与密码学中的抗碰撞概念是不相容的。2026 年的一篇论文精确地形式化了这种权衡,并展示了攻击者可以构造出处于阈值“剃刀边缘”的输入:在语义上足以承载不同的意图,但在数学上又足够相似以命中目标查询的缓存条目。他们的 CacheAttack 框架在针对主流工业实现的响应劫持测试中,报告了 86% 的成功率。

关键不在于对抗性输入是主要风险。主要风险是良性碰撞 —— 普通用户在并无恶意的情况下,提出了一些相似的问题,但其答案取决于缓存键未捕获的上下文。授权范围是上下文。租户身份是上下文。用户个性化也是上下文。这些都不存在于自然语言查询的 embedding 中,因为 embedding 根本不知道这些东西的存在。缓存键只是 embedding “认为”用户所问的内容。而正确的答案取决于用户被“允许”知道什么。

命中率仪表盘无法显示的故障模式

缓存的可观测性是围绕命中率、延迟和节省的成本构建的。这些指标中没有一个能对跨用户泄露发出警报。泄露表现为一次耗时 0.5 毫秒并返回了存储响应的缓存命中。仪表盘显示一切正常。

以下是我在三个不同团队的生产级语义缓存部署中亲眼见过的泄露形式:

授权范围坍塌。 像“显示我的待处理工单”这样的查询,其缓存响应(即实际的工单列表)仅以查询的 embedding 为键。下一个询问“显示我的待处理工单”的用户命中了缓存,并看到了第一个用户的工单。这听起来太蠢了,不应该发布,但我确实见过它发布了两次,两次都是因为团队将语义缓存视为“带有向量功能的 Redis”,而忘记了响应是个性化的。

个性化溢出。 “推荐一个健身方案”返回了一个缓存响应,其中提到了前一个用户的年龄、受伤史和器械情况。查询 embedding 并不知晓这些信息,而响应却将其泄露了。这种泄露很难检测,因为这两个查询确实具有相同的意图 —— 缓存“应该”匹配。Bug 在于该响应根本不该被缓存,或者应该在剥离个性化变量后再进行缓存。

权限绕过。 一个免费层级的用户询问“总结这次财报电话会议”,并得到了昨天为付费层级用户缓存的响应 —— 其中包含了免费层级无权访问的专业分析师评论。缓存愉快地提供了响应,因为访问层级不是缓存键的一部分。该用户免费获得了功能升级,而产品团队的指标显示免费层级的参与度异常高,却没人将其与缓存关联起来。

撤销后过期失效。 用户 A 向用户 B 共享了一个文档,随后撤销了访问权限。用户 B 询问了一个关于该文档的问题;缓存提供了撤销前的答案。检索层正确地从新查询中排除了该文档 —— 但缓存完全绕过了检索过程。在缓存条目过期前,用户可以阅读他们已无权访问的内容。

跨租户语言冲突。 租户 Acme 和租户 Globex 都询问“我们的第四季度营收预测是多少?”。这两个查询的 embedding 几乎完全相同,因为自然语言形式是一致的,尽管语义上指向的文档、模型和答案完全不同。在没有租户前缀的全局缓存中,一个租户的预测数据会被提供给另一个租户。

以上每一种情况在标准的缓存仪表盘中都是不可见的。它们会出现在支持工单、合规审计中,甚至在最糟糕的情况下,出现在新闻报道里。

缓存键即变相的授权谓词

这种修复是结构性的,而非统计性的。语义缓存键(Semantic cache key)是一种契约,规定了哪些其他查询可以命中该条目。该契约的设计应像按用户 ID 过滤的 SQL WHERE 子句一样受到严格审查,因为从功能上讲,它确实就是如此。

在任何多用户系统中,语义缓存的最小可行安全键包含三个必需组件和一个可选组件:

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates