RAG 系列(七):检索策略——如何找到最相关的内容

为什么检索策略很重要?

前面六篇文章,我们搞定了文档分块、Embedding 生成、向量库存储。现在假设用户问了一个问题:"Python 异步编程有什么最佳实践?"

你的向量数据库里有 10 万篇文档。最 naive 的做法是:直接做相似度检索,返回 Top-K 最相似的文档。

但问题来了:

  • 问题 1:结果重复。返回的 5 篇文章可能都在讲 asyncio,没有任何一篇讲 aiohttp 或实际踩坑经验。
  • 问题 2:低质量混入。第 5 篇文章虽然语义上有点相关,但其实是在讲 Go 的并发模型,对 Python 用户毫无帮助。
  • 问题 3:查询含明确条件。用户问的是 "2024 年关于 Python 的文章",但纯向量检索完全无视了 "2024 年" 这个时间条件。

本文会对比 4 种检索策略,帮你解决这些问题。


四种检索策略速览

策略 核心思想 解决的问题 适用场景
相似度检索 按向量相似度排序 基础检索 通用场景
MMR 相关性与多样性权衡 结果重复 需要多角度回答
阈值过滤 只保留高相似度结果 低质量混入 宁可少不可错
Self-Query 解析查询生成过滤条件 查询含明确条件 时间/类别限定

实验环境

我们用 10 篇技术博客文章作为测试数据,每篇带有元数据(年份、类别、标签):

可运行的实验源码在文章最后

json 复制代码
[
  {"title": "Python 异步编程实战:从 asyncio 到 aiohttp", "year": 2024, "category": "后端开发"},
  {"title": "2024 年 Python 性能优化指南", "year": 2024, "category": "后端开发"},
  {"title": "JavaScript 异步编程:Promise 与 async/await", "year": 2023, "category": "前端开发"},
  {"title": "2023 年前端框架对比:React vs Vue vs Angular", "year": 2023, "category": "前端开发"},
  {"title": "Go 语言微服务实战:gRPC 与 Kubernetes", "year": 2024, "category": "后端开发"},
  {"title": "Rust 系统编程:内存安全与零成本抽象", "year": 2023, "category": "系统编程"},
  {"title": "Python 机器学习入门:从 NumPy 到 PyTorch", "year": 2024, "category": "人工智能"},
  {"title": "2024 年云原生技术趋势:Service Mesh 与 eBPF", "year": 2024, "category": "云原生"},
  {"title": "数据库选型指南:PostgreSQL vs MySQL vs MongoDB", "year": 2023, "category": "数据库"},
  {"title": "Python 爬虫开发:Scrapy 与 Playwright 对比", "year": 2024, "category": "后端开发"}
]

查询统一用:"Python 异步编程"


策略 1:相似度检索(Similarity Search)

原理

最基础的检索方式。把查询文本转成向量,在向量库里找最相似的 K 个文档。

python 复制代码
results = vectorstore.similarity_search("Python 异步编程", k=4)

实验结果

召回 4 条,覆盖 3 个类别:

排名 年份 类别 标题
1 2024 云原生 2024 年云原生技术趋势:Service Mesh 与 eBPF
2 2023 前端开发 2023 年前端框架对比:React vs Vue vs Angular
3 2024 后端开发 Python 爬虫开发:Scrapy 与 Playwright 对比
4 2024 后端开发 2024 年 Python 性能优化指南

分析

  • ✅ 简单直接,一行代码搞定
  • ❌ 结果集中在少数类别(后端开发出现 2 次)
  • ❌ 可能遗漏其他相关角度的内容

注意:排名第一的是"云原生"文章,这看起来有点反直觉。原因是 BGE 模型从语义角度认为这篇文章和查询有一定关联(都涉及"技术趋势"和"服务"概念),但对我们人类来说明显不够精准。这正是为什么要用多种策略组合的原因。


策略 2:MMR(Maximum Marginal Relevance)

原理

MMR 的核心公式:

scss 复制代码
MMR = λ × Sim(query, di) - (1-λ) × max(Sim(di, dj))
  • 第一项:文档 di 和查询的相关性(越大越好)
  • 第二项:文档 di 和已选文档的相似度(越小越好,保证多样性)
  • λ(lambda_mult):平衡参数,0.5 表示相关性和多样性各占一半
python 复制代码
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 4, "lambda_mult": 0.5, "fetch_k": 20},
)

fetch_k=20 表示先从 20 个候选中筛选,再用 MMR 从中选 4 个。候选池越大,多样性越好。

实验结果

召回 4 条,覆盖 4 个类别

