langChain 构建 RAG04 - 检索

本文介绍检索向量数据库的多种方式

检索是指根据用户的问题去向量数据库中搜索与问题相关的文档内容,当我们访问和查询向量数据库时可能会运用到如下几种技术:

  • 基本语义相似度 (Basic semantic similarity)
  • 最大边际相关性 (Maximum marginal relevance,MMR)
  • 过滤元数据
  • LLM 辅助检索

向量数据库检索

相似度检索

  1. 首先创建向量数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings

persist_directory_chinese = 'docs/chroma/matplotlib/'

embedding = OpenAIEmbeddings()

texts_chinese = [
"""毒鹅膏菌(Amanita phalloides)具有大型且引人注目的地上(epigeous)子实体(basidiocarp)""",
"""一种具有大型子实体的蘑菇是毒鹅膏菌(Amanita phalloides)。某些品种全白。""",
"""A. phalloides,又名死亡帽,是已知所有蘑菇中最有毒的一种。""",
]

# 用三句话创建向量数据库
smalldb_chinese = Chroma.from_texts(texts_chinese, embedding=embedding)

基于 embedding 实现基于语义的相似度检索

1
2
question_chinese = "告诉我关于具有大型子实体的全白色蘑菇的信息"
smalldb_chinese.similarity_search(question_chinese, k=2)
1
2
[Document(page_content='一种具有大型子实体的蘑菇是毒鹅膏菌(Amanita phalloides)。某些品种全白。', metadata={}),
Document(page_content='毒鹅膏菌(Amanita phalloides)具有大型且引人注目的地上(epigeous)子实体(basidiocarp)', metadata={})]

向量数据库返回了 2 个文档,就是我们存入向量数据库中的第一句和第二句。这里我们很明显的就可以看到 chroma 的 similarity_search(相似性搜索) 方法可以根据问题的语义去数据库中搜索与之相关性最高的文档,也就是搜索到了第一句和第二句的文本。但这似乎还存在一些问题,因为第一句和第二句的含义非常接近,他们都是描述 “鹅膏菌” 及其 “子实体” 的,所以假如只返回其中的一句就足以满足要求了,如果返回两句含义非常接近的文本感觉是一种资源的浪费。下面我们来看一下 max_marginal_relevance_search 的搜索结果。

最大边际相关性 (MMR)

MMR 的基本思想是同时考量查询与文档的相关度,以及文档之间的相似度。相关度确保返回结果对查询高度相关,相似度则鼓励不同语义的文档被包含进结果集。具体来说,它计算每个候选文档与查询的相关度,并减去与已经选入结果集的文档的相似度。这样更不相似的文档会有更高的得分。

总之,MMR 是解决检索冗余问题、提供多样性结果的一种简单高效的算法。它平衡了相关性和多样性,适用于对多样信息需求较强的应用场景。

1
smalldb_chinese.max_marginal_relevance_search(question_chinese,k=2, fetch_k=3)
1
2
[Document(page_content='一种具有大型子实体的蘑菇是毒鹅膏菌(Amanita phalloides)。某些品种全白。', metadata={}),
Document(page_content='A. phalloides,又名死亡帽,是已知所有蘑菇中最有毒的一种。', metadata={})]

max_marginal_relevance_search(最大边际相关搜索) 返回了第二和第三句的文本,尽管第三句与我们的问题的相关性不太高,但是这样的结果其实应该是更加的合理,因为第一句和第二句文本本来就有着相似的含义,所以只需要返回其中的一句就可以了,另外再返回一个与问题相关性弱一点的答案 (第三句文本),这样似乎增强了答案的多样性,相信用户也会更加偏爱

解决特殊性:使用元数据

得到的结果中也包括了来自其他讲的结果。这是我们所不希望看到的结果,之所以产生这样的结果是因为当我们向向量数据库提出问题时,数据库并没有很好的理解问题的语义,所以返回的结果不如预期。要解决这个问题,我们可以通过过滤元数据的方式来实现精准搜索,当前很多向量数据库都支持对 元数据(metadata) 的操作

