RAG 多路召回与重排序:从检索策略到答案融合的工程实践
2026/6/11 4:53:54 网站建设 项目流程

RAG 多路召回与重排序:从检索策略到答案融合的工程实践

一、单路检索的瓶颈:向量召回为何总是"差一点"

在 RAG(检索增强生成)系统中,向量检索是最常用的召回方式。将文档分块后编码为向量,查询时计算余弦相似度取 Top-K,看似简单直接。然而,生产环境中的反馈往往令人沮丧:专业术语查询召回不相关文档、长尾知识被高频内容淹没、精确匹配场景反而不如关键词检索。

根本原因在于:单一检索通道存在固有的"盲区"。向量检索擅长语义相似度匹配,但对精确关键词、专业缩写、编号类查询表现不佳;BM25 关键词检索擅长精确匹配,但无法理解语义近义关系。多路召回(Multi-Route Recall)通过同时使用多种检索策略,再用重排序模型融合结果,是当前 RAG 系统提升召回质量的主流方案。

二、多路召回与重排序的架构原理

2.1 多路召回的整体架构

flowchart TB Q[用户查询] --> P[查询预处理与改写] P --> V[向量检索通道] P --> K[关键词检索通道 BM25] P --> R[规则检索通道] V --> V1[Embedding 编码] V1 --> V2[ANN 索引 Top-K] K --> K1[分词与停用词过滤] K1 --> K2[倒排索引 Top-K] R --> R1[实体识别] R1 --> R2[知识图谱查询] V2 --> M[结果合并与去重] K2 --> M R2 --> M M --> RR[重排序模型 Reranker] RR --> F[Top-N 精排结果] F --> G[LLM 答案生成]

2.2 各检索通道的互补性分析

检索通道擅长场景弱点典型召回率
向量检索语义近义、模糊查询精确匹配、专业术语70-80%
BM25 关键词精确匹配、专业术语语义近义、同义替换60-70%
知识图谱实体关系、结构化查询非结构化文本40-50%

多路召回的理论基础是:不同通道的召回结果存在互补性。向量检索的"漏召"可能被关键词检索补回,反之亦然。通过 Reciprocal Rank Fusion(RRF)或重排序模型融合,可以显著提升最终召回质量。

三、多路召回与重排序的代码实现

3.1 检索通道接口定义

package retrieval import "context" // Document 检索到的文档片段 type Document struct { ID string // 文档唯一标识 Content string // 文档内容 Score float64 // 检索得分 Source string // 来源通道标识 Metadata map[string]string } // Retriever 检索通道接口 type Retriever interface { Retrieve(ctx context.Context, query string, topK int) ([]Document, error) Name() string }

3.2 向量检索通道实现

package retrieval import ( "context" "sort" ) // VectorRetriever 向量检索通道 type VectorRetriever struct { embedder Embedder // 文本向量化接口 index ANNIndex // 近似最近邻索引 store DocumentStore // 文档原文存储 } // Embedder 文本向量化接口 type Embedder interface { Embed(ctx context.Context, text string) ([]float32, error) } // ANNIndex 近似最近邻索引接口 type ANNIndex interface { Search(ctx context.Context, vector []float32, topK int) ([]SearchHit, error) } // SearchHit 索引搜索结果 type SearchHit struct { ID string Score float64 } func (v *VectorRetriever) Retrieve(ctx context.Context, query string, topK int) ([]Document, error) { // 将查询文本编码为向量 vector, err := v.embedder.Embed(ctx, query) if err != nil { return nil, fmt.Errorf("embed query: %w", err) } // 在 ANN 索引中搜索 hits, err := v.index.Search(ctx, vector, topK) if err != nil { return nil, fmt.Errorf("search index: %w", err) } // 获取文档原文并组装结果 docs := make([]Document, 0, len(hits)) for _, hit := range hits { content, err := v.store.Get(ctx, hit.ID) if err != nil { continue // 单条获取失败不影响整体 } docs = append(docs, Document{ ID: hit.ID, Content: content, Score: hit.Score, Source: v.Name(), }) } return docs, nil } func (v *VectorRetriever) Name() string { return "vector" }

3.3 BM25 关键词检索通道

package retrieval import ( "context" "strings" ) // BM25Retriever BM25 关键词检索通道 type BM25Retriever struct { index BM25Index } type BM25Index interface { Search(ctx context.Context, terms []string, topK int) ([]SearchHit, error) } func (b *BM25Retriever) Retrieve(ctx context.Context, query string, topK int) ([]Document, error) { // 分词处理 terms := b.tokenize(query) if len(terms) == 0 { return nil, nil } hits, err := b.index.Search(ctx, terms, topK) if err != nil { return nil, fmt.Errorf("bm25 search: %w", err) } docs := make([]Document, 0, len(hits)) for _, hit := range hits { docs = append(docs, Document{ ID: hit.ID, Score: hit.Score, Source: b.Name(), }) } return docs, nil } // tokenize 简单分词:按空格和标点切分,过滤停用词 func (b *BM25Retriever) tokenize(query string) []string { stopWords := map[string]bool{"的": true, "了": true, "是": true, "在": true, "the": true, "is": true, "a": true, "an": true} parts := strings.FieldsFunc(query, func(r rune) bool { return r == ' ' || r == ',' || r == ',' || r == '。' || r == '?' }) var terms []string for _, p := range parts { p = strings.TrimSpace(p) if p != "" && !stopWords[strings.ToLower(p)] { terms = append(terms, p) } } return terms } func (b *BM25Retriever) Name() string { return "bm25" }