排名 年份 类别 标题
1 2024 云原生 2024 年云原生技术趋势:Service Mesh 与 eBPF
2 2024 后端开发 Python 爬虫开发:Scrapy 与 Playwright 对比
3 2023 系统编程 Rust 系统编程:内存安全与零成本抽象
4 2023 数据库 数据库选型指南:PostgreSQL vs MySQL vs MongoDB

对比分析

指标 相似度检索 MMR
覆盖类别数 3 4
类别列表 后端开发、云原生、前端开发 后端开发、云原生、系统编程、数据库
特点 集中在少数类别 更分散、更多样

MMR 参数调优

python 复制代码
# 只追求相关性
search_kwargs={"k": 4, "lambda_mult": 1.0}  # 等同于相似度检索

# 只追求多样性
search_kwargs={"k": 4, "lambda_mult": 0.0}  # 结果可能和查询不太相关

# 平衡两者(推荐)
search_kwargs={"k": 4, "lambda_mult": 0.5, "fetch_k": 20}

策略 3:相似度阈值过滤

原理

只保留相似度分数(距离)超过阈值的结果,低于阈值的直接丢弃。

重要认知 :Chroma 返回的是距离(distance),不是相似度分数。距离越小表示越相似。

python 复制代码
# 先查看距离分布
results_with_score = vectorstore.similarity_search_with_score(query, k=10)
for doc, score in results_with_score:
    print(f"距离={score:.4f} | {doc.metadata['title']}")

距离分布实测

javascript 复制代码
距离=0.8652 | 2024 年云原生技术趋势:Service Mesh 与 eBPF
距离=0.8764 | 2023 年前端框架对比:React vs Vue vs Angular
距离=0.8833 | Python 爬虫开发:Scrapy 与 Playwright 对比
距离=0.8857 | 2024 年 Python 性能优化指南
距离=0.8906 | Python 机器学习入门:从 NumPy 到 PyTorch
距离=0.9019 | Rust 系统编程:内存安全与零成本抽象
距离=0.9024 | Python 异步编程实战:从 asyncio 到 aiohttp
距离=0.9145 | JavaScript 异步编程:Promise 与 async/await
距离=0.9147 | 数据库选型指南:PostgreSQL vs MySQL vs MongoDB
距离=0.9481 | Go 语言微服务实战:gRPC 与 Kubernetes

手动阈值过滤

python 复制代码
threshold = 0.89
filtered = [(doc, score) for doc, score in results_with_score if score <= threshold]
# 结果:4 条(前 4 个距离 <= 0.89)

分析

  • ✅ 能剔除明显不相关的结果(如 Go 语言文章距离 0.9481)
  • ⚠️ 阈值设定需要实验:设太高可能一条都没有,设太低等于没过滤
  • 💡 建议:先跑一批查询看距离分布,再设定阈值

策略 4:Self-Query(查询解析 + 元数据过滤)

原理

用户查询往往不是纯语义问题,而是带有明确条件的:

  • "2024 年 关于 Python 的文章" → year=2024, tags=Python
  • "后端开发类别的文章" → category=后端开发
  • "2023 年前端相关的文章" → year=2023, category=前端开发

Self-Query 的核心流程:

复制代码
自然语言查询 → 解析器 → 结构化过滤条件 → 元数据过滤 → 向量检索

解析器实现

生产环境可以用 LLM(如 LangChain 的 SelfQueryRetriever)做解析,这里用规则解析器演示核心逻辑:

python 复制代码
def parse_query(query: str) -> dict:
    filters = {}
    semantic = query

    # 提取年份
    if match := re.search(r'(20\d{2})\s*年', query):
        filters["year"] = int(match.group(1))

    # 提取类别
    for cat in ["后端开发", "前端开发", "系统编程", ...]:
        if cat in query:
            filters["category"] = cat

    # 提取标签
    for tag in ["Python", "JavaScript", "Go", ...]:
        if tag in query:
            filters["tags"] = tag

    return {"semantic_query": semantic, "filters": filters}

实验结果

查询 1:「2024 年关于 Python 的文章」

markdown 复制代码
解析结果:
  语义查询:Python
  过滤条件:{'year': 2024, 'tags': 'Python'}
元数据过滤后剩余 4 篇:
  - Python 异步编程实战:从 asyncio 到 aiohttp
  - 2024 年 Python 性能优化指南
  - Python 机器学习入门:从 NumPy 到 PyTorch
  - Python 爬虫开发:Scrapy 与 Playwright 对比

查询 2:「后端开发类别的文章」

markdown 复制代码
解析结果:
  语义查询:后端开发
  过滤条件:{'category': '后端开发'}
