别再只用向量检索了!RAG混检+Rerank提升40_
前三篇我们把RAG的地基打好了——原理搞清楚了、文档切对了、Embedding和向量库选好了。但如果你只用纯向量检索,大概率还是会遇到这些问题:
用户问"2025年差旅报销标准",检索结果里混进了2023年的旧文档;问"年假申请流程",返回的却是社保缴纳比例——向量相似度确实高,但语义不对。
这就是纯向量检索的致命问题:**“找到相似的”≠“找到对的”**。混合检索+Rerank重排序,就是来解决这个问题的。这是目前生产环境RAG的标配架构,没有之一。
为什么纯向量检索不够用?
向量检索擅长语义相似匹配,但对这些场景无能为力:
| 问题
|
纯向量检索
|
原因
精确关键词匹配
|
差
|
“python3.11"和"python3.12"向量很近,但你只要3.11的文档
| |
专有名词检索
|
差
|
人名、产品名、编号等,关键词匹配更准
| |
时效性过滤
|
不行
|
向量不看日期,2023和2025的文档向量可能很近
| |
多义词消歧
|
差
|
“苹果"是手机还是水果?纯向量分不清
|
关键词检索(BM25)恰好擅长这些向量检索不擅长的事——精确匹配、专有名词、关键词权重。两路召回再融合,就是混合检索。
BM25:向量检索的最佳搭档
BM25是经典的关键词检索算法,基于词频和文档频率来计算相关性。它不关心语义,只看关键词是否出现、出现了几次、在多少文档中出现。
<span><span leaf=""># 用Elasticsearch做BM25检索</span></span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.retrievers </span><span><span leaf="">import</span></span><span leaf=""> ElasticSearchBM25Retriever</span><br><br><span><span leaf=""># 初始化</span></span><br><span leaf="">retriever = ElasticSearchBM25Retriever(</span><br><span leaf=""> elasticsearch_url=</span><span><span leaf="">"http://localhost:9200"</span></span><span leaf="">,</span><br><span leaf=""> index_name=</span><span><span leaf="">"knowledge_base"</span></span><span leaf="">,</span><br><span leaf="">)</span><br><br><span><span leaf=""># 检索</span></span><br><span leaf="">results = retriever.invoke(</span><span><span leaf="">"年假申请流程"</span></span><span leaf="">)</span><br>
没有Elasticsearch也可以用rank_bm25做纯Python的BM25:
<span><span leaf="">from</span></span><span leaf=""> rank_bm25 </span><span><span leaf="">import</span></span><span leaf=""> BM25Okapi</span><br><span><span leaf="">import</span></span><span leaf=""> jieba</span><br><br><span><span leaf=""># 中文分词</span></span><br><span leaf="">tokenized_corpus = [list(jieba.cut(doc.page_content)) </span><span><span leaf="">for</span></span><span leaf=""> doc </span><span><span leaf="">in</span></span><span leaf=""> chunks]</span><br><span leaf="">bm25 = BM25Okapi(tokenized_corpus)</span><br><br><span><span leaf=""># 检索</span></span><br><span leaf="">query_tokens = list(jieba.cut(</span><span><span leaf="">"年假申请流程"</span></span><span leaf="">))</span><br><span leaf="">scores = bm25.get_scores(query_tokens)</span><br><span leaf="">top_indices = scores.argsort()[</span><span><span leaf="">-5</span></span><span leaf="">:][::</span><span><span leaf="">-1</span></span><span leaf="">]</span><br><span leaf="">results = [chunks[i] </span><span><span leaf="">for</span></span><span leaf=""> i </span><span><span leaf="">in</span></span><span leaf=""> top_indices]</span><br>
BM25的特点:快、对精确匹配敏感、不理解语义。跟向量检索刚好互补。
混合检索:两路召回+融合排序
混合检索的流程:
<span leaf="">用户Query</span><br><span leaf=""> ├──→ [向量检索] → Top-K候选集A</span><br><span leaf=""> ├──→ [BM25检索] → Top-K候选集B</span><br><span leaf=""> ↓</span><br><span leaf="">[结果融合(RRF)]</span><br><span leaf=""> ↓</span><br><span leaf="">[融合后的Top-N结果]</span><br>
核心问题:两路检索的结果怎么融合?用RRF(Reciprocal Rank Fusion)。
RRF融合算法
RRF的思路是"民主投票”——不关心具体分数,只关心排名,排名越靠前得分越高:
<span leaf="">RRF_score(doc) = Σ 1/(k + rank_i(doc))</span><br>
k通常取60。一个文档在向量检索中排第1,在BM25中排第3,它的RRF分数 = 1/(60+1) + 1/(60+3) = 0.0164 + 0.0159 = 0.0323
<span><span><span leaf="">def</span></span><span leaf=""> </span><span><span leaf="">reciprocal_rank_fusion</span></span><span><span leaf="">(lists_of_results, k=</span><span><span leaf="">60</span></span><span leaf="">)</span></span><span leaf="">:</span></span><br><span leaf=""> </span><span><span leaf="">"""RRF融合多路检索结果"""</span></span><br><span leaf=""> fused_scores = {}</span><br><span leaf=""> </span><br><span leaf=""> </span><span><span leaf="">for</span></span><span leaf=""> results </span><span><span leaf="">in</span></span><span leaf=""> lists_of_results:</span><br><span leaf=""> </span><span><span leaf="">for</span></span><span leaf=""> rank, doc </span><span><span leaf="">in</span></span><span leaf=""> enumerate(results):</span><br><span leaf=""> doc_key = doc.page_content </span><span><span leaf=""># 用内容作为唯一标识</span></span><br><span leaf=""> </span><span><span leaf="">if</span></span><span leaf=""> doc_key </span><span><span leaf="">not</span></span><span leaf=""> </span><span><span leaf="">in</span></span><span leaf=""> fused_scores:</span><br><span leaf=""> fused_scores[doc_key] = {</span><span><span leaf="">"score"</span></span><span leaf="">: </span><span><span leaf="">0</span></span><span leaf="">, </span><span><span leaf="">"doc"</span></span><span leaf="">: doc}</span><br><span leaf=""> fused_scores[doc_key][</span><span><span leaf="">"score"</span></span><span leaf="">] += </span><span><span leaf="">1.0</span></span><span leaf=""> / (k + rank + </span><span><span leaf="">1</span></span><span leaf="">)</span><br><span leaf=""> </span><br><span leaf=""> </span><span><span leaf=""># 按融合分数排序</span></span><br><span leaf=""> sorted_results = sorted(</span><br><span leaf=""> fused_scores.values(), key=</span><span><span leaf="">lambda</span></span><span leaf=""> x: x[</span><span><span leaf="">"score"</span></span><span leaf="">], reverse=</span><span><span leaf="">True</span></span><br><span leaf=""> )</span><br><span leaf=""> </span><span><span leaf="">return</span></span><span leaf=""> [item[</span><span><span leaf="">"doc"</span></span><span leaf="">] </span><span><span leaf="">for</span></span><span leaf=""> item </span><span><span leaf="">in</span></span><span leaf=""> sorted_results]</span><br>
完整的混合检索实现
<span><span leaf="">from</span></span><span leaf=""> langchain_community.vectorstores </span><span><span leaf="">import</span></span><span leaf=""> Chroma</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.retrievers </span><span><span leaf="">import</span></span><span leaf=""> BM25Retriever</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_core.runnables </span><span><span leaf="">import</span></span><span leaf=""> RunnableParallel</span><br><br><span><span leaf=""># 向量检索器</span></span><br><span leaf="">vector_retriever = vectorstore.as_retriever(search_kwargs={</span><span><span leaf="">"k"</span></span><span leaf="">: </span><span><span leaf="">10</span></span><span leaf="">})</span><br><br><span><span leaf=""># BM25检索器</span></span><br><span leaf="">bm25_retriever = BM25Retriever.from_documents(chunks, k=</span><span><span leaf="">10</span></span><span leaf="">)</span><br><br><span><span leaf=""># 并行执行两路检索</span></span><br><span leaf="">parallel_retrieval = RunnableParallel({</span><br><span leaf=""> </span><span><span leaf="">"vector"</span></span><span leaf="">: vector_retriever,</span><br><span leaf=""> </span><span><span leaf="">"bm25"</span></span><span leaf="">: bm25_retriever,</span><br><span leaf="">})</span><br><br><span><span leaf=""># 融合</span></span><br><span><span><span leaf="">def</span></span><span leaf=""> </span><span><span leaf="">hybrid_fusion</span></span><span><span leaf="">(results)</span></span><span leaf="">:</span></span><br><span leaf=""> vector_results = results[</span><span><span leaf="">"vector"</span></span><span leaf="">]</span><br><span leaf=""> bm25_results = results[</span><span><span leaf="">"bm25"</span></span><span leaf="">]</span><br><span leaf=""> </span><span><span leaf="">return</span></span><span leaf=""> reciprocal_rank_fusion([vector_results, bm25_results])</span><br><br><span><span leaf=""># 构建混合检索链</span></span><br><span leaf="">hybrid_retriever = parallel_retrieval | hybrid_fusion</span><br>
实测效果:在法律合同知识库上,纯向量检索Top-5准确率68%,纯BM25是55%,混合检索达到82%。两路融合比单路的提升是实打实的。
Rerank重排序:海选之后的决赛
混合检索解决了"召回"的问题——确保相关文档被找到。但找到的文档排序可能不对——最相关的排第5,不太相关的排第1。
Rerank就是在检索结果上再做一次精排,用Cross-Encoder模型同时看问题和文档,给出更精准的相关性分数。
为什么需要Rerank?
向量检索用的是Bi-Encoder(问题和文档分别编码再算相似度),快但不够精准。Rerank用的是Cross-Encoder(问题和文档一起编码,让模型看两者的交互关系),慢但精准。
类比:Bi-Encoder是"看简历筛选”(快但粗),Cross-Encoder是"面试精评"(慢但准)。
Rerank模型选型
| 模型
|
特点
|
适用场景
bge-reranker-v2-m3
|
BGE系列,多语言,开源
| 中文首选推荐 | |
bge-reranker-large
|
BGE系列,英文强
|
英文项目
| |
Cohere Rerank
|
API调用,效果很好
|
不想自己部署
| |
Jina Reranker v2
|
多语言,开源
|
替代bge
|
用Cohere Rerank(最快上手)
<span><span leaf="">from</span></span><span leaf=""> langchain_community.document_compressors </span><span><span leaf="">import</span></span><span leaf=""> CohereRerank</span><br><span><span leaf="">from</span></span><span leaf=""> langchain.retrievers </span><span><span leaf="">import</span></span><span leaf=""> ContextualCompressionRetriever</span><br><br><span><span leaf=""># Rerank模型</span></span><br><span leaf="">compressor = CohereRerank(top_n=</span><span><span leaf="">5</span></span><span leaf="">) </span><span><span leaf=""># 从候选中选最相关的5个</span></span><br><br><span><span leaf=""># 包装检索器:检索→Rerank</span></span><br><span leaf="">compression_retriever = ContextualCompressionRetriever(</span><br><span leaf=""> base_compressor=compressor,</span><br><span leaf=""> base_retriever=vector_retriever, </span><span><span leaf=""># 换成hybrid_retriever也行</span></span><br><span leaf="">)</span><br><br><span><span leaf=""># 使用</span></span><br><span leaf="">results = compression_retriever.invoke(</span><span><span leaf="">"年假申请流程"</span></span><span leaf="">)</span><br>
用bge-reranker(本地部署)
<span><span leaf="">from</span></span><span leaf=""> langchain_community.document_compressors </span><span><span leaf="">import</span></span><span leaf=""> CrossEncoderReranker</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.cross_encoders </span><span><span leaf="">import</span></span><span leaf=""> HuggingFaceCrossEncoder</span><br><br><span><span leaf=""># 加载本地Rerank模型</span></span><br><span leaf="">cross_encoder = HuggingFaceCrossEncoder(model_name=</span><span><span leaf="">"BAAI/bge-reranker-v2-m3"</span></span><span leaf="">)</span><br><span leaf="">compressor = CrossEncoderReranker(model=cross_encoder, top_n=</span><span><span leaf="">5</span></span><span leaf="">)</span><br><br><span leaf="">compression_retriever = ContextualCompressionRetriever(</span><br><span leaf=""> base_compressor=compressor,</span><br><span leaf=""> base_retriever=hybrid_retriever,</span><br><span leaf="">)</span><br>
实测对比
在同一个知识库上,用30个测试问题跑对比:
| 方案
|
Top-5准确率
|
平均延迟
纯向量检索
|
68%
|
50ms
| |
混合检索(BM25+向量)
|
82%
|
80ms
| |
混合检索+Rerank
| 93% |
250ms
|
Rerank的代价是延迟增加了约170ms,但准确率提升了11个百分点。对大多数业务场景来说,这个trade-off完全值得。
Java类比:这就像你做搜索系统——先ES粗排(Bi-Encoder向量检索,快),再用Learning to Rank精排(Cross-Encoder Rerank,准)。两段式检索是搜索引擎的经典架构,RAG里一模一样的思路。
上下文压缩:给检索结果"瘦身"
有时候检索到的chunk很长,但只有一两句话跟问题相关。把整个chunk都塞进提示词,浪费Token还干扰模型。
上下文压缩就是在不丢失关键信息的前提下,压缩检索结果:
<span><span leaf="">from</span></span><span leaf=""> langchain.retrievers </span><span><span leaf="">import</span></span><span leaf=""> ContextualCompressionRetriever</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.document_compressors </span><span><span leaf="">import</span></span><span leaf=""> LLMChainExtractor</span><br><br><span><span leaf=""># 用LLM提取跟问题相关的部分</span></span><br><span leaf="">compressor = LLMChainExtractor.from_llm(llm=model)</span><br><br><span leaf="">compression_retriever = ContextualCompressionRetriever(</span><br><span leaf=""> base_compressor=compressor,</span><br><span leaf=""> base_retriever=hybrid_retriever,</span><br><span leaf="">)</span><br><br><span><span leaf=""># 原始chunk可能是1000字,压缩后可能只剩200字跟问题相关的部分</span></span><br><span leaf="">results = compression_retriever.invoke(</span><span><span leaf="">"年假申请流程"</span></span><span leaf="">)</span><br>
注意:LLM压缩会增加延迟和成本。如果检索结果本身就比较短(500字以内),不用压缩。
我的生产配置推荐
<span><span leaf=""># 标准生产级RAG检索配置</span></span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.vectorstores </span><span><span leaf="">import</span></span><span leaf=""> Milvus</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.retrievers </span><span><span leaf="">import</span></span><span leaf=""> BM25Retriever</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.document_compressors </span><span><span leaf="">import</span></span><span leaf=""> CrossEncoderReranker</span><br><span><span leaf="">from</span></span><span leaf=""> langchain_community.cross_encoders </span><span><span leaf="">import</span></span><span leaf=""> HuggingFaceCrossEncoder</span><br><span><span leaf="">from</span></span><span leaf=""> langchain.retrievers </span><span><span leaf="">import</span></span><span leaf=""> ContextualCompressionRetriever</span><br><br><span><span leaf=""># 1. 向量检索</span></span><br><span leaf="">vector_retriever = vectorstore.as_retriever(search_kwargs={</span><span><span leaf="">"k"</span></span><span leaf="">: </span><span><span leaf="">15</span></span><span leaf="">})</span><br><br><span><span leaf=""># 2. BM25检索</span></span><br><span leaf="">bm25_retriever = BM25Retriever.from_documents(chunks, k=</span><span><span leaf="">15</span></span><span leaf="">)</span><br><br><span><span leaf=""># 3. 混合检索 + RRF融合</span></span><br><span leaf="">hybrid_retriever = HybridRetriever(</span><br><span leaf=""> vector_retriever=vector_retriever,</span><br><span leaf=""> bm25_retriever=bm25_retriever,</span><br><span leaf=""> k=</span><span><span leaf="">15</span></span><span leaf="">,</span><br><span leaf="">)</span><br><br><span><span leaf=""># 4. Rerank精排</span></span><br><span leaf="">cross_encoder = HuggingFaceCrossEncoder(model_name=</span><span><span leaf="">"BAAI/bge-reranker-v2-m3"</span></span><span leaf="">)</span><br><span leaf="">compressor = CrossEncoderReranker(model=cross_encoder, top_n=</span><span><span leaf="">5</span></span><span leaf="">)</span><br><br><span><span leaf=""># 5. 最终检索器</span></span><br><span leaf="">final_retriever = ContextualCompressionRetriever(</span><br><span leaf=""> base_compressor=compressor,</span><br><span leaf=""> base_retriever=hybrid_retriever,</span><br><span leaf="">)</span><br>
这套配置:两路召回各15条→RRF融合→Rerank精排5条→送入模型。兼顾了召回率和精确率,延迟控制在300ms以内。
本篇要点
| 要点
|
说明
混合检索
|
向量+BM25两路召回,RRF融合
| |
RRF融合
|
1/(60+rank),简单有效不用调参
| |
Bi-Encoder
|
问题和文档分别编码,快但粗
| |
Cross-Encoder
|
问题和文档一起编码,慢但准
| |
Rerank模型
|
bge-reranker-v2-m3中文首选
| |
效果对比
|
纯向量68% → 混合82% → +Rerank 93%
| |
延迟
|
Rerank增加~170ms,值得
| |
上下文压缩
|
LLM提取相关部分,短chunk不用
| |
生产配置
|
粗排Top-15 → Rerank → 精排Top-5
|
下篇预告
下一篇进入RAG的高级架构——GraphRAG、Agentic RAG、查询改写、多跳检索。这些是2025-2026年RAG的前沿方向,适合你的知识库已经跑起来了、基础检索也调好了,但还想要更高精度的场景。
你现在的RAG用的什么检索方案?纯向量还是已经上了混合检索?评论区说说效果。
觉得有用就点个在看,下一篇讲高级RAG架构——基础RAG搞不定的场景怎么办。
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/ai002/post/20260822/%E5%88%AB%E5%86%8D%E5%8F%AA%E7%94%A8%E5%90%91%E9%87%8F%E6%A3%80%E7%B4%A2%E4%BA%86RAG%E6%B7%B7%E6%A3%80Rerank%E6%8F%90%E5%8D%8740_/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com