个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain SelfQueryRetriever:让模型自己写过滤条件](#LangChain SelfQueryRetriever:让模型自己写过滤条件)
-
- 一、问题长什么样
- 二、上手
- [三、metadata 描述怎么写(最重要)](#三、metadata 描述怎么写(最重要))
-
- [3.1 description 的三个要点](#3.1 description 的三个要点)
- [3.2 字段名必须和 metadata 一致](#3.2 字段名必须和 metadata 一致)
- 四、支持的查询类型
- 五、看看它生成了什么(调试必备)
- 六、向量库的支持程度
- 七、成本与延迟
- 八、什么时候用
- 九、常见坑
- 十、小结
LangChain SelfQueryRetriever:让模型自己写过滤条件
用户问"2024 年技术部发布的制度有哪些?"------
这句话里既有语义(制度),又有结构化条件(year=2024, dept=技术部)。
纯向量检索会把"2024""技术部"也当成语义去匹配,效果很差。
SelfQueryRetriever 让模型先把这句话拆成"语义查询 + 过滤条件",再分别执行。
一、问题长什么样
先看纯向量检索的失败案例:
python
vectorstore.similarity_search("2024 年技术部发布的制度", k=4)
问题在于,"2024""技术部"这些词在向量空间里和"制度""流程"很近 ,
结果召回了:
- 2022 年的制度(因为都是"制度");
- 市场部的文件(因为都提到"部门");
- 2024 年的预算表(因为都有"2024")。
用户要的"且"关系,向量检索做不到。
正确做法是把它拆成:
语义查询(交给向量):制度 流程规范
过滤条件(交给数据库):year == 2024 AND dept == "技术部"
SelfQueryRetriever 就是自动做这个拆分的。
二、上手
python
from langchain.chains.query_constructor.base import AttributeInfo
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain_openai import ChatOpenAI
# 1. 声明 metadata 字段(关键)
metadata_field_info = [
AttributeInfo(
name="year",
description="文档发布年份,整数,如 2022 / 2023 / 2024",
type="integer",
),
AttributeInfo(
name="dept",
description="发布部门,如 技术部 / 市场部 / 人力资源部",
type="string",
),
AttributeInfo(
name="doc_type",
description="文档类型:制度 / 通知 / 手册",
type="string",
),
]
document_content_description = "公司内部制度与通知文档"
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = SelfQueryRetriever.from_llm(
llm,
vectorstore,
document_content_description,
metadata_field_info,
enable_limit=True, # 支持"前 N 条"这类表达
)
results = retriever.invoke("2024 年技术部发布的制度有哪些?")
模型内部会生成类似这样的结构化查询:
query="制度 流程规范"
filter=AND(eq("year", 2024), eq("dept", "技术部"))
limit=None
然后向量库先按 filter 过滤,再在结果里做语义检索。
三、metadata 描述怎么写(最重要)
这一步决定了 SelfQuery 能不能用对。
AttributeInfo 的三个字段都要认真写:
python
AttributeInfo(
name="year", # 必须和 metadata 里的 key 完全一致
description="...", # 给模型看的说明
type="integer", # string / integer / float / boolean
)
3.1 description 的三个要点
① 说清楚取值范围
python
# ❌ 太模糊
description="部门"
# ✅ 枚举出来
description="发布部门。可选值:技术部、市场部、人力资源部、财务部"
LLM 不知道你的部门有哪些 ,你不告诉它,它只能猜,
猜错就过滤出空结果。
② 说清楚类型和格式
python
AttributeInfo(name="year", description="发布年份", type="integer")
# ✅ 明确是整数,模型才会生成 eq("year", 2024) 而不是 eq("year", "2024年")
③ 说清楚这个字段的用途
python
AttributeInfo(
name="is_active",
description="该制度是否仍然有效。true=现行有效,false=已废止。"
"当用户问'现行''有效''最新'时应过滤 true",
type="boolean",
)
把"用户说什么话时该用这个字段"写进描述,模型判断会更准。
3.2 字段名必须和 metadata 一致
python
# 入库时
Document(page_content="...", metadata={"publish_year": 2024})
# 声明时
AttributeInfo(name="publish_year", ...) # ✅ 必须叫 publish_year
AttributeInfo(name="year", ...) # ❌ 对不上,过滤失效
这是最常见的低级错误,而且不会报错,只是过滤没生效,很难发现。
四、支持的查询类型
SelfQuery 能生成的过滤器不止等于:
| 类型 | 例子 |
|---|---|
| 等于 | eq("dept", "技术部") |
| 不等于 | ne("dept", "技术部") |
| 大于/小于 | gt("year", 2023)、lt("amount", 1000) |
| 大于等于/小于等于 | gte("year", 2023) |
| 介于 | gte("year", 2022) AND lte("year", 2024) |
| 包含 | in(["制度", "通知"]) |
| 且/或 | AND(...)、OR(...) |
对应的自然语言:
| 用户说 | 生成的过滤 |
|---|---|
| "2024 年的" | eq("year", 2024) |
| "2023 年之后的" | gt("year", 2023) |
| "2022 到 2024 年的" | gte("year", 2022) AND lte("year", 2024) |
| "技术部和市场部的" | in(["技术部", "市场部"]) |
| "不是技术部的" | ne("dept", "技术部") |
五、看看它生成了什么(调试必备)
python
from langchain.chains.query_constructor.base import (
StructuredQueryOutputParser, get_query_constructor_prompt,
)
prompt = get_query_constructor_prompt(
document_content_description,
metadata_field_info,
)
output_parser = StructuredQueryOutputParser.from_components()
query_constructor = prompt | llm | output_parser
result = query_constructor.invoke({"query": "2024 年技术部的制度"})
print("语义:", result.query)
print("过滤:", result.filter)
print("限制:", result.limit)
调试 SelfQuery 一定要先看这一步的输出 ------
如果过滤条件生成错了,后面再怎么调都没用,
问题一定出在 AttributeInfo 的描述上。
六、向量库的支持程度
不是所有向量库都支持 SelfQuery。
它需要一个 StructuredQuery 翻译器。
| 向量库 | 支持 | 备注 |
|---|---|---|
| Chroma | ✅ | 开箱可用 |
| PGVector | ✅ | 生产推荐 |
| Elasticsearch | ✅ | 过滤器能力强 |
| Pinecone / Weaviate / Qdrant | ✅ | 各有 translator |
| FAISS | ❌ | 不支持,原生无 metadata 过滤 |
如果用 FAISS,SelfQuery 用不了 ------这是很多人踩的坑。
要么换 Chroma/PGVector,要么自己在检索后手动过滤:
python
def manual_filter(query: str, filters: dict, k=4):
docs = vectorstore.similarity_search(query, k=k * 10)
out = []
for d in docs:
if all(d.metadata.get(key) == val for key, val in filters.items()):
out.append(d)
if len(out) >= k:
break
return out
七、成本与延迟
SelfQuery 每次检索都要多一次 LLM 调用(生成结构化查询)。
| 方案 | 额外成本 | 额外延迟 |
|---|---|---|
| 普通向量检索 | --- | --- |
| SelfQuery | 1 次 LLM 调用 | +0.5~1.5 秒 |
优化办法:缓存结构化查询。
python
from functools import lru_cache
@lru_cache(maxsize=200)
def parse_query(question: str):
return query_constructor.invoke({"query": question})
相似问题会命中缓存,省掉这次调用。
八、什么时候用
| 场景 | 用不用 |
|---|---|
| 文档有明确的 metadata(年份、部门、类型、状态) | 强烈推荐 |
| 用户提问经常带限定词("2024 年的""技术部的") | 强烈推荐 |
| metadata 只有 source 一个字段 | 没必要 |
| 用户提问都很模糊 | 收益有限 |
| 用 FAISS | 用不了 |
前提条件:你的 metadata 得有价值。
如果入库时只存了 source,SelfQuery 无从发挥------
回到前面讲过的那句:metadata 要在加载阶段就埋好。
九、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 过滤没生效 | 字段名和 metadata 不一致 | 逐个核对 |
| 过滤出空结果 | 描述里没说取值范围,模型猜错 | 枚举可选值 |
| 年份比较出错 | type 写成 string | 改 type="integer" |
| FAISS 报错 | 不支持结构化查询 | 换 Chroma/PGVector |
| 每次都很慢 | 多了 LLM 调用 | 缓存结构化查询 |
| 模型不用过滤 | 描述太简单 | 说明"用户说什么时该用它" |
十、小结
- SelfQuery 解决"语义 + 结构化条件 "的混合查询,
纯向量检索做不好"2024 年技术部"这类"且"关系; - 核心是
AttributeInfo:字段名要和 metadata 完全一致,
描述要枚举取值范围、说清类型、说明什么时候用; - 支持 eq / ne / gt / lt / gte / lte / in / AND / OR;
- 调试时先打印生成的结构化查询,问题多半在描述上;
- ⚠️ FAISS 不支持 (无原生 metadata 过滤),
要用就换 Chroma 或 PGVector; - 每次检索多一次 LLM 调用,用缓存省掉重复解析。
下一篇讲时间加权检索------让最近的记忆更容易被想起。