前言
上一期我们聊完了文本数据的加载、切分,以及如何把这些碎片向量化并存入向量数据库。做完这些,前期的数据处理工作就算告一段落了——后续哪怕需要载入更多数据,重复相同的流程就行。在上节课最后,我们简单试了试用 similarity_search 在向量数据库里检索相关数据,这节课就来深入展开这部分内容。

说到检索(Retrieval),除了最基础的 similarity_search(通过计算查询向量和所有候选文档向量的相似度来匹配),其实还有不少其他玩法,比如 Maximum Marginal Relevance(MMR,最大边际相关性)、利用 Metadata(元数据)进行过滤,以及 LLM Aided Retrieval(大模型辅助检索)等。下面就来逐一拆解。
Similarity Search 方法详解
Similarity Search(相似度搜索) 是向量数据库里最基础的检索方式。它的逻辑很直接:把用户的查询转成一个向量,然后跟数据库里所有文档向量算相似度,挑出最接近的那些。常用的相似度度量包括余弦相似度、欧氏距离等,这些方法能有效衡量两个向量在高维空间里的“距离”。
打个比方,用户搜“深度学习的基本概念”,系统就会把这句话向量化,然后跟库里的向量比较,返回最匹配的文档片段。这种方法的优点就是简单直观,适合快速找到高度相关的结果。
不过,单纯依赖相似度搜索有一个明显的短板:结果容易缺乏多样性。因为它只盯着“跟查询像不像”,完全不管结果之间是不是重复或者相似过头。在一些需要覆盖多个不同方面的场景下,光靠相似度可能不够用——这时候就需要 Maximum Marginal Relevance (MMR) 这类方法登场,在相关性和多样性之间找到平衡。
Maximum Marginal Relevance 方法详解
在很多信息检索任务里,“最相似”并不总是最优答案。尤其是当用户的需求涉及多个侧面时,只挑最相似的那几个结果,很容易导致信息重复。为了解决这个问题,Maximum Marginal Relevance (MMR) 应运而生。它的核心思路是:在保证相关性的同时,尽量让返回的结果之间差异更大,从而提供更全面的信息。
MMR 方法的原理与动机
举个例子,用户问“Tell me about all-white mushrooms with large fruiting bodies”。如果只按相似度搜,很可能返回几个高度相似甚至重复的答案——虽然每个都跟查询相关,但并没有覆盖不同角度。MMR 要做的就是:不让结果扎堆,而是让它们互相补充。
MMR 的思想本质上是“既要又要”:既要跟用户的问题高度相关,又要让结果之间有足够多的差异。这样一来,用户既能得到直接回答,又能看到不同维度的信息,比如具体细节和大局概述。
MMR 算法的工作流程
MMR 的具体流程可以通过下面的图来理解,这里逐步拆解一下主要步骤:
- 查询向量存储(Query the Vector Store):先把用户的查询转成向量,去向量库里匹配。每个文档片段都对应一个向量(通常由嵌入模型生成)。
- 选择最相似的
fetch_k个响应:从库里挑出与查询最相似的前k个结果(基于余弦相似度等)。这一步保证了基础的相关性。 - 在这些候选里选
k个最具多样性的结果:接着,MMR 会从刚选出的最相似结果中,再挑出k个彼此差异最大的。计算每对候选结果之间的相似度,偏好那些“不太像”的,从而增加信息覆盖面。
MMR 通过一个权重参数 λ 来调节相关性与多样性之间的比重。当 λ 接近 1 时,更偏向相似性;接近 0 时,更偏向多样性。实际应用时可以根据场景灵活调整。
MMR 的实际应用
MMR 在信息检索和推荐系统里用得很广,特别适合那些需要广泛覆盖信息的场景。比如搜索某个主题时,MMR 能同时返回具体细节和整体概述,帮助用户对这个主题形成更完整的理解。其实本质上它还是基于 similarity_search 的,只不过像大语言模型调整 temperature 一样,通过增加随机性来让结果更丰富——毕竟人类交流时也从来不会只有“最优解”这一种答案。
利用元数据提高检索精确度:基于自查询检索器的方法
在检索过程中,如何提高精度,尤其是在面对大量复杂文档时,是个关键问题。前面的 similarity_search 和 Maximum Marginal Relevance 已经能帮我们找到相似的文档,但有时仅靠相似度还不够精确,特别是当用户的查询带有明确的限定条件时。这时候元数据(Metadata)就派上用场了。
元数据是与每个文本片段绑定的上下文信息,比如来源文档、页码、作者、时间戳等。利用这些信息,可以精准限定检索范围。举个例子:当用户问“第三讲中提到的回归分析内容是什么”,我们当然不希望返回其他讲次的结果。很多向量数据库支持在检索时直接对元数据进行过滤,从而大幅提升准确性。
具体来说,对于上面那个问题,可以在 similarity_search 时加入一个过滤条件,比如指定 source 为 "docs/cs229_lectures/MachineLearning-Lecture03.pdf",这样就能确保只从第三讲的文档里找,不会被其他讲次干扰。
实战演练
接下来进入动手环节,看看如何在代码里实现这些检索方法。环境版本如下:
langchain 0.3.0
langchain-community 0.3.0
pypdf 5.0.0
openai 1.47.0
beautifulsoup4 4.12.3
chromadb 0.5.15
首先,需要运行上一期(大模型检索增强生成(三):向量数据库及嵌入)的代码,在指定位置生成一个 Chroma 数据库。看到文件生成即表示数据库创建成功。
新建一个 retrieval.py 文件,写入以下代码来验证数据库是否正常。如果能打印出文档数量,就说明一切正常。
from langchain_community.embeddings import BaichuanTextEmbeddings
from langchain_chroma import Chroma
persist_directory = r'D:\langchain'
embeddings = BaichuanTextEmbeddings(baichuan_api_key="sk-xxx") # 填入上期内容的百川 API Key
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
print(vectordb._collection.count())
Similarity Search(相似度搜索)
先试试最基础的方法——Similarity Search。在刚才的代码基础上加上以下内容,向向量数据库提问:“大语言模型是怎么推理的?”
question = "大语言模型是怎么推理的?"
docs_ss = vectordb.similarity_search(question, k=2)
print(docs_ss[0].page_content[:100])
print(docs_ss[1].page_content[:100])
输出的结果与推理确实相关,第一部分是“现代循环神经网络”的目录结构,第二部分是“注意力机制”的目录结构。
# 第一部分
9. 现代循环神经网络
9.1. 门控循环单元(GRU)
9.2. 长短期记忆网络(LSTM)
9.3. 深度循环神经网络
9.4. 双向循环神经网络
9.5. 机器翻译与数据集
9.6. 编码器-解
# 第二部分
10. 注意力机制
10.1. 注意力提示
10.2. 注意力汇聚:Nadaraya-Watson 核回归
10.3. 注意力评分函数
10.4. Bahdanau 注意力
10.5. 多头注意力
Maximum Marginal Relevance (MMR)
接下来换成 MMR 试试效果:
question = "大语言模型是怎么推理的?"
docs_mmr = vectordb.max_marginal_relevance_search(question, k=2)
print(docs_mmr[0].page_content[:100])
print(docs_mmr[1].page_content[:100])
输出结果有点意思。终端先弹了一个警告,说默认请求 20 条结果,但数据库里总共才 18 条。而且虽然第一部分内容跟之前一样,第二部分却变成了一堆大学名称,跟推理完全无关——从这里能看出,加了随机性后,效果未必会更好。
# 警告
Number of requested results 20 is greater than number of elements in index 18, updating n_results = 18
# 第一部分
9. 现代循环神经网络
9.1. 门控循环单元(GRU)
9.2. 长短期记忆网络(LSTM)
9.3. 深度循环神经网络
9.4. 双向循环神经网络
9.5. 机器翻译与数据集
9.6. 编码器-解
# 第二部分
Vardhman Maha veer Open University
Vietnamese-German University
Vignana Jyothi Institute
要消除这个警告,需要加一个参数 fetch_k=8。这个参数告诉 MMR 先从数据库里取前 8 个最相似的文档作为候选,然后再从这 8 个里挑出 k=2 个多样性最好的结果。
docs_mmr = vectordb.max_marginal_relevance_search(question, k=2, fetch_k=8)
print(docs_mmr[0].page_content[:100])
print(docs_mmr[1].page_content[:100])
再次运行,警告消失了,但结果跟之前差不多。
9. 现代循环神经网络
9.1. 门控循环单元(GRU)
9.2. 长短期记忆网络(LSTM)
9.3. 深度循环神经网络
9.4. 双向循环神经网络
9.5. 机器翻译与数据集
9.6. 编码器-解
Vardhman Maha veer Open University
Vietnamese-German University
Vignana Jyothi Institute
通过调整 fetch_k 的值,可以控制随机性的强弱。试试 fetch_k=2:
docs_mmr = vectordb.max_marginal_relevance_search(question, k=2, fetch_k=2)
print(docs_mmr[0].page_content[:100])
print(docs_mmr[1].page_content[:100])
结果跟 Similarity Search 完全一样——因为候选池只有 2 个,MMR 没有多样化的空间,只能照单全收。
9. 现代循环神经网络
9.1. 门控循环单元(GRU)
9.2. 长短期记忆网络(LSTM)
9.3. 深度循环神经网络
9.4. 双向循环神经网络
9.5. 机器翻译与数据集
9.6. 编码器-解
10. 注意力机制
10.1. 注意力提示
10.2. 注意力汇聚:Nadaraya-Watson 核回归
10.3. 注意力评分函数
10.4. Bahdanau 注意力
10.5. 多头注意力
再试试 fetch_k=3:
docs_mmr = vectordb.max_marginal_relevance_search(question, k=2, fetch_k=3)
print(docs_mmr[0].page_content[:100])
print(docs_mmr[1].page_content[:100])
结果有了变化:第二部分变成了书籍的目录(包括前言、安装、符号等)。因为候选池里出现了一个不同角度的文档,为了满足多样性要求,MMR 强行把这块内容塞进了结果。
9. 现代循环神经网络
9.1. 门控循环单元(GRU)
9.2. 长短期记忆网络(LSTM)
9.3. 深度循环神经网络
9.4. 双向循环神经网络
9.5. 机器翻译与数据集
9.6. 编码器-解
参考文献
Table Of Contents
前言
安装
符号
1. 引言
2. 预备知识
2.1. 数据操作
2.2. 数据预处理
2.3. 线性代数
2.4.
从这个实验可以看出,在 MMR 里调整 fetch_k 等参数可以控制输出的随机性和多样性。用得好,能显著提升检索效果。
元数据(Metadata)
更进一步,我们可以利用元数据做更精确的搜索。先看看数据库里有哪些元数据:
all_docs = vectordb.similarity_search("", k=18) # 用空查询获取所有文档
for i, doc in enumerate(all_docs):
print(f"Document {i+1} metadata: {doc.metadata}")
输出显示,所有文档的元数据都一样,因为都来自同一个网页链接:
"metadata": {
"source": "https://zh.d2l.ai/",
"language": "en",
"title": "《动手学深度学习》 — 动手学深度学习 2.0.0 documentation",
}
为了演示元数据过滤,可以往数据库里添加另一个来源的文档。在上一期代码的基础上加上以下内容(注意运行前最好删掉原有数据库,否则会重复添加):
new_loader = WebBaseLoader("https://www.deeplearning.ai/")
new_docs = new_loader.load()
new_splits = text_splitter.split_documents(new_docs)
vectordb.add_documents(new_splits)
print(f"更新后的文档数量: {vectordb._collection.count()}")
运行后,文档数量从 18 变成了 20,说明新网页提供了两个片段。再次查看元数据,会发现多了两条来自 https://www.deeplearning.ai/ 的记录。
Document 8 metadata: {'description': 'DeepLearning.AI | Andrew Ng | Join over 7 million people...', 'source': 'https://www.deeplearning.ai/', ...}
Document 18 metadata: {'description': '...', 'source': 'https://www.deeplearning.ai/', ...}
现在,就可以通过指定 source 来过滤检索了:
question = "大语言模型是怎么推理的?"
docs_meta = vectordb.similarity_search(question, k=1, filter={"source": "https://www.deeplearning.ai/"})
print(docs_meta[0].page_content[:100])
结果只返回了 DeepLearning.AI 网页里的内容,与其他来源完全隔离。
prevnextIn Collaboration WithprevnextThe largest weekly AI newsletterWhat matters in AI right nowOct
本门课完整代码展示
main.py —— 前期准备
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import BaichuanTextEmbeddings
from langchain_chroma import Chroma
loader = WebBaseLoader("https://zh.d2l.ai/")
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 1500,
chunk_overlap = 150
)
splits = text_splitter.split_documents(docs)
print(len(splits))
embeddings = BaichuanTextEmbeddings(baichuan_api_key="sk-83842453061e34d80b392edba11f62fe")
persist_directory = r'D:\langchain'
vectordb = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory=persist_directory
)
print(vectordb._collection.count())
# 添加新文档
new_loader = WebBaseLoader("https://www.deeplearning.ai/")
new_docs = new_loader.load()
new_splits = text_splitter.split_documents(new_docs)
vectordb.add_documents(new_splits)
print(f"更新后的文档数量: {vectordb._collection.count()}")
retrieval.py —— 检索方法
from langchain_community.embeddings import BaichuanTextEmbeddings
from langchain_chroma import Chroma
persist_directory = r'D:\langchain'
embeddings = BaichuanTextEmbeddings(baichuan_api_key="sk-83842453061e34d80b392edba11f62fe")
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
# question = "大语言模型是怎么推理的?"
# docs_ss = vectordb.similarity_search(question, k=2)
# print(docs_ss[0].page_content[:100])
# print(docs_ss[1].page_content[:100])
# docs_mmr = vectordb.max_marginal_relevance_search(question, k=2, fetch_k=3)
# print(docs_mmr[0].page_content[:100])
# print(docs_mmr[1].page_content[:100])
# all_docs = vectordb.similarity_search("", k=20)
# for i, doc in enumerate(all_docs):
# print(f"Document {i+1} metadata: {doc.metadata}")
question = "大语言模型是怎么推理的?"
docs_meta = vectordb.similarity_search(question, k=1, filter={"source": "https://www.deeplearning.ai/"})
print(docs_meta[0].page_content[:100])
总结
这篇文章深入探讨了向量数据库中几种精准检索的方法:从基础的相似度搜索,到能平衡相关性与多样性的 Maximum Marginal Relevance (MMR),再到利用 元数据 进行精确过滤,以及结合 大语言模型 实现智能检索的思路。相似度搜索提供最直接的匹配,MMR 则通过调整参数让结果更丰富多样,而元数据过滤让我们能定向锁定特定来源的内容。实际应用中,根据场景灵活组合这些方法,才能让检索效果真正“聪明”起来。
