为什么检索策略很重要?
前面六篇文章,我们搞定了文档分块、Embedding 生成、向量库存储。现在假设用户问了一个问题:“Python 异步编程有什么最佳实践?”
你的向量数据库里有 10 万篇文档。最 naive 的做法是:直接做相似度检索,返回 Top-K 最相似的文档。
但问题来了:
- 问题 1:结果重复。返回的 5 篇文章可能都在讲 asyncio,没有任何一篇讲 aiohttp 或实际踩坑经验。
- 问题 2:低质量混入。第 5 篇文章虽然语义上有点相关,但其实是在讲 Go 的并发模型,对 Python 用户毫无帮助。
- 问题 3:查询含明确条件。用户问的是 “2024 年关于 Python 的文章”,但纯向量检索完全无视了 “2024 年” 这个时间条件。
本文会对比4 种检索策略,帮你解决这些问题。
四种检索策略速览
| 策略 | 核心思想 | 解决的问题 | 适用场景 |
|---|---|---|---|
| 相似度检索 | 按向量相似度排序 | 基础检索 | 通用场景 |
| MMR | 相关性与多样性权衡 | 结果重复 | 需要多角度回答 |
| 阈值过滤 | 只保留高相似度结果 | 低质量混入 | 宁可少不可错 |
| Self-Query | 解析查询生成过滤条件 | 查询含明确条件 | 时间/类别限定 |
实验环境
我们用 10 篇技术博客文章作为测试数据,每篇带有元数据(年份、类别、标签):
可运行的实验源码在文章最后
[{"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 个文档。
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 的核心公式:
MMR = λ × Sim(query, di) - (1-λ) × max(Sim(di, dj))- 第一项:文档 di 和查询的相关性(越大越好)
- 第二项:文档 di 和已选文档的相似度(越小越好,保证多样性)
- λ(lambda_mult):平衡参数,0.5 表示相关性和多样性各占一半
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 参数调优
# 只追求相关性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),不是相似度分数。距离越小表示越相似。
# 先查看距离分布results_with_score=vectorstore.similarity_search_with_score(query,k=10)fordoc,scoreinresults_with_score:print(f"距离={score:.4f}|{doc.metadata['title']}")距离分布实测
距离=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手动阈值过滤
threshold=0.89filtered=[(doc,score)fordoc,scoreinresults_with_scoreifscore<=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)做解析,这里用规则解析器演示核心逻辑:
defparse_query(query:str)->dict:filters={}semantic=query# 提取年份ifmatch:=re.search(r'(20\d{2})\s*年',query):filters["year"]=int(match.group(1))# 提取类别forcatin["后端开发","前端开发","系统编程",...]:ifcatinquery:filters["category"]=cat# 提取标签fortagin["Python","JavaScript","Go",...]:iftaginquery:filters["tags"]=tagreturn{"semantic_query":semantic,"filters":filters}实验结果
查询 1:「2024 年关于 Python 的文章」
解析结果: 语义查询:Python 过滤条件:{'year': 2024, 'tags': 'Python'} 元数据过滤后剩余 4 篇: - Python 异步编程实战:从 asyncio 到 aiohttp - 2024 年 Python 性能优化指南 - Python 机器学习入门:从 NumPy 到 PyTorch - Python 爬虫开发:Scrapy 与 Playwright 对比查询 2:「后端开发类别的文章」
解析结果: 语义查询:后端开发 过滤条件:{'category': '后端开发'} 元数据过滤后剩余 4 篇: - Python 异步编程实战:从 asyncio 到 aiohttp - 2024 年 Python 性能优化指南 - Go 语言微服务实战:gRPC 与 Kubernetes - Python 爬虫开发:Scrapy 与 Playwright 对比查询 3:「2023 年前端相关的文章」
解析结果: 语义查询:前端 过滤条件:{'year': 2023} 元数据过滤后剩余 4 篇: - JavaScript 异步编程:Promise 与 async/await 深度解析 - 2023 年前端框架对比:React vs Vue vs Angular - Rust 系统编程:内存安全与零成本抽象 - 数据库选型指南:PostgreSQL vs MySQL vs MongoDB分析
- ✅ 精准响应用户的明确条件(时间、类别、标签)
- ✅ 先过滤再检索,大幅减少向量比较的范围
- ⚠️ 解析器质量决定效果(规则解析 vs LLM 解析)
生产环境用 LLM 解析
fromlangchain.retrievers.self_query.baseimportSelfQueryRetriever 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 解析 |
组合使用建议
真正的生产环境中,组合使用效果更佳:
用户查询 ↓ Self-Query 解析 → 元数据过滤(缩小范围) ↓ 向量检索 → MMR(保证多样性) ↓ 阈值过滤(剔除低质量) ↓ Top-K 结果 → LLM 生成回答# 组合示例retriever=vectorstore.as_retriever(search_type="mmr",search_kwargs={"k":5,"lambda_mult":0.5,"fetch_k":50,"filter":{"year":2024,"category":"后端开发"}# Self-Query 解析出的条件})完整代码
本文的完整代码已开源:
https://github.com/chendongqi/llm-in-action/tree/main/07-retrieval-strategies
核心文件:
retrieval_strategies.py— 四种检索策略的完整对比实验data/sample_articles.json— 10 篇测试文章数据
小结
本文通过代码实验对比了 4 种检索策略:
- 相似度检索— 简单直接,适合通用场景
- MMR— 用 λ 参数平衡相关性和多样性,解决结果重复问题
- 阈值过滤— 通过距离分布设定阈值,剔除低质量结果
- Self-Query— 把自然语言解析成结构化过滤条件,精准响应限定查询
关键认知:没有最好的检索策略,只有最适合当前查询的策略。组合使用 Self-Query + MMR + 阈值过滤,才能构建一个既精准又全面的检索系统。
参考资料
- LangChain Retrievers 文档
- MMR 算法论文:Maximal Marginal Relevance
- Self-Query Retriever 指南