1
2
3
4
5
6
7
8
9
10
question_chinese = "他们在第二讲中对Figure说了些什么?"  
docs_chinese = vectordb_chinese.similarity_search(
question_chinese,
k=3,
filter={"source":"docs/matplotlib/第二回:艺术画笔见乾坤.pdf"}
)

for d in docs_chinese:
print(d.metadata)

1
2
3
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 9}
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 10}
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 0}

解决特殊性:在元数据中使用自查询检索器(LLM 辅助检索)

在上例中,我们手动设置了过滤参数 filter 来过滤指定文档。但这种方式不够智能,需要人工指定过滤条件。如何自动从用户问题中提取过滤信息呢?

LangChain 提供了 SelfQueryRetriever 模块,它可以通过语言模型从问题语句中分析出:

  1. 向量搜索的查询字符串 (search term)
  2. 过滤文档的元数据条件 (Filter)

它使用语言模型自动解析语句语义,提取过滤信息,无需手动设置。这种基于理解的元数据过滤更加智能方便,可以自动处理更复杂的过滤逻辑。

掌握利用语言模型实现自动化过滤的技巧,可以大幅降低构建针对性问答系统的难度。这种自抽取查询的方法使检索更加智能和动态。

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
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo

llm = OpenAI(temperature=0)

metadata_field_info_chinese = [
AttributeInfo(
name="source",
description="The lecture the chunk is from, should be one of `docs/matplotlib/第一回:Matplotlib初相识.pdf`, `docs/matplotlib/第二回:艺术画笔见乾坤.pdf`, or `docs/matplotlib/第三回:布局格式定方圆.pdf`",
type="string",
),
AttributeInfo(
name="page",
description="The page from the lecture",
type="integer",
),
]

document_content_description_chinese = "Matplotlib 课堂讲义"
retriever_chinese = SelfQueryRetriever.from_llm(
llm,
vectordb_chinese,
document_content_description_chinese,
metadata_field_info_chinese,
verbose=True
)

question_chinese = "他们在第二讲中对Figure做了些什么?"
docs_chinese = retriever_chinese.get_relevant_documents(question_chinese)

首先定义了 metadata_field_info_chinese ,它包含了元数据的过滤条件 source 和 page , 其中 source 的作用是告诉 LLM 我们想要的数据来自于哪里, page 告诉 LLM 我们需要提取相关的内容在原始文档的哪一页。有了 metadata_field_info_chinese 信息后,LLM 会自动从用户的问题中提取出上图中的 Filter 和 Search term 两项,然后向量数据库基于这两项去搜索相关的内容。

1
2
for d in docs_chinese:
print(d.metadata)
1
2
3
4
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 9}
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 10}
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 0}
{'source': 'docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 6}

其他技巧:压缩

在使用向量检索获取相关文档时,直接返回整个文档片段可能带来资源浪费,因为实际相关的只是文档的一小部分。为改进这一点,LangChain 提供了一种 “压缩” 检索机制。其工作原理是,先使用标准向量检索获得候选文档,然后基于查询语句的语义,使用语言模型压缩这些文档,只保留与问题相关的部分。例如,对 “蘑菇的营养价值” 这个查询,检索可能返回整篇有关蘑菇的长文档。经压缩后,只提取文档中与 “营养价值” 相关的句子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

def pretty_print_docs(docs):
print(f"\n{'-' * 100}\n".join([f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]))

llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm) # 压缩器

compression_retriever_chinese = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectordb_chinese.as_retriever()
)
# 对源文档进行压缩

question_chinese = "Matplotlib是什么?"
compressed_docs_chinese = compression_retriever_chinese.get_relevant_documents(question_chinese)
pretty_print_docs(compressed_docs_chinese)
1
2
3
4
5
6
7
Document 1:

Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静态,动态,交互式的图表。
----------------------------------------------------------------------------------------------------
Document 2:

Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静态,动态,交互式的图表。

现在当我们提出问题后,查看结果文档,我们可以看到两件事。

  1. 它们比正常文档短很多
  2. 仍然有一些重复的东西,这是因为在底层我们使用的是语义搜索算法。

