为什么检索策略很重要?
前面六篇文章,我们搞定了文档分块、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 解析出的条件
}
)
完整代码
本文的完整代码已开源:
核心文件:
retrieval_strategies.py--- 四种检索策略的完整对比实验data/sample_articles.json--- 10 篇测试文章数据
小结
本文通过代码实验对比了 4 种检索策略:
- 相似度检索 --- 简单直接,适合通用场景
- MMR --- 用 λ 参数平衡相关性和多样性,解决结果重复问题
- 阈值过滤 --- 通过距离分布设定阈值,剔除低质量结果
- Self-Query --- 把自然语言解析成结构化过滤条件,精准响应限定查询
关键认知:没有最好的检索策略,只有最适合当前查询的策略。组合使用 Self-Query + MMR + 阈值过滤,才能构建一个既精准又全面的检索系统。