跳转到内容

语义缓存

当多个用户或应用提出相同的问题时,再次调用 LLM 会浪费时间和 token。语义缓存按 prompt 内容和模型作为键来存储响应,因此相同或几乎相同的请求可以立即获得缓存的答案。

缓存在请求到达 provider 之前拦截 AI 请求。它通过对模型名称和请求中最后一条 user 消息的内容取哈希来构建缓存键。如果存在匹配且未过期的缓存条目,则立即返回缓存的响应,完全不会调用 provider。

缓存键使用完整内容的哈希,而不是截断的前缀。两个措辞不同但含义相同的请求会生成不同的键,从而产生不同的缓存条目。缓存是精确匹配,而非基于嵌入的相似匹配。这意味着只有逐字符完全相同的 prompt 才能共享缓存的响应。

缓存支持通过 CacheBackend trait 接入可插拔后端:

后端适用场景
内存(DashMap单实例部署,低延迟,无外部依赖。默认容量为 10,000 条。
Redis多实例部署,所有数据面 Pod 共享一份缓存。条目在 Pod 重启后仍然存在。
pgvector已经运行 PostgreSQL 并希望将缓存与其他数据放在一起的环境。支持比内存更大的工作集。

后端在配置阶段选定。网关对存储层做了抽象,所有后端共享相同的缓存键格式、TTL 语义和淘汰行为。

默认的内存后端使用 DashMap 实现无锁并发访问。默认最多容纳 10,000 条。当缓存达到容量上限时:

  1. 首先淘汰所有过期条目。
  2. 如果淘汰后缓存仍满,则移除最旧的条目以腾出空间。

这种两阶段淘汰机制在保持缓存容量有界的同时,优先保留较新的条目。

通过 AIService 资源启用和调优语义缓存。默认 TTL 为一小时(3600 秒),默认 maxTokens 上限为 4096:

apiVersion: gateway.nantian.dev/v1alpha1
kind: AIService
metadata:
name: cached-openai
namespace: nantian-demo
spec:
provider: openai
format: openai
model: gpt-4o
semanticCache:
enabled: true
ttl: 3600s
maxTokens: 4096

ttl 字段控制缓存条目从创建起的存在时间。较短的 TTL 让响应更及时,但代价是更多的 provider 调用。较长的 TTL 节省更多 token,但如果底层模型行为发生变化,可能会提供过时的响应。

maxTokens 字段限定了可缓存的响应大小上限。超过 maxTokens 的响应不会被存储,防止缓存被长篇生成内容填满。如果你的典型请求产出的补全较短,设置较低的 maxTokens(如 1024)可以让缓存聚焦于最可复用的响应。

enabled 设为 false 可以完全绕过缓存,无需删除配置。这在调试缓存相关问题或在模型迁移期间临时关闭缓存时很有用。

缓存命中时,网关返回存储的响应,其中包含与原始 provider 调用相同的 ID、模型、choices 和 usage 数据。客户端无法区分缓存响应与新鲜响应。

缓存未命中时,请求正常发送到 provider,响应被存储以供后续使用。只有在响应满足 maxTokens 阈值且后端有空间时才会存储。

缓存不会在命中时延长 TTL。以一小时 TTL 存储的条目会在创建后恰好一小时过期,无论被访问了多少次。这保证了缓存生命周期的可预测性,避免过期条目超出预期窗口继续存在。

AI Gateway 指标通过命中和未命中计数器反映缓存行为。利用这些指标评估缓存效果:

  • 命中率高意味着缓存通过避免 provider 调用获得了回报。生产流量上超过 30% 的命中率是保留缓存的强烈信号。
  • 命中率低说明 prompt 变化太大,精确匹配缓存效果不佳,或者 TTL 对你的访问模式来说太短。
  • 将未命中计数器和 provider 延迟一起观察。缓存命中率突然下降伴随延迟上升,可能表明缓存后端故障。
  • 随时间追踪缓存条目数,确认淘汰策略是否将后端控制在合理范围内。
  • 从适中的 TTL(600 到 3600 秒)开始,根据你的 prompt 多样性和响应新鲜度需求调整。知识库问答类工作负载通常比创意写作类工作负载可以容忍更长的 TTL。
  • 单 Pod 部署使用内存后端。扩展到多个数据面副本时切换到 Redis,让所有实例共享一份缓存。
  • maxTokens 应与你想缓存的响应类型对齐。设低一些(如 1024)可以避免存储长篇补全,调高则可以覆盖更多响应形态。
  • 缓存键与模型名绑定。如果你更改 spec.model,已有的缓存条目不会被复用。在模型迁移期间清空缓存或让条目自然过期。
  • 在调整 TTL 或 maxTokens 后监控命中率。大幅调整可能会显著改变缓存行为。

缓存键由两部分构成:模型名和请求中最后一条 user 消息的内容。两者通过标准哈希函数一起哈希,生成十六进制编码的标识符,格式为 cache:0123456789abcdef

只有最后一条 user 消息参与生成键。system 消息和对话历史中的早期消息被排除在外。这意味着两个 system prompt 不同但最终用户问题相同的请求将共享一个缓存条目。同时也意味着,在一次多轮对话中,用户在第三轮和第五轮问出相同问题时,会生成相同的缓存键。

全内容哈希的方式避免了截断前缀可能带来的碰撞。两个开头相同但后续内容不同的 prompt(例如 “What is the capital of France?” 与 “What is the capital of Germany?”)会生成不同的缓存键。

由于缓存是对最后一条 user 消息做精确匹配,它在相同 prompt 能稳定产出一致质量响应的环境中效果最好。以下场景中缓存可能效果较差:

  • 包含时间戳、随机 ID 或会话特定数据的 prompt。每个变体都会生成不同的缓存键,导致缓存未命中。
  • 频繁变化的 system prompt。由于 system 消息不参与键生成,两个 system prompt 不同但 user 消息相同的请求会收到相同的缓存响应,即使 system prompt 本应改变输出。
  • 返回非确定性响应的模型。如果相同的 prompt 在不同调用中产生不同的答案,首个响应会被缓存,后续调用都返回首个版本,即使一次新的调用本可能产生不同的答案。

在这些情况下,考虑缩短 TTL 或完全禁用缓存。