检索策略
检索决定 RAG 的上限
RAG 的效果有一个铁律:如果检索阶段没找到正确的信息,LLM 再强也没法给出正确答案。
生成模型的能力已经很强了,RAG 系统的瓶颈往往在检索。这一章讲几种提升检索效果的策略。
基础向量搜索
最基本的检索:把查询转为向量,在向量库中找最相似的 Top-K。
query_embedding = embed("如何申请退款?")
results = vector_db.search(query_embedding, top_k=5)
这能处理大部分场景,但有局限:
- 同义词问题:用户说"退货",文档写的是"退款",向量搜索通常能处理,但精确关键词匹配做不到
- 关键词依赖:用户搜"Python 3.12 新特性",可能更需要精确的关键词匹配而不是语义匹配
混合搜索(Hybrid Search)
混合搜索 = 向量搜索 + 关键词搜索,取两者的优势:
# 向量搜索:语义相似
vector_results = vector_db.search(query_embedding, top_k=10)
# 关键词搜索:精确匹配(BM25)
keyword_results = bm25_search(query, top_k=10)
# 合并结果(Reciprocal Rank Fusion)
final_results = rrf_merge(vector_results, keyword_results, top_k=5)
RRF(Reciprocal Rank Fusion) 是最常用的结果合并算法:
def rrf_merge(result_lists, k=60):
scores = {}
for result_list in result_lists:
for rank, doc in enumerate(result_list):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
混合搜索在大多数 RAG 场景中都比纯向量搜索效果好。很多向量数据库(Weaviate、Qdrant)已经内置了混合搜索功能。
重排序(Re-ranking)
检索通常分两阶段:
- 初筛(Retrieval):用向量/关键词搜索快速找出候选文档(Top-20 到 Top-100)
- 精排(Re-ranking):用更精确的模型对候选文档重新打分排序
重排序模型(Cross-Encoder)比 Embedding 模型更准确,因为它同时看查询和文档,而不是分别编码再比较。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
# 初筛:向量搜索得到 20 个候选
candidates = vector_db.search(query_embedding, top_k=20)
# 精排:用 Cross-Encoder 重排
pairs = [(query, doc.text) for doc in candidates]
scores = reranker.predict(pairs)
# 按新分数排序,取 Top-5
reranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]
也可以使用 API 服务:
# Cohere Rerank API
import cohere
co = cohere.Client("your-api-key")
results = co.rerank(
query="如何申请退款",
documents=[doc.text for doc in candidates],
top_n=5
)
多查询检索(Multi-Query)
用户的查询可能不够精确,或者只从一个角度描述了需求。多查询检索让 LLM 生成多个变体查询,扩大搜索范围:
# 让 LLM 生成查询变体
prompt = f"""为以下用户问题生成 3 个不同角度的搜索查询:
用户问题:{query}
输出 3 个查询,每行一个:"""
queries = llm.generate(prompt).split("\n")
# 结果可能是:
# "退款流程步骤"
# "退货退款政策规定"
# "申请退款需要什么条件"
# 对每个查询分别检索
all_results = []
for q in queries:
results = vector_db.search(embed(q), top_k=5)
all_results.extend(results)
# 去重并合并
final_results = deduplicate(all_results)
HyDE(Hypothetical Document Embeddings)
一个巧妙的技巧:让 LLM 先生成一个假设的答案,用这个答案的向量去搜索,而不是用问题的向量。
原理是:答案和文档的语义比问题和文档更接近。
# 生成假设答案
hypothetical_answer = llm.generate(
f"请简要回答:{query}\n(不需要准确,只是为了搜索用)"
)
# 用假设答案的向量搜索
hyde_embedding = embed(hypothetical_answer)
results = vector_db.search(hyde_embedding, top_k=5)
HyDE 在某些场景下效果显著,但也会增加延迟(多一次 LLM 调用)。
元数据过滤
在向量搜索之前或之后,用元数据缩小范围:
# 时间过滤:只搜最近更新的文档
results = vector_db.search(
query_embedding,
top_k=5,
filter={"updated_at": {"$gte": "2024-01-01"}}
)
# 分类过滤:只在特定分类中搜索
results = vector_db.search(
query_embedding,
top_k=5,
filter={"category": "退款政策"}
)
评估检索效果
怎么知道你的检索做得好不好?几个常用指标:
Recall@K:前 K 个结果中包含了多少相关文档(占所有相关文档的比例)。
Precision@K:前 K 个结果中有多少是相关的。
MRR(Mean Reciprocal Rank):第一个相关文档出现在第几位(排名的倒数的平均值)。
# 简单的评估框架
test_cases = [
{"query": "退款政策", "expected_docs": ["policy-001", "policy-002"]},
{"query": "联系客服", "expected_docs": ["faq-005"]},
]
for case in test_cases:
results = retrieve(case["query"], top_k=5)
result_ids = [r.id for r in results]
recall = len(set(result_ids) & set(case["expected_docs"])) / len(case["expected_docs"])
print(f"Query: {case['query']}, Recall@5: {recall}")
建议:在优化检索策略之前,先建立一个评估集。 否则你不知道改动是变好了还是变差了。
要点总结
- 检索质量决定 RAG 的上限。 如果检索没找到正确信息,LLM 也无法给出好答案。
- 混合搜索(向量 + 关键词)通常优于纯向量搜索。 对大多数场景推荐使用。
- 重排序(Re-ranking)是提升检索精度最有效的手段之一——用 Cross-Encoder 对候选结果精排。
- 多查询和 HyDE 能扩大搜索范围,但增加延迟。适合对质量要求高的场景。
- 先建评估集,再优化。 没有评估,优化就是盲目的。