从上述例子中,我们可以发现这种压缩可以有效提升输出质量,同时节省通过长文档带来的计算资源浪费,降低成本。上下文相关的压缩检索技术,使得到的支持文档更严格匹配问题需求,是提升问答系统效率的重要手段。读者可以在实际应用中考虑这一技术

结合各种技术

为了去掉结果中的重复文档,我们在从向量数据库创建检索器时,可以将搜索类型设置为 MMR 。然后我们可以重新运行这个过程,可以看到我们返回的是一个过滤过的结果集,其中不包含任何重复的信息。

1
2
3
4
5
6
7
8
compression_retriever_chinese = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectordb_chinese.as_retriever(search_type = "mmr")
)

question_chinese = "Matplotlib是什么?"
compressed_docs_chinese = compression_retriever_chinese.get_relevant_documents(question_chinese)
pretty_print_docs(compressed_docs_chinese)
1
2
3
Document 1:

Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静态,动态,交互式的图表。

其他类型的检索

值得注意的是,vetordb 并不是唯一一种检索文档的工具。LangChain 还提供了其他检索文档的方式,例如:TF-IDF 或 SVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain.retrievers import SVMRetriever
from langchain.retrievers import TFIDFRetriever
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 加载PDF
loader_chinese = PyPDFLoader("docs/matplotlib/第一回:Matplotlib初相识.pdf")
pages_chinese = loader_chinese.load()
all_page_text_chinese = [p.page_content for p in pages_chinese]
joined_page_text_chinese = " ".join(all_page_text_chinese)

# 分割文本
text_splitter_chinese = RecursiveCharacterTextSplitter(chunk_size = 1500,chunk_overlap = 150)
splits_chinese = text_splitter_chinese.split_text(joined_page_text_chinese)

# 检索
svm_retriever = SVMRetriever.from_texts(splits_chinese, embedding)
tfidf_retriever = TFIDFRetriever.from_texts(splits_chinese)

这里我们定义了 SVMRetriever ,和 TFIDFRetriever 两个检索器,接下来我们分别测试 TF-IDF 检索以及 SVM 检索的效果:

1
2
3
question_chinese = "这门课的主要主题是什么?" 
docs_svm_chinese = svm_retriever.get_relevant_documents(question_chinese)
print(docs_svm_chinese[0])
1
page_content='fig, ax = plt.subplots()  \n# step4 绘制图像,  这⼀模块的扩展参考第⼆章进⼀步学习\nax.plot(x, y, label=\'linear\')  \n# step5 添加标签,⽂字和图例,这⼀模块的扩展参考第四章进⼀步学习\nax.set_xlabel(\'x label\') \nax.set_ylabel(\'y label\') \nax.set_title("Simple Plot")  \nax.legend() ;\n思考题\n请思考两种绘图模式的优缺点和各⾃适合的使⽤场景\n在第五节绘图模板中我们是以 OO 模式作为例⼦展示的,请思考并写⼀个 pyplot 绘图模式的简单模板' metadata={}

可以看出,SVM 检索的效果要差于 VectorDB。

1
2
3
question_chinese = "Matplotlib是什么?"
docs_tfidf_chinese = tfidf_retriever.get_relevant_documents(question_chinese)
print(docs_tfidf_chinese[0])
1
page_content='fig, ax = plt.subplots()  \n# step4 绘制图像,  这⼀模块的扩展参考第⼆章进⼀步学习\nax.plot(x, y, label=\'linear\')  \n# step5 添加标签,⽂字和图例,这⼀模块的扩展参考第四章进⼀步学习\nax.set_xlabel(\'x label\') \nax.set_ylabel(\'y label\') \nax.set_title("Simple Plot")  \nax.legend() ;\n思考题\n请思考两种绘图模式的优缺点和各⾃适合的使⽤场景\n在第五节绘图模板中我们是以 OO 模式作为例⼦展示的,请思考并写⼀个 pyplot 绘图模式的简单模板' metadata={}

同样,TF-IDF 检索的效果也不尽如人意