检索增强技术
Hybrid Search、ReRank、Query Rewriting、HyDE — 全方位提升 RAG 检索质量的进阶技术
Hybrid Search 混合检索
单一检索方式往往存在盲区。稠密检索擅长捕捉语义相似但词汇不匹配的内容,稀疏检索则在精确关键词匹配上无可替代。Hybrid Search 将二者结合,显著提升召回的全面性和鲁棒性。
稠密检索(Dense Retrieval)
稠密检索将文本映射到高维语义向量空间,通过向量相似度(如余弦相似度、内积)衡量语义相关性。常用模型包括 BGE、text-embedding-ada-002、E5 等。
优点:
- 理解同义替换和语义近似
- 对未见过的术语组合仍有较好的泛化能力
缺点:
- 对低频实体和专有名词的匹配不够精确
- 训练数据分布外的查询效果下降明显
稀疏检索(Sparse Retrieval)
稀疏检索基于词袋模型或倒排索引,代表方法为 BM25 和 TF-IDF。它依赖词项在文档中的统计权重进行匹配。
优点:
- 精准匹配关键词、ID、代码片段
- 不依赖训练数据,开箱即用
- 可解释性强
缺点:
- 词汇鸿沟问题:语义相关但用词不同的查询无法召回
- 无法理解上下文语义
混合策略
混合检索的核心在于如何融合稠密和稀疏两路的得分。常见策略有三种:
| 策略 | 描述 | 特点 |
|---|---|---|
| 加权融合(Weighted Sum) | score = alpha * dense_score + (1-alpha) * sparse_score | 简单高效,alpha 需调参 |
| 倒数排名融合(RRF) | score = sum(1/(k + rank_i)) | 无需归一化得分,鲁棒性好 |
| 基于学习融合 | 用轻量模型(如 MLP / LambdaRank)学习融合权重 | 效果最优但需要标注数据 |
代码示例:Elasticsearch Hybrid Search
以下示例展示如何使用 Elasticsearch 的 knn 查询结合 bool 查询实现混合检索。
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch("http://localhost:9200")
# 创建索引时配置 dense 向量和 text 字段
mapping = {
"mappings": {
"properties": {
"content": {"type": "text", "analyzer": "ik_max_word"},
"content_vector": {
"type": "dense_vector",
"dims": 768,
"index": True,
"similarity": "cosine"
}
}
}
}
es.indices.create(index="hybrid_demo", body=mapping, ignore=400)
# 混合查询:knn 语义检索 + BM25 关键词检索,RRF 融合
query = {
"query": {
"bool": {
"must": {"match": {"content": "深度学习框架"}}
}
},
"knn": {
"field": "content_vector",
"query_vector": [0.1] * 768,
"k": 10,
"num_candidates": 100
},
"rank": {
"rrf": {
"rank_constant": 60,
"window_size": 200
}
}
}
results = es.search(index="hybrid_demo", body=query)重排序(ReRank)
为什么需要 ReRank
在第一阶段检索(粗排)中,为了效率通常使用 Embedding 向量检索或 BM25,从海量文档中召回数百条候选。但这些方法对细粒度语义区分的精度有限。ReRank 引入第二阶段——用更强大但也更慢的模型对候选列表重新打分排序,形成"初筛 + 精排"的两阶段范式。
Cross-Encoder 重排序模型
Embedding 模型(Bi-Encoder)将查询和文档独立编码为向量后计算相似度,效率高但丢失了交叉注意力信息。Cross-Encoder 则将查询和文档拼接后输入一个 Transformer 模型,利用完整的自注意力机制捕捉二者间的细粒度交互。
| 特性 | Bi-Encoder(Embedding) | Cross-Encoder(ReRank) |
|---|---|---|
| 编码方式 | 查询和文档独立编码 | 查询和文档拼接编码 |
| 交互粒度 | 仅有最后一层向量比较 | 每层都有交叉注意力 |
| 推理速度 | 快(可预计算文档向量) | 慢(需逐对计算) |
| 适用阶段 | 第一阶段召回 | 第二阶段精排 |
| 效果精度 | 一般 | 优秀 |
主流 ReRank 模型
- BGE-Reranker(BAAI/bge-reranker-v2-m3):开源重排序模型,支持多语言,基于 XLM-RoBERTa 架构,在 MTEB 榜单上表现突出。
- Cohere Rerank:商业 API,简单易用,适合生产环境快速接入。
- MonoT5:基于 T5 的序列到序列重排序模型,通过生成"true"/"false" token 来判断相关性。
- RankZephyr / RankLLaMA:基于 LLaMA 的列表式重排序模型,能同时考虑多个文档间的相对顺序。
代码示例:使用 BGE-Reranker
from FlagEmbedding import FlagReranker
# 加载 BGE-Reranker 模型
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
query = "Transformers 的注意力机制是如何工作的"
candidates = [
"Transformer 模型使用自注意力机制来捕获序列中所有位置的依赖关系",
"CNN 通过卷积核提取局部特征,适合图像处理任务",
"自注意力机制允许每个位置关注序列中的所有其他位置",
]
# 计算相关性得分
pairs = [(query, doc) for doc in candidates]
scores = reranker.compute_score(pairs)
# 按得分降序排序
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
for doc, score in ranked:
print(f"[{score:.4f}] {doc}")查询重写(Query Rewriting)
用户的原始查询往往简短、模糊或不完整,直接检索效果不佳。查询重写旨在将原始查询转化为更利于检索的形式。
多轮对话中的查询重写
在对话式 RAG 场景中,用户常省略上下文信息。例如:
- 用户:"什么是注意力机制?"
- 用户追问:"它和 RNN 比有什么优势?"
- 需要重写为:"注意力机制相比于 RNN 的优势是什么"
常用的重写方法包括:
- HyDE(假设文档嵌入):见下节详细说明。
- Step-back Prompting:引导 LLM 先抽象出更广泛的问题(如先问"什么是注意力机制?",再检索具体细节),适用于需要推理的多跳问题。
- LLM-based Rewriting:直接让 LLM 将对话历史和当前问题重写为独立的检索查询。
from openai import OpenAI
client = OpenAI()
def rewrite_query(history: list, current: str) -> str:
prompt = f"""根据对话历史,将最新问题重写为一个独立、完整的检索查询。
对话历史:
{chr(10).join(history)}
最新问题:{current}
重写后的查询:"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.1
)
return response.choices[0].message.content.strip()查询扩展(Query Expansion)
查询扩展通过增加相关词项来丰富原始查询,提升召回覆盖面。
- 伪相关反馈(Pseudo-Relevance Feedback, PRF):先进行一次检索,从前 k 个结果的摘要中提取高频词,加入原查询重新检索。Rocchio 算法是其经典实现。
- 同义词扩展:利用同义词词典或 Embedding 查找语义相近的词添加到查询中。
- LLM 生成扩展:让 LLM 基于原始查询生成多个语义等价的表述版本。
HyDE(假设文档嵌入)
HyDE(Hypothetical Document Embeddings)是一种非常巧妙的检索增强思路,核心思想是:让 LLM 根据查询先生成一篇"假设文档"(即一个理想化的回答),然后用这篇假设文档的 Embedding 去检索真实文档库。
工作原理
- 用户输入查询 Q
- LLM 根据 Q 生成一段假设文档 D_hypo(格式类似真实文档的回答)
- 将 D_hypo 编码为向量 v_hypo
- 用 v_hypo 在向量库中做相似性检索,召回真实文档
为什么有效
假设文档在语义空间上比原始查询更接近真实相关文档的分布。这是因为查询通常较短且抽象,而假设文档具备与真实文档相似的句长、术语密度和表达风格,因此其 Embedding 能更精准地定位到相关文档。
def hyde_retrieve(query: str, retriever, llm_fn, top_k: int = 10):
# Step 1: LLM 生成假设文档
prompt = f"请根据以下问题写一段详细的回答:\n\n{query}"
hypo_doc = llm_fn(prompt)
# Step 2: 使用假设文档的 Embedding 检索
results = retriever.search(hypo_doc, top_k=top_k)
return results
# 示例
query = "LoRA 微调的原理是什么"
results = hyde_retrieve(query, vector_retriever, llm_generate)HyDE 的局限性
- 依赖 LLM 的生成质量,如果生成内容偏离真实答案,反而引入噪声
- 增加了额外的大模型推理开销
- 对极度开放或知识密集型的查询效果更好,简单事实查询收益不大
对比与应用场景
| 技术 | 核心思路 | 适合场景 | 效果提升 | 额外开销 |
|---|---|---|---|---|
| Hybrid Search | 稠密 + 稀疏检索融合 | 混合类型文档库,既有语义内容也有精确关键词 | 召回率提升 10%-30% | 低(维护双路索引) |
| ReRank | 初筛 + Cross-Encoder 精排 | 高精度要求的问答、事实验证 | NDCG@10 提升 5%-15% | 中(需在线推理) |
| Query Rewriting | 重写/扩展查询文本 | 多轮对话、短查询、模糊查询 | 召回率提升 15%-40% | 低(LLM 调用成本) |
| HyDE | LLM 生成假设文档再检索 | 知识密集型、长尾查询 | 召回率提升 10%-25% | 中(LLM 生成+向量化) |
推荐组合策略
在实际生产系统中,通常将多种技术组合使用:
- 第一层:Query Rewriting 对用户查询进行规范化处理
- 第二层:Hybrid Search 从多路索引中召回候选文档(top-100 到 top-200)
- 第三层:ReRank 对候选文档进行精排,截取 top-10 到 top-20
- 最终:将精排结果送入 LLM 生成最终回答
这种四级流水线架构已在多个工业级 RAG 系统中验证有效,能够在保持较低延迟的同时大幅提升检索质量。