使用Langchain访问个人数据
在大数据时代,数据价值逐渐凸显,打造定制化、个性化服务,个人数据尤为重要。要开发一个具备较强服务能力、能够充分展现个性化智能的应用程序,大模型与个人数据的对齐是一个重要步骤。作为针对大模型开发应运而生的框架,LangChain 提供了众多结合大模型开发的工具与功能,支持大模型访问个人数据的能力自然必不少。
在上一部分中,我们讲解了如何基于 LangChain 来实现基本应用程序的开发。通过使用 LangChain 提供的框架与功能,我们可以较为便捷地实现存储、模型链、评估、代理等多样化功能,对接用户的实际 需求。在这一部分,我们基于上一部分深入拓展 LangChain 提供的个人数据访问能力,指导开发者如何 使用 LangChain 开发能够访问用户个人数据、提供个性化服务的大模型应用。通过学习本部分,您能够 进一步掌握 LangChain 框架与大模型开发的进阶能力,为针对生成需要的实际开发奠定基础。
本部分的主要内容包括:加载并切割文档; 向量数据库与词向量; 检索与问答; 聊天等。
一、简介
本课程基于 LangChain 创始人哈里森·蔡斯 (Harrison Chase)与 Deeplearning.ai 合作开发的 《LangChain Chat With your Data》课程,将介绍如何利用 LangChain 框架,使语言模型访问并应用 用户自有数据的强大能力。
1.1 背景
大语言模型(Large Language Model, LLM), 比如ChatGPT, 可以回答许多不同的问题。但是大语言模型的 知识来源于其训练数据集,并没有用户的信息(比如用户的个人数据,公司的自有数据),也没有最新 发生时事的信息(在大模型数据训练后发表的文章或者新闻)。因此大模型能给出的答案比较受限。
如果能够让大模型在训练数据集的基础上,利用我们自有数据中的信息来回答我们的问题,那便能够得 到更有用的答案。
1.2 项目基本内容
LangChain 是用于构建大模型应用程序的开源框架,有 Python 和 JavaScript 两个不同版本的包。它由
模块化的组件构成,可单独使用也可链式组合实现端到端应用。
LangChain的组件包括:
- 提示(Prompts): 使模型执行操作的方式。
- 模型(Models): 大语言模型、对话模型,文本表示模型。目前包含多个模型的集成。
- 索引(Indexes): 获取数据的方式,可以与模型结合使用。
- 链(Chains): 端到端功能实现。
- 代理(Agents): 使用模型作为推理引擎
本项目介绍使用 LangChain 的典型场景——基于自有数据的对话系统。我们首先学习如何使用 LangChain 的文档加载器 (Document Loader)从不同数据源加载文档。然后,我们学习如何将这些文档切割为具有语意的段落。这步看起来简单,不同的处理可能会影响颇大。接下来,我们讲解语义搜索 (Semantic search)与信息检索的方法,获取用户问题相关的参考文档。该方法很简单,但是在某些情 况下可能无法使用。我们将分析这些情况并给出解决方案。最后,我们介绍如何使用检索得到的文档, 来让大语言模型(LLM)来回答关于文档的问题。
整个流程涵盖数据导入、信息检索与问答生成等关键环节。读者既可以学习各个组件的用法,也可以整 体实践构建对话系统的思路。如果你想要先了解关于 LangChain 的基础知识,可以学习《LangChain for LLM Application Development》部分的内容。
本单元将帮助你掌握利用自有数据的语言模型应用开发技能。让我们开始探索个性化对话系统的魅力吧!
二、文档加载
用户个人数据可以以多种形式呈现: PDF 文档、视频、网页等。基于 LangChain 提供给 LLM 访问用户个人数据的能力,首先要加载并处理用户的多样化、非结构化个人数据。
在本章,我们首先介绍如何加 载文档(包括文档、视频、网页等),这是访问个人数据的第一步。
让我们先从 PDF 文档开始。
2.1 PDF文档
首先,我们将从以下链接加载一个PDF文档。这是 DataWhale 提供的开源教程,名为《Fantastic Matplotlib》。
注意,要运行以下代码,你需要安装第三方库 pypdf:
pip install -q pypdf
2.1.1 加载PDF文档
首先,我们将利用 PyPDFLoader 来对 PDF 文件进行读取和加载。
from langchain.document_loaders import PyPDFLoader
if __name__ == '__main__':# 创建一个 PyPDFLoader Class 实例,输入为待加载的pdf文档路径loader = PyPDFLoader("./第一回:Matplotlib初相识.pdf")# 调用 PyPDFLoader Class 的函数 load对pdf文件进行加载pages = loader.load()print(pages)
一旦文档被加载,它会被存储在名为 pages 的变量里。此外, pages 的数据结构是一个 List 类型。为
了确认其类型,我们可以借助Python内建的 type 函数来查看 pages 的确切数据类型。
print(type(pages))
输出结果
<class 'list'>
通过输出 pages 的长度,我们可以轻松地了解该PDF文件包含的总页数。
print(len(pages))
输出结果
4
在 page 变量中,每一个元素都代表一个文档,它们的数据类型是 langchain_core.documents.base.Document 。
page = pages[0]
print(type(page))
输出结果
<class 'langchain_core.documents.base.Document'>
langchain_core.documents.base.Document类型包含两个属性:
- page_content :包含该文档页面的内容。
print(page.page_content[0:500])
输出结果
第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静
态,动态,交互式的图表。
Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包
等。
Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具,我们所熟知的 pandas 和 seaborn 的绘
图接⼝其实也是基于 matplotlib 所作的⾼级封装。
为了对 matplotlib 有更好的理解,让我们从⼀些最基本的概念开始认识它,再逐渐过渡到⼀些⾼级技巧中。
⼆、⼀个最简单的绘图例⼦
Matplotlib 的图像是画在 figure (如 windows , jupyter 窗体)上的,每⼀个 figure ⼜包含了⼀个或多个 axes (⼀个可以指定坐标系
的⼦区域)。最简单的创建 fig
- meta_data :为文档页面相关的描述性数据。
print(page.metadata)
输出结果
{'producer': 'Skia/PDF m138', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'creationdate': '2025-07-04T16:00:40+00:00', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'moddate': '2025-07-04T16:00:40+00:00', 'source': './第一回:Matplotlib初相识.pdf', 'total_pages': 4, 'page': 0, 'page_label': '1'}
2.2 Youtube 音频
在上个部分的内容,我们已经探讨了如何加载 PDF 文档。在这部分的内容中,对于给定的 YouTube 视频链接,我们会详细讨论:
- 利用 langchain 加载工具,为指定的 YouTube 视频链接下载对应的音频至本地
- 通过 OpenAIWhisperPaser 工具,将这些音频文件转化为可读的文本内容
注意,要运行以下代码,你需要安装如下两个第三方库:
pip install yt_dlp
pip install pydub
2.2.1 加载Youtube音频文档
首先,我们将构建一个 GenericLoader 实例来对 Youtube 视频的下载到本地并加载。
2.2.2 探索加载出来的数据
Youtube 音频文件加载得到的变量同上文类似,此处不再一一解释,通过类似代码可以展示加载数据:
2.3 网页文档
在2.2部分,我们对于给定的 YouTube 视频链接 (URL),使用 LangChain 加载器将视频的音频下载到本地,然后使用 OpenAIWhisperPaser 解析器将音频转化为文本。
本部分,我们将研究如何处理网页链接(URLs)。为此,我们会以 GitHub 上的一个markdown格式文 档为例,学习如何对其进行加载。
2.3.1 加载网页文档
首先,我们将构建一个 WebBaseLoader 实例来对网页进行加载。
from langchain_community.document_loaders import WebBaseLoaderif __name__ == '__main__':# 创建一个 WebBaseLoader Class 实例url = "https://github.com/datawhalechina/d2l-ai-solutions- manual/blob/master/docs/README.md"header = {'User-Agent': 'python-requests/2.27.1','Accept-Encoding': 'gzip, deflate, br','Accept': '*/*','Connection': 'keep-alive'}loader = WebBaseLoader(web_path=url,header_template=header)# 调用 WebBaseLoader Class 的函数 load对文件进行加载pages = loader.load()
2.3.2 探索加载出来的数据
print("Type of pages: ", type(pages))
print("Length of pages: ", len(pages))
page = pages[0]
print("Type of page: ", type(page))
print("Page_content: ", page.page_content[:500])
print("Meta Data: ", page.metadata)
/Users/zhourundong/opt/anaconda3/envs/chatgpt/bin/python3 /Users/zhourundong/PycharmProjects/llms-cookbook-learning/part4/chapter2/example3.py
USER_AGENT environment variable not set, consider setting it to identify your requests.
Type of pages: <class 'list'>
Length of pages: 1
Type of page: <class 'langchain_core.documents.base.Document'>
Page_content: 第一回:Matplotlib初相识 — fantastic-matplotlibfantastic-matplotlib第一回:Matplotlib初相识第二回:艺术画笔见乾坤第三回:布局格式定方圆第四回:文字图例尽眉目第五回:样式色彩秀芳华Theme by the Executable Book Project.md.pdfrepository
open issue
suggest editContents一、认识matplotlib二、一个最简单的绘图例子三、Figure的组成四、两种绘图接口五、通用绘图模板思考题第一回:Matpl
Meta Data: {'source': 'https://datawhalechina.github.io/fantastic-matplotlib/%E7%AC%AC%E4%B8%80%E5%9B%9E%EF%BC%9AMatplotlib%E5%88%9D%E7%9B%B8%E8%AF%86/index.html', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'language': 'zh'}Process finished with exit code 0
可以看到上面的文档内容包含许多冗余的信息,比如空行。通常来讲,我们需要进行对这种数据进行进一步处理 (Post Processing)。
# 去掉 page.page_content 中的所有空行
content_without_empty_lines = "\n".join(line for line in page.page_content.splitlines() if line.strip())
print("Page_content: ", content_without_empty_lines[:500])
2.4 Notion文档
点击Notion示例文档右上方复制按钮(Duplicate),复制文档到你的Notion空间
点击右上方⋯ 按钮,选择导出为Mardown&CSV。
导出的文件将为zip文件夹 解压并保存mardown文档到本地路径 docs/Notion_DB/
2.4.1 加载Notion Markdown文档
首先,我们将使用 NotionDirectoryLoader 来对Notion Markdown文档进行加载。
from langchain_community.document_loaders import NotionDirectoryLoaderif __name__ == '__main__':loader = NotionDirectoryLoader("./docs/Notion_DB")pages = loader.load()
2.4.2 探索加载的数据
同理,使用上文代码:
print("Type of pages: ", type(pages))
print("Length of pages: ", len(pages))
page = pages[0]
print("Type of page: ", type(page))
print("Page_content: ", page.page_content[:500])
print("Meta Data: ", page.metadata)
Type of pages: <class 'list'>
Length of pages: 1
Type of page: <class 'langchain_core.documents.base.Document'>
Page_content: # Quick NoteQuickly create a rich document.# Jot down some text---They found Mary, as usual, deep in the study of thorough-bass and human nature; and had some extracts to admire, and some new observations of threadbare morality to listen to. Catherine and Lydia had information for them of a different sort.# Make a to-do list---- [ ] Wake up
- [ ] Eat breakfast
- [x] Brush teeth# Create sub-pages---[Sub Page](https://www.notion.so/Sub-Page-800f66ed79a44444a3b6a0e91c539249?p
Meta Data: {'source': 'docs/Notion_DB/Quick Note ab33515f539f444ab24832a38c6bc796.md'}
三、文档分割
在上一章中,我们刚刚讨论了如何将文档加载到标准格式中,现在我们要谈论如何将它们分割成较小的块。这听起来可能很简单,但其中有很多微妙之处会对后续工作产生重要影响。
3.1 为什么要进行文档分割
- 模型大小和内存限制: GPT 模型,特别是大型版本如 GPT-3 或 GPT-4 ,具有数十亿甚至上百亿的参数。为了在一次前向传播中处理这么多的参数,需要大量的计算能力和内存。但是,大多数硬件设备(例如 GPU 或 TPU )有内存限制。文档分割使模型能够在这些限制内工作。
- 计算效率: 处理更长的文本序列需要更多的计算资源。通过将长文档分割成更小的块,可以更高效地进行计算。
- 序列长度限制: GPT 模型有一个固定的最大序列长度,例如2048个token 。这意味着模型一次只能处理这么多 token 。对于超过这个长度的文档,需要进行分割才能被模型处理。
- 更好的泛化: 通过在多个文档块上进行训练,模型可以更好地学习和泛化到各种不同的文本样式和结构。
- 数据增强: 分割文档可以为训练数据提供更多的样本。例如,一个长文档可以被分割成多个部分, 并分别作为单独的训练样本。
需要注意的是,虽然文档分割有其优点,但也可能导致一些上下文信息的丢失,尤其是在分割点附近。因此,如何进行文档分割是一个需要权衡的问题。
图3.1 文档分割的意义
若仅按照单一字符进行文本分割,很容易使文本的语义信息丧失,这样在回答问题时可能会出现偏差。因此,为了确保语义的准确性,我们应该尽量将文本分割为包含完整语义的段落或单元。
3.2 文档分割方式
Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。
图3.2 文档分割示例
- chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量;
- chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息;
图3.3 文档分割工具
Langchain提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及
如何测量块大小;
3.3 基于字符分割
如何进行文本分割,往往与我们的任务类型息息相关。当我们拆分代码时,这种相关性变得尤为突出。 因此,我们引入了一个语言文本分割器,其中包含各种为 Python、Ruby、C 等不同编程语言设计的分隔 符。在对这些文档进行分割时,必须充分考虑各种编程语言之间的差异。
我们将从基于字符的分割开始探索,借助 LangChain 提供的 RecursiveCharacterTextSplitter 和 CharacterTextSplitter 工具来实现此目标。
- CharacterTextSplitter 是字符文本分割,分隔符的参数是单个的字符串;
- RecursiveCharacterTextSplitter 是递归字符文本分割,将按不同的字符递归地分割(按照这个优 先级[“\n\n”, “\n”, " ", “”]),这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置。因 此, RecursiveCharacterTextSplitter 比 CharacterTextSplitter 对文档切割得更加碎片化;
RecursiveCharacterTextSplitter 需要关注的是如下4个参数:
- separators - 分隔符字符串数组
- chunk_size - 每个文档的字符数量限制
- chunk_overlap - 两份文档重叠区域的长度
- length_function - 长度计算函数
3.3.1 短句分割
# 导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitterif __name__ == '__main__':chunk_size = 20 #设置块大小chunk_overlap = 10 #设置块重叠大小# 初始化递归字符文本分割器r_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size,chunk_overlap=chunk_overlap)# 初始化字符文本分割器c_splitter = CharacterTextSplitter(chunk_size=chunk_size,chunk_overlap=chunk_overlap)
接下来我们对比展示两个字符文本分割器的效果。
#测试文本
text = "在AI的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好训练时间 成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。"
texts1 = r_splitter.split_text(text)
print("texts1: ", texts1)
texts1: ['在AI的研究中,由于大模型规模非常大,模',
'大模型规模非常大,模型参数很多,在大模型',
'型参数很多,在大模型上跑完来验证参数好不',
'上跑完来验证参数好不好训练时间',
'成本很高,所以一般会在小模型上做消融实',
'会在小模型上做消融实验来验证哪些改进是有',
'验来验证哪些改进是有效的再去大模型上做实',
'效的再去大模型上做实验。']
可以看到,分割结果中,第二块是从“大模型规模非常大,模”开始的,刚好是我们设定的块重叠大小;
# 字符文本分割器
texts2 = c_splitter.split_text(text)
print("texts2: ", texts2)
texts2: ['在AI的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好
训练时间 成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']
可以看到字符分割器没有分割这个文本,因为字符文本分割器默认以换行符为分隔符,因此需要设置“,” 为分隔符。
# 设置,分隔符
c_splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap, separator=',')
texts3 = c_splitter.split_text(text)
print("texts3: ", texts3)
Created a chunk of size 24, which is longer than the specified 20
texts3: ['在AI的研究中,由于大模型规模非常大',
'由于大模型规模非常大,模型参数很多',
'在大模型上跑完来验证参数好不好训练时间 成本很高',
'所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']
设置“,”为分隔符后,分割效果与递归字符文本分割器类似。
可以看到出现了提示"Created a chunk of size 24, which is longer than the specified 20",意思 是“创建了一个长度为24的块,这比指定的20要长。”。这是因为 CharacterTextSplitter 优先使 用我们自定义的分隔符进行分割,所以在长度上会有较小的差距;
3.3.2 长文本分割
接下来,我们来尝试对长文本进行分割。
# 导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitterif __name__ == '__main__':#测试长文本some_text = """在编写文档时,作者将使用文档结构对内容进行分组。 \这可以向读者传达哪些想法是相关的。 例如,密切相关的想法\是在句子中。 类似的想法在段落中。 段落构成文档。 \n\n\段落通常用一个或两个回车符分隔。 \回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 \句子末尾有一个句号,但也有一个空格。\并且单词之间用空格分隔"""print(len(some_text))
178
我们使用以上长文本作为示例。
c_splitter = CharacterTextSplitter(chunk_size=80,chunk_overlap=0,separator=' ')
'''
对于递归字符分割器,依次传入分隔符列表,分别是双换行符、单换行符、空格、空字符,
因此在分割文本时,首先会采用双分换行符进行分割,同时依次使用其他分隔符进行分割
'''
r_splitter = RecursiveCharacterTextSplitter(chunk_size=80,chunk_overlap=0,separators=["\n\n", "\n", " ", ""]
)
字符分割器结果:
texts1 = c_splitter.split_text(some_text)
print("texts1: ", texts1)
texts1: ['在编写文档时,作者将使用文档结构对内容进行分组。 这可以向读者传达哪些想法是相关的。 例如,密切相关的想法 是在句子中。 类似的想法在段落中。 段落构成文档。',
'段落通常用一个或两个回车符分隔。 回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 句子末尾有一个句号,但也有一个空格。 并且单词之间用空格分隔']
递归字符分割器效果:
texts2 = r_splitter.split_text(some_text)
print("texts2: ", texts2)
texts2: ['在编写文档时,作者将使用文档结构对内容进行分组。 这可以向读者传达哪些想法是相关的。 例如,密切相关的想法 是在句子中。 类似的想法在段落中。',
'段落构成文档。',
'段落通常用一个或两个回车符分隔。 回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 句子末尾有一个句号,但也有一个空格。',
'并且单词之间用空格分隔']
如果需要按照句子进行分隔,则还要用正则表达式添加一个句号分隔符
r_splitter = RecursiveCharacterTextSplitter(chunk_size=30,chunk_overlap=0,separators=["\n\n", "\n", "(?<=\。 )", " ", ""])
texts3 = r_splitter.split_text(some_text)
print("texts3: ", texts3)
texts3: ['在编写文档时,作者将使用文档结构对内容进行分组。',
'这可以向读者传达哪些想法是相关的。 例如,密切相关的想法',
'是在句子中。 类似的想法在段落中。 段落构成文档。',
'段落通常用一个或两个回车符分隔。',
'回车符是您在该字符串中看到的嵌入的“反斜杠 n”。',
'句子末尾有一个句号,但也有一个空格。',
'并且单词之间用空格分隔']
这就是递归字符文本分割器名字中“递归”的含义,总的来说,我们更建议在通用文本中使用递归字符文本分割器;
3.4 基于Token分割
很多 LLM 的上下文窗口长度限制是按照 Token 来计数的。因此,以 LLM 的视角,按照 Token 对文本进 行分隔,通常可以得到更好的结果。
通过一个实例理解基于字符分割和基于 Token 分割的区别
# 使用token分割器进行分割,
# 将块大小设为1,块重叠大小设为0,相当于将任意字符串分割成了单个Token组成的列
from langchain.text_splitter import TokenTextSplitterif __name__ == '__main__':text_splitter = TokenTextSplitter(chunk_size=1, chunk_overlap=0)# 注:目前 LangChain 基于 Token 的分割器还不支持中文text = "foo bar bazzyfoo"texts = text_splitter.split_text(text)print(texts)
['foo', ' bar', ' b', 'az', 'zy', 'foo']
可以看出token长度和字符长度不一样,token通常为4个字符
3.5 分割Markdown文档
3.5.1 分割一个自定义 Markdown 文档
分块的目的是把具有上下文的文本放在一起,我们可以通过使用指定分隔符来进行分隔,但有些类型的文档(例如 Markdown )本身就具有可用于分割的结构(如标题)。
Markdown 标题文本分割器会根据标题或子标题来分割一个 Markdown 文档,并将标题作为元数据添加到每个块中;
# 定义一个Markdown文档
from langchain_community.document_loaders import NotionDirectoryLoader#Notion加载器
from langchain.text_splitter import MarkdownHeaderTextSplitter#markdown分割器
if __name__ == '__main__':markdown_document = """# Title\n\n \## 第一章\n\n \李白乘舟将欲行\n\n 忽然岸上踏歌声\n\n \### Section \n\n \桃花潭水深千尺 \n\n ## 第二章\n\n \不及汪伦送我情"""
我们以上述文本作为 Markdown 文档的示例,上述文本格式遵循了 Markdown 语法
# 定义想要分割的标题列表和名称
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on
) # message_typemessage_type
md_header_splits = markdown_splitter.split_text(markdown_document)
print("第一个块")
print(md_header_splits[0])
print("第二个块")
print(md_header_splits[1])
第一个块
page_content='李白乘舟将欲行
忽然岸上踏歌声' metadata={'Header 1': 'Title', 'Header 2': '第一章'}
第二个块
page_content='桃花潭水深千尺' metadata={'Header 1': 'Title', 'Header 2': '第一章', 'Header 3': 'Section'}
可以看到,每个块都包含了页面内容和元数据,元数据中记录了该块所属的标题和子标题。
3.5.2 分割数据库中的 Markdown 文档
在上一章中,我们尝试了 Notion 数据库的加载,Notion 文档就是一个 Markdown 文档。我们在此处加 载 Notion 数据库中的文档并进行分割。
# 定义一个Markdown文档
from langchain_community.document_loaders import NotionDirectoryLoader#Notion加载器
from langchain.text_splitter import MarkdownHeaderTextSplitter#markdown分割器
if __name__ == '__main__':loader = NotionDirectoryLoader("./docs/Notion_DB")docs = loader.load()txt = ' '.join([d.page_content for d in docs]) # 拼接文档headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),]# 加载文档分割器markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)md_header_splits = markdown_splitter.split_text(txt) # 分割文本内容print(md_header_splits[1])#分割结果
page_content='---
They found Mary, as usual, deep in the study of thorough-bass and human nature; and had some extracts to admire, and some new observations of threadbare morality to listen to. Catherine and Lydia had information for them of a different sort.' metadata={'Header 1': 'Jot down some text'}
四、向量数据库与词向量
让我们一起回顾一下检索增强生成(RAG)的整体工作流程:
图4.1 检索增强生成整体流程
前两个章节我们讨论了 Document Loading (文档加载)和 Splitting (分割)。
下面我们将使用前两节课的知识对文档进行加载分割。
4.1 读取文档
下面文档是 datawhale 官方开源的 matplotlib 教程链接 https://datawhalechina.github.io/fantastic-m atplotlib/index.html ,可在该网站上下载对应的教程,在代码中放在chapter4下。
注意,本章节需要安装第三方库 pypdf 、 chromadb;
from langchain_community.document_loaders import PyPDFLoader
if __name__ == '__main__':# 加载 PDFloaders_chinese = [# 故意添加重复文档,使数据混乱PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第二回:艺术画笔见乾坤.pdf"),PyPDFLoader("./docs/matplotlib/第三回:布局格式定方圆.pdf"),PyPDFLoader("./docs/matplotlib/第四回:文字图例尽眉目.pdf"),PyPDFLoader("./docs/matplotlib/第五回:样式色彩秀芳华.pdf")]docs = []for loader in loaders_chinese:docs.extend(loader.load())print(len(docs))
43
在文档加载后,我们可以使用 RecursiveCharacterTextSplitter (递归字符文本拆分器)来创建块。
# 分割文本
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, # 每个文本块的大小。这意味着每次切分文本时,会尽量使每个块包含 1500 个字符。chunk_overlap=150 # 每个文本块之间的重叠部分。
)
splits = text_splitter.split_documents(docs)
print(len(splits))
52
4.2 Embedding
什么是 Embeddings ?
在机器学习和自然语言处理(NLP)中, Embeddings (嵌入)是一种将类别数据,如单词、句子或者整 个文档,转化为实数向量的技术。这些实数向量可以被计算机更好地理解和处理。嵌入背后的主要想法 是,相似或相关的对象在嵌入空间中的距离应该很近。
举个例子,我们可以使用词嵌入(word embeddings)来表示文本数据。在词嵌入中,每个单词被转换 为一个向量,这个向量捕获了这个单词的语义信息。例如,“king” 和 “queen” 这两个单词在嵌入空间中 的位置将会非常接近,因为它们的含义相似。而 “apple” 和 “orange” 也会很接近,因为它们都是水果。 而 “king” 和 “apple” 这两个单词在嵌入空间中的距离就会比较远,因为它们的含义不同。
让我们取出我们的切分部分并对它们进行 Embedding 处理。
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型from tool import get_azure_endpoint, get_api_key, get_api_versionif __name__ == '__main__':embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())
在使用真实文档数据的例子之前,让我们用几个测试案例的句子来试试,以便了解 embedding 。 下面有几个示例句子,其中前两个非常相似,第三个与之无关。然后我们可以使用 embedding 类为每个
句子创建一个 embedding 。
sentence1_chinese = "我喜欢狗"
sentence2_chinese = "我喜欢犬科动物"
sentence3_chinese = "外面的天气很糟糕"
embedding1_chinese = embedding.embed_query(sentence1_chinese)
embedding2_chinese = embedding.embed_query(sentence2_chinese)
embedding3_chinese = embedding.embed_query(sentence3_chinese)
然后我们可以使用 numpy 来比较它们,看看哪些最相似。 我们期望前两个句子应该非常相似。
然后,第一和第二个与第三个相比应该相差很大。
我们将使用点积来比较两个嵌入。 如果你不知道什么是点积,没关系。你只需要知道的重要一点是,分数越高句子越相似。
import numpy as npscore1 = np.dot(embedding1_chinese, embedding2_chinese)
print(score1)
0.7231750688442332
我们可以看到前两个 embedding 的分数比较高,为0.72。
score2 = np.dot(embedding1_chinese, embedding3_chinese)
print(score2)
0.18712050416386503
如果我们将第一个 embedding 与第三个 embedding 进行比较,我们可以看到它明显较低,约为0.19。
score3 = np.dot(embedding2_chinese, embedding3_chinese)
print(score3)
0.13901511325093913
我们将第二个 embedding 和第三个 embedding 进行比较,我们可以看到它的分数大约为0.14。
4.3 Vectorstores
4.3.1 初始化Chroma
Langchain集成了超过30个不同的向量存储库。我们选择Chroma是因为它轻量级且数据存储在内存中,
这使得它非常容易启动和开始使用。
首先我们指定一个持久化路径:
from langchain_community.vectorstores import Chroma
if __name__ == '__main__':persist_directory_chinese = './docs/chroma/matplotlib/'
如果该路径存在旧的数据库文件,可以通过以下命令删除:
rm -rf './docs/chroma/matplotlib'
接着从已加载的文档中创建一个向量数据库:
vectordb_chinese = Chroma.from_documents(documents=splits,embedding=embedding,persist_directory=persist_directory_chinese # 允许我们将persist_directory目录保存到磁盘上)
可以看到数据库长度也是43,这与我们之前的切分数量是一样的。现在让我们开始使用
print(vectordb_chinese._collection.count())
104
4.3.2 相似性搜索(Similarity Search)
首先我们定义一个需要检索答案的问题:
question_chinese = "Matplotlib是什么?"
接着调用已加载的向量数据库根据相似性检索答案:
docs_chinese = vectordb_chinese.similarity_search(question_chinese,k=3)
查看检索答案数量:
print(len(docs_chinese))
3
打印其 page_content 属性可以看到检索答案的文本:
print(docs_chinese[0].page_content)
第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静
态,动态,交互式的图表。
Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包
等。
Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具,我们所熟知的 pandas 和 seaborn 的绘
图接⼝其实也是基于 matplotlib 所作的⾼级封装。
为了对 matplotlib 有更好的理解,让我们从⼀些最基本的概念开始认识它,再逐渐过渡到⼀些⾼级技巧中。
⼆、⼀个最简单的绘图例⼦
Matplotlib 的图像是画在 figure (如 windows , jupyter 窗体)上的,每⼀个 figure ⼜包含了⼀个或多个 axes (⼀个可以指定坐标系
的⼦区域)。最简单的创建 figure 以及 axes 的⽅式是通过pyplot.subplots命令,创建 axes 以后,可以使⽤Axes.plot绘制最简
易的折线图。
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
fig, ax = plt.subplots() # 创建⼀个包含⼀个 axes 的 figure
ax.plot([1 , 2 , 3 , 4 ], [1 , 4 , 2 , 3 ]); # 绘制图像
Trick: 在 jupyter notebook 中使⽤ matplotlib 时会发现,代码运⾏后⾃动打印出类似<matplotlib.lines.Line2D at
0x23155916dc0>这样⼀段话,这是因为 matplotlib 的绘图代码默认打印出最后⼀个对象。如果不想显示这句话,有以下三种⽅
法,在本章节的代码示例中你能找到这三种⽅法的使⽤。
1. 在代码块最后加⼀个分号;
2. 在代码块最后加⼀句 plt.show()
3. 在绘图时将绘图对象显式赋值给⼀个变量,如将 plt.plot([1, 2, 3, 4]) 改成 line =plt.plot([1, 2, 3, 4])
和 MATLAB 命令类似,你还可以通过⼀种更简单的⽅式绘制图像,matplotlib.pyplot⽅法能够直接在当前 axes 上绘制图像,如
果⽤户未指定 axes , matplotlib 会帮你⾃动创建⼀个。所以上⾯的例⼦也可以简化为以下这⼀⾏代码。
line =plt.plot([1 , 2 , 3 , 4 ], [1 , 4 , 2 , 3 ])
Contents
⼀、认识 matplotlib
⼆、⼀个最简单的绘图例⼦
三、 Figure 的组成
四、两种绘图接⼝
五、通⽤绘图模板
思考题
P rin t to P D F
在此之后,我们要确保通过运行vectordb.persist来持久化向量数据库,以便我们在未来的课程中使用。在chromdb 0.4.2的版本,会自动持久化存储;
# 已经废弃,会自动保存
vectordb_chinese.persist()
4.4 失败的情况
这看起来很好,基本的相似性搜索很容易就能让你完成80%的工作。但是,可能会出现一些相似性搜索失败的情况。这里有一些可能出现的边缘情况————我们将在本章节中修复它们。
4.4.1 重复块
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from langchain_community.document_loaders import PyPDFLoader
from tool import get_azure_endpoint, get_api_key, get_api_version
if __name__ == '__main__':embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)question_chinese = "Matplotlib是什么?"docs_chinese = vectordb_chinese.similarity_search(question_chinese, k=5)
请注意,我们得到了重复的块(因为索引中有重复的 第一回:Matplotlib初相识.pdf)。 语义搜索获取所有相似的文档,但不强制多样性。
docs[0] 和 docs[1] 是完全相同的。
print("docs[0]")
print(docs_chinese[0])
print("docs[1]")
print(docs_chinese[1])
docs[0]
page_content='第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静
态,动态,交互式的图表。
Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包
等。
Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具,我们所熟知的 pandas 和 seaborn 的绘
图接⼝其实也是基于 matplotlib 所作的⾼级封装。
为了对 matplotlib 有更好的理解,让我们从⼀些最基本的概念开始认识它,再逐渐过渡到⼀些⾼级技巧中。
⼆、⼀个最简单的绘图例⼦
Matplotlib 的图像是画在 figure (如 windows , jupyter 窗体)上的,每⼀个 figure ⼜包含了⼀个或多个 axes (⼀个可以指定坐标系
的⼦区域)。最简单的创建 figure 以及 axes 的⽅式是通过pyplot.subplots命令,创建 axes 以后,可以使⽤Axes.plot绘制最简
易的折线图。
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
fig, ax = plt.subplots() # 创建⼀个包含⼀个 axes 的 figure
ax.plot([1 , 2 , 3 , 4 ], [1 , 4 , 2 , 3 ]); # 绘制图像
Trick: 在 jupyter notebook 中使⽤ matplotlib 时会发现,代码运⾏后⾃动打印出类似<matplotlib.lines.Line2D at
0x23155916dc0>这样⼀段话,这是因为 matplotlib 的绘图代码默认打印出最后⼀个对象。如果不想显示这句话,有以下三种⽅
法,在本章节的代码示例中你能找到这三种⽅法的使⽤。
1. 在代码块最后加⼀个分号;
2. 在代码块最后加⼀句 plt.show()
3. 在绘图时将绘图对象显式赋值给⼀个变量,如将 plt.plot([1, 2, 3, 4]) 改成 line =plt.plot([1, 2, 3, 4])
和 MATLAB 命令类似,你还可以通过⼀种更简单的⽅式绘制图像,matplotlib.pyplot⽅法能够直接在当前 axes 上绘制图像,如
果⽤户未指定 axes , matplotlib 会帮你⾃动创建⼀个。所以上⾯的例⼦也可以简化为以下这⼀⾏代码。
line =plt.plot([1 , 2 , 3 , 4 ], [1 , 4 , 2 , 3 ])
Contents
⼀、认识 matplotlib
⼆、⼀个最简单的绘图例⼦
三、 Figure 的组成
四、两种绘图接⼝
五、通⽤绘图模板
思考题
P rin t to P D F' metadata={'page': 0, 'creationdate': '2025-07-04T16:00:40+00:00', 'producer': 'Skia/PDF m138', 'moddate': '2025-07-04T16:00:40+00:00', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'total_pages': 4, 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'page_label': '1', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'}
docs[1]
page_content='第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静
态,动态,交互式的图表。
Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包
等。
Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具,我们所熟知的 pandas 和 seaborn 的绘
图接⼝其实也是基于 matplotlib 所作的⾼级封装。
为了对 matplotlib 有更好的理解,让我们从⼀些最基本的概念开始认识它,再逐渐过渡到⼀些⾼级技巧中。
⼆、⼀个最简单的绘图例⼦
Matplotlib 的图像是画在 figure (如 windows , jupyter 窗体)上的,每⼀个 figure ⼜包含了⼀个或多个 axes (⼀个可以指定坐标系
的⼦区域)。最简单的创建 figure 以及 axes 的⽅式是通过pyplot.subplots命令,创建 axes 以后,可以使⽤Axes.plot绘制最简
易的折线图。
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
fig, ax = plt.subplots() # 创建⼀个包含⼀个 axes 的 figure
ax.plot([1 , 2 , 3 , 4 ], [1 , 4 , 2 , 3 ]); # 绘制图像
Trick: 在 jupyter notebook 中使⽤ matplotlib 时会发现,代码运⾏后⾃动打印出类似<matplotlib.lines.Line2D at
0x23155916dc0>这样⼀段话,这是因为 matplotlib 的绘图代码默认打印出最后⼀个对象。如果不想显示这句话,有以下三种⽅
法,在本章节的代码示例中你能找到这三种⽅法的使⽤。
1. 在代码块最后加⼀个分号;
2. 在代码块最后加⼀句 plt.show()
3. 在绘图时将绘图对象显式赋值给⼀个变量,如将 plt.plot([1, 2, 3, 4]) 改成 line =plt.plot([1, 2, 3, 4])
和 MATLAB 命令类似,你还可以通过⼀种更简单的⽅式绘制图像,matplotlib.pyplot⽅法能够直接在当前 axes 上绘制图像,如
果⽤户未指定 axes , matplotlib 会帮你⾃动创建⼀个。所以上⾯的例⼦也可以简化为以下这⼀⾏代码。
line =plt.plot([1 , 2 , 3 , 4 ], [1 , 4 , 2 , 3 ])
Contents
⼀、认识 matplotlib
⼆、⼀个最简单的绘图例⼦
三、 Figure 的组成
四、两种绘图接⼝
五、通⽤绘图模板
思考题
P rin t to P D F' metadata={'producer': 'Skia/PDF m138', 'total_pages': 4, 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'page': 0, 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'creationdate': '2025-07-04T16:00:40+00:00', 'page_label': '1', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'moddate': '2025-07-04T16:00:40+00:00'}
4.4.2 检索错误答案
我们可以看到一种新的失败的情况。
下面的问题询问了关于第二讲的问题,但也包括了来自其他讲的结果。
question_chinese = "他们在第二讲中对Figure说了些什么?"
docs_chinese = vectordb_chinese.similarity_search(question_chinese,k=5)
for doc_chinese in docs_chinese:print(doc_chinese.metadata)
metadata={'moddate': '2025-07-04T16:00:40+00:00', 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'creationdate': '2025-07-04T16:00:40+00:00', 'page_label': '1', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'producer': 'Skia/PDF m138', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'page': 0, 'total_pages': 4}
{'producer': 'Skia/PDF m138', 'creationdate': '2025-07-04T16:00:40+00:00', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'moddate': '2025-07-04T16:00:40+00:00', 'page_label': '2', 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'total_pages': 4, 'page': 1}
{'creationdate': '2025-07-04T16:00:40+00:00', 'moddate': '2025-07-04T16:00:40+00:00', 'page_label': '2', 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'page': 1, 'producer': 'Skia/PDF m138', 'total_pages': 4}
{'page': 1, 'moddate': '2025-07-04T16:00:40+00:00', 'total_pages': 4, 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'producer': 'Skia/PDF m138', 'page_label': '2', 'creationdate': '2025-07-04T16:00:40+00:00'}
{'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'creationdate': '2025-07-04T16:00:40+00:00', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'producer': 'Skia/PDF m138', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'moddate': '2025-07-04T16:00:40+00:00', 'page': 1, 'total_pages': 4, 'page_label': '2'}
{'page': 1, 'moddate': '2025-07-04T16:00:40+00:00', 'producer': 'Skia/PDF m138', 'creationdate': '2025-07-04T16:00:40+00:00', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'page_label': '2', 'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'total_pages': 4}
可见,虽然我们询问的问题是第二讲,但第一个出现的答案却是第一讲的内容。
在接下来的章节中,我们将探讨的方法能够有效地解答这两个问题!
五、检索(Retrieval)
在构建检索增强生成 (RAG) 系统时,信息检索是核心环节。检索模块负责对用户查询进行分析,从知识库中快速定位相关文档或段落,为后续的语言生成提供信息支持。检索是指根据用户的问题去向量数据库中搜索与问题相关的文档内容,当我们访问和查询向量数据库时可能会运用到如下几种技术:
- 基本语义相似度(Basic semantic similarity)
- 最大边际相关性(Maximum marginal relevance,MMR)
- 过滤元数据
- LLM辅助检索
图5.1 检索技术
使用基本的相似性搜索大概能解决你80%的相关检索工作,但对于那些相似性搜索失败的边缘情况该如何解决呢?这一章节我们将介绍几种检索方法,以及解决检索边缘情况的技巧,让我们一起开始学习吧!
5.1 向量数据库检索
本章节需要使用 lark 包,若环境中未安装过此包,请运行以下命令安装:
pip install lark
5.1.1 相似性检索(Similarity Search)
以我们的流程为例,前面课程已经存储了向量数据库( VectorDB ),包含各文档的语义向量表示。首先将
上节课所保存的向量数据库( VectorDB )加载进来,但是为了保证每个章节的独立性,我这里在第五章重新整理一份数据以及向量数据;
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from langchain_community.document_loaders import PyPDFLoader
from tool import get_azure_endpoint, get_api_key, get_api_versionif __name__ == '__main__':persist_directory_chinese = './docs/chroma/matplotlib/'embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())# 加载 PDFloaders_chinese = [# 故意添加重复文档,使数据混乱PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第二回:艺术画笔见乾坤.pdf"),PyPDFLoader("./docs/matplotlib/第三回:布局格式定方圆.pdf"),PyPDFLoader("./docs/matplotlib/第四回:文字图例尽眉目.pdf"),PyPDFLoader("./docs/matplotlib/第五回:样式色彩秀芳华.pdf")]docs = []for loader in loaders_chinese:docs.extend(loader.load())print(len(docs))# 分割文本from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, # 每个文本块的大小。这意味着每次切分文本时,会尽量使每个块包含 1500 个字符。chunk_overlap=150 # 每个文本块之间的重叠部分。)splits = text_splitter.split_documents(docs)vectordb_chinese = Chroma.from_documents(documents=splits,embedding=embedding,persist_directory=persist_directory_chinese # 允许我们将persist_directory目录保存到磁盘上)# 已经废弃,会自动保存vectordb_chinese.persist()vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)print(vectordb_chinese._collection.count())
52
下面我们来实现一下语义的相似度搜索,我们把三句话存入向量数据库Chroma中,然后我们提出问题让 向量数据库根据问题来搜索相关答案:
texts_chinese = ["""毒鹅膏菌(Amanita phalloides)具有大型且引人注目的地上(epigeous)子实体(basidiocarp)蘑菇""","""一种具有大型子实体的蘑菇是毒鹅膏菌(Amanita phalloides)。某些品种全白。""","""A. phalloides,又名死亡帽,是已知所有蘑菇中最有毒的一种。""",]
我们可以看到前两句都是描述的是一种叫“鹅膏菌”的菌类,包括它们的特征:有较大的子实体,第三句描 述的是“鬼笔甲”,一种已知的最毒的蘑菇,它的特征就是:含有剧毒。对于这个例子,我们将创建一个小 数据库,我们可以作为一个示例来使用。
smalldb_chinese = Chroma.from_texts(texts_chinese, embedding=embedding)
下面是我们对于这个示例所提出的问题:
question_chinese = "告诉我关于具有大型子实体的全白色蘑菇的信息"
现在,让针对上面问题进行相似性搜索,设置 k=2 ,只返回两个最相关的文档。
texts1 = smalldb_chinese.similarity_search(question_chinese, k=2)
print("texts1:", texts1)
texts1: [Document(metadata={}, page_content='一种具有大型子实体的蘑菇是毒鹅膏菌(Amanita phalloides)。某些品种全白。'), Document(metadata={}, page_content='毒鹅膏菌(Amanita phalloides)具有大型且引人注目的地上(epigeous)子实体(basidiocarp)蘑菇')]
我们现在可以看到,向量数据库返回了 2 个文档,就是我们存入向量数据库中的第二句和第一句。这里 我们很明显的就可以看到 chroma 的 similarity_search(相似性搜索) 方法可以根据问题的语义去数据 库中搜索与之相关性最高的文档,也就是搜索到了第一句和第二句的文本。但这似乎还存在一些问题, 因为第一句和第二句的含义非常接近,他们都是描述“鹅膏菌”及其“子实体”的,所以假如只返回其中的一 句就足以满足要求了,如果返回两句含义非常接近的文本感觉是一种资源的浪费。下面我们来看一下 max_marginal_relevance_search 的搜索结果。
5.1.2 解决多样性:最大边际相关性(MMR)
最大边际相关模型 (MMR,Maximal Marginal Relevance) 是实现多样性检索的常用算法。
图5.2 MMR
MMR 的基本思想是同时考量查询与文档的相关度,以及文档之间的相似度。相关度确保返回结果对查询 高度相关,相似度则鼓励不同语义的文档被包含进结果集。具体来说,它计算每个候选文档与查询的相 关度,并减去与已经选入结果集的文档的相似度。这样更不相似的文档会有更高的得分。
总之,MMR 是解决检索冗余问题、提供多样性结果的一种简单高效的算法。它平衡了相关性和多样性, 适用于对多样信息需求较强的应用场景。
我们来看一个利用 MMR 从蘑菇知识库中检索信息的示例。首先加载有关蘑菇的文档,然后运行 MMR 算法,设置 fetch_k 参数,用来告诉向量数据库我们最终需要 k 个结果返回。fetch_k=3 ,也就是我们最 初获取 3 个文档,k=2 表示返回最不同的 2 个文档。
texts2 = smalldb_chinese.max_marginal_relevance_search(question_chinese, k=2, fetch_k=3)
print("texts2:", texts2)
texts2: [Document(metadata={}, page_content='一种具有大型子实体的蘑菇是毒鹅膏菌(Amanita phalloides)。某些品种全白。'),Document(metadata={}, page_content='A. phalloides,又名死亡帽,是已知所有蘑菇中最有毒的一种。')]
这里我们看到 max_marginal_relevance_search(最大边际相关搜索) 返回了第二和第三句的文本,尽管第三句与我们的问题的相关性不太高,但是这样的结果其实应该是更加的合理,因为第一句和第二句文本本来就有着相似的含义,所以只需要返回其中的一句就可以了,另外再返回一个与问题相关性弱一点的答案(第三句文本),这样似乎增强了答案的多样性,相信用户也会更加偏爱;
还记得在上一节中我们介绍了两种向量数据在查询时的失败场景吗?当向量数据库中存在相同的文档时,而用户的问题又与这些重复的文档高度相关时,向量数据库会出现返回重复文档的情况。现在我们 就可以运用Langchain的 max_marginal_relevance_search 来解决这个问题:
我们首先看看前两个文档,只看前几个字符,可以看到它们是相同的。
question_chinese = "Matplotlib是什么?"
docs_ss_chinese = vectordb_chinese.similarity_search(question_chinese,k=3)
print("docs[0]: ")
print(docs_ss_chinese[0].page_content[:100])
print()
print("docs[1]: ")
print(docs_ss_chinese[1].page_content[:100])
docs[0]:
第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制docs[1]:
第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制
这里如果我们使用相似查询,会得到两个重复的结果,读者可以自己尝试一下,这里不再展示。我们可 以使用 MMR 得到不一样的结果。
docs_mmr_chinese = vectordb_chinese.max_marginal_relevance_search(question_chinese, k=3)
print("docs[0]: ")
print(docs_mmr_chinese[0].page_content[:100])
print()
print("docs[1]: ")
print(docs_mmr_chinese[1].page_content[:100])
docs[0]:
第⼀回: Matplotlib 初相识
⼀、认识 matplotlib
Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制docs[1]:
4. 单字符基本颜⾊
# matplotlib 有⼋个基本颜⾊,可以⽤单字符串来表示,分别是 'b', 'g', 'r', 'c', 'm', 'y', 'k',
'w' ,对应的是 blue, g
从以上结果中可以看到,向量数据库返回了 2 篇完全不同的文档,这是因为我们使用的是 MMR 搜索, 它把搜索结果中相似度很高的文档做了过滤,所以它保留了结果的相关性又同时兼顾了结果的多样性。
5.1.3 解决特殊性:使用元数据
在上一个章节中,关于失败的应用场景我们还提出了一个问题,是询问了关于文档中某一讲的问题,但得到的结果中也包括了来自其他讲的结果。这是我们所不希望看到的结果,之所以产生这样的结果是因为当我们向向量数据库提出问题时,数据库并没有很好的理解问题的语义,所以返回的结果不如预期。要 解决这个问题,我们可以通过过滤元数据的方式来实现精准搜索,当前很多向量数据库都支持对元数据的操作:
metadata 为每个嵌入的块(embedded chunk)提供上下文。
question_chinese = "他们在第二讲中对Figure说了些什么?"
现在,我们以手动的方式来解决这个问题,我们会指定一个元数据过滤器 filter
docs_chinese = vectordb_chinese.similarity_search(question_chinese,k=3,filter={"source": "./docs/matplotlib/第二回:艺术画笔见乾坤.pdf"})
接下来,我们可以看到结果都来自对应的章节
for d in docs_chinese:print(d.metadata)
{'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'creationdate': '2025-07-05T03:01:39+00:00', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'moddate': '2025-07-05T03:01:39+00:00', 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'page': 9, 'page_label': '10', 'total_pages': 13, 'producer': 'Skia/PDF m138'}
{'total_pages': 13, 'page_label': '11', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'moddate': '2025-07-05T03:01:39+00:00', 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'producer': 'Skia/PDF m138', 'page': 10, 'creationdate': '2025-07-05T03:01:39+00:00'}
{'total_pages': 13, 'page': 0, 'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'creationdate': '2025-07-05T03:01:39+00:00', 'producer': 'Skia/PDF m138', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'page_label': '1', 'moddate': '2025-07-05T03:01:39+00:00', 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf'}
当然,我们不能每次都采用手动的方式来解决这个问题,这会显得不够智能。下一小节中,我们将展示 通过LLM来解决这个问题。
5.1.4 解决特殊性:在元数据中使用自查询检索器(LLM辅助检索)
在上例中,我们手动设置了过滤参数 filter 来过滤指定文档。但这种方式不够智能,需要人工指定过滤条
件。如何自动从用户问题中提取过滤信息呢? LangChain提供了SelfQueryRetriever模块,它可以通过语言模型从问题语句中分析出:
- 向量搜索的查询字符串(search term)
- 过滤文档的元数据条件(Filter)
以“除了维基百科,还有哪些健康网站”为例,SelfQueryRetriever可以推断出“除了维基百科”表示需要过滤的条件,即排除维基百科的文档。
它使用语言模型自动解析语句语义,提取过滤信息,无需手动设置。这种基于理解的元数据过滤更加智能方便,可以自动处理更复杂的过滤逻辑。
掌握利用语言模型实现自动化过滤的技巧,可以大幅降低构建针对性问答系统的难度。这种自抽取查询的方法使检索更加智能和动态。
其原理如下图所示:
图5.3 自抽取查询
下面我们就来实现一下LLM辅助检索:
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import AzureChatOpenAIfrom tool import get_azure_endpoint,get_api_version,get_api_keyif __name__ == '__main__':llm = AzureChatOpenAI(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLazure_deployment="gpt-3.5-turbo", # 重命名为 azure_deploymentopenai_api_version=get_api_version(), # 参数名不变openai_api_key=get_api_key(),openai_api_type="azure",)
这里我们首先定义了 metadata_field_info_chinese ,它包含了元数据的过滤条件 source 和 page , 其 中 source 的作用是告诉 LLM 我们想要的数据来自于哪里, page 告诉 LLM 我们需要提取相关的内容在 原始文档的哪一页。有了 metadata_field_info_chinese 信息后,LLM会自动从用户的问题中提取出上图 中的 Filter 和 Search term 两项,然后向量数据库基于这两项去搜索相关的内容。下面我们看一下查询结果:
embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version()
)
vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,
)
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`, or `./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)
for d in docs_chinese:print(d.metadata)
{'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'page_label': '1', 'total_pages': 13, 'producer': 'Skia/PDF m138', 'moddate': '2025-07-05T03:01:39+00:00', 'page': 0, 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'creationdate': '2025-07-05T03:01:39+00:00'}
{'page_label': '6', 'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'moddate': '2025-07-05T03:01:39+00:00', 'creationdate': '2025-07-05T03:01:39+00:00', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'page': 5, 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'producer': 'Skia/PDF m138', 'total_pages': 13}
{'creationdate': '2025-07-05T03:01:39+00:00', 'moddate': '2025-07-05T03:01:39+00:00', 'page_label': '10', 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'page': 9, 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'producer': 'Skia/PDF m138', 'total_pages': 13}
{'title': '第二回:艺术画笔见乾坤 — fantastic-matplotlib', 'page_label': '7', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'page': 6, 'source': './docs/matplotlib/第二回:艺术画笔见乾坤.pdf', 'total_pages': 13, 'moddate': '2025-07-05T03:01:39+00:00', 'creationdate': '2025-07-05T03:01:39+00:00', 'producer': 'Skia/PDF m138'}
5.1.5 其他技巧:压缩
在使用向量检索获取相关文档时,直接返回整个文档片段可能带来资源浪费,因为实际相关的只是文档的一小部分。为改进这一点,LangChain提供了一种“ 压缩 ”检索机制。其工作原理是,先使用标准向量检索获得候选文档,然后基于查询语句的语义,使用语言模型压缩这些文档,只保留与问题相关的部分。 例如,对“蘑菇的营养价值”这个查询,检索可能返回整篇有关蘑菇的长文档。经压缩后,只提取文档中与“营养价值”相关的句子。
图5.4 压缩
从上图中我们看到,当向量数据库返回了所有与问题相关的所有文档块的全部内容后,会有一个 Compression LLM来负责对这些返回的文档块的内容进行压缩,所谓压缩是指仅从文档块中提取出和用 户问题相关的内容,并舍弃掉那些不相关的内容。
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import AzureChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from tool import get_azure_endpoint,get_api_version,get_api_key
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)]))if __name__ == '__main__':llm = AzureChatOpenAI(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLazure_deployment="gpt-4o-mini", # 重命名为 azure_deploymentopenai_api_version=get_api_version(), # 参数名不变openai_api_key=get_api_key(),openai_api_type="azure",)embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)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)
Document 1:Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静态,动态,交互式的图表。 Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包等。 Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具,我们所熟知的 pandas 和 seaborn 的绘图接⼝其实也是基于 matplotlib 所作的⾼级封装。
----------------------------------------------------------------------------------------------------
Document 2:Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静态,动态,交互式的图表。 Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包 等。 Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具,我们所熟知的 pandas 和 seaborn 的绘图接⼝其实也是基于 matplotlib 所作的⾼级封装。
----------------------------------------------------------------------------------------------------
Document 3:五、通⽤绘图模板
由于 matplotlib 的知识点⾮常繁杂,在实际使⽤过程中也不可能将全部 API 都记住,很多时候都是边⽤边查。因此这⾥提供⼀个通
⽤的绘图基础模板,任何复杂的图表⼏乎都可以基于这个模板⻣架填充内容⽽成。初学者刚开始学习时只需要牢记这⼀模板就⾜
以应对⼤部分简单图表的绘制,在学习过程中可以将这个模板模块化,了解每个模块在做什么,在绘制复杂图表时如何修改,填
充对应的模块。
# step1 准备数据
x = np.linspace(0 , 2 , 1 0 0 )
y = x**2
# step2 设置绘图样式,这⼀模块的扩展参考第五章进⼀步学习,这⼀步不是必须的,样式也可以在绘制图像是进⾏设置
mpl.rc('lines', linewidth=4 , linestyle='-.')
# step3 定义布局, 这⼀模块的扩展参考第三章进⼀步学习
fig, ax = plt.subplots()
# step4 绘制图像, 这⼀模块的扩展参考第⼆章进⼀步学习
ax.plot(x, y, label='linear')
# step5 添加标签,⽂字和图例,这⼀模块的扩展参考第四章进⼀步学习
ax.set_xlabel('x label')
ax.set_ylabel('y label')
ax.set_title("Simple Plot")
ax.legend() ;
在上面的代码中我们定义了一个 LLMChainExtractor ,它是一个压缩器,它负责从向量数据库返回的文 档块中提取相关信息,然后我们还定义了 ContextualCompressionRetriever ,它有两个参数: base_compressor 和 base_retriever,其中 base_compressor 是我们前面定义的 LLMChainExtractor 的实例,base_retriever是早前定义的 vectordb 产生的检索器。
现在当我们提出问题后,查看结果文档,我们可以看到两件事。
- 它们比正常文档短很多
- 仍然有一些重复的东西,这是因为在底层我们使用的是语义搜索算法。
从上述例子中,我们可以发现这种压缩可以有效提升输出质量,同时节省通过长文档带来的计算资源浪
费,降低成本。上下文相关的压缩检索技术,使得到的支持文档更严格匹配问题需求,是提升问答系统
效率的重要手段。读者可以在实际应用中考虑这一技术。
5.2 结合各种技术
为了去掉结果中的重复文档,我们在从向量数据库创建检索器时,可以将搜索类型设置为 MMR 。然后 我们可以重新运行这个过程,可以看到我们返回的是一个过滤过的结果集,其中不包含任何重复的信息。
compression_retriever_chinese1 = ContextualCompressionRetriever(base_compressor=compressor,base_retriever=vectordb_chinese.as_retriever(search_type="mmr"))
question_chinese = "Matplotlib是什么?"
compressed_docs_chinese = compression_retriever_chinese1.get_relevant_documents(question_chinese)
pretty_print_docs(compressed_docs_chinese)
Document 1:Matplotlib 是⼀个 Python 2D 绘图库,能够以多种硬拷⻉格式和跨平台的交互式环境⽣成出版物质量的图形,⽤来绘制各种静态,动态,交互式的图表。
Matplotlib 可⽤于 Python 脚本, Python 和 IPython Shell 、 Jupyter notebook , Web 应⽤程序服务器和各种图形⽤户界⾯⼯具包等。
Matplotlib 是 Python 数据可视化库中的泰⽃,它已经成为 python 中公认的数据可视化⼯具。
5.3 其他类型的检索
值得注意的是,vetordb 并不是唯一一种检索文档的工具。 LangChain 还提供了其他检索文档的方式, 例如: TF-IDF 或 SVM 。
from langchain_community.retrievers import SVMRetriever
from langchain_community.retrievers import TFIDFRetriever
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from tool import get_azure_endpoint,get_api_version,get_api_key
if __name__ == '__main__':embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())# 加载PDFloader_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 检索的效果:
question_chinese = "这门课的主要主题是什么?"
docs_svm_chinese = svm_retriever.get_relevant_documents(question_chinese)
print(docs_svm_chinese[0])
page_content='mpl.rc('lines', linewidth=4 , linestyle='-.')
# step3 定义布局, 这⼀模块的扩展参考第三章进⼀步学习
fig, ax = plt.subplots()
# step4 绘制图像, 这⼀模块的扩展参考第⼆章进⼀步学习
ax.plot(x, y, label='linear')
# step5 添加标签,⽂字和图例,这⼀模块的扩展参考第四章进⼀步学习
ax.set_xlabel('x label')
ax.set_ylabel('y label')
ax.set_title("Simple Plot")
ax.legend() ; By Datawhale 数据可视化开源⼩组
© Copyright © Copyright 2021.
思考题
请思考两种绘图模式的优缺点和各⾃适合的使⽤场景
在第五节绘图模板中我们是以 OO 模式作为例⼦展示的,请思考并写⼀个 pyplot 绘图模式的简单模板'
可以看出,SVM 检索的效果要差于 VectorDB。
question_chinese = "Matplotlib是什么?"
docs_tfidf_chinese = tfidf_retriever.get_relevant_documents(question_chinese) print(docs_tfidf_chinese[0])
page_content='mpl.rc('lines', linewidth=4 , linestyle='-.')
# step3 定义布局, 这⼀模块的扩展参考第三章进⼀步学习
fig, ax = plt.subplots()
# step4 绘制图像, 这⼀模块的扩展参考第⼆章进⼀步学习
ax.plot(x, y, label='linear')
# step5 添加标签,⽂字和图例,这⼀模块的扩展参考第四章进⼀步学习
ax.set_xlabel('x label')
ax.set_ylabel('y label')
ax.set_title("Simple Plot")
ax.legend() ; By Datawhale 数据可视化开源⼩组
© Copyright © Copyright 2021.
思考题
请思考两种绘图模式的优缺点和各⾃适合的使⽤场景
在第五节绘图模板中我们是以 OO 模式作为例⼦展示的,请思考并写⼀个 pyplot 绘图模式的简单模板'
同样,TF-IDF 检索的效果也不尽如人意。
5.4 总结
今天的课程涵盖了向量检索的多项新技术,让我们快速回顾关键要点:
- MMR 算法可以实现兼具相关性与多样性的检索结果,避免信息冗余。
- 定义元数据字段可以进行针对性过滤,提升匹配准确率。
- SelfQueryRetriever 模块通过语言模型自动分析语句,提取查询字符串与过滤条件,无需手动设 置,使检索更智能。
- ContextualCompressionRetriever 实现压缩检索,仅返回与问题相关的文档片段,可以大幅提升 效率并节省计算资源。
- 除向量检索外,还简要介绍了基于 SVM 和 TF-IDF 的检索方法。
这些技术为我们构建可交互的语义搜索模块提供了重要支持。熟练掌握各检索算法的适用场景,将大大增强问答系统的智能水平。希望本节的教程能够对大家有所帮助!
六、问答
Langchain 在实现与外部数据对话的功能时需要经历下面的5个阶段,它们分别是:
Document Loading- >Splitting->Storage->Retrieval->Output
如下图所示:
图6.1 访问个人数据全流程
我们已经完成了整个存储和获取,获取了相关的切分文档之后,现在我们需要将它们传递给语言模型,以获得答案。这个过程的一般流程如下:
- 首先问题被提出,
- 然后我们查找相关的文档,
- 接着将这些切分文档和系统提示一起传递给语言模型
- 获得答案。
默认情况下,我们将所有的文档切片都传递到同一个上下文窗口中,即同一次语言模型调用中。然而, 有一些不同的方法可以解决这个问题,它们都有优缺点。大部分优点来自于有时可能会有很多文档,但你简单地无法将它们全部传递到同一个上下文窗口中。MapReduce、Refine 和 MapRerank 是三种方法,用于解决这个短上下文窗口的问题。我们将在本章中进行简要介绍。
在上一章,我们已经讨论了如何检索与给定问题相关的文档。下一步是获取这些文档,拿到原始问题,将它们一起传递给语言模型,并要求它回答这个问题。在本节中,我们将详细介绍这一过程,以及完成这项任务的几种不同方法。
6.1 加载向量数据库
首先我们加载之前已经进行持久化的向量数据库:
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from langchain_community.document_loaders import PyPDFLoader
from tool import get_azure_endpoint, get_api_key, get_api_versionif __name__ == '__main__':persist_directory_chinese = './docs/chroma/matplotlib/'embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())# 加载 PDFloaders_chinese = [# 故意添加重复文档,使数据混乱PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第二回:艺术画笔见乾坤.pdf"),PyPDFLoader("./docs/matplotlib/第三回:布局格式定方圆.pdf"),PyPDFLoader("./docs/matplotlib/第四回:文字图例尽眉目.pdf"),PyPDFLoader("./docs/matplotlib/第五回:样式色彩秀芳华.pdf")]docs = []for loader in loaders_chinese:docs.extend(loader.load())print(len(docs))# 分割文本from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, # 每个文本块的大小。这意味着每次切分文本时,会尽量使每个块包含 1500 个字符。chunk_overlap=150 # 每个文本块之间的重叠部分。)splits = text_splitter.split_documents(docs)vectordb_chinese = Chroma.from_documents(documents=splits,embedding=embedding,persist_directory=persist_directory_chinese # 允许我们将persist_directory目录保存到磁盘上)# 已经废弃,会自动保存vectordb_chinese.persist()vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)print(vectordb_chinese._collection.count())
52
我们可以测试一下对于一个提问进行向量检索。如下代码会在向量数据库中根据相似性进行检索,返回 给你 k 个文档。
question = "这节课的主要话题是什么"
docs = vectordb_chinese.similarity_search(question, k=3)
print(len(docs))
3
6.2 构造检索式问答链
基于 LangChain,我们可以构造一个使用 GPT4o 进行问答的检索式问答链,这是一种通过检索步骤进行问答的方法。我们可以通过传入一个语言模型和一个向量数据库来创建它作为检索器。然后,我们可以用问题作为查询调用它,得到一个答案。
图6.2 检索式问答链
from langchain.chains import RetrievalQA
from langchain_openai import AzureChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from tool import get_azure_endpoint,get_api_version,get_api_key
if __name__ == '__main__':llm = AzureChatOpenAI(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLazure_deployment="gpt-4o-mini", # 重命名为 azure_deploymentopenai_api_version=get_api_version(), # 参数名不变openai_api_key=get_api_key(),openai_api_type="azure",)embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)# 声明一个检索式问答链qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever())# 可以以该方式进行检索问答question = "这节课的主要话题是什么" result = qa_chain({"query": question})print(result["result"])
这节课的主要话题是关于matplotlib库的基础知识,包括其三层API的结构、Artist对象的分类(primitives和containers),以及它们在图形绘制中的作用。
6.3 深入探究检索式问答链
在获取与问题相关的文档后,我们需要将文档和原始问题一起输入语言模型,生成回答。默认是合并所有文档,一次性输入模型。但存在上下文长度限制的问题,若相关文档量大,难以一次将全部输入模型。针对这一问题,本章将介绍 MapReduce 、Refine 和 MapRerank 三种策略。
- MapReduce 通过多轮检索与问答实现长文档处理
- Refine 让模型主动请求信息
- MapRerank 则通过问答质量调整文档顺序。
图6.3 三种其他策略
三种策略各有优劣 MapReduce 分批处理长文档,Refine 实现可交互问答,MapRerank 优化信息顺序,掌握这些技巧,可以应对语言模型的上下文限制,解决长文档问答困难,提升问答覆盖面。
通过上述代码,我们可以实现一个简单的检索式问答链。接下来,让我们深入其中的细节,看看在这个检索式问答链中,LangChain 都做了些什么。
6.3.1 基于模板的检索式问答链
我们首先定义了一个提示模板。它包含一些关于如何使用下面的上下文片段的说明,然后有一个上下文变量的占位符。
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
from langchain_openai import AzureChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from tool import get_azure_endpoint,get_api_version,get_api_keyif __name__ == '__main__':# Build prompttemplate = """使用以下上下文片段来回答最后的问题。如果你不知道答案,只需说不知道,不要试图编造 答案。答案最多使用三个句子。尽量简明扼要地回答。在回答的最后一定要说"感谢您的提问!"{context}问题:{question}有用的回答:"""QA_CHAIN_PROMPT = PromptTemplate.from_template(template)llm = AzureChatOpenAI(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLazure_deployment="gpt-4o-mini", # 重命名为 azure_deploymentopenai_api_version=get_api_version(), # 参数名不变openai_api_key=get_api_key(),openai_api_type="azure",)embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)
接着我们基于该模板来构建检索式问答链:
# Run chain
qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever(),return_source_documents=True,chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)
构建出的检索式问答链使用方法同上:
question = "这门课需要学习 python 吗"
result = qa_chain({"query": question})
print(result["result"])
是的,这门课需要学习 Python,特别是涉及到使用 Matplotlib 进行绘图的内容。掌握 Python 编程语言将有助于顺利进行数据可视化和相关的实践。感谢您的提问!
可以查看其检索到的源文档:
print(result["source_documents"][0])
page_content='⽽如果采⽤第⼆种绘图接⼝,绘制同样的图,代码是这样的:
x = np.linspace(0 , 2 , 1 0 0 )
plt.plot(x, x, label='linear')
plt.plot(x, x**2 , label='quadratic')
plt.plot(x, x**3 , label='cubic')
plt.xlabel('x label')
plt.ylabel('y label')
plt.title("Simple Plot")
plt.legend()
plt.show()
五、通⽤绘图模板
由于 matplotlib 的知识点⾮常繁杂,在实际使⽤过程中也不可能将全部 API 都记住,很多时候都是边⽤边查。因此这⾥提供⼀个通
⽤的绘图基础模板,任何复杂的图表⼏乎都可以基于这个模板⻣架填充内容⽽成。初学者刚开始学习时只需要牢记这⼀模板就⾜
以应对⼤部分简单图表的绘制,在学习过程中可以将这个模板模块化,了解每个模块在做什么,在绘制复杂图表时如何修改,填
充对应的模块。
# step1 准备数据
x = np.linspace(0 , 2 , 1 0 0 )
y = x**2
# step2 设置绘图样式,这⼀模块的扩展参考第五章进⼀步学习,这⼀步不是必须的,样式也可以在绘制图像是进⾏设置
mpl.rc('lines', linewidth=4 , linestyle='-.')
# step3 定义布局, 这⼀模块的扩展参考第三章进⼀步学习
fig, ax = plt.subplots()
# step4 绘制图像, 这⼀模块的扩展参考第⼆章进⼀步学习
ax.plot(x, y, label='linear')
# step5 添加标签,⽂字和图例,这⼀模块的扩展参考第四章进⼀步学习
ax.set_xlabel('x label')
ax.set_ylabel('y label')
ax.set_title("Simple Plot")
ax.legend() ;' metadata={'source': './docs/matplotlib/第一回:Matplotlib初相识.pdf', 'page': 2, 'producer': 'Skia/PDF m138', 'title': '第一回:Matplotlib初相识 — fantastic-matplotlib', 'creator': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 'moddate': '2025-07-04T16:00:40+00:00', 'creationdate': '2025-07-04T16:00:40+00:00', 'page_label': '3', 'total_pages': 4}
这种方法非常好,因为它只涉及对语言模型的一次调用。然而,它也有局限性,即如果文档太多,可能无法将它们全部适配到上下文窗口中。我们可以使用另一种技术来对文档进行问答,即 MapReduce 技术。
6.3.2 基于 MapReduce 的检索式问答链
在 MapReduce 技术中,首先将每个独立的文档单独发送到语言模型以获取原始答案。然后,这些答案通过最终对语言模型的一次调用组合成最终的答案。虽然这样涉及了更多对语言模型的调用,但它的优势在于可以处理任意数量的文档。
# Run chain
qa_chain_mr = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever(),chain_type="map_reduce"
)
question = "这门课需要学习 python 吗"
result = qa_chain_mr({"query": question})
print(result["result"])
文本中没有直接提到是否需要学习 Python,但提到使用 Python 的 matplotlib 库进行绘图。因此可以推测,这门课可能需要学习 Python,尤其是在数据可视化方面。
当我们将之前的问题通过这个链进行运行时,我们可以看到这种方法的两个问题。第一,速度要慢得多。第二,结果实际上更差。根据给定文档的这一部分,对这个问题并没有明确的答案。这可能是因为它是基于每个文档单独回答的。因此,如果信息分布在两个文档之间,它并没有在同一上下文中获取到所有的信息。
6.3.3 基于 Refine 的检索式问答链
我们还可以将链式类型设置为 Refine ,这是一种新的链式策略。Refine 文档链类似于 MapReduce ,对于每一个文档,会调用一次 LLM。但改进之处在于,最终输入语言模型的 Prompt 是一个序列,将之前的回复与新文档组合在一起,并请求得到改进后的响应。因此,这是一种类似于 RNN 的概念,增强了上下文信息,从而解决信息分布在不同文档的问题。
例如第一次调用,Prompt 包含问题与文档 A ,语言模型生成初始回答。第二次调用,Prompt 包含第一次回复、文档 B ,请求模型更新回答,以此类推。
# Run chain
qa_chain_mr = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever(),chain_type="refine"
)
question = "这门课需要学习 python 吗"
result = qa_chain_mr({"query": question})
print(result["result"])
是的,这门课需要学习 Python,尤其是使用 Python 的 matplotlib 库进行绘图。根据提供的代码示例,学习如何使用 matplotlib 来绘制不同类型的图表是课程的核心内容之一。课程中提供了一个通用的绘图模板,让初学者可以通过以下五个步骤来绘制图形:1. **准备数据**:使用 `np.linspace()` 生成所需的 x 数据点,例如 `x = np.linspace(0, 2, 100)`,然后定义 y 数据。
2. **设置绘图样式**:通过 `mpl.rc()` 配置线条样式,这个步骤不是必须的,但可以提升图形的美观程度。
3. **定义布局**:使用 `plt.subplots()` 创建图形和子图,这样可以轻松管理图形的布局。
4. **绘制图像**:使用 `ax.plot()` 方法绘制线条和数据点,例如绘制线性、二次和三次函数。
5. **添加标签和图例**:使用 `ax.set_xlabel()`、`ax.set_ylabel()`、`ax.set_title()` 和 `ax.legend()` 来为图形添加相应的标签和图例,以便于解读。由于 matplotlib 的知识点非常繁杂,掌握这个基本的绘图模板可以帮助你应对大部分简单图表的绘制。初学者可以在学习过程中将这个模板模块化,了解每个模块的功能,逐步掌握更复杂的绘图技巧。因此,学习 Python 和 matplotlib 是非常必要和有益的。
你会注意到,这个结果比 MapReduce 链的结果要好。这是因为使用 Refine 文档链通过累积上下文,使语言模型能渐进地完善答案,而不是孤立处理每个文档。这种策略可以有效解决信息分散带来的语义不完整问题。
6.4 实验:状态记录
让我们在这里做一个实验。我们将创建一个 QA 链,使用默认的 stuff 。让我们问一个问题,这门课要学习 Python 吗? 它会回答,学习 Python 是必要的。
# Run chain
qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever(),return_source_documents=True,chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)
question = "这门课需要学习 python 吗"
result = qa_chain({"query": question})
print(result["result"])
这门课程涉及到 matplotlib 的使用,而 matplotlib 是 Python 的一个数据可视化库,因此学习 Python 是必要的。掌握 Python 能帮助学生更好地理解并应用该库进行数据可视化。感谢您的提问!
我们将追问,为什么需要这一前提?然后我们得到了一个答案:“这一前提介绍了 Matplotlib 是什么以及 它的基本概念,包括 Figure、Axes、Artist 等,这些是 Matplotlib 绘图的基础,了解这些概念可以帮助 用户更好地理解 Matplotlib 的使用方法和绘图原理。因此,在学习 Matplotlib 之前,了解这些基本概念 是非常必要的。”这与之前问有关 Python 的问题毫不相关。
question1 = "为什么需要这一前提"
result1 = qa_chain({"query": question1})
print(result1["result"])
这一前提可以帮助我们更好地理解和比较不同的绘图模式,以便选择最合适的工具来满足特定的可视化需求。了解这两种模式的优缺点也有助于在实际应用中避开潜在的问题。感谢您的提问!
基本上,我们使用的链式(chain)没有任何状态的概念。它不记得之前的问题或之前的答案。为了实现 这一点,我们需要引入内存,这是我们将在下一节中讨论的内容。
七、聊天Chat
回想一下检索增强生成 (retrieval augmented generation,RAG) 的整体工作流程:
图7.1 RAG
我们已经接近完成一个功能性的聊天机器人了。我们讨论了 **文档加载 **、 切分 、 **存储 **和 检索 。我们展示了如何使用 检索 QA 链在 Q+A 中使用 检索 生成输出。 我们的机器人已经可以回答问题了,但还无法处理后续问题,无法进行真正的对话。
在本章中,我们将解决这个问题。我们现在将创建一个问答聊天机器人。它与之前非常相似,但我们将添加聊天历史的功能。这是您之前进行的任何对话或消息。这将使机器人在尝试回答问题时能够考虑到聊天历史的上下文。所以,如果您继续提问,它会知道您想谈论什么。
7.1 复现之前的代码
首先我们加载在前几节课创建的向量数据库,并测试一下:
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import AzureOpenAIEmbeddings # 导入嵌入模型
from langchain_community.document_loaders import PyPDFLoader
from tool import get_azure_endpoint, get_api_key, get_api_versionif __name__ == '__main__':persist_directory_chinese = './docs/chroma/matplotlib/'embedding = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())# 加载 PDFloaders_chinese = [# 故意添加重复文档,使数据混乱PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第一回:Matplotlib初相识.pdf"),PyPDFLoader("./docs/matplotlib/第二回:艺术画笔见乾坤.pdf"),PyPDFLoader("./docs/matplotlib/第三回:布局格式定方圆.pdf"),PyPDFLoader("./docs/matplotlib/第四回:文字图例尽眉目.pdf"),PyPDFLoader("./docs/matplotlib/第五回:样式色彩秀芳华.pdf")]docs = []for loader in loaders_chinese:docs.extend(loader.load())print(len(docs))# 分割文本from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, # 每个文本块的大小。这意味着每次切分文本时,会尽量使每个块包含 1500 个字符。chunk_overlap=150 # 每个文本块之间的重叠部分。)splits = text_splitter.split_documents(docs)vectordb_chinese = Chroma.from_documents(documents=splits,embedding=embedding,persist_directory=persist_directory_chinese # 允许我们将persist_directory目录保存到磁盘上)# 已经废弃,会自动保存vectordb_chinese.persist()vectordb_chinese = Chroma(persist_directory="./docs/chroma/matplotlib",embedding_function=embedding,)print(vectordb_chinese._collection.count())question = "这节课的主要话题是什么"docs = vectordb_chinese.similarity_search(question, k=3)print(len(docs))
3
接着我们从 OpenAI 的 API 创建一个 LLM:
from langchain_openai import AzureChatOpenAI # 导入嵌入模型
from tool import get_azure_endpoint, get_api_version, get_api_key
llm = AzureChatOpenAI(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLazure_deployment="gpt-4o-mini", # 重命名为 azure_deploymentopenai_api_version=get_api_version(), # 参数名不变openai_api_key=get_api_key(),openai_api_type="azure",
)
response = llm.predict("你好")
print(response)
你好!有什么我可以帮助你的吗?
再创建一个基于模板的检索链:
# 构建 prompt
from langchain.prompts import PromptTemplate
template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答 案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
有用的回答:"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"], template=template, )
# 运行 chain
from langchain.chains import RetrievalQA
question = "这门课的主题是什么?"
qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever(),return_source_documents=True,chain_type_kwargs = {"prompt":QA_CHAIN_PROMPT})
result = qa_chain({"query": question})
print(result["result"])
这门课的主题主要是关于基本元素和对象容器的使用,特别是在图形绘制和文本标注方面。它涵盖了2D线条、图形补丁、集合、图像等基础元素,以及如何使用不同的对象容器来组织和管理图形内容。谢谢你的提问!
7.2 记忆
现在让我们更进一步,添加一些记忆功能。
我们将使用 ConversationBufferMemory 。它保存聊天消息历史记录的列表,这些历史记录将在回答问题时与问题一起传递给聊天机器人,从而将它们添加到上下文中。
需要注意的是,我们之前讨论的上下文检索等方法,在这里同样可用。
from langchain.memory import ConversationBufferMemory# 自定义内存类
class CustomConversationBufferMemory(ConversationBufferMemory):def save_context(self, inputs: dict, outputs: dict) -> None:# 只保存 'answer' 键的值if 'answer' in outputs:super().save_context(inputs, {'answer': outputs['answer']}
memory = CustomConversationBufferMemory(memory_key="chat_history", return_messages=True)
7.3 对话检索链
对话检索链(ConversationalRetrievalChain)在检索 QA 链的基础上,增加了处理对话历史的能力。 它的工作流程是:
- 将之前的对话与新问题合并生成一个完整的查询语句。
- 在向量数据库中搜索该查询的相关文档。
- 获取结果后,存储所有答案到对话记忆区。
- 用户可在 UI 中查看完整的对话流程。
图7.2 对话检索链
这种链式方式将新问题放在之前对话的语境中进行检索,可以处理依赖历史信息的查询。并保留所有信息在对话记忆中,方便追踪。 接下来让我们可以测试这个对话检索链的效果: 首先提出一个无历史的问题“这门课会学习 Python 吗?”,并查看回答。
qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb_chinese.as_retriever(),chain_type="stuff",return_source_documents=True,chain_type_kwargs={"prompt": QA_CHAIN_PROMPT},memory=memory,output_key="answer")question = "这门课需要学习 python 吗"
result = qa_chain({"query": question})
print(result["answer"])
是的,这门课程涉及到使用 matplotlib 进行数据可视化,因此需要学习 Python 语言。掌握 Python 可以帮助更好地理解和应用相关的绘图技术。谢谢你的提问!
然后基于答案进行下一个问题“为什么这门课需要这个前提?”
question1 = "为什么这门课需要这个前提?"
result1 = qa_chain({"question": question1})
print(result['answer'])
这门课需要这些基本元素和对象容器作为前提,因为它们构成了理解图形和数据可视化的基础。学习这些概念有助于掌握如何有效创建和管理图形对象。谢谢你的提问!
可以看到,虽然 LLM 的回答有些不对劲,但它准确地判断了这个前提的指代内容是学习 Python,也就 是我们成功地传递给了它历史信息。这种持续学习和关联前后问题的能力,可大大增强问答系统的连续 性和智能水平。
7.4 定义一个适用于您文档的聊天机器人
通过上述所学内容,我们可以通过以下代码来定义一个适用于私人文档的聊天机器人:
import osfrom langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import DocArrayInMemorySearchfrom langchain.chains import ConversationalRetrievalChainfrom langchain_community.document_loaders import PyPDFLoader
from langchain_openai.embeddings import AzureOpenAIEmbeddings
from langchain_openai import AzureChatOpenAI # 导入嵌入模型
from dotenv import load_dotenv, find_dotenv
import panel as pn
import param
from bokeh.io import curdocdef get_api_version():_ = load_dotenv(find_dotenv())api_version = os.environ.get('API_VERSION')print(f"API Version: {api_version}")return api_versiondef get_azure_endpoint():_ = load_dotenv(find_dotenv())azure_endpoint = os.environ.get('AZURE_ENDPOINT')print(f"Azure Endpoint: {azure_endpoint}")return azure_endpointdef get_api_key():_ = load_dotenv(find_dotenv())api_key = os.environ.get('API_KEY')print(f"API Key: {api_key}")return api_keydef load_db(file, chain_type, k):"""该函数用于加载 PDF 文件,切分文档,生成文档的嵌入向量,创建向量数据库,定义检索器,并创建聊天机器人实例。参数:file (str): 要加载的 PDF 文件路径。chain_type (str): 链类型,用于指定聊天机器人的类型。 k (int): 在检索过程中,返回最相似的 k 个结果。返回:qa (ConversationalRetrievalChain): 创建的聊天机器人实例。"""# 载入文档loader = PyPDFLoader(file)documents = loader.load()print(f"Documents loaded: {len(documents)}")# 切分文档text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)docs = text_splitter.split_documents(documents) # 定义 Embeddingsprint(f"Documents split into chunks: {len(docs)}")embeddings = AzureOpenAIEmbeddings(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLmodel="text-embedding-3-small", # 重命名为 azure_deploymentapi_key=get_api_key(),api_version=get_api_version())# 根据数据创建向量数据库db = DocArrayInMemorySearch.from_documents(docs, embeddings)print("Vector database created.")# 定义检索器retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": k}) # 创建 chatbot 链,Memory 由外部管理llm = AzureChatOpenAI(azure_endpoint=get_azure_endpoint().rstrip('/'), # 移除尾部斜杠,只保留基础URLazure_deployment="gpt-4o-mini", # 重命名为 azure_deploymentopenai_api_version=get_api_version(), # 参数名不变openai_api_key=get_api_key(),openai_api_type="azure",)qa = ConversationalRetrievalChain.from_llm(llm=llm,chain_type=chain_type,retriever=retriever,return_source_documents=True,return_generated_question=True,)print("Conversational Retrieval Chain created.")return qa# 用于存储聊天记录、回答、数据库查询和回复
class cbfs(param.Parameterized):chat_history = param.List([])answer = param.String("")db_query = param.String("")db_response = param.List([])def __init__(self, **params):super(cbfs, self).__init__(**params)self.panels = []self.loaded_file = "./part4/chapter7/docs/matplotlib/第一回:Matplotlib初相识.pdf"self.qa = load_db(self.loaded_file, "stuff", 4)# 将文档加载到聊天机器人中def call_load_db(self, event):if event.new == 0 or file_input.value is None: # 初始化或未指定文件return pn.pane.Markdown(f"Loaded File: {self.loaded_file}")else:file_input.save("temp.pdf") # 本地副本self.loaded_file = file_input.filenamebutton_load.button_style = "outline"self.qa = load_db("temp.pdf", "stuff", 4)button_load.button_style = "solid"self.clr_history()return pn.pane.Markdown(f"Loaded File: {self.loaded_file}")# 处理对话链def convchain(self, query):if not query:return pn.WidgetBox(pn.Row('User:', pn.pane.Markdown("", width=600)), scroll=True)result = self.qa({"question": query, "chat_history": self.chat_history})self.chat_history.extend([(query, result["answer"])])self.db_query = result["generated_question"]self.db_response = result["source_documents"]self.answer = result['answer']self.panels.extend([pn.Row('User:', pn.pane.Markdown(query, width=600)),pn.Row('ChatBot:', pn.pane.Markdown(f"<div style='background-color:#F6F6F6; padding:10px;'>{self.answer}</div>",width=600))])inp.value = '' # 清除输入框return pn.WidgetBox(*self.panels, scroll=True)# 获取最后发送到数据库的问题@param.depends('db_query')def get_lquest(self):if not self.db_query:return pn.Column(pn.Row(pn.pane.Markdown(f"Last question to DB:", styles={'background-color': '#F6F6F6'})),pn.Row(pn.pane.Str("no DB accesses so far")))return pn.Column(pn.Row(pn.pane.Markdown(f"DB query:", styles={'background-color': '#F6F6F6'})),pn.pane.Str(self.db_query))# 获取数据库返回的源文件@param.depends('db_response')def get_sources(self):if not self.db_response:returnrlist = [pn.Row(pn.pane.Markdown(f"Result of DB lookup:", styles={'background-color': '#F6F6F6'}))]for doc in self.db_response:rlist.append(pn.Row(pn.pane.Str(doc.page_content[:100] + '...'))) # 显示部分内容return pn.WidgetBox(*rlist, width=600, scroll=True)# 获取当前聊天记录@param.depends('chat_history')def get_chats(self):if not self.chat_history:return pn.WidgetBox(pn.Row(pn.pane.Str("No History Yet")), width=600, scroll=True)rlist = [pn.Row(pn.pane.Markdown(f"Current Chat History variable", styles={'background-color': '#F6F6F6'}))]for exchange in self.chat_history:rlist.append(pn.Row(pn.pane.Str(exchange)))return pn.WidgetBox(*rlist, width=600, scroll=True)# 清除聊天记录def clr_history(self, event=None):self.chat_history = []self.panels = [] # 清空面板列表return# 初始化聊天机器人
cb = cbfs()# 定义界面的小部件
file_input = pn.widgets.FileInput(accept='.pdf') # PDF 文件的文件输入小部件
button_load = pn.widgets.Button(name="Load DB", button_type='primary') # 加载数据库 的按钮
button_clearhistory = pn.widgets.Button(name="Clear History", button_type='warning') # 清除聊天记录的按钮
inp = pn.widgets.TextInput(placeholder='Enter text here...') # 用于用户查询的文本输入小部件# 将加载数据库和对话的函数绑定到相应的部件上
button_load.on_click(cb.call_load_db)
button_clearhistory.on_click(cb.clr_history)
conversation = pn.bind(cb.convchain, inp)# 使用 Panel 定义界面布局
tab1 = pn.Column(pn.Row(inp),pn.layout.Divider(),pn.panel(conversation, loading_indicator=True, height=300),pn.layout.Divider(),
)
tab2 = pn.Column(pn.panel(cb.get_lquest),pn.layout.Divider(),pn.panel(cb.get_sources),
)
tab3 = pn.Column(pn.panel(cb.get_chats),pn.layout.Divider(),
)
tab4 = pn.Column(pn.Row(file_input, button_load),pn.Row(button_clearhistory, pn.pane.Markdown("Clears chat history. Can use to start a new topic")),pn.layout.Divider(),
)# 将所有选项卡合并为一个仪表盘
dashboard = pn.Column(pn.Row(pn.pane.Markdown('# ChatWithYourData_Bot')),pn.Tabs(('Conversation', tab1), ('Database', tab2), ('Chat History', tab3), ('Configure', tab4))
)dashboard.servable()# 进入项目根目录:
# 运行脚本:panel serve ./part4/chapter7/example2.py --autoreload
# 打开浏览器:http://localhost:5006/example2
以下截图展示了该机器人的运行情况:
您可以自由使用并修改上述代码,以添加自定义功能。例如,可以修改 load_db 函数和 convchain
方法中的配置,尝试不同的存储器模块和检索器模型。
此外,panel 和 Param 这两个库提供了丰富的组件和小工具,可以用来扩展和增强图形用户界面。 Panel 可以创建交互式的控制面板,Param 可以声明输入参数并生成控件。组合使用可以构建强大的可 配置GUI。
您可以通过创造性地应用这些工具,开发出功能更丰富的对话系统和界面。自定义控件可以实现参数配 置、可视化等高级功能。欢迎修改和扩展示例代码,开发出功能更强大、体验更佳的智能对话应用。
八、总结
让我们快速回顾本部分的主要内容:
- 使用 LangChain 的多种文档加载器,从不同源导入各类数据。
- 将文档分割为语义完整的文本块,并讨论了其中的一些微妙之处。
- 为这些块创建了 Embedding,并将它们放入向量存储器中,并轻松实现语义搜索。 4. 讨论了语义搜索的一些缺点,以及在某些边缘情况中可能会发生的搜索失败。
- 介绍多种高级检索算法,用于克服那些边缘情况。
- 与 LLMs 相结合,将检索结果与问题传递给 LLM ,生成对原始问题的答案。
- 对对话内容进行了补全,创建了一个完全功能的、端到端的聊天机器人。
通过学习本部分内容,我们已经掌握了如何使用 LangChain 框架,访问私有数据并建立个性化的问答系统。这是一个快速迭代的领域,希望您能持续关注新技术。
九、完整代码 - LLM Cookbook Learning
如果觉得有帮助的话,麻烦给个star