课程0基础Agent开发课 / RAG与向量数据库 / RAG优化技术-混合检索与重排序
— 13 min read

RAG优化技术-混合检索与重排序

**本文适合谁**:基础 RAG 已上线,但发现对精确关键词(订单号、产品型号、人名)的查询效果不好,或者总体检索精度不够满意的读者。本篇是必读五篇中最重要的优化篇。

RAG 优化技术:混合检索与重排序

本文适合谁:基础 RAG 已上线,但发现对精确关键词(订单号、产品型号、人名)的查询效果不好,或者总体检索精度不够满意的读者。本篇是必读五篇中最重要的优化篇。


基础 RAG 搭建完成并上线后,检索阶段的问题会逐渐显现:向量检索有时会返回与用户问题语义相近但内容不相关的结果,或者对精确关键词(如订单号、产品型号)的匹配效果很差。这是基础 RAG 的天花板——想突破它,需要更精细的检索策略。


1.1 纯向量检索的局限

用户查询 Query

稠密检索
Dense / 向量

稀疏检索
Sparse / BM25

Top-N 候选

Top-N 候选

RRF 融合
Reciprocal Rank Fusion

重排序模型
Reranker / Cross-Encoder

Top-5 最终结果

送入 LLM

混合检索 + 重排序流程——稠密检索与稀疏检索并行召回,经 RRF 融合后由 Reranker 精排,最终取 Top-5 送入 LLM

向量检索(Dense Retrieval)的强项是语义理解。"苹果手机怎么设置"和"iPhone 如何配置"向量距离很近,能检索到一起。这很好。

但它有一个明显的软肋:对精确关键词匹配不敏感

比如用户查询"订单号 20240315001 的状态",向量化之后,这个查询向量会和各种"订单状态"相关的文档都有一定相似度,但那个精确的订单号 20240315001 在向量空间里几乎没有区分度——因为数字串的语义向量天然就不稳定。

再比如查询一个产品型号"SKU-X200A",或者一个专有缩写"OKR"、一个人名——这类需要字面精确匹配的查询,向量检索往往找不准。

这不是 bug,是向量检索的设计使然。解决方案是:用擅长精确匹配的传统方法来补足它的短板


1.2 混合检索:Dense + Sparse

混合检索的思路很直接:把向量检索和传统的关键词检索结合起来,各取所长。

**稠密检索(Dense Retrieval)**就是向量相似度检索,依赖 Embedding 模型,擅长语义理解。

**稀疏检索(Sparse Retrieval)**是基于词频的传统方法,最有代表性的是 BM25(Best Match 25,一种经典的文本相关性评分算法)。BM25 是 TF-IDF(词频-逆文档频率,一种衡量一个词对文档重要程度的统计方法)的改进版,不需要 Embedding 模型,直接对文本做词频统计。"订单号 20240315001"这个查询,BM25 能精确找到包含这个字符串的文档,因为它就是在做词匹配。

两路检索各自拿到一个排序列表,怎么融合?最常用的方法是 RRF(Reciprocal Rank Fusion,倒数排名融合——把多个排序列表中每个文档的名次倒数相加,分数越高说明在各路检索中越靠前)

code
RRF_score(d) = Σ 1 / (k + rank_i(d))

每个检索系统给文档一个排名,RRF 把各系统的排名倒数加起来作为最终分数。常数 k(通常取 60)用来平滑,避免排名第 1 的文档权重过高。

这个公式不需要调权重,不需要对两路检索的分数做归一化,非常稳健。

用 LangChain(一个构建 AI 应用的 Python 框架,提供文档加载、检索、链式调用等现成组件)的 EnsembleRetriever 实现混合检索:

python
# hybrid_retrieval.py
# 混合检索示例

from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.schema import Document

# 准备测试文档
docs = [
    Document(page_content="苹果公司发布了新款 iPhone 16,售价 5999 元起。"),
    Document(page_content="订单号 20240315001 于 3 月 15 日下单,商品为蓝牙耳机,状态:已发货。"),
    Document(page_content="退货政策:电子产品 7 天无理由退货,需保持包装完整。"),
    Document(page_content="iPhone 的设置方法:进入「设置」App,选择「通用」,可以调整语言、字体大小等。"),
    Document(page_content="订单查询方式:登录 App,进入「我的」页面,点击「订单管理」。"),
]

