RAG 检索精度上不去?混合检索 + 重排,效果立竿见影(附实现思路)

RAG 检索精度上不去?混合检索 + 重排,效果立竿见影(附实现思路)

做 RAG 的人,都经历过同一个尴尬:向量检索"看着像"但"答不对"。

问"订单 A2024-001 的状态",向量检索召回一堆无关文档——因为向量擅长语义,不擅长精确匹配编号;问"年假几天",纯关键词又召回不到同义表达。解法是行业标配的混合检索 + 重排。这篇讲原理、讲实现,附可直接参考的代码思路。

在这里插入图片描述

一、纯向量检索为什么不够

向量检索(Embedding + 相似度)的本质是"语义匹配":

"订单 A2024-001 的状态" → 向量 → 找语义相近的块

它有两个硬伤:

场景 向量检索表现 原因
语义相同、用词不同("年假" vs "带薪休假") ✅ 擅长 语义空间里本来就接近
精确编号/型号/人名("A2024-001"、"OSPF") ❌ 经常失败 编号没有"语义梯度",向量彼此等距
短查询、少上下文("第 3 条规则") ❌ 召回分散 信息量太少,语义锚点不足

所以纯向量检索,是"够用但不好用"——最致命的就是精确匹配类问题,而这恰恰是企知识库、工单、法务合同里最高频的查询。

二、混合检索:两路召回,各取所长

思路:BM25(关键词)管精确匹配,向量(语义)管同义表达,两路结果融合。

BM25 是老牌关键词检索算法,按词频和逆文档频率打分,对"完整编号、专有名词、术语"命中极准,但理解不了同义词。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

融合算法 RRF(Reciprocal Rank Fusion),核心就一行逻辑——不依赖两路各自的分值量纲(BM25 分和余弦分根本不可比),只信"排名":

# 常数 k 默认 60,越大表示越"平滑"两路的排名差异
def rrf_score(rank: int, k: int = 60) -> float:
    return 1.0 / (k + rank)   # rank 从 1 开始

def fuse(bm25_results, vector_results, k: int = 60, top_k: int = 10):
    """两路结果都是 [(doc_id, rank), ...],rank 从 1 开始"""
    fused = {}
    for doc_id, rank in bm25_results:
        fused[doc_id] = fused.get(doc_id, 0.0) + rrf_score(rank, k)
    for doc_id, rank in vector_results:
        fused[doc_id] = fused.get(doc_id, 0.0) + rrf_score(rank, k)
    # 按总分降序取 Top-K
    return sorted(fused.items(), key=lambda x: x[1], reverse=True)[:top_k]

关键点:精确编号类问题,BM25 路命中;同义表达类问题,向量路命中;两路都命中的文档排名最高——这就是"1+1>2"。RRF 的好处是不用纠结两路分数怎么归一化,直接按排名融合,工程上最省心。

三、实现要点:BM25 路的中文问题

BM25 按词频统计,中文必须先分词,否则"订单/编号/状态"会被切成单字,命中率大降。这一步做错,混合检索直接退化成"半残"。

import jieba

def tokenize(text: str) -> list[str]:
    # 索引和查询必须用同一套分词,否则词表对不上
    return [w for w in jieba.cut(text) if w.strip()]

corpus_tokens = [tokenize(chunk) for chunk in chunks]
query_tokens  = tokenize(question)

推荐用 bm25s 库(轻量、纯 Python,比自己写快很多),索引和检索都几行:

import bm25s

bm25 = bm25s.BM25()
bm25.index(corpus_tokens)                   # 建索引
results = bm25.retrieve(query_tokens, k=20) # 检索 Top-20,返回 (doc_ids, scores)

小坑:专有名词(如 "A2024-001")建议加一层"不过词"保护,或检索前对查询里的编号做正则提取、直接做子串匹配兜底,能再救回一批精确命中。

四、进阶:重排(Re-Rank)——融合后再精排

混合检索出了 Top-50,还要不要更准?加重排:用一个模型对候选逐条打分,精挑 Top-5。

混合检索 Top-50 → 重排模型打分 → 精排 Top-5 → 喂给大模型

重排的价值:先广撒网(召回多),再精挑(保精度)。推荐用交叉编码器类模型(如 bge-reranker),它对"问题-文档"这一对 jointly 编码打分,比纯向量相似度准一个档次——因为它能看到问题和文档的交互信息,而不是各算各的再比距离。

# 伪代码思路:对每个候选文档,算 (问题, 文档) 的相关性分
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

pairs = [(question, doc) for doc in candidates]   # 候选通常是 Top-50
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
top5 = [doc for doc, _ in ranked[:5]]

工程注意:重排模型比向量模型重,别对全库跑,只对召回后的 Top-50 跑,成本可控;中文场景优先选在中文数据上训过的 reranker。

五、完整流水线(直接参考的架构)

1. 文档切分(按语义块,500-800 字,留 10% 重叠,避免切断关键信息)
2. BM25 索引(jieba 分词)+ 向量索引(Embedding)双路建库
3. 查询:两路召回各 Top-20
4. RRF 融合 → Top-10
5. 重排模型精排 → Top-5
6. 拼进提示词 → 大模型基于资料回答

每一步都能单独评测、单独优化——这也是生产级 RAG 的标准形态。常见踩坑:切块太大会导致召回的块里噪声多、重叠不够会切断上下文,这两点先调,往往比上重排收益还大。

六、我的观点

RAG 检索的真相:没有银弹,只有组合拳。

  • 数据量小、问题简单、纯同义查询 → 纯向量就够
  • 有精确编号/专有名词/术语(企知识库、工单、合同)→ 必须上混合检索
  • 追求极致精度、问答质量直接决定业务结果 → 混合 + 重排

别一开始就上全套——先量问题类型,再决定复杂度。我的经验是:大多数场景,混合检索(不加重排)已经是"性价比拐点",能解决 80% 的"答不对";重排是最后 20% 精度的锦上添花,等召回稳定了再上。


你的 RAG 卡在召回不准还是回答不准?评论区说说你的场景(数据量、有没有编号类查询),我帮你判断该上哪一层。

关注我,下期拆解:RAG 评估:怎么量化"回答得对不对"。

posted @ 2026-08-21 00:36  橘和柠  阅读(20)  评论(0)    收藏  举报