跳到主要内容

检索与重排优化

基础的向量相似度检索在实践中往往无法达到生产级别的要求。本文介绍从稠密检索到混合检索、从基础检索到高级优化的完整技术体系。

稠密检索 vs 稀疏检索

稠密检索(Dense Retrieval)

基于 Embedding 向量的语义相似度检索:

  • 原理:将查询和文档都编码为稠密向量,计算余弦相似度
  • 优势:能理解语义,捕捉同义词、近义词、隐含含义
  • 劣势:对专业术语、缩写、代码变量名等词汇精确匹配效果差
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

query_vector = get_embedding("机器学习算法")
doc_vectors = [get_embedding(doc) for doc in documents]

similarities = cosine_similarity([query_vector], doc_vectors)[0]
top_k_indices = np.argsort(similarities)[-10:][::-1]

稀疏检索(Sparse Retrieval)

基于关键词频率的传统信息检索:

BM25(Best Match 25) 是最常用的稀疏检索算法,对 TF-IDF 的改进版本:

from rank_bm25 import BM25Okapi
import jieba

# 中文分词
def tokenize_zh(text):
return list(jieba.cut(text))

corpus_tokenized = [tokenize_zh(doc) for doc in documents]
bm25 = BM25Okapi(corpus_tokenized)

query_tokens = tokenize_zh("Python 异步编程 asyncio")
scores = bm25.get_scores(query_tokens)
top_k_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:10]

稀疏检索的优势:

  • 对精确关键词匹配效果极好(商品型号、人名、专有名词)
  • 无需 Embedding 计算,速度快
  • 可解释性强(可以看到哪些词贡献了得分)

将稠密检索和稀疏检索的结果融合,取长补短:

RRF(Reciprocal Rank Fusion)融合排序

def reciprocal_rank_fusion(
search_results: list[list[tuple]], k: int = 60
) -> list[tuple]:
"""
融合多路检索结果
search_results: 每路结果为 [(doc_id, score), ...] 列表
k: RRF 参数,通常设为 60
"""
rrf_scores = {}

for results in search_results:
for rank, (doc_id, score) in enumerate(results, 1):
if doc_id not in rrf_scores:
rrf_scores[doc_id] = 0
rrf_scores[doc_id] += 1 / (k + rank)

return sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)

# 使用
dense_results = dense_search(query, top_k=20)
sparse_results = bm25_search(query, top_k=20)
fused_results = reciprocal_rank_fusion([dense_results, sparse_results])

Qdrant 原生混合检索

from qdrant_client.models import SparseVector, FusionQuery, Prefetch

results = client.query_points(
collection_name="documents",
prefetch=[
Prefetch(query=dense_query_vector, using="dense", limit=20),
Prefetch(
query=SparseVector(indices=[100, 500, 2000], values=[0.5, 0.3, 0.8]),
using="sparse",
limit=20
),
],
query=FusionQuery(fusion="rrf"),
limit=10
)

重排器(Reranker)

重排器是在初步检索之后,用更精确的模型对候选结果重新打分排序的组件:

  • 检索阶段:快速从大量文档中找出候选(Top-50 或 Top-100)
  • 重排阶段:用精度更高的交叉编码器(Cross-Encoder)对候选精排
  • 生成阶段:取重排后 Top-5 到 Top-10 注入 LLM

BGE-Reranker(本地)

from FlagEmbedding import FlagReranker

reranker = FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=True)

# 输入为 [查询, 文档] 对
pairs = [[query, doc] for doc in candidate_docs]
scores = reranker.compute_score(pairs, normalize=True)

# 按分数排序
ranked_results = sorted(
zip(candidate_docs, scores),
key=lambda x: x[1],
reverse=True
)
top_5 = [doc for doc, score in ranked_results[:5]]

Cohere Rerank(API)

import cohere

co = cohere.Client("your-api-key")

results = co.rerank(
model="rerank-multilingual-v3.0",
query=query,
documents=candidate_docs,
top_n=5,
return_documents=True
)

for result in results.results:
print(f"分数:{result.relevance_score:.4f}")
print(f"内容:{result.document.text[:200]}")

重排器显著提升精度的原因:双塔模型(Bi-Encoder,用于 Embedding)分别编码查询和文档,而交叉编码器(Cross-Encoder,用于 Reranker)将查询和文档一起输入,能够捕捉更细腻的相关性信号。

HyDE(假设文档嵌入)

HyDE 是一种反直觉但效果显著的检索优化技术:

  • 问题:用户问题(短、口语化)和文档(长、专业)的向量分布差距大,导致检索效果不佳
  • 解决方案:让 LLM 先生成一个假设性的答案,用假设答案的向量做检索
from openai import OpenAI

client = OpenAI()

def hyde_retrieve(query: str, retriever, top_k: int = 5):
# 1. 生成假设文档
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是专业的技术文档作者,请生成一段回答以下问题的专业文档段落:"},
{"role": "user", "content": query}
],
max_tokens=300
)
hypothetical_doc = response.choices[0].message.content

# 2. 用假设文档的向量检索
hyp_vector = get_embedding(hypothetical_doc)
results = retriever.retrieve_by_vector(hyp_vector, top_k=top_k)

return results

results = hyde_retrieve("Python 中如何实现线程安全的单例模式?", retriever)

查询扩展与改写

查询改写

将用户的自然语言问题改写为更适合检索的形式:

def rewrite_query(original_query: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""将以下用户问题改写为更适合文档检索的搜索查询。
要求:去除口语化表达,使用专业术语,突出关键词。

原始问题:{original_query}
改写后的查询:"""
}]
)
return response.choices[0].message.content.strip()

MultiQueryRetriever(多查询检索)

从不同角度生成多个查询,扩大检索覆盖面:

from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI

retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=ChatOpenAI(model="gpt-4o-mini")
)

# 会自动生成多个相关查询并合并结果
docs = retriever.get_relevant_documents("如何优化 Python 代码性能?")
# 内部生成如:["Python 性能优化技巧", "Python 代码加速方法", "Python 分析工具 profiler"]

上下文压缩

从检索到的大块文档中提取最相关的片段,减少注入 LLM 的噪声:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# 使用 LLM 提取最相关句子
compressor = LLMChainExtractor.from_llm(ChatOpenAI(model="gpt-4o-mini"))
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever
)

compressed_docs = compression_retriever.get_relevant_documents(query)
# 只返回文档中真正相关的句子,而非整块

GraphRAG(知识图谱增强检索)

GraphRAG 是微软提出的 RAG 增强方案,将知识图谱与向量检索结合:

核心思路:

  • 从文档中提取实体和关系,构建知识图谱
  • 对知识图谱进行社区检测,生成各社区的摘要(Global Search)
  • 检索时结合图结构关系,找到相关实体周围的文档(Local Search)

适用场景:

  • 需要跨文档推理关系("A 和 B 有什么间接联系?")
  • 需要全局性的综述("文档集合的整体主题是什么?")
  • 实体关系密集的领域(医疗、法律、金融)

局限性:

  • 构建图谱成本高(需要多次 LLM 调用)
  • 维护图谱随文档更新的一致性复杂
  • 对单一事实查询的提升有限,不值得引入额外复杂度
# 使用 Microsoft GraphRAG 库
pip install graphrag
python -m graphrag.index --root ./data
python -m graphrag.query --root ./data --method global "AI 领域的主要研究方向"