# 稀疏检索器:BM25,基于关键词
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 3

# 稠密检索器:向量相似度
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(docs, embeddings)
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 融合检索器:weights 控制两路的权重
# 0.5/0.5 是等权,可以根据场景调整
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, dense_retriever],
    weights=[0.5, 0.5]
)

# 测试
test_queries = [
    "订单号 20240315001 的发货状态",   # 精确字符串,BM25 应该更有帮助
    "苹果手机怎么调字体",             # 语义查询,向量检索更有帮助
]

for query in test_queries:
    print(f"\n查询:{query}")
    results = ensemble_retriever.invoke(query)
    for i, doc in enumerate(results):
        print(f"  [{i+1}] {doc.page_content[:60]}...")

实际测试中,对精确字符串查询,混合检索的召回率比纯向量检索提升明显,通常能提升 15-30%。


1.3 重排序:从粗召回到精排

混合检索解决的是召回问题:把相关文档找出来。但 top-k 召回的文档里,和问题真正相关的排序不一定靠前。

重排序(Reranking) 是在召回之后做的精排步骤:先用快速的方法召回较多候选(比如 top-20),再用更精准但更慢的模型对这 20 个结果重新打分排序,取排名靠前的 3-5 个送给 LLM。

召回和精排是搜索系统的两个阶段:召回(Retrieval)负责从海量数据中快速找出可能相关的候选集;精排(Reranking)负责对候选集精细打分,选出最相关的结果。

用户问题

混合检索
召回 Top-20

Reranker 精排
对 20 个文档重新打分

取 Top-5

拼入 Prompt

LLM 生成答案

为什么要两个阶段?因为精排模型(Cross-encoder)计算量大,对 20 个文档精排可以接受,对全量文档精排会慢到不可用。

Bi-encoder vs Cross-encoder

向量检索用的 Embedding 模型是 Bi-encoder(双编码器,查询和文档分别独立编码成向量):查询和文档分别编码成向量,然后算距离。快,但精度有限——因为查询和文档是独立编码的,没有交互信息。

Cross-encoder(交叉编码器,把查询和文档拼在一起交给同一个模型处理)把查询和文档拼在一起输入模型,让模型直接给出相关性分数。因为有充分的注意力交互,精度高得多,但推理速度慢得多(不能预计算文档向量)。

这就是为什么重排序用 Cross-encoder 做精排,而不能用它做第一阶段的检索。

常用 Reranker

  • Cohere Rerank:云服务,API 调用,效果好,支持多语言。有免费额度。
  • BGE RerankerBAAI/bge-reranker-large):开源,可本地部署,中文效果很好,是国内场景的首选。
  • JinaAI Reranker:开源,多语言支持,轻量版可以在 CPU 上跑。
python
# reranking_demo.py
# 用 BGE Reranker 做重排序(本地运行,免费)

from langchain.schema import Document
from sentence_transformers import CrossEncoder

# 加载 BGE Reranker(第一次运行会自动下载模型,约 1GB)
reranker = CrossEncoder("BAAI/bge-reranker-base")  # base 版本小一些,large 效果更好

# 模拟第一阶段召回的文档(实际是混合检索的结果)
query = "iPhone 16 多少钱?"
candidate_docs = [
    "苹果公司发布了新款 iPhone 16,售价 5999 元起,提供黑白粉三种颜色。",
    "退货政策:电子产品 7 天无理由退货,需保持包装完整。",
    "iPhone 的设置方法:进入设置 App,选择通用,可以调整语言。",
    "iPhone 16 Pro 版售价 8999 元起,搭载 A18 Pro 芯片,支持摄影风格功能。",
    "订单查询方式:登录 App,进入我的页面,点击订单管理。",
]

# Cross-encoder 打分:输入 (query, doc) 对,输出相关性分数
pairs = [(query, doc) for doc in candidate_docs]
scores = reranker.predict(pairs)