3.4 RRF 融合与重排序

package retrieval import ( "context" "sort" ) // MultiRouteRetriever 多路召回聚合器 type MultiRouteRetriever struct { retrievers []Retriever reranker Reranker } // Reranker 重排序模型接口 type Reranker interface { Rerank(ctx context.Context, query string, docs []Document) ([]Document, error) } // NewMultiRouteRetriever 创建多路召回聚合器 func NewMultiRouteRetriever(reranker Reranker, retrievers ...Retriever) *MultiRouteRetriever { return &MultiRouteRetriever{ retrievers: retrievers, reranker: reranker, } } // Retrieve 执行多路召回、RRF 融合与重排序 func (m *MultiRouteRetriever) Retrieve(ctx context.Context, query string, topK int) ([]Document, error) { // 并行执行各路召回 type result struct { docs []Document err error } ch := make(chan result, len(m.retrievers)) for _, r := range m.retrievers { go func(ret Retriever) { docs, err := ret.Retrieve(ctx, query, topK*2) // 每路多召回一些 ch <- result{docs: docs, err: err} }(r) } // 收集所有通道结果 var allDocs []Document for i := 0; i < len(m.retrievers); i++ { res := <-ch if res.err != nil { continue // 单通道失败不影响整体 } allDocs = append(allDocs, res.docs...) } // RRF 融合 fused := m.rrfFuse(allDocs, 60) // k=60 是 RRF 常用参数 // 取 Top-K 送入重排序模型 if len(fused) > topK*2 { fused = fused[:topK*2] } // 重排序精排 reranked, err := m.reranker.Rerank(ctx, query, fused) if err != nil { // 重排序失败时降级使用 RRF 结果 if len(fused) > topK { return fused[:topK], nil } return fused, nil } if len(reranked) > topK { return reranked[:topK], nil } return reranked, nil } // rrfFuse Reciprocal Rank Fusion 融合算法 // RRF 公式:score(d) = Σ 1/(k + rank_i(d)) func (m *MultiRouteRetriever) rrfFuse(docs []Document, k int) []Document { // 按来源通道分组排序 sourceDocs := make(map[string][]Document) for _, d := range docs { sourceDocs[d.Source] = append(sourceDocs[d.Source], d) } // 计算每个文档的 RRF 得分 scores := make(map[string]float64) docMap := make(map[string]Document) for source, sDocs := range sourceDocs { // 按原始得分降序排序 sort.Slice(sDocs, func(i, j int) bool { return sDocs[i].Score > sDocs[j].Score }) for rank, doc := range sDocs { scores[doc.ID] += 1.0 / float64(k+rank+1) docMap[doc.ID] = doc } } // 按 RRF 得分排序 var result []Document for id, score := range scores { doc := docMap[id] doc.Score = score doc.Source = "rrf_fused" result = append(result, doc) } sort.Slice(result, func(i, j int) bool { return result[i].Score > result[j].Score }) return result }

四、多路召回的架构权衡

4.1 通道数量与延迟的权衡

每增加一个检索通道,召回率理论上会提升,但延迟也会增加。即使各通道并行执行,最终延迟取决于最慢的通道。在生产环境中,建议设置通道超时(如 200ms),超时的通道直接跳过,用已有结果融合。

4.2 RRF vs 重排序模型

  • RRF:无需额外模型,计算速度快,适合通道数较少(2-3路)且对延迟敏感的场景。
  • 重排序模型:精度更高,能理解查询与文档的深层语义关系,但需要额外的推理开销。适合对召回质量要求极高的场景。

4.3 适用边界

场景推荐策略
通用知识库问答向量 + BM25 双路 + RRF
专业领域(医疗、法律)向量 + BM25 + 知识图谱三路 + 重排序
精确匹配为主(编号、代码)BM25 为主 + 向量辅助
实时性要求极高单路向量检索,牺牲召回换延迟

五、总结

RAG 系统的召回质量直接决定生成答案的可靠性。单路检索存在固有的"盲区",多路召回通过组合不同检索策略的互补性,显著提升了召回覆盖率。RRF 融合算法提供了轻量级的结果合并方案,重排序模型则进一步提升了精排质量。落地时建议从向量 + BM25 双路召回起步,根据业务反馈逐步增加通道和引入重排序模型。核心指标是召回率(Recall@K)和最终答案的准确率,而非单一通道的得分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询