# VectorStore 深入:相似度检索、得分与过滤

VectorStore 是 RAG 链路里连接 embedding 与检索的核心组件。Loader 和 Splitter 解决“文档怎么进来、怎么切”,Embedding 解决“文本如何映射到向量空间”,VectorStore 则负责“向量如何存、如何查、如何过滤、如何返回”。

带阈值的相似性搜索

最大边际相关性搜索

# VectorStore 抽象

LangChain 的 VectorStore 是一个统一接口。不同向量数据库的存储结构、索引方式、过滤语法和部署形态不一样,但在应用层一般都需要这些能力:

能力 作用
add_documents 把 Document 写入向量库
similarity_search 按相似度返回文档
similarity_search_with_relevance_scores 返回文档和相关性得分
max_marginal_relevance_search 在相关性和多样性之间做平衡
as_retriever 把向量库包装成 Retriever,接入 Runnable 链路

相同的 Loader、Splitter、Embedding,换不同的检索策略,最终返回给 LLM 的上下文可能完全不同。

# 为什么需要得分阈值

普通相似性搜索通常会返回 top_k。问题是:即使 query 与知识库毫不相关,它也会从库里找出“相对最像”的几条。这些结果不是正确证据,只是库里最接近的噪声。

similarity_search_with_relevance_scores 可以返回分数,并配合 score_threshold 过滤弱相关内容。当分数不足时宁可少返回,也不要把低质量上下文交给 LLM。

import dotenv
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

dotenv.load_dotenv()

embedding = OpenAIEmbeddings(model="text-embedding-3-small")

documents = [
    Document(page_content="笨笨是一只很喜欢睡觉的猫咪", metadata={"page": 1}),
    Document(page_content="我喜欢在夜晚听音乐,这让我感到放松。", metadata={"page": 2}),
    Document(page_content="猫咪在窗台上打盹,看起来非常可爱。", metadata={"page": 3}),
    Document(page_content="掌握新技能是每个人都应该追求的目标。", metadata={"page": 4}),
    Document(page_content="我最喜欢的食物是意大利面,尤其是番茄酱的那种。", metadata={"page": 5}),
    Document(page_content="昨晚我做了一个奇怪的梦,梦见自己在太空飞行。", metadata={"page": 6}),
    Document(page_content="我的手机突然关机了,让我有些焦虑。", metadata={"page": 7}),
    Document(page_content="阅读是我每天都会做的事情,我觉得很充实。", metadata={"page": 8}),
    Document(page_content="他们一起计划了一次周末的野餐,希望天气能好。", metadata={"page": 9}),
    Document(page_content="我的狗喜欢追逐球,看起来非常开心。", metadata={"page": 10}),
]

db = FAISS.from_documents(documents, embedding)

results = db.similarity_search_with_relevance_scores(
    "我养了一只猫,叫笨笨",
    score_threshold=0.4,
)
print(results)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这个例子里,query 同时包含“猫”和“笨笨”。如果不设阈值,可能返回所有相对接近的文本;加入阈值后,更可能留下真正包含关键信息的文档。

# as_retriever 的搜索类型

as_retriever() 会把 VectorStore 包装成 Retriever。常见配置如下:

retriever = db.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 10, "score_threshold": 0.5},
)
1
2
3
4

search_type 常见有三种。

similarity:只做相似性搜索,返回 top_k。适合多数常规问答,链路简单,延迟低。

similarity_score_threshold:在相似性搜索基础上增加分数阈值。适合要求答案必须有依据的场景。

mmr:最大边际相关性搜索,会从候选集中选择相关但不重复的文档。适合知识点分散、需要覆盖多个角度的查询。

search_kwargs 常见参数包括 k、filter、score_threshold、fetch_k、lambda_mult。k 是最终返回数量,fetch_k 是 MMR 的初始候选数量,lambda_mult 控制相关性和多样性的权重。

# Weaviate 检索器示例

import dotenv
import weaviate
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_weaviate import WeaviateVectorStore
from weaviate.auth import AuthApiKey

dotenv.load_dotenv()

loader = UnstructuredMarkdownLoader("./项目API文档.md")
text_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "。|!|?", r"\.\s|\!\s|\?\s", ";|;\\s", ",|,\\s", " ", ""],
    is_separator_regex=True,
    chunk_size=500,
    chunk_overlap=50,
    add_start_index=True,
)

documents = loader.load()
chunks = text_splitter.split_documents(documents)

db = WeaviateVectorStore(
    client=weaviate.connect_to_wcs(
        cluster_url="https://example.weaviate.cloud",
        auth_credentials=AuthApiKey("WEAVIATE_API_KEY"),
    ),
    index_name="DatasetDemo",
    text_key="text",
    embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)

