自查询检索 Self-Querying:结构化过滤与语义的融合
什么是自查询检索?
自查询检索(Self-Querying Retrieval)是一种将结构化过滤与语义搜索相结合的信息检索技术。它的核心思想是让语言模型在理解用户自然语言查询的同时,自动抽取出可用于数据库过滤的结构化条件,从而在不牺牲语义匹配质量的前提下,大幅提升检索的精准度和效率。
传统的关键词搜索引擎只能处理显式的字段匹配,而纯粹的向量语义检索又忽略了元数据(如日期、类别、价格区间)。自查询检索正好弥补了两者的不足:它先用 LLM 解析出用户意图中的“过滤器”(filter),再在该过滤器的范围内进行向量相似度搜索,最终返回既语义相关又严格符合约束条件的结果。
为什么需要自查询检索?
在面对大规模、多类型的数据时,单纯依赖语义向量相似度会遇到几个痛点:
- 冷启动问题:对于全新或冷门内容,向量表示可能不够精准,返回大量无关结果。
- 结构化条件缺失:用户说“去年发布的 Python 异步教程”,纯语义搜索无法理解“去年”这一时间条件。
- 多维度过滤困难:当查询涉及价格、评分、地理位置等多个维度时,手工构造多重过滤器既繁琐又容易出错。
自查询检索让系统自动将自然语言转换为查询过滤条件,用户只需像平常说话一样提问,就能得到精确的范围结果。
自查询检索的工作原理
整个流程可以分为四个阶段:
-
查询理解与过滤提取 用户输入自然语言查询,如:“推荐几本 2023 年出版、评分高于 4.5 的 Python 入门书”。LLM 提取出结构化过滤器:
- 出版年份 = 2023
- 评分 > 4.5
- 标签包含 “Python” 和 “入门”
-
源数据过滤 利用提取出的过滤器对数据库(比如支持元数据过滤的向量数据库)执行
filter操作,大幅缩小候选集。 -
语义检索 在过滤后的数据集上,使用查询的语义向量进行相似度搜索,找到最相关的记录。
-
结果合成与返回 将检索到的文档(可能还包括原始查询的解释)一起返回给用户或下游 LLM 进行回答生成。
自查询检索的典型实现方式
目前主流的自查询检索通常借助 LangChain 等框架快速搭建,但底层原理是通用的。下面以 LangChain 提供的 SelfQueryRetriever 为例,展示核心步骤。
基础环境与数据准备
假设我们有一批视频教程的元数据,结构如下:
from langchain.schema import Document
docs = [
Document(
page_content="本教程深入讲解 Python 异步编程,包含大量实战案例。",
metadata={
"title": "Python 异步实战",
"year": 2023,
"rating": 4.8,
"language": "中文"
}
),
Document(
page_content="面向初学者的 Python 基础入门,从零开始。",
metadata={
"title": "Python 零基础入门",
"year": 2022,
"rating": 4.2,
"language": "中文"
}
),
# 更多文档...
]
这些文档需要存入向量数据库(如 Chroma、Pinecone、Weaviate),并确保向量数据库支持元数据字段的筛选。
构建自查询检索器
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
# 描述元数据字段,帮助 LLM 理解可用的过滤条件
metadata_field_info = [
AttributeInfo(
name="title",
description="视频教程的标题",
type="string",
),
AttributeInfo(
name="year",
description="发布年份",
type="integer",
),
AttributeInfo(
name="rating",
description="用户评分,0-5 之间的浮点数",
type="float",
),
AttributeInfo(
name="language",
description="视频语言",
type="string",
),
]
# 描述文档的主要内容(用于生成语义查询)
document_content_description = "编程教程的详细内容描述"
# 初始化 LLM 和向量存储
llm = OpenAI(temperature=0)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(docs, embeddings)
# 创建自查询检索器
retriever = SelfQueryRetriever.from_llm(
llm,
vectorstore,
document_content_description,
metadata_field_info,
verbose=True
)
执行查询
现在可以直接用自然语言查询,检索器会自动拆解过滤器并执行语义搜索。
# 查询:找 2023 年高分(>4.5)的中文 Python 教程
result = retriever.get_relevant_documents(
"推荐 2023 年发布,评分高于 4.5 的中文 Python 教程"
)
for doc in result:
print(doc.metadata["title"])
在此过程中,LLM 会构造类似以下结构的查询对象:
{
"query": "Python 教程",
"filter": "and(gt('rating', 4.5), eq('year', 2023), eq('language', '中文'))"
}
然后向量数据库先应用过滤器,再在过滤后的集合中执行 query 的语义搜索。
设计高效自查询检索的关键点
精心定义元数据字段描述
metadata_field_info 的质量直接决定 LLM 能否准确生成过滤器。每个字段的 description 要清晰、具体,避免歧义。例如,不要只写 "year",而要写明 "发布年份"。
简化过滤逻辑
尽量避免要求 LLM 生成极其复杂的嵌套逻辑(如多层 AND/OR 组合)。如果业务需要,可以后置用程序对原始过滤器再进行组合,而不是完全依赖 LLM。
选择合适的向量数据库
不是所有向量库都支持原生结构化过滤。常用的支持方案包括:
- Chroma:支持基本的
where过滤。 - Weaviate:GraphQL 接口,支持丰富的操作符(
GreaterThan、Like等)。 - Pinecone:通过元数据过滤实现。
- Milvus:具备标量过滤能力。
选择时请确认其过滤器表述语言与自查询检索框架的兼容性。
异常处理与兜底
当 LLM 无法提取有效过滤器时(例如查询模糊或超出字段范围),自查询检索器可能会降级为纯语义搜索。可以在代码中加入兜底逻辑,保证系统在任何情况下都能返回结果,即使只是纯语义相关的结果。
常见应用场景
- 电商搜索:用户说“500 元以内的蓝牙耳机,降噪好”,自查询检索会过滤价格 < 500,再语义匹配“降噪好”。
- 知识库问答:用户问“去年第三季度上线的功能有哪些?”,系统筛选时间范围后再搜索文档。
- 内容推荐:“评分 4 星以上、时长 10-20 分钟的健身视频”,多条件过滤 + 语义理解。
- 招聘系统:自动解析“3 年以上经验,本科以上,Python 后端”等条件。
自查询检索 vs. 其他检索增强技术
| 方法 | 优点 | 缺点 |
|---|---|---|
| 关键词检索 (BM25) | 速度快,对精确词匹配好 | 不理解语义,无法处理同义词 |
| 纯语义检索 (向量) | 理解语义,容错性强 | 忽略结构化约束,冷门内容召回差 |
| 手工过滤 + 语义搜索 | 精准可控 | 需要用户或开发者构造复杂查询 |
| 自查询检索 | 全自动,平衡语义与结构,用户体验好 | 依赖 LLM 解析准确性,复杂嵌套逻辑可能偏差 |
自查询检索特别适合用户无需了解底层数据结构,但希望得到精确结果的场景。
调试与优化建议
- 打印中间查询结构:在开发阶段开启
verbose=True,观察 LLM 生成的过滤条件是否符合预期。 - 使用小样本提示(Few-shot Examples):如果 LLM 总是误解某些字段,可以在
SelfQueryRetriever中提供示例来引导。 - 限制元数据字段数量:只暴露真正需要过滤的字段,避免 LLM 混淆无关信息。
- 后验证过滤器:收到过滤器后,可用正则或解析器检查是否合法,过滤掉无效条件。
总结
自查询检索通过将自然语言理解与数据库过滤深度结合,为现代信息检索提供了一种优雅且强大的范式。它让“像对话一样搜索”成为可能,用户在表达复杂需求时不再需要学习专用查询语法。对于希望构建智能、精准且对用户友好的搜索体验的开发者来说,掌握自查询检索是重要的一步。
现在,你可以在自己的项目中尝试引入自查询检索,用更少的代码实现更聪明的搜索。