# 按分数降序排列
ranked = sorted(zip(scores, candidate_docs), key=lambda x: x[0], reverse=True)

print(f"查询:{query}\n")
print("重排序结果:")
for score, doc in ranked:
    print(f"  [{score:.4f}] {doc[:60]}...")

# 取 top-3 送给 LLM
top_docs = [doc for _, doc in ranked[:3]]

加了 BGE Reranker 之后,RAG 的答案准确率有明显提升,尤其是那些"召回了相关文档但排名靠后"的问题。


1.4 查询改写:让问题更适合检索

有时候问题本身就是障碍。

用户可能说"刚买的那个东西能退吗",这种口语化的问题,向量检索出来的结果质量不高。如果把问题先改写成"购买商品的退货政策和时限",检索效果就好得多。

**查询改写(Query Rewriting)**就是在检索之前,用 LLM 把用户问题转化成更适合检索的形式。

python
# query_rewriting.py
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

rewrite_prompt = ChatPromptTemplate.from_template("""
你是一个搜索查询优化助手。请将用户的口语化问题改写成更适合文档检索的查询语句。
要求:保留关键信息,去掉口语词,扩展同义词。

用户问题:{question}
改写后的查询(只输出查询语句,不要解释):
""")

def rewrite_query(question: str) -> str:
    chain = rewrite_prompt | llm
    result = chain.invoke({"question": question})
    return result.content.strip()

# 测试
test_questions = [
    "刚买的东西能退吗",
    "我在哪里看订单",
    "手机坏了咋整",
]

for q in test_questions:
    rewritten = rewrite_query(q)
    print(f"原始:{q}")
    print(f"改写:{rewritten}\n")

HyDE(Hypothetical Document Embeddings,假设性文档嵌入) 是查询改写的一个进阶变体。逻辑是:与其直接用问题去检索文档,不如先让 LLM 生成一个"假设答案",用假设答案去检索,因为假设答案和真实文档的向量距离通常比问题和文档的距离更近。

python
# HyDE 示例
hyde_prompt = ChatPromptTemplate.from_template("""
请根据以下问题,生成一段可能的答案文档。这个答案不需要正确,只需要格式和风格
像真实文档一样,用于辅助检索。

问题:{question}
假设答案(100字以内):
""")

def hyde_retrieval(question: str, retriever) -> list:
    # 第一步:生成假设答案
    chain = hyde_prompt | llm
    hypothetical_answer = chain.invoke({"question": question}).content

    # 第二步:用假设答案去检索(而不是用原始问题)
    docs = retriever.invoke(hypothetical_answer)
    return docs

1.5 优化优先级:按成本收益排

不是每个优化都值得做,尤其在资源有限的情况下。建议按以下顺序实施:

第一优先:混合检索

投入低(LangChain 的 EnsembleRetriever 几行代码搞定),收益稳定(几乎所有场景都能受益),没有副作用。这个优化应当首先实施。

第二优先:重排序

投入中等(需要加载或调用一个额外的模型),收益明显(尤其是知识库文档质量参差不齐的情况)。强烈推荐在生产环境使用。

第三优先:查询改写

有收益,但也有风险——改写有时候会引入错误,把问题改偏了。需要评估用户问题质量。如果用户提问都比较规范,这个优化的收益相对小。

优化之前,先建一个评估集:收集 50-100 个真实问题和标准答案,用 RAGAS 或者手动打分来衡量每个优化的实际效果,数据说话。


1.6 RAG 优化是个系统工程

RAG 不是搭好就完了的事情。从基础 RAG 到生产级 RAG,中间有一整条优化链:更好的文档预处理、混合检索、重排序、查询改写、更合理的 Prompt 模板、更科学的 chunk 策略……

每一步都有收益,但也都有成本。关键是知道当前的瓶颈在哪里——是召回不够?还是精度不够?还是 LLM 拿到了正确文档但没看进去?对症下药,比盲目堆技术有用得多。

从混合检索开始,加上重排序,大多数场景的效果就已经足够好了。剩下的,用数据说话。

本页目录