RAG 缓存策略生产实战:多层缓存、权限隔离、版本失效与防击穿
从 Embedding、Retrieval、Rerank、Context、Answer、HTTP 到 Provider Prompt Cache,设计带版本、权限、新鲜度、失效事件、防击穿、可观测性与回滚能力的生产级 RAG 缓存体系。
缓存通常被描述为最简单的性能优化:
先查 Cache
→ 命中就返回
→ Miss 再访问下游但在 RAG 系统里,“同一个请求”很难定义。
下面任意一项不同,都可能让旧结果不可复用:
- 用户租户、角色和 ACL;
- Knowledge Base Scope;
- Query Rewrite 和 Filter;
- 语料 Revision;
- Elasticsearch 物理索引;
- Embedding 模型、维度和输入模板;
- RRF、Reranker、Context Packer;
- Prompt、生成模型和 Guardrail;
- Citation Policy;
- 当前时间与业务
as_of; - Answer Mode;
- Degraded 状态。
因此,RAG 缓存不是“把 Query 和 Answer 放进 Redis”,而是一套正确性协议:
定义等价请求
→ 冻结依赖版本
→ 隔离权限范围
→ 约束新鲜度
→ 处理并发 Miss
→ 传播失效事件
→ 记录命中来源
→ 支持回放和回滚资料快照:本文核查截至 2026-08-06。HTTP 语义参考 RFC 9111;Cache-aside 与防击穿参考 Redis 官方文档;Elasticsearch 部分区分 Node Query Cache 与 Shard Request Cache;Prompt Cache 部分参考 OpenAI 官方资料中的 Exact Prefix 原则。具体 Provider 的命中窗口、计费和支持模型会变化,生产实现应读取真实 API Usage 字段并锁定 Provider / Model Version,而不是写死历史价格或 TTL。
图 1:每一层缓存不同对象,也拥有不同 Key、TTL、失效条件和安全边界。
一句话结论
生产级 RAG Cache 应遵循下面六个原则:
- 缓存 Key 表达完整依赖,而不是只表达用户问题。
- 版本变化产生 Natural Miss,避免把手工清库作为一致性机制。
- 任何私有结果都按 Tenant 与 Permission Scope 隔离。
- TTL 只是陈旧上限,关键变更仍需主动失效。
- Cache Miss 需要 Single-flight、Jitter 和受控 Stale,避免击穿。
- Cache Hit 也必须进入 Trace,才能复现答案来自哪次计算。
1. 先区分八类不同缓存
图 2:缓存层级越靠后,结果越接近最终答案,但与权限、语料、Prompt、模型和业务时间的耦合也越强。
| 缓存层 | 缓存对象 | 主要收益 | 主要风险 |
|---|---|---|---|
| HTTP / Edge | 公开页面、静态资源、公共 API | 网络与渲染延迟 | 私有内容被共享缓存 |
| Embedding | 文本向量 | 降低 Embedding 调用 | 模型或模板变化仍复用旧向量 |
| Query Plan | Rewrite、Intent、Filter Plan | 降低 Planner 成本 | 会话、时间和权限语义丢失 |
| Retrieval | BM25 / kNN / RRF 候选 | 降低 ES 与向量查询 | 语料更新、Scope 越权、旧 Revision |
| Rerank | Query–Candidate 分数和排序 | 降低 GPU / API 成本 | Candidate 内容变化但 ID 不变 |
| Context | 选中 Evidence 与组装结果 | 降低回填、去重、压缩 | Token Budget、ACL、Citation 过期 |
| Answer | 最终答案、Claim、Citation | 最大延迟与成本收益 | 陈旧回答、权限泄漏、个性化错误 |
| Provider Prompt | 推理端共享前缀计算 | 降低输入处理成本和延迟 | Prompt 顺序变化导致 Miss;不可替代业务缓存 |
这八层不能使用同一个 Key Schema 和 TTL。
1.1 应用 Cache 与 Provider Prompt Cache 不同
应用 Cache 命中时:
不再调用模型Provider Prompt Cache 命中时:
仍然调用模型
但复用相同 Prompt Prefix 的部分计算Provider Prompt Cache 不会自动保证:
- 语料新鲜度;
- Answer 正确性;
- 用户权限;
- Citation 有效性;
- 相同输出。
它只是推理层优化。
2. 请求等价性:缓存的真正定义
两个用户 Query 字符串相同,不代表两个请求等价。
“退款最晚什么时候申请?”可能因为下面差异得到不同答案:
用户 A 可看内部政策
用户 B 只能看公开帮助中心
用户 A 问当前政策
用户 B 在历史审计中问 2025 年政策
Tenant A 使用政策 v3
Tenant B 仍使用定制政策 v2可以把 Cache Equivalence 定义为:
Equivalent(Request A, Request B)
=
相同身份与权限语义
AND 相同 Query Plan
AND 相同语料状态
AND 相同 Pipeline
AND 相同时间语境
AND 相同输出策略任何一项不同,都应 Miss 或进入不同 Namespace。
3. Versioned Cache Key:让版本变化自然失效
图 3:新 Prompt、新模型、新索引或新 ACL 应生成新 Key;旧 Key 可以自然过期,不必在每次发布时扫描整个 Redis。
推荐 Key 生成流程:
结构化依赖对象
→ Canonical JSON
→ SHA-256
→ namespace:schema_version:tenant:hash示例:
{
"cache_schema": "answer-cache-v6",
"tenant_id": "tenant_001",
"permission_scope_hash": "scope_7a9e",
"normalized_query": "退款最晚什么时候申请",
"query_plan_version": "query-plan-v8",
"knowledge_base_ids": ["kb_policy"],
"corpus_revision": "kb_policy@1942",
"index_alias": "kb_chunks",
"physical_indices": ["kb_chunks_v5"],
"embedding_model_key": "embedding-x|template-v3",
"retrieval_config": "retrieval-v12",
"reranker_model_key": "reranker-y|input-v2",
"context_packer_version": "context-v7",
"answer_prompt_version": "answer-v15",
"generation_model": "model-z",
"citation_policy_version": "citation-v4",
"confidence_policy_version": "confidence-v8",
"business_as_of": "2026-08-06",
"answer_mode": "grounded_concise"
}最终 Key:
rag:answer:v6:tenant_001:8d6f2f1a...3.1 不要把明文 Query 和 ACL 放进 Key
Redis Key 会出现在:
- Metrics;
- Slow Log;
- 运维工具;
- Debug 输出;
- 内存分析。
因此建议只保留低敏感 Namespace 和 Hash。
3.2 Key Schema 自身要版本化
当 Key 逻辑变化时:
v5 → v6新请求自然 Miss。不要让新旧代码对同一 Key 有不同解释。
4. Embedding Cache
Embedding 是最适合缓存的 RAG 对象之一,因为它通常是确定性的、输入较稳定、复用率高。
4.1 Key 必须包含什么
{
"normalized_text_sha256": "...",
"model_provider": "provider-a",
"model_id": "embedding-model-x",
"model_revision": "2026-07-18",
"input_type": "query",
"input_template_version": "embed-query-v3",
"dimension": 1024,
"normalization": "unicode-nfc-whitespace-v2"
}只使用 text_hash + model_name 仍然可能错误,因为:
- 同一模型支持 Query / Document 不同前缀;
- 模型服务可能在相同名称下升级;
- 维度或归一化方式可能变化;
- 文本预处理变化会影响向量。
4.2 Query Embedding 与 Document Embedding 分开
query: "query: ..."
document: "passage: ..."即使原始文本相同,输入角色不同也不能复用。
4.3 跨租户复用是否安全
对公开、去标识化文本,Embedding 可以按内容 Hash 跨租户复用。
对可能包含:
- PII;
- 私有代码;
- 企业文档;
- 用户查询;
建议:
- Tenant-scoped Key;
- 加密 Value;
- 不记录明文;
- 设置 Retention;
- 对 ZDR / 合规模式单独 Namespace 或完全 No-store。
4.4 Value 应带 Manifest
{
"vector": [0.012, -0.041],
"dimension": 1024,
"model_key": "embedding-model-x@2026-07-18",
"template_version": "embed-query-v3",
"text_sha256": "...",
"created_at": "2026-08-06T10:00:00Z"
}读取时校验 Manifest,避免错误反序列化或维度污染。
5. Query Plan Cache
可以缓存:
- Query Classification;
- Standalone Rewrite;
- Entity Resolution;
- Lexical Expansion;
- Subquery Decomposition;
- Router Decision。
5.1 Key 不能只有当前问题
Follow-up Query:
“那第二个方案呢?”依赖会话历史,因此 Key 应包含:
conversation_state_hash
summary_version
visible_history_hash5.2 权限 Scope 不应由缓存的 Planner 决定
可以缓存:
模型提出的逻辑 Filter Intent不能缓存并跨用户复用:
最终 Tenant / ACL Filter最终 Scope 必须由当前请求的权限系统重新编译。
5.3 时间表达需要时间基准
“最近一周”
“现在有效的政策”
“昨天的版本”Key 必须包含 business_as_of 或解析后的绝对时间范围。
6. Retrieval Cache
Retrieval Cache 保存候选结果,而不是最终答案。
6.1 推荐缓存 Candidate Manifest
{
"query_plan_hash": "...",
"scope_hash": "...",
"corpus_revision": "kb_policy@1942",
"physical_indices": ["kb_chunks_v5"],
"retrieval_config": "retrieval-v12",
"candidates": [
{
"chunk_id": "chunk_001",
"source_revision_id": "policy@rev_43",
"bm25_rank": 1,
"dense_rank": 3,
"rrf_rank": 1,
"content_hash": "..."
}
],
"partial": false,
"created_at": "2026-08-06T10:00:00Z"
}6.2 Corpus Revision 是关键
TTL 不能知道哪些文档发生变化。
推荐每个检索 Scope 有稳定 Revision:
KB revision
Index generation
Source snapshot set文档更新后:
kb_policy@1942 → kb_policy@1943新请求自然 Miss。
6.3 大 KB 的 Revision 粒度
全局 Revision 简单但会导致大量 Miss。
可以分层:
tenant_revision
knowledge_base_revision
source_group_revision
entity_revisionKey 只包含当前 Scope 涉及的 Revision。
但 Revision 越细,复杂度越高。早期优先使用 KB 级 Revision。
6.4 Retrieval Cache 必须包含 Scope
至少包含:
- Tenant;
- KB IDs;
- ACL / Group Hash;
- Language;
- Document Status;
- Time Filter;
- Source Type;
- Answer Mode。
权限相关结果绝不能只按 Query 共享。
7. Elasticsearch 自带缓存不是应用 Retrieval Cache
7.1 Node Query Cache
Elasticsearch Query Cache 主要缓存 Filter Context 中的查询结果:
- 每个 Node 一个 Cache;
- 多个 Shard 共享;
- LRU 驱逐;
- 按 Segment 缓存;
- Segment Merge 可能使结果失效;
- 普通 Term Query 和非 Filter Context 不一定可缓存。
它优化的是底层 Filter Bitset,不会替应用保存完整的多路候选、RRF、Rerank 和 Trace。
7.2 Shard Request Cache
Shard Request Cache 缓存每个 Shard 的本地 Search Result。
官方文档说明:
- 默认主要缓存
size=0请求的 Aggregation、Total 和 Suggestion; - 使用
now的多数查询不可缓存; - 非确定性 Script 不可缓存;
- Shard Refresh 或 Mapping 更新会自动失效;
- 整个 JSON Body 的 Hash 参与 Key;
- JSON 字段顺序不同可能产生不同 Key。
因此,应用应 Canonicalize JSON:
{
"aggs": {...},
"query": {...},
"size": 0
}而不是每次随机序列化字段顺序。
7.3 三层关系
Elasticsearch Query Cache
→ 底层 Filter / Segment 优化
Elasticsearch Shard Request Cache
→ Shard Search Response 优化
Application Retrieval Cache
→ 完整 Query Plan、Scope、候选、版本和 Trace 复用三者可以共存,但监控指标必须分开。
8. Rerank Cache
Rerank 输入通常是:
Query × N 个 Candidate Text成本与 Candidate 数和 Token 数成正比,适合缓存。
8.1 Key 设计
{
"query_hash": "...",
"candidate_manifest_hash": "...",
"reranker_model": "reranker-y",
"model_revision": "2026-06-01",
"input_template": "rerank-v4",
"max_length": 1024,
"truncation_policy": "head-tail-v2"
}8.2 Candidate ID 不够
如果 Chunk 内容改变但 ID 没变,旧 Rerank Score 会污染结果。
Candidate Manifest Hash 应包含:
candidate_id
source_revision
content_hash
input_text_hash8.3 缓存单 Pair 还是整批
单 Pair Cache
query_hash + candidate_hash + model
→ score优点:候选组合变化时仍能复用。
缺点:Key 数量大,网络 Round Trip 多。
Batch Cache
query_hash + ordered_candidate_manifest_hash
→ ranked list优点:读取简单。
缺点:候选稍变就整体 Miss。
生产中可以:
L1 in-process Pair Cache
+
L2 Redis Batch Cache9. Context Cache
Context Cache 保存经过:
Hydrate
→ Parent / Neighbor Expansion
→ Dedup
→ MMR
→ Compression
→ Token Budget
→ Citation Mapping后的 Context Manifest。
9.1 Key 必须包含
ordered candidate manifest
context packer version
token budget
citation policy
source revision
permission scope
language
model context limit9.2 不要只缓存拼接文本
错误 Value:
一大段 Context String推荐:
{
"context_manifest_id": "ctx_001",
"blocks": [
{
"citation_key": "E1",
"evidence_id": "evidence_2031",
"source_revision_id": "policy@rev_43",
"content_hash": "...",
"context_order": 1,
"token_count": 120,
"truncated": false
}
],
"packer_version": "context-v7",
"token_budget": 6000
}这样才能验证 Citation、Revision 和权限。
10. Answer Cache
Answer Cache 收益最大,风险也最大。
10.1 什么时候可以缓存
- Grounded Fact Answer;
- Query 与 Scope 稳定;
- Source Revision 固定;
- Citation Validation 通过;
- 没有用户个性化;
- 没有高敏感信息;
- 没有快速变化数据;
- Answer Policy 允许。
10.2 Key 必须包含
normalized query
context manifest hash
prompt version
model / reasoning configuration
answer mode
citation policy
confidence policy
permission scope
business as-of
locale10.3 Value 保存什么
{
"answer": "...",
"claims": [...],
"citations": [...],
"answer_status": "verified",
"context_manifest_id": "ctx_001",
"source_revisions": ["policy@rev_43"],
"prompt_version": "answer-v15",
"model": "model-z",
"created_at": "2026-08-06T10:00:00Z",
"soft_expire_at": "2026-08-06T10:10:00Z",
"hard_expire_at": "2026-08-06T11:00:00Z"
}10.4 不建议缓存最终答案的场景
- 医疗、法律、财务高风险建议;
- 个性化身份信息;
- 实时价格、库存、天气、比赛和运营状态;
- Tool 执行结果;
- 权限频繁变化;
- Evidence 冲突未解决;
- Citation 未验证;
- Degraded / Partial Answer;
- 用户要求最新状态。
可以只缓存 Retrieval / Context,不缓存 Answer。
11. Semantic Answer Cache
Semantic Cache 使用 Query Embedding 找相似历史问题:
新 Query
→ 与历史 Query 向量近邻匹配
→ 超过阈值则复用答案它比 Exact Cache 风险高得多。
11.1 语义相似不等价
“员工可以报销出租车吗?”
“员工不可以报销出租车吗?”Embedding 可能非常接近,但逻辑相反。
“2025 年政策是什么?”
“当前政策是什么?”主题相同,时间语义不同。
11.2 必须先通过 Constraint Equivalence
至少比较:
- 实体;
- 否定;
- 时间;
- 数值;
- 地域;
- Tenant / ACL;
- Query Intent;
- Required Facts;
- Answer Mode。
11.3 推荐只复用中间结果
更安全的方式:
相似 Query
→ 复用 Query Plan / Candidate Seeds
→ 重新做权限过滤、检索校验和生成而不是直接返回旧答案。
11.4 Semantic Cache 需要独立评测
指标:
semantic_cache_hit_rate
semantic_false_hit_rate
constraint_mismatch_rate
stale_semantic_hit_rate
unauthorized_semantic_hit_rate
quality_delta_on_hitsFalse Hit 必须作为 Hard Gate。
12. Provider Prompt Cache
OpenAI 官方资料说明,Prompt Cache 依赖共享的精确 Prefix;为了提高命中率,应将稳定指令和示例放在前面,将用户特定、请求特定内容放在后面。
12.1 推荐 Prompt 顺序
System / Developer Instructions
稳定工具定义
稳定输出 Schema
稳定 Few-shot Examples
稳定领域规则
---------------- Shared Prefix ----------------
Tenant / User Scope
Current Context
User Query
Dynamic Tool Results12.2 会造成 Miss 的变化
- Model 改变;
- System / Developer Prompt 改变;
- Tool 列表或顺序改变;
- JSON Schema 字段顺序改变;
- 图片或文件改变;
- 静态示例中插入动态时间;
- MCP Tool 动态顺序不稳定;
- Sandbox / Execution Context 改变。
12.3 Canonicalize Tool 和 Schema
按稳定名称排序工具
固定 JSON 字段顺序
不要在静态 Prefix 中写 request_id / timestamp
Prompt Template 必须版本化12.4 监控真实 Usage
不要根据“Prompt 看起来一样”推断命中。
记录 Provider 返回的:
input_tokens
cached_input_tokens
cache_write_tokens(如 Provider 提供)
provider
model
prompt_prefix_hash12.5 Prompt Cache 不等于数据缓存
Prompt Cache 仍然可能:
- 重新生成不同答案;
- 使用过时 Context;
- 产生错误 Citation;
- 受到动态内容影响。
它不能替代 Source Revision 和 Answer Cache Key。
13. HTTP / Edge Cache
RFC 9111 区分:
private cache
→ 单一用户专用
shared cache
→ 多用户共享的中间缓存RAG 私有答案默认不应进入 Shared Cache。
13.1 推荐响应头
私有、可短期复用:
Cache-Control: private, max-age=60, must-revalidate
Vary: Authorization, Accept-Language
ETag: "answer-manifest-hash"敏感、不可缓存:
Cache-Control: no-store注意:RFC 9111 也提醒,no-store 不是完整的隐私安全机制;仍需 TLS、服务端控制和数据治理。
13.2 Vary 不是万能权限隔离
CDN 对 Authorization、Cookie 和自定义 Header 的行为不同。高敏感答案应绕过 Shared CDN Cache,而不是依赖复杂 Vary 组合。
13.3 ETag 与 Revalidation
对于公开知识页和 Citation Preview:
ETag = content_hash / revision_hash客户端可以:
If-None-Match: "..."Source Revision 变化后返回新内容。
14. TTL:它是陈旧上限,不是一致性保证
Redis 官方 Cache-aside 文档强调,TTL 决定缓存可能陈旧的最长窗口;更短 TTL 降低陈旧风险,但会降低 Hit Rate、增加下游负载。
14.1 按对象设计 TTL
| 对象 | TTL 示例 | 失效主因 |
|---|---|---|
| Query Embedding | 天 / 周 | 模型或模板版本 |
| Document Embedding | 长期 | Source Revision / Model |
| Query Plan | 分钟 / 小时 | 会话、时间和 Prompt |
| Retrieval | 秒 / 分钟 | Corpus / ACL Revision |
| Rerank | 分钟 / 小时 | Candidate / Model |
| Context | 秒 / 分钟 | Revision、Policy、Budget |
| Answer | 秒 / 分钟 | Freshness、Scope、Prompt |
| Negative Cache | 很短 | 新数据创建 |
| Citation Preview | 分钟 / 小时 | Source Revision / ACL |
这些只是示例。
14.2 Soft TTL 与 Hard TTL
Soft TTL
→ 超过后结果可以短暂使用
→ 同时后台刷新
Hard TTL
→ 超过后绝不能复用适合:
{
"soft_expire_at": "2026-08-06T10:10:00Z",
"hard_expire_at": "2026-08-06T11:00:00Z"
}高风险和实时数据通常不允许 Stale。
14.3 TTL Jitter
base_ttl = 600s
jitter = random(-60s, +60s)避免大量 Key 同时过期。
15. 主动失效:写入成功后应该发生什么
Cache-aside 常见写路径:
写 Source of Truth
→ Commit
→ 发布 Invalidation Event
→ 删除或推进 Revision
→ 下一次读取重新填充Redis 官方文档推荐在更新 Primary 后删除 Cache Key,让下一次读从 Primary 重新填充,而不是在应用层尝试双写保持完全同步。
15.1 RAG 失效事件
{
"event_id": "evt_001",
"event_type": "knowledge_revision_published",
"tenant_id": "tenant_001",
"knowledge_base_id": "kb_policy",
"knowledge_id": "policy_refund",
"old_revision": 42,
"new_revision": 43,
"affected_namespaces": [
"retrieval",
"context",
"answer",
"citation_preview"
],
"committed_at": "2026-08-06T10:00:00Z"
}15.2 优先推进 Revision,而不是枚举所有 Key
如果 Key 包含 kb_revision=43:
新请求自动使用新 Key旧 Key 留待 TTL / LRU 清理。
事件仍然有价值:
- 主动删除高价值旧答案;
- 预热新版本;
- 更新 Dashboard;
- 防止内存浪费;
- 触发回归测试。
16. Cache Stampede、Penetration 与 Avalanche
图 4:热门 Key 过期后只能有一个 Loader 访问昂贵下游,其余请求等待、读取受控 Stale 或快速降级。
16.1 Stampede / Breakdown
热门 Key 同时 Miss:
1000 个请求
→ 同时查询 ES
→ 同时调用 Reranker
→ 同时调用 LLM防护:
- Single-flight;
- 分布式锁;
- Soft TTL;
- Probabilistic Early Refresh;
- 预热;
- 请求合并;
- 下游限流。
16.2 Cache Penetration
不断请求不存在对象:
每次 Miss
→ 每次打 Primary可使用 Negative Cache:
{
"status": "not_found",
"source_revision": 43,
"expires_in": 30
}Negative TTL 应显著短于正向结果;对象创建后必须主动失效。
16.3 Cache Avalanche
大量 Key 同时过期或 Redis 故障:
- TTL Jitter;
- 多级 Cache;
- 限流;
- Circuit Breaker;
- 下游容量保护;
- Stale-if-error;
- 批量预热;
- Redis 高可用。
17. Single-flight 正确实现
17.1 锁需要唯一 Token
SET lock:key token NX PX 5000释放时只能删除自己的锁:
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0避免 A 的锁过期后 B 获得新锁,A 又误删 B 的锁。
17.2 Lock TTL 必须覆盖 Loader 上限
lock_ttl > downstream_deadline + publish_margin如果 Loader 可能超过 TTL,需要续租或 Fencing Token。
17.3 等待者策略
短暂等待 Cache 填充
→ 超时后读取 Stale
→ 没有 Stale 则快速降级不要让所有等待者最终都回源。
17.4 成功、失败和 Partial 分 Namespace
rag:answer:success:...
rag:answer:partial:...
rag:answer:error:...失败结果不能覆盖成功结果。
18. Stale-While-Revalidate 与 Stale-if-Error
18.1 Stale-While-Revalidate
Soft TTL 已过
Hard TTL 未过
→ 返回旧值
→ 后台单飞刷新适合:
- 低风险公共知识;
- 允许几分钟陈旧;
- 下游昂贵;
- 高并发。
18.2 Stale-if-Error
下游故障时使用上次已验证结果:
{
"cache_status": "stale_if_error",
"age_seconds": 420,
"source_revision": 42,
"warning": "当前检索服务不可用,答案来自最近一次已验证结果。"
}18.3 不允许 Stale 的场景
- ACL 已撤销;
- Source 被删除;
- Critical Policy 更新;
- 安全修复;
- 实时金融 / 医疗 / 法律状态;
- 用户明确要求当前状态;
- 旧 Answer 使用已失效 Citation。
需要支持:
hard_invalidation
→ 即使下游错误也不能返回旧值19. 多租户与权限隔离
19.1 最低要求
任何含私有内容的 Key 必须包含:
tenant_id
permission_scope_hash
policy_version19.2 User ID 不是完整 Scope
同一用户的角色和群组可能变化。
permission_scope_hash 可以基于:
tenant
roles
groups
resource grants
policy versionCanonicalize 后 Hash。
19.3 共享结果的安全策略
允许跨用户共享之前证明:
same tenant
AND same effective scope
AND no user-specific context
AND no PII
AND same answer policy19.4 Revocation
权限撤销需要:
- 推进
permission_policy_version; - 主动删除高风险 Answer / Context Cache;
- 撤销 Citation Preview Token;
- 记录 Audit Event。
仅依赖 TTL 可能泄漏撤权后的旧结果。
19.5 加密与 Redis 隔离
高敏感场景:
- Value 加密;
- Tenant 独立 Prefix / Database / Cluster;
- Redis ACL;
- TLS;
- 内存和 Snapshot 治理;
- Key 不包含 PII;
- Operator Access Audit。
20. Cache Key 的 Go 实现
package cachekey
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
)
type AnswerKeyInput struct {
SchemaVersion string `json:"schema_version"`
TenantID string `json:"tenant_id"`
PermissionScope string `json:"permission_scope_hash"`
NormalizedQuery string `json:"normalized_query"`
KnowledgeBaseIDs []string `json:"knowledge_base_ids"`
CorpusRevision string `json:"corpus_revision"`
QueryPlanVersion string `json:"query_plan_version"`
RetrievalVersion string `json:"retrieval_version"`
RerankerVersion string `json:"reranker_version"`
ContextVersion string `json:"context_version"`
PromptVersion string `json:"prompt_version"`
ModelKey string `json:"model_key"`
CitationPolicy string `json:"citation_policy"`
ConfidencePolicy string `json:"confidence_policy"`
BusinessAsOf string `json:"business_as_of"`
AnswerMode string `json:"answer_mode"`
}
func BuildAnswerKey(in AnswerKeyInput) (string, error) {
if in.SchemaVersion == "" || in.TenantID == "" {
return "", fmt.Errorf("schema version and tenant are required")
}
ids := append([]string(nil), in.KnowledgeBaseIDs...)
sort.Strings(ids)
in.KnowledgeBaseIDs = ids
payload, err := json.Marshal(in)
if err != nil {
return "", fmt.Errorf("marshal cache key input: %w", err)
}
sum := sha256.Sum256(payload)
return fmt.Sprintf(
"rag:answer:%s:%s:%s",
in.SchemaVersion,
in.TenantID,
hex.EncodeToString(sum[:]),
), nil
}生产中应使用稳定 Canonical JSON Library,并为 Key Builder 编写 Golden Test。
21. Cache-aside 读取流程
21.1 Cache Failure 不应拖垮主链路
Redis 超时:
短 Deadline
→ Cache Bypass
→ 保护下游容量
→ 记录 cache_backend_error缓存是优化层,不应该成为单点故障;但 Cache 大面积失效时必须有限流,否则所有流量会打穿下游。
22. 数据模型与 Cache Manifest
CREATE TABLE cache_namespaces (
namespace_id VARCHAR(64) PRIMARY KEY,
cache_type VARCHAR(32) NOT NULL,
schema_version VARCHAR(32) NOT NULL,
default_ttl_sec INT NOT NULL,
max_value_bytes INT NOT NULL,
sensitivity_class VARCHAR(32) NOT NULL,
created_at DATETIME(6) NOT NULL
);
CREATE TABLE cache_invalidation_events (
event_id VARCHAR(64) PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
resource_type VARCHAR(32) NOT NULL,
resource_id VARCHAR(128) NOT NULL,
old_revision VARCHAR(128),
new_revision VARCHAR(128),
affected_namespaces JSON NOT NULL,
status VARCHAR(24) NOT NULL,
created_at DATETIME(6) NOT NULL,
processed_at DATETIME(6)
);
CREATE TABLE cache_audit_samples (
sample_id VARCHAR(64) PRIMARY KEY,
request_id VARCHAR(64) NOT NULL,
namespace_id VARCHAR(64) NOT NULL,
cache_key_hash CHAR(64) NOT NULL,
cache_status VARCHAR(32) NOT NULL,
value_manifest JSON,
age_ms BIGINT,
stale BOOLEAN NOT NULL,
created_at DATETIME(6) NOT NULL
);不需要把每次 Cache Read 都写进 MySQL;可以采样、聚合或写入 Trace Backend。
23. 可观测性
每次 Cache Span 至少记录:
cache.system
cache.operation
cache.namespace
cache.key_hash
cache.hit
cache.status
cache.age_ms
cache.ttl_remaining_ms
cache.value_size_bytes
cache.schema_version
cache.source_revision
cache.scope_hash
cache.stale
cache.refresh_triggered
cache.singleflight_role
cache.error.type23.1 RAG Trace 中的位置
Hit 时也要保留逻辑阶段,否则 Trace 会误以为 Retrieval / Generation 没有发生。
23.2 指标
hit_rate
miss_rate
stale_hit_rate
bypass_rate
eviction_rate
load_latency
cache_latency
singleflight_waiters
stampede_prevented
negative_hit_rate
false_hit_rate
invalidation_lag
prompt_cached_token_ratio
cost_saved23.3 Hit Rate 不是唯一目标
高 Hit Rate 可能来自:
- TTL 太长;
- Key 缺少版本;
- 权限范围过度共享;
- Semantic Threshold 太宽;
- 旧答案重复使用。
必须同时监控:
stale_answer_rate
unauthorized_cache_hit_rate
cache_related_bad_case_rate
quality_delta_on_cache_hits24. 成本与容量模型
24.1 每层价值
Saved Cost
=
Hit Count
× Avoided Downstream Cost
-
Cache Storage
-
Network
-
Serialization
-
Invalidation
-
Operational CostEmbedding Cache 的单次节省小但复用高;Answer Cache 单次节省大但风险高。
24.2 Value Size
不要把巨大 Candidate / Context / Answer 无限放进 Redis。
建议:
- 设置
max_value_bytes; - 大对象存对象存储,Redis 只存 Manifest URI;
- 压缩前先评估 CPU;
- 对热点大对象做 Local L1;
- 禁止无界 JSON。
24.3 Memory Budget
memory ≈ unique_keys × avg_value_size × replication_factor × overhead还要考虑:
- Redis Object Overhead;
- Fragmentation;
- Replica;
- Persistence;
- Failover Buffer;
- Hot Key Distribution。
24.4 Eviction Policy
不同 Namespace 最好不同实例或不同预算:
Embedding
→ 长寿命、价值稳定
Answer
→ 短寿命、权限敏感
Rate Limit / Session
→ 不能被 Answer Cache 挤出不要把所有用途放在同一个无边界 Redis 中。
25. 预热与发布
25.1 什么时候预热
- 新索引 Alias 切换;
- 新 Embedding / Reranker;
- 大促或发布会;
- 高频 FAQ;
- 已知热门 Query;
- Disaster Recovery 切换。
25.2 预热不能绕过权限
预热 Retrieval / Answer 时使用明确的 Scope Cohort:
public
employee
support
admin不能用超级管理员结果服务普通用户。
25.3 版本发布顺序
发布新索引 / Pipeline
→ 生成新 Cache Namespace
→ 影子流量预热
→ 验证命中质量
→ Canary
→ 全量
→ 旧 Namespace 保留回滚窗口
→ 再自然清理25.4 回滚
回滚不需要恢复旧 Key:
切回旧 Pipeline Version
→ 自动重新命中旧 Namespace前提是旧 Cache 尚未被主动删除。
26. 测试策略
26.1 Key Golden Tests
固定输入应产生固定 Hash。
覆盖:
- KB IDs 顺序不同;
- JSON 字段顺序不同;
- Scope 变化;
- Revision 变化;
- Prompt 变化;
as_of变化;- Unicode 与空白规范化。
26.2 Security Tests
用户 A 写入
→ 用户 B 不能命中
ACL 撤销
→ 旧 Answer / Context 不能继续返回
Tenant 相同但 Group 不同
→ Scope Hash 不同26.3 Invalidation Tests
- Source 更新;
- 删除;
- 权限变化;
- Index Alias 切换;
- Prompt 升级;
- Model 升级;
- Citation Policy 升级;
- Confidence Policy 升级。
26.4 Stampede Tests
对一个冷 Key 并发 100 / 1000 请求:
loader_count ≈ 1
waiter_timeout 在预算内
下游没有突发放大26.5 Chaos Tests
- Redis Timeout;
- Redis Failover;
- Pub/Sub / Event 延迟;
- Loader 超过 Lock TTL;
- Partial Write;
- Object Storage 不可用;
- Clock Skew。
27. Release Gate
rag_cache_gate_version: cache-gate-v5
hard_gates:
cross_tenant_cache_hit_rate:
max: 0
unauthorized_cache_hit_rate:
max: 0
stale_critical_answer_rate:
max: 0
semantic_false_hit_rate:
max: 0
cache_manifest_validation_rate:
min: 1.0
quality_gates:
answer_quality_delta_on_hits:
min: -0.005
citation_validity_on_hits:
min: 0.995
cache_related_bad_case_rate:
max: 0.001
performance_gates:
cache_p95_ms:
max: 10
singleflight_loader_amplification:
max: 1.05
invalidation_p95_ms:
max: 5000
cost_gates:
prompt_cached_token_ratio:
min: 0.50
downstream_call_reduction:
min: 0.20这些阈值是示例。
28. 常见反模式
- Answer Cache Key 只有 Query。
- 用 User ID 代替 Effective Permission Scope。
- 只靠 TTL,不处理 Source / ACL 更新。
- 每次发布
FLUSHALL。 - Document 内容变了,但 Chunk ID 不变,Rerank Cache 仍命中。
- Context Cache 只存 String,不存 Evidence Manifest。
- Semantic Cache 只看 cosine threshold。
- 高风险答案进入 Shared CDN。
no-store被当成全部隐私方案。- Prompt Prefix 中包含动态时间和 Request ID。
- Tool 定义顺序随机,导致 Prompt Cache Miss。
- 缓存 Degraded Answer 覆盖 Verified Answer。
- 热门 Key 过期没有 Single-flight。
- Lock 无唯一 Token,误删他人锁。
- Negative Cache TTL 与正向 Cache 一样长。
- Cache Backend 故障时所有请求直接打下游。
- 只看 Hit Rate,不看 Stale / Unauthorized / False Hit。
- Cache Hit 不进 Trace,Bad Case 无法复现。
- 所有 Namespace 共用一个 Redis 内存预算。
- 缓存值没有 Schema / Model / Revision Manifest。
29. P0 / P1 / P2 落地路线
P0:安全的精确缓存
- Embedding Cache;
- Query Plan Cache;
- Retrieval Cache;
- Versioned Key;
- Tenant / Scope Hash;
- Cache-aside;
- TTL + Invalidation;
- Single-flight;
- Negative Cache;
- Cache Trace;
- 禁止高风险 Answer Cache。
验收标准:
任何 Cache Hit 都能证明:
当前请求与原计算在权限、语料、Pipeline 和时间语境上等价。P1:Context / Answer 与发布治理
- Context Manifest Cache;
- Verified Answer Cache;
- Soft / Hard TTL;
- Stale-while-revalidate;
- Stale-if-error;
- Event-driven Invalidation;
- Prompt Prefix 优化;
- Cache Quality Gate;
- Shadow Prewarm;
- Namespace Rollback。
P2:高级缓存
- Semantic Cache;
- Learned TTL;
- Probabilistic Early Refresh;
- Cost-aware Admission;
- Multi-region Cache;
- Tenant-specific Policy;
- Adaptive L1 / L2;
- Popularity Forecast;
- Cache-aware Query Planner;
- Cross-request Rerank Pair Cache;
- Automatic Prompt Prefix Optimization。
30. 上线检查清单
- 是否列出所有 Cache Namespace?
- 每一层是否定义缓存对象和正确性边界?
- Cache Key 是否包含 Schema Version?
- 是否使用 Canonical JSON?
- Key 是否避免明文 Query、PII 和 ACL?
- Tenant 是否进入 Key?
- Effective Permission Scope 是否进入 Key?
- Permission Policy Version 是否进入 Key?
- Corpus / KB Revision 是否进入 Retrieval Key?
- Physical Index / Alias Generation 是否可追踪?
- Embedding Model、Revision、Input Type 和 Template 是否进入 Key?
- Rerank Candidate 是否包含 Content Hash?
- Context Cache 是否保存 Evidence Manifest?
- Answer Cache 是否包含 Prompt、Model、Citation 和 Confidence Policy?
- Business
as_of是否进入时间敏感 Key? - Answer Mode 和 Locale 是否进入 Key?
- 是否区分 Query Cache、Shard Request Cache 和应用 Retrieval Cache?
- ES JSON 是否稳定序列化?
- 是否定义 Soft TTL 和 Hard TTL?
- TTL 是否匹配业务陈旧容忍度?
- 是否加入 TTL Jitter?
- 写入 Source of Truth 后是否发布 Invalidation Event?
- 是否优先推进 Revision 而不是扫描所有 Key?
- ACL 撤销是否执行 Hard Invalidation?
- 热门 Key 是否有 Single-flight?
- Lock 是否有唯一 Token 和合理 TTL?
- 等待者是否有超时和 Stale 策略?
- Negative Cache 是否使用短 TTL?
- 新对象创建是否使 Negative Cache 失效?
- Redis 故障是否有 Bypass、限流和下游保护?
- Verified、Partial、Degraded、Error 是否分 Namespace?
- Semantic Cache 是否先验证 Constraint Equivalence?
- Semantic False Hit 是否是 Hard Gate?
- 私有 Answer 是否绕过 Shared CDN?
- HTTP
private、no-store、Vary和 ETag 是否正确? - Prompt 静态 Prefix 是否在前?
- Tool 与 JSON Schema 顺序是否稳定?
- 是否记录 Provider 返回的 Cached Token Usage?
- Cache Hit 是否保留原始 Manifest / Trace Link?
- 是否监控 Hit、Stale、False Hit、Unauthorized 和 Invalidation Lag?
- 是否为不同 Namespace 分配独立内存预算?
- 是否有预热、Canary、回滚和旧 Namespace 保留窗口?
- 是否做 Stampede、Security 和 Chaos Test?
总结
RAG Cache 的工程价值不是“少调一次模型”,而是在不改变请求语义的前提下安全复用已有计算:
Embedding Cache
→ 复用稳定向量
Retrieval Cache
→ 复用同 Scope、同语料版本的候选
Rerank Cache
→ 复用同 Query–Candidate–Model 评分
Context Cache
→ 复用同 Evidence、同 Token Budget 的组装结果
Answer Cache
→ 复用同权限、同 Revision、同 Prompt 和 Policy 的已验证答案
Prompt Cache
→ 复用 Provider 内部的相同 Prefix 计算最重要的工程原则是:
缓存 Key 必须证明两个请求等价;Revision 必须让变化自然产生 Miss;权限必须阻止不该共享的结果;Trace 必须让每次命中仍然可复现。
做到这些,缓存才真正降低成本和延迟;否则它只是把旧错误、旧权限和旧答案更快地交付给更多用户。
官方资料与相关阅读
- RFC 9111: HTTP Caching
- Redis Cache-aside
- Elasticsearch Node Query Cache
- Elasticsearch Shard Request Cache
- OpenAI:Prompt Caching in the API
- OpenAI:Unrolling the Codex Agent Loop
- MySQL 到 Elasticsearch 的一致性
- Hybrid Search 生产实战
- Reranker 生产实战
- Query Rewriting 生产实战
- Citation Engineering 生产实战
- RAG 答案置信度生产实战
- RAG 可观测性生产实战
- RAG 发布门禁生产实战
讨论
继续讨论这篇笔记
有问题、补充案例或不同观点,可以通过 GitHub Discussions 继续交流。