RAG 缓存策略:相同查询别重复检索,热门问题预加载

📅 2026/7/12 17:04:18
RAG 缓存策略:相同查询别重复检索,热门问题预加载
RAG 缓存策略相同查询别重复检索热门问题预加载一、每问必查的性能黑洞用户问退货政策是什么Agent 去向量库检索、拼接上下文、调用模型推理耗时 2.3 秒。一分钟后同一个用户又问了一遍你们的退货政策。Agent 重新走完整条链路检索→拼接→推理又花了 2.1 秒。这就是典型的知识问答场景20% 的热门问题贡献了 80% 的检索和推理负载。如果你的 RAG 系统每次查询都从头检索一遍相当于每处理一个问题都在重新发明轮子。缓存不是可选优化而是 RAG 系统从 Demo 到生产的关键一步。二、RAG 的多级缓存架构RAG 的缓存可以在三个层次上生效语义缓存Semantic Cache对用户问题做语义相似度匹配命中后直接返回缓存的答案。适合高频重复问题。检索缓存Retrieval Cache缓存向量检索的结果Top-K 文档片段跳过向量库查询。适合相似但不完全相同的问题。Embedding 缓存Embedding Cache缓存文本的向量表示避免对相同文本重复计算 Embedding。适合知识库文档不变但频繁查询的场景。flowchart TD Q[用户问题] -- S1{语义缓存} S1 --|相似度 0.95| AC[返回缓存答案] S1 --|未命中| E1[计算 Embedding] E1 -- S2{Embedding 缓存} S2 --|命中| EC[复用已有向量] S2 --|未命中| CEM[调用 Embedding 模型] CEM -- EC EC -- S3{检索缓存} S3 --|命中| RC[复用检索结果] S3 --|未命中| VS[向量库检索] VS -- RC RC -- S4{是否热门问题} S4 --|是| PR[预加载到语义缓存] S4 --|否| LLM[模型推理] PR -- LLM LLM -- R[返回结果] R -- SC[写入语义缓存]多级缓存的核心是让每个请求从最合适的缓存层命中逐级向后穿透只有最冷门的全新问题才会走到最慢的完整链路。三、Go 实现带 TTL 和 LRU 的多级 RAG 缓存package ragcache import ( container/list context crypto/sha256 encoding/hex fmt math sync time ) // 三层数据结构 // CacheEntry 通用缓存条目 type CacheEntry struct { Key string Value interface{} ExpiresAt time.Time } func (e *CacheEntry) IsExpired() bool { return time.Now().After(e.ExpiresAt) } // LRUCache 带 TTL 的 LRU 缓存 type LRUCache struct { capacity int items map[string]*list.Element lruList *list.List mu sync.RWMutex } type lruItem struct { key string entry *CacheEntry } func NewLRUCache(capacity int) *LRUCache { return LRUCache{ capacity: capacity, items: make(map[string]*list.Element), lruList: list.New(), } } func (c *LRUCache) Get(key string) (interface{}, bool) { c.mu.Lock() defer c.mu.Unlock() elem, ok : c.items[key] if !ok { return nil, false } item : elem.Value.(*lruItem) if item.entry.IsExpired() { c.removeElement(elem) return nil, false } // 移到链表头部最近使用 c.lruList.MoveToFront(elem) return item.entry.Value, true } func (c *LRUCache) Set(key string, value interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() // 已存在则更新 if elem, ok : c.items[key]; ok { c.lruList.MoveToFront(elem) item : elem.Value.(*lruItem) item.entry.Value value item.entry.ExpiresAt time.Now().Add(ttl) return } // LRU 淘汰 if c.lruList.Len() c.capacity { oldest : c.lruList.Back() if oldest ! nil { c.removeElement(oldest) } } entry : CacheEntry{ Key: key, Value: value, ExpiresAt: time.Now().Add(ttl), } elem : c.lruList.PushFront(lruItem{key: key, entry: entry}) c.items[key] elem } func (c *LRUCache) removeElement(elem *list.Element) { c.lruList.Remove(elem) item : elem.Value.(*lruItem) delete(c.items, item.key) } // 多级 RAG 缓存管理器 // MultiLevelCache 多级 RAG 缓存 type MultiLevelCache struct { // 第一层语义缓存问题 → 最终答案 semanticCache *LRUCache // 第二层检索结果缓存问题哈希 → Top-K 文档片段 retrievalCache *LRUCache // 第三层Embedding 缓存文本哈希 → 向量 embeddingCache *LRUCache // 热门问题预加载列表 hotQuestions map[string]string // 问题 → 答案 hotMu sync.RWMutex // 统计信息 stats CacheStats } // CacheStats 缓存统计 type CacheStats struct { SemanticHits int64 RetrievalHits int64 EmbeddingHits int64 TotalRequests int64 mu sync.Mutex } func NewMultiLevelCache() *MultiLevelCache { return MultiLevelCache{ semanticCache: NewLRUCache(1000), // 缓存 1000 个问答对 retrievalCache: NewLRUCache(500), // 缓存 500 个检索结果 embeddingCache: NewLRUCache(10000), // 缓存 10000 个向量 hotQuestions: make(map[string]string), } } // GetAnswer 多级缓存查询入口 func (m *MultiLevelCache) GetAnswer(ctx context.Context, question string) (string, bool) { m.stats.mu.Lock() m.stats.TotalRequests m.stats.mu.Unlock() // 第一级语义缓存 // 实际项目中用余弦相似度匹配 if answer, ok : m.semanticCache.Get(question); ok { m.stats.mu.Lock() m.stats.SemanticHits m.stats.mu.Unlock() return answer.(string), true } return , false } // GetRetrieval 检索缓存查询 func (m *MultiLevelCache) GetRetrieval(ctx context.Context, question string) ([]Document, bool) { key : m.hashKey(retrieval: question) if docs, ok : m.retrievalCache.Get(key); ok { m.stats.mu.Lock() m.stats.RetrievalHits m.stats.mu.Unlock() return docs.([]Document), true } return nil, false } // GetEmbedding 向量缓存查询 func (m *MultiLevelCache) GetEmbedding(ctx context.Context, text string) ([]float64, bool) { key : m.hashKey(text) if vec, ok : m.embeddingCache.Get(key); ok { m.stats.mu.Lock() m.stats.EmbeddingHits m.stats.mu.Unlock() return vec.([]float64), true } return nil, false } // StoreSemantic 存储语义缓存 func (m *MultiLevelCache) StoreSemantic(question, answer string, ttl time.Duration) { m.semanticCache.Set(question, answer, ttl) } // StoreRetrieval 存储检索结果缓存 func (m *MultiLevelCache) StoreRetrieval(question string, docs []Document, ttl time.Duration) { key : m.hashKey(retrieval: question) m.retrievalCache.Set(key, docs, ttl) } // StoreEmbedding 存储向量缓存 func (m *MultiLevelCache) StoreEmbedding(text string, vec []float64, ttl time.Duration) { key : m.hashKey(text) m.embeddingCache.Set(key, vec, ttl) } // PreloadHotQuestions 预加载热门问题 func (m *MultiLevelCache) PreloadHotQuestions() { m.hotMu.RLock() defer m.hotMu.RUnlock() for question, answer : range m.hotQuestions { // 热门问题用较长的 TTL30 分钟 m.semanticCache.Set(question, answer, 30*time.Minute) } } // UpdateHotQuestions 更新热门问题列表可从监控数据自动生成 func (m *MultiLevelCache) UpdateHotQuestions(questions map[string]string) { m.hotMu.Lock() defer m.hotMu.Unlock() m.hotQuestions questions } // 余弦相似度计算简化版 func cosineSimilarity(a, b []float64) float64 { if len(a) ! len(b) || len(a) 0 { return 0 } var dotProduct, normA, normB float64 for i : range a { dotProduct a[i] * b[i] normA a[i] * a[i] normB b[i] * b[i] } if normA 0 || normB 0 { return 0 } return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB)) } func (m *MultiLevelCache) hashKey(text string) string { h : sha256.Sum256([]byte(text)) return hex.EncodeToString(h[:])[:16] } // 集成到 RAG 查询流程 type Document struct { Content string Score float64 } func QueryWithCache(ctx context.Context, question string, cache *MultiLevelCache) (string, error) { // 1. 检查语义缓存 if answer, ok : cache.GetAnswer(ctx, question); ok { return answer, nil } // 2. 检查检索缓存 var docs []Document if cachedDocs, ok : cache.GetRetrieval(ctx, question); ok { docs cachedDocs } else { // 实际向量检索 docs []Document{} // 实际实现中从向量库检索 cache.StoreRetrieval(question, docs, 5*time.Minute) } // 3. 调用模型推理 context_str : for _, d : range docs { context_str d.Content \n } answer : fmt.Sprintf(基于 %d 条文档生成的回答, len(docs)) // 4. 存储语义缓存 cache.StoreSemantic(question, answer, 10*time.Minute) return answer, nil }四、缓存策略的边界与权衡语义相似度阈值是核心参数。阈值太低会导致错误命中用户问怎么退货、缓存的答案是怎么下单阈值太高则命中率低。建议从 0.90 开始根据实际数据调整。也可以分场景客服场景可以低一些0.85医疗场景必须严格0.95。缓存失效策略要与知识库更新联动。知识库更新后相关的检索缓存和语义缓存都应该失效。否则会出现知识库已更新但 Agent 还在回答旧信息的情况。最简单的做法是按知识库的更新时间戳来标记缓存版本。Embedding 缓存的收益取决于知识库稳定性。如果知识库天天变Embedding 缓存的命中率会很低。对于静态知识库如产品手册、API 文档Embedding 缓存可以省掉 90% 以上的 Embedding 调用。热门问题预加载需要数据支撑。盲猜哪些问题是热门的很容易猜错。建议从生产日志中提取 Top-100 高频查询自动化生成预加载列表每日更新。五、总结RAG 的多级缓存策略不是一上来就全上而是按收益从高到低逐级实现语义缓存最高命中率→ 检索缓存中等收益→ Embedding 缓存适合静态知识库。每一级缓存的 TTL 要根据数据变化频率独立设置。核心目标让 80% 的常见查询在缓存中命中把宝贵的 GPU 资源留给那 20% 真正需要深度推理的问题。