元数据过滤后剩余 4 篇:
  - Python 异步编程实战:从 asyncio 到 aiohttp
  - 2024 年 Python 性能优化指南
  - Go 语言微服务实战:gRPC 与 Kubernetes
  - Python 爬虫开发:Scrapy 与 Playwright 对比

查询 3:「2023 年前端相关的文章」

javascript 复制代码
解析结果:
  语义查询:前端
  过滤条件:{'year': 2023}
元数据过滤后剩余 4 篇:
  - JavaScript 异步编程:Promise 与 async/await 深度解析
  - 2023 年前端框架对比:React vs Vue vs Angular
  - Rust 系统编程:内存安全与零成本抽象
  - 数据库选型指南:PostgreSQL vs MySQL vs MongoDB

分析

  • ✅ 精准响应用户的明确条件(时间、类别、标签)
  • ✅ 先过滤再检索,大幅减少向量比较的范围
  • ⚠️ 解析器质量决定效果(规则解析 vs LLM 解析)

生产环境用 LLM 解析

python 复制代码
from langchain.retrievers.self_query.base import SelfQueryRetriever

self_query_retriever = SelfQueryRetriever.from_llm(
    llm=llm,
    vectorstore=vectorstore,
    document_contents="技术博客文章",
    metadata_field_info=[...],  # 定义元数据字段
)
results = self_query_retriever.invoke("2024 年关于 Python 的文章")

注:LangChain 1.2.16 的社区包中 SelfQueryRetriever 的模块位置可能有变化,请根据实际安装的版本调整导入路径。


四种策略对比总结

策略 适用场景 核心参数 注意点
相似度检索 通用场景,追求最高相关性 k 结果可能重复
MMR 需要多角度回答 lambda_mult, fetch_k 参数需调优
阈值过滤 质量要求高,宁可少不可错 score_threshold 需先实验确定阈值
Self-Query 查询含时间/类别等明确条件 解析器质量 可用规则或 LLM 解析

组合使用建议

真正的生产环境中,组合使用效果更佳:

css 复制代码
用户查询
    ↓
Self-Query 解析 → 元数据过滤(缩小范围)
    ↓
向量检索 → MMR(保证多样性)
    ↓
阈值过滤(剔除低质量)
    ↓
Top-K 结果 → LLM 生成回答
python 复制代码
# 组合示例
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 5,
        "lambda_mult": 0.5,
        "fetch_k": 50,
        "filter": {"year": 2024, "category": "后端开发"}  # Self-Query 解析出的条件
    }
)

完整代码

本文的完整代码已开源:

github.com/chendongqi/...

核心文件:

  • retrieval_strategies.py --- 四种检索策略的完整对比实验
  • data/sample_articles.json --- 10 篇测试文章数据

小结

本文通过代码实验对比了 4 种检索策略:

  1. 相似度检索 --- 简单直接,适合通用场景
  2. MMR --- 用 λ 参数平衡相关性和多样性,解决结果重复问题
  3. 阈值过滤 --- 通过距离分布设定阈值,剔除低质量结果
  4. Self-Query --- 把自然语言解析成结构化过滤条件,精准响应限定查询

关键认知:没有最好的检索策略,只有最适合当前查询的策略。组合使用 Self-Query + MMR + 阈值过滤,才能构建一个既精准又全面的检索系统。


参考资料

相关推荐
薛定猫AI1 小时前
【深度解析】DeepSeek V4 + Cloud Code:构建低成本、高吞吐的混合 AI 编码工作流
人工智能·log4j
数智工坊1 小时前
【Transfer CLIP论文阅读】跨模态大模型赋能!CLIP迁移学习实现超强泛化图像去噪
论文阅读·人工智能·迁移学习
Resistance丶未来1 小时前
TradingAgents 多智能体交易框架深度评测
gpt·大模型·llm·agent·claude·多智能体·trading agents
科研前沿1 小时前
MatrixFusion™+ 云边端协同,百路视频全域融合实现零延时指令闭环
大数据·人工智能·音视频
AI周红伟1 小时前
三年狂赚1.75亿!卖课,才是中国AI最容易赚钱的生意
人工智能·深度学习·学习·机器学习·copilot·openclaw
R御1 小时前
Mem0g用图谱拿到 68.4%,TiMem5 层时间树为什么走另一条路
人工智能
月诸清酒2 小时前
52-260504 AI 科技日报 (四月AI架构密集发布,模型更新潮来临)
人工智能
AI周红伟2 小时前
一天赚5个亿的超级个体天花板李一舟:普通人可借鉴的6点
大数据·人工智能·搜索引擎·copilot·openclaw
MATLAB代码顾问2 小时前
AI Agent智能体开发实战:LangChain自动化工作流
人工智能·langchain·自动化