db.add_documents(chunks)

retriever = db.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 10, "score_threshold": 0.5},
)

documents = retriever.invoke("关于配置接口的信息有哪些")
print(list(document.page_content[:50] for document in documents))
print(len(documents))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

这里的关键不是 Weaviate 本身,而是完整链路:加载文档、切分、写入向量库、转换成 Retriever、通过 invoke() 查询。

# MMR 检索

最大边际相关性搜索适合“既要相关,又不要全都重复”的场景。它通常先取 fetch_k 个候选,再从中选择 k 个结果。选择时会同时考虑 query 与文档的相关性,以及候选文档之间的相似度。

search_documents = db.max_marginal_relevance_search(
    "关于应用配置的接口有哪些?",
    k=4,
    fetch_k=20,
    lambda_mult=0.5,
)

for document in search_documents:
    print(document.page_content[:100])
    print("===========")
1
2
3
4
5
6
7
8
9
10

lambda_mult 越接近 1,越偏向相似性;越接近 0,越偏向多样性。多数问答场景先用相似性搜索即可;当召回结果重复、多个 chunk 都来自同一小段内容时,再考虑 MMR。

# 如何保留分数

Retriever 默认返回 Document 列表,不一定保留得分。如果业务要展示置信度、做二次排序或阈值告警,可以直接调用 similarity_search_with_relevance_scores,把分数写入 metadata,再继续下游链路。

from langchain_core.runnables import RunnableLambda


def search_with_scores(query: str):
    pairs = db.similarity_search_with_relevance_scores(query, score_threshold=0.5)
    documents = []
    for document, score in pairs:
        document.metadata["score"] = score
        documents.append(document)
    return documents

retriever_with_scores = RunnableLambda(search_with_scores)
documents = retriever_with_scores.invoke("配置接口有哪些?")
1
2
3
4
5
6
7
8
9
10
11
12
13

# 最新版 LangChain 用法提示

当前 VectorStore 抽象位于 langchain_core.vectorstores,FAISS 集成来自 langchain_community.vectorstores,OpenAI Embedding 建议从 langchain_openai 导入,Weaviate 建议使用 langchain_weaviate.WeaviateVectorStore。

新代码优先使用 invoke() 调 Retriever。不要依赖旧式 get_relevant_documents() 作为主调用入口。

as_retriever() 仍然是把向量库接入 LCEL/Runnable 链路的常用方式,但具体 search_kwargs 是否支持,要看对应向量库集成。

# 拓展

向量库检索通常还会结合 metadata filter。比如知识库多租户场景必须过滤 tenant_id、dataset_id;文档多版本场景要过滤版本号;权限场景要过滤用户可见范围。

还可以做混合检索:向量召回负责语义相似,关键词或 BM25 召回负责精确词命中,然后合并去重、rerank。API 文档、错误码、字段名这类内容,纯向量检索可能不如混合检索稳定。

# 常见问题

为什么 query 不相关时仍然能返回结果?

因为 top_k 返回的是相对最相似,而不是绝对相关。需要得分阈值、拒答策略或结果质量检查。

score_threshold 应该设多少?

没有固定值。要基于语料、embedding 模型、切分方式和业务容错做评测。可以从 0.4 或 0.5 开始,观察召回率和误召回。

MMR 会不会降低准确率?

可能。MMR 为了多样性会牺牲部分相似度。它适合结果重复严重或问题需要多角度证据的场景。

# 面试题

VectorStore 和 Retriever 的关系是什么?

VectorStore 负责向量存储和检索能力,Retriever 是面向查询链路的抽象。as_retriever() 可以把 VectorStore 包装成 Retriever。

为什么要使用带阈值的相似性搜索?

为了避免不相关 query 也返回低质量 top_k,从而降低 LLM 基于错误上下文生成答案的风险。

MMR 的核心思想是什么?

在 query 相关性和候选文档之间的差异性之间做权衡,尽量返回相关且不重复的上下文。

# 生产问题排查

问题 常见原因 处理方式
不相关问题仍有答案 低相关 chunk 被返回 增加阈值、拒答策略和引用校验
召回全来自同一段 chunk 重叠过大或相似性搜索重复 使用 MMR,降低 overlap,去重
重要结果被过滤掉 score_threshold 太高或 embedding 不合适 降低阈值,重评 embedding,检查切分
metadata filter 不生效 字段名、类型或索引配置错误 写入后抽样查询,增加过滤单测
成本和延迟升高 k、fetch_k 过大或多路召回无缓存 控制候选数量,缓存检索结果,增加超时策略