RAG检索增强策略混合检索、重排序与Query改写前面的文章里我们把数据层、向量层都搭好了Demo 也跑通了。但到了这一步你会发现一个新问题“今天的召回结果怎么跟昨天不一样”“明明有这篇文档为什么搜不出来”检索是 RAG 系统里调优空间最大的环节。没有好的检索LLM 就是巧妇难为无米之炊。今天我把最实用的三个检索增强策略掰开来讲。大家好我是黒漂技术佬。一、为什么单纯的向量检索不够向量检索的本质是语义相似。这个机制在 80% 的场景下工作得很好但剩下 20% 会出问题场景 1精确匹配失败用户问张三的报销单向量检索可能搜出一堆讲报销流程的文档但就是搜不出张三那张——因为张三这个人名的语义信息非常弱在向量空间里和其他人名混在一起没有区分度。场景 2问法差异用户嘴上说这玩意儿咋整心里想的是服务重启操作步骤。口语化提问和文档的书面语之间存在巨大的语义鸿沟向量很难跨越。场景 3查询太短年假两个字向量化之后信息量太少和任何文档的相似度都差不多区分不开。这三个问题分别对应三个解决方案混合检索、Query 改写、重排序。二、混合检索向量不够关键词来凑核心思路两条路同时走结果再融合用户问题 张三的报销单 │ ├──→ 向量检索语义召回→ Top 20 │ 能搜到报销流程、财务制度…… │ └──→ 关键词检索BM25→ Top 20 能搜到包含张三的文档 │ └──→ 融合算法 → Top 10 → 送入 LLM怎么实现如果你用的是Elasticsearch 8.x一个查询搞定responsees.search(indexknowledge_base,body{query:{bool:{should:[# 关键词匹配BM25算法{match:{content:张三 报销}},]}},knn:{field:content_vector,query_vector:query_embedding,k:10,num_candidates:100},size:20})如果你用的是Milvus ES 双库架构需要自己做结果融合。最常用的融合算法叫RRFReciprocal Rank Fusion倒数排名融合defrrf_fusion(vector_results,keyword_results,k60): 倒数排名融合每个文档的最终分数 Σ(1 / (排名 k)) 文档在两个列表中排名都靠前 → 分数高 → 排前面 scores{}forrank,docinenumerate(vector_results):doc_iddoc[id]scores[doc_id]scores.get(doc_id,0)1.0/(rankk)forrank,docinenumerate(keyword_results):doc_iddoc[id]scores[doc_id]scores.get(doc_id,0)1.0/(rankk)# 按分数降序排列returnsorted(scores.items(),keylambdax:x[1],reverseTrue)[:10]k60 是经验值这个值越大排名靠后的文档权重越低越不容易干扰前面的结果相当于是个平滑因子。三、Query 改写帮用户把话说明白用户不是搜索引擎专家他们的提问常常是模糊的、口语化的甚至语焉不详的。方案用 LLM 改写 Query在检索之前先让一个小模型把用户的原始问题翻译成检索友好的查询query_rewrite_prompt 你是一个搜索查询优化助手。请将用户的口语化问题改写成更适合文档检索的关键词串。 规则 1. 补充缩写词的全称如 ES → Elasticsearch 2. 将口语转为书面语如 这玩意儿 → 这个功能 3. 提取核心概念去掉语气词 4. 生成 2-3 个不同角度的改写 用户问题{question} 请输出每行一个改写 defrewrite_query(question):用 LLM 改写用户问题生成多个搜索变体responsellm.invoke(query_rewrite_prompt.format(questionquestion))rewrites[line.strip()forlineinresponse.split(\n)ifline.strip()]returnrewrites# 实际使用original上次说的那个代码检查的是什么东西来着rewritesrewrite_query(original)# 输出# [代码检查工具 是什么, Code Review 代码检查 工具 介绍, 代码静态分析 检查 说明]# 用所有改写分别检索合并去重all_results[]forqinrewrites:q_vectorembedding_model.encode(q)resultsvectorstore.search(q_vector,k5)all_results.extend(results)Query 改写要控制成本每次多调用一次 LLM虽然可以用便宜的模型比如 DeepSeek 轻量版或本地小模型但如果你的 QPS 很高改写开销不能忽视。建议对改写结果做缓存——同一个问题不需要每次重新改写。四、重排序Reranker最后一公里的精度提升向量检索返回的 Top-20 只是大概相关。真正相关的可能排在第 15 位。这时候需要一个更强的模型重新精排。Reranker 是什么Reranker 是一个专门训练的排序模型输入是(Query, Document)对输出是相关性分数。它比 Embedding 模型的距离计算更精细——因为它同时看了 Query 和 Document 的完整内容而不是分别 Embedding 后算距离。代码实现fromFlagEmbeddingimportFlagReranker# 加载中文最强的 RerankerrerankerFlagReranker(BAAI/bge-reranker-v2-m3,use_fp16True)defrerank(query,candidates,top_k5):对候选文档重新排序返回最相关的 top_k 个pairs[[query,doc.page_content]fordocincandidates]scoresreranker.compute_score(pairs)# scores[i] 是 query 和 candidates[i] 的匹配分数# 按分数降序排列rankedsorted(zip(candidates,scores),keylambdax:x[1],reverseTrue)return[docfordoc,_inranked[:top_k]]为什么 Reranker 效果更好Embedding 模型做的是压缩——把长文本压缩成一个固定长度的向量。这个过程不可避免地会丢失细粒度信息。但 Reranker 不需要压缩——它直接对(Query, Document)全文做 Cross-Attention交叉注意力Query 里的每个词都能和 Document 里的每个词做交互。代价是Reranker 比单纯的向量距离计算慢 10~50 倍所以不能对全库做只能对 Top-K 候选项精排。五、三个策略怎么组合我的实战流水线把混合检索、Query 改写、重排序串联起来就是一个完整的检索增强流水线classAdvancedRetriever:def__init__(self,vectorstore,es_client,reranker,llm):self.vectorstorevectorstore self.eses_client self.rerankerreranker self.llmllmdefretrieve(self,question:str,filters:dictNone,top_k:int5):# Step 1: Query 改写生成 3 个变体rewritten_queriesrewrite_query(question)# Step 2: 多路召回all_candidates[]seen_idsset()forqinrewritten_queries[question]:# 所有改写 原文q_vecself._embed(q)# 向量召回vec_resultsself.vectorstore.search(q_vec,k20,filterfilters)# BM25 关键词召回kw_resultsself._keyword_search(q,k20,filterfilters)# 用 RRF 融合两路结果fusedrrf_fusion(vec_results,kw_results)# 去重同一个chunk可能被不同改写命中fordoc_id,_infused:ifdoc_idnotinseen_ids:all_candidates.append(self._get_doc(doc_id))seen_ids.add(doc_id)# Step 3: 重排序精排final_docsrerank(question,all_candidates,top_ktop_k)returnfinal_docs效果对比我在同一份测试集上对比了四种检索策略策略 Recall5 MRR 延迟毫秒 ─────────────────────────────────────────────────────────── 纯向量检索 0.86 0.74 80ms 向量 BM25 混合检索 0.91 0.80 120ms 混合 Query 改写 0.93 0.84 350ms 混合 Query 改写 重排序 0.96 0.88 480ms每增加一个策略效果都提升但延迟也在增加。480ms 对用户体验来说是可以接受的但需要做好异步和超时控制。六、调优的实战经验不是所有问题都需要 Query 改写对短查询 10 字和口语化明显的做改写长且清晰的问题直接检索节省延迟。Reranker 的 top_k 不是越大越好向量检索阶段取 20~30 个候选项给 Reranker 就够取 100 个只会让精排耗时翻倍但对效果提升很小。缓存是调优的秘密武器热门问题的检索结果缓存 5 分钟能挡掉 20%~30% 的重复查询相当于免费的性能提升。日志比直觉靠谱把每次检索的 Query、命中结果、用户反馈点赞/点踩打全日志跑一个月的统计分析比靠直觉调参准十倍。 你有没有遇到过明明文档里有答案但偏偏搜不出来的情况你是怎么调的评论区交流