Agent 记忆系统的存储优化:向量检索与全文搜索的混合索引策略

Agent 记忆系统的存储优化:向量检索与全文搜索的混合索引策略

一、Agent 记忆系统的特殊需求

Agent 的记忆系统不是通用的向量数据库。它需要同时满足三种检索模式:语义相似("上次开会说了什么关于预算的事")、精确匹配("2025年Q3的营收数字")、时间序列("最近三天跟进的客户")。纯向量检索在精确匹配上天然弱势——Embedding 模型把"42"和"四十二"映射到相近向量,但对"收入 100 万"和"收入一百万"可能产生偏差。

混合索引架构的设计目标是:语义检索用向量索引覆盖模糊查询,全文检索用倒排索引覆盖精确匹配,两者通过融合排序合并结果

graph TD A[用户查询] --> B[查询分析器] B --> C{查询类型判断} C -->|语义查询| D[向量检索引擎] C -->|精确匹配| E[全文检索引擎] C -->|混合查询| F[并行双路检索] D --> G[Embedding 模型编码] E --> H[分词 + 倒排索引] F --> D F --> E G --> I[向量相似度排序 Top-K] H --> J[BM25 评分排序 Top-K] I --> K[融合排序 RRF] J --> K K --> L[返回 Top-N 结果]

二、双重索引的存储设计

存储层的核心数据结构:

type MemoryStore struct { // 全文索引:倒排索引 invertedIndex *inverted.Index // 向量索引:HNSW 图 vectorIndex *hnsw.Index // 原始数据存储 documents map[string]*Document // Embedding 客户端 embedder Embedder } type Document struct { ID string Content string Metadata map[string]string Timestamp time.Time Embedding []float32 Tokens []string // 预分词结果,用于全文索引 }

写入路径的优化关键:向量编码和分词并行执行

func (s *MemoryStore) Insert(doc *Document) error { var ( wg sync.WaitGroup embErr error tokErr error ) // 并行执行 Embedding 编码和分词 wg.Add(2) go func() { defer wg.Done() doc.Embedding, embErr = s.embedder.Encode(doc.Content) }() go func() { defer wg.Done() doc.Tokens, tokErr = s.tokenizer.Tokenize(doc.Content) }() wg.Wait() if embErr != nil { return fmt.Errorf("embedding failed: %w", embErr) } if tokErr != nil { return fmt.Errorf("tokenization failed: %w", tokErr) } s.mu.Lock() defer s.mu.Unlock() s.documents[doc.ID] = doc s.vectorIndex.Insert(doc.ID, doc.Embedding) s.invertedIndex.Index(doc.ID, doc.Tokens) return nil }

三、混合检索的融合排序

混合检索的难点在于两种排序分数的归一化——向量相似度的余弦距离是 [0,1],BM25 分数是无界的。需要先归一化再融合。

Reciprocal Rank Fusion (RRF) 是最简单的融合算法:

RRF_score(doc) = sum over all rankers: 1 / (k + rank(doc, ranker))

其中 k 是平滑参数(通常 60),rank 是文档在单个排序器中的排名。

func (s *MemoryStore) HybridSearch(query string, topK int) ([]*SearchResult, error) { // 并行执行两路检索 var ( vectorResults []*ScoredDoc textResults []*ScoredDoc wg sync.WaitGroup ) wg.Add(2) go func() { defer wg.Done() embedding, _ := s.embedder.Encode(query) vectorResults = s.vectorIndex.Search(embedding, topK*2) }() go func() { defer wg.Done() textResults = s.invertedIndex.Search(query, topK*2) }() wg.Wait() // RRF 融合 scores := make(map[string]float64) k := 60.0 for rank, doc := range vectorResults { scores[doc.ID] += 1.0 / (k + float64(rank+1)) } for rank, doc := range textResults { scores[doc.ID] += 1.0 / (k + float64(rank+1)) } // 按融合分数排序 var results []*SearchResult for id, score := range scores { doc := s.documents[id] results = append(results, &SearchResult{ Document: doc, Score: score, }) } sort.Slice(results, func(i, j int) bool { return results[i].Score > results[j].Score }) if len(results) > topK { results = results[:topK] } return results, nil }

四、内存管理与存储压缩

混合索引的存储开销是单索引的 2-3 倍。向量索引(HNSW 图)的内存占用与被索引的向量数成正比,每个 1536 维的 float32 向量占 6KB。10 万条记忆约 600MB 向量数据 + 300MB 倒排索引 ≈ 1GB。

压缩策略:

  1. 向量量化:使用 PQ (Product Quantization) 将 1536 维压缩到 96 维,内存减少 16 倍,召回率损失 < 3%
  2. 倒排索引剪枝:对低频词合并存储,对超高频词(停用词)跳过索引
  3. 分层存储:热数据(最近 7 天)完整索引,温数据(30 天内)仅向量索引,冷数据(归档)仅保留摘要
type TieredMemoryStore struct { hot *MemoryStore // 完整双重索引 warm *MemoryStore // 仅向量索引(全文搜索降级到扫描) cold *ArchiveStore // 文件归档,按需加载 } func (t *TieredMemoryStore) Promote() { now := time.Now() t.mu.Lock() defer t.mu.Unlock() // 每天凌晨执行分层迁移 for id, doc := range t.hot.documents { if now.Sub(doc.Timestamp) > 7*24*time.Hour { t.warm.Insert(doc) t.hot.Delete(id) } } for id, doc := range t.warm.documents { if now.Sub(doc.Timestamp) > 30*24*time.Hour { t.cold.Archive(doc) t.warm.Delete(id) } } }

五、总结

Agent 记忆系统的检索需求跨越了语义相似和精确匹配两个维度,单一索引无法同时满足。混合索引架构通过向量检索 + 全文检索双路并行、RRF 融合排序,兼顾召回率和准确率。存储优化的关键是:向量量化压缩、倒排索引剪枝、按时间分层的三级存储架构。实现复杂度可控(核心代码约 500 行),适合需要自建 Agent 记忆系统的场景。