跳转到内容

检索器:相似度搜索与高级检索策略

前三节我们完成了 RAG 的数据准备阶段——文档加载、分块、向量化、存储。现在进入在线查询阶段的核心环节:如何从向量库中精准地找到与用户问题最相关的那几个文档块?

这一节我们将学习从基础到高级的各种检索策略。

基础:相似度搜索

最直接的检索方式——把用户的问题向量化,然后在向量空间中找距离最近的 top-k 个文档:

python
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings
)

# 基础相似度搜索
results = vectorstore.similarity_search("Python 装饰器的用法", k=3)

for i, doc in enumerate(results, 1):
    print(f"\n--- 结果 {i} ---")
    print(f"来源: {doc.metadata.get('source', '?')}")
    print(doc.page_content[:120] + "...")

带分数的搜索

如果你需要知道每个结果的相关性分数(用于过滤低质量结果):

python
results_with_scores = vectorstore.similarity_search_with_score(
    "Python GIL 机制",
    k=3
)

for doc, score in results_with_scores:
    similarity = 1 - score   # Chroma 返回的是距离,越小越近
    print(f"[{similarity:.2f}] {doc.page_content[:60]}...")

输出示例:

[0.89] Python 的 GIL(全局解释器锁)使得同一时刻只有一个线程...
[0.72] 在多线程编程中,由于 GIL 的存在,Python 无法利用多核...
[0.65] Go 语言的 goroutine 可以充分利用多核 CPU 进行并发计算...

有了分数后就可以设置阈值过滤低质量结果:

python
MIN_SCORE = 0.6

filtered = [
    (doc, score) for doc, score in results_with_scores
    if (1 - score) >= MIN_SCORE
]

if not filtered:
    print("⚠️ 未找到足够相关的文档")
else:
    for doc, _ in filtered:
        print(doc.page_content)

Retriever:统一的检索接口

直接调用 vectorstore.similarity_search() 是可行的,但在 LangChain 的标准 Chain 架构中,推荐使用 Retriever 接口。每个 Vector Store 都可以通过 .as_retriever() 创建一个 Retriever:

python
retriever = vectorstore.as_retriever(
    search_type="similarity",      # 搜索类型
    k=3                            # 返回数量
)

# 用法和直接调用类似,但可以接入 Chain 管道
results = retriever.invoke("什么是 RAG?")
for doc in results:
    print(doc.page_content[:80])

.as_retriever() 支持的参数:

参数说明示例值
search_type搜索策略"similarity", "mmr"
k返回结果数3, 5
search_kwargs额外参数{"score_threshold": 0.5}

高级策略一:MMR — 增加结果多样性

默认的相似度搜索返回的是与查询最相似的结果,但这些结果之间可能高度重复。MMR(Maximal Marginal Relevance) 在相关性和多样性之间做平衡:

python
retriever_mmr = vectorstore.as_retriever(
    search_type="mmr",
    k=3,
    search_kwargs={"lambda_mult": 0.7}   # 0=纯多样性, 1=纯相关性
)

lambda_mult 控制平衡点:

  • 接近 1(如 0.9):优先保证每条结果都与查询高度相关,允许重复
  • 接近 0(如 0.3):优先保证各条结果之间差异大,可能牺牲部分相关性
  • 0.7:常用起点值

适用场景:

  • 当知识库中有大量重复或高度相似的内容时
  • 当你需要覆盖多个不同方面的信息时(如"总结一下这篇文档的所有要点")

高级策略二:Metadata 过滤

很多时候你需要在特定范围内搜索——比如只在某个分类、某个来源或某段时间的文档中查找:

python
# 创建带丰富 metadata 的向量库
from langchain_core.documents import Document

docs = [
    Document(page_content="Python 的 GIL 限制多线程性能", metadata={"category": "language", "topic": "concurrency"}),
    Document(page_content="Java 的线程模型基于 OS 原生线程", metadata={"category": "language", "topic": "concurrency"}),
    Document(page_content="Docker 容器化部署最佳实践", metadata={"category": "devops", "topic": "deployment"}),
    Document(page_content="Kubernetes 集群管理入门", metadata={"category": "devops", "topic": "deployment"}),
]

vectorstore = Chroma.from_documents(docs, embeddings)

# 只在 language 类别中搜索
retriever_lang = vectorstore.as_retriever(
    search_kwargs={
        "filter": {"category": "language"}
    }
)

results = retriever_lang.invoke("并发编程")
# 只会返回 language 类别的结果,不会出现 devops 的内容

这在企业知识库中特别有用——用户选择了"HR 政策"分类,就只在这个分类内搜索。

高级策略三:上下文压缩检索

基础检索返回的文本块可能包含大量无关信息。Contextual Compression Retriever 先用廉价的方法做粗筛,再用 LLM 对结果进行精炼和压缩:

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

# 基础检索器(先用便宜的方式粗筛)
base_retriever = vectorstore.as_retriever(search_type="mmr", k=5)

# 压缩器(用 LLM 从每个块中提取最相关的片段)
compressor = LLMChainExtractor.from_llm(ChatOpenAI(model="gpt-4o-mini"))

# 组合成压缩检索器
compressed_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=base_retriever
)

results = compressed_retriever.invoke("GIL 如何影响多线程性能?")

压缩后的结果通常比原始块更短更聚焦,减少了送入提示词的噪音。代价是每次检索多一次 LLM 调用,增加了延迟和成本。

四种检索策略对比

策略速度精准度成本适用场景
Similarity⚡ 最快★★★大多数场景的默认选择
MMR⚡ 快★★☆有重复内容 / 需要覆盖面广
Metadata Filter⚡ 快★★★已知分类/来源范围
Contextual Compression🐢 较慢★★★★需要高精度 / token 预算紧张

检索质量调优指南

在实际项目中,检索质量直接影响 RAG 的最终效果。以下是几个关键的调优方向:

方向一:调整 k 值

k 值太小 → 可能遗漏关键信息;k 值太大 → 引入过多噪音。

python
# 尝试不同的 k 值,观察答案质量的变化
for k in [2, 3, 5]:
    retriever = vectorstore.as_retriever(k=k)
    docs = retriever.invoke(user_question)
    total_chars = sum(len(d.page_content) for d in docs)
    print(f"k={k}: {len(docs)} 个块, ~{total_chars} 字符")

经验规则:

  • 简单事实问答 → k=2~3
  • 需要多角度分析 → k=5
  • 复杂推理任务 → k=5~10

方向二:优化分块策略

如果检索结果总是不相关,大概率是分块出了问题:

  • 切得太碎 → 一个完整概念被拆散在多个块里 → 增大 chunk_size 或 overlap
  • 切得太粗糙 → 单个块包含太多主题 → 减小 chunk_size
  • 在语义边界处切断 → 关键信息被截断 → 使用 MarkdownHeaderTextSplitter 或增大 overlap

方向三:混合检索

纯向量搜索有时不如关键词匹配精确(比如搜专有名词、代码标识符)。混合检索(Hybrid Search) 同时使用向量相似度和关键词匹配:

python
# 某些向量库原生支持混合搜索
# 以 Chroma 为例,可以通过 query 方式实现近似的关键词匹配
results = vectorstore.search(
    query_texts=["Python GIL threading"],
    n_results=3,
    where={"$or": [
        {"page_content": {"$contains": "GIL"}},
        {"page_content": {"$contains": "threading"}}
    ]}
)

到这里,RAG 的全部技术组件都已经介绍完毕——从文档加载、分块、向量化存储,到各种检索策略。下一节我们将把它们全部串联起来,搭建一个完整的、可运行的 RAG 问答系统。

基于 MIT 许可发布