DLAI-Langchain-数据对话笔记-全-

DLAI Langchain 数据对话笔记(全)

001:课程介绍与概述 🚀

在本节课中,我们将要学习如何利用LangChain框架,使大型语言模型能够与你的私有数据进行对话。我们将概述整个课程的目标、核心挑战以及你将学到的关键技术模块。


大型语言模型(LLMs),例如ChatGPT,能够回答许多领域的问题。但一个孤立的大型语言模型仅知道其训练数据中的内容,这并不包括你的个人数据。例如,如果你在一家公司,拥有未公开在互联网上的专有文档,或者模型训练完成后才出现的数据和文章。

那么,如果你或你的客户能够与这些文档进行对话,并利用其中的信息通过大型语言模型来回答问题,这将会非常有用。

在这门短期课程中,我们将介绍如何使用LangChain来与你的数据对话。LangChain是一个用于构建LLM应用的开源开发者框架。它由多个模块化组件以及更完整的端到端模板构成。

LangChain中的模块化组件包括:提示词(Prompts)模型(Models)索引(Indexes)链(Chains)智能体(Agents)。若想更详细地了解这些组件,你可以参考我与Andrew共同讲授的第一门课程。

在本课程中,我们将聚焦于LangChain一个非常流行的应用场景:如何使用LangChain与你的数据对话。


课程内容路线图 🗺️

以下是本课程将涵盖的核心步骤:

首先,我们将学习如何使用LangChain的文档加载器(Document Loaders)从各种来源加载数据。

上一节我们介绍了课程目标,本节中我们来看看数据处理的第一步。接着,我们将探讨如何将这些文档分割成具有语义意义的片段。这个预处理步骤看似简单,实则包含许多细节。

接下来,我们将概述语义搜索(Semantic Search),这是一种根据用户问题获取相关信息的基本方法。这是入门最简单的方法,但在某些情况下会失效。我们将讨论这些情况,并学习如何修复它们。

然后,我们将展示如何利用检索到的文档,使大型语言模型能够回答关于文档的问题。但此时,要完全复现聊天机器人的体验,还缺少一个关键部分。

最后,我们将填补这个缺失的部分——记忆(Memory),并展示如何构建一个功能完整的聊天机器人,让你能够真正地与你的数据对话。


课程团队与资源 💡

这将是一门激动人心的短期课程。我们非常感谢Ansh Goer以及LangChain团队的Lance Martin,他们与稍后为大家讲解的Harrison共同准备了所有课程材料。在DeepLearning.AI方面,感谢Jeff Ludwig和Dila Eadin。

如果你在学习本课程时,希望复习一下LangChain的基础知识,我鼓励你也去学习Harrison之前提到的、关于LLM应用开发的早期短期课程。


本节课中我们一起学习了本课程的核心目标:利用LangChain解锁大型语言模型与私有数据对话的能力。我们概述了从数据加载、处理、检索到最终构建对话系统的完整路径。

接下来,让我们进入下一个视频,Harrison将向你展示如何使用LangChain非常便捷的文档加载器集合。

002:文档加载器 📄

在本节课中,我们将学习如何将不同来源和格式的数据加载到LangChain应用程序中。这是构建“与数据对话”应用的第一步。

为了创建一个能与你的数据对话的应用程序,首先需要将数据加载到一种可以处理的格式中。这正是LangChain文档加载器的作用所在。LangChain提供了超过80种不同类型的文档加载器。在本节中,我们将介绍其中最重要的一些,并帮助你熟悉这个概念。

文档加载器的作用

文档加载器处理从各种不同格式和来源访问和转换数据的细节,并将其转换为标准化格式。

数据可能来自不同的地方,例如网站、数据库、YouTube。这些文档也可以是不同的数据类型,如PDF、HTML、JSON。因此,文档加载器的全部目的是将这些多样的数据源加载到一个标准的文档对象中,该对象由内容和相关的元数据组成。

文档加载器的分类

LangChain中有很多不同类型的文档加载器,我们无法一一介绍,但这里对现有的80多种加载器进行一个粗略的分类。

许多加载器用于从公共数据源(如YouTube、Twitter、Hacker News)加载非结构化数据(如文本文件)。还有更多加载器用于从你或你的公司可能拥有的专有数据源(如Figma、Notion)加载非结构化数据。

文档加载器也可用于加载结构化数据,即表格格式的数据。这些表格的某些单元格或行中可能包含文本数据,你仍然希望对其进行问答或语义搜索。这类数据源包括Airtable、Stripe等。

使用文档加载器

现在,让我们开始实际使用文档加载器。首先,我们需要加载一些必要的环境变量,例如OpenAI API密钥。

我们将要处理的第一类文档是PDF。让我们从LangChain导入相关的文档加载器。我们将使用PyPDFLoader

工作区的documents文件夹中已加载了一些PDF文件。让我们选择一个并将其路径放入加载器。现在,通过调用load方法来加载文档。

让我们看看具体加载了什么。默认情况下,这会加载一个文档列表。在这个例子中,这个PDF有22个不同的页面,每个页面都是一个独立的文档。让我们查看第一个文档,看看它包含什么。

文档首先包含一些页面内容,即该页的文本。这可能有点长,所以我们只打印前几百个字符。另一个非常重要的信息是与每个文档关联的元数据,可以通过metadata属性访问。

你可以看到这里有两部分信息。一个是来源信息,即我们加载的PDF文件名。另一个是page字段,它对应于加载的PDF页面。

加载YouTube视频

接下来我们要看的文档加载器是从YouTube加载的。YouTube上有许多有趣的内容,因此很多人使用这个文档加载器来向他们喜欢的视频、讲座等提问。

我们将在这里导入几个不同的东西。关键部分是YoutubeAudioLoader,它从YouTube视频加载音频文件。另一个关键部分是OpenAIWhisperParser。这将使用OpenAI的Whisper模型(一个语音转文本模型)将YouTube音频转换为我们可以处理的文本格式。

现在我们可以指定一个URL,指定一个保存音频文件的目录,然后将通用加载器创建为YoutubeAudioLoaderOpenAIWhisperParser的组合。然后我们可以调用loader.load()来加载与此YouTube视频对应的文档。

这可能需要几分钟,所以我们加速并跳过等待。加载完成后,我们可以查看加载内容的页面内容。这是YouTube视频转录文本的第一部分。这是一个很好的时机,你可以暂停,去选择你最喜欢的YouTube视频,看看这个转录是否适合你。

加载网页URL

接下来我们将学习如何从互联网加载URL。互联网上有很多非常棒的教育内容,如果你能直接与它对话,岂不是很酷?

我们将通过从LangChain导入WebBaseLoader来实现这一点。然后我们可以选择任何URL,这里我们选择这个GitHub页面上的一个Markdown文件,并为其创建一个加载器。接下来,我们可以调用loader.load(),然后查看页面的内容。

在这里,你会注意到有很多空白,后面跟着一些初始文本,然后是更多文本。这是一个很好的例子,说明了为什么实际上需要对信息进行一些后处理,才能将其变成可用的格式。

加载Notion数据

最后,我们将介绍如何从Notion加载数据。Notion是个人和公司数据非常流行的存储库。很多人已经创建了与他们的Notion数据库对话的聊天机器人。

在你的笔记本中,你会看到如何将数据从Notion数据库导出为我们可以加载到LangChain的格式的说明。一旦我们有了这种格式的数据,就可以使用NotionDirectoryLoader来加载它,并获得我们可以处理的文档。

如果我们查看这里的内容,可以看到它是Markdown格式,这个Notion文档来自Bdel的员工手册。我相信很多听众都使用过Notion,并且有一些他们想与之对话的Notion数据库,所以这是一个很好的机会,可以导出这些数据,将其导入这里,并开始以这种格式处理它。

总结与过渡

以上就是文档加载的内容。我们介绍了如何从各种来源加载数据,并将其转换为标准化的文档接口。

然而,这些文档仍然相当大。因此,在下一节中,我们将介绍如何将它们分割成更小的块。这一点很重要,因为在进行检索增强生成时,你只需要检索最相关的内容片段,而不是我们在这里加载的整个文档,而是只检索与你谈论的主题最相关的段落或几句话。

这也是一个更好的机会,去思考目前还没有加载器但我们可能仍想探索的数据源。谁知道呢,也许你甚至可以向LangChain提交一个PR(拉取请求)来添加它?

003:文档分块 📄

概述

在本节课中,我们将要学习如何将加载好的文档分割成更小的、语义相关的“块”。这是构建高效检索系统(RAG)的关键步骤,直接影响后续检索和问答的质量。

我们刚刚介绍了如何将文档加载为标准格式。现在,我们来探讨如何将它们分割成更小的块。这听起来可能很简单,但其中有许多细微之处,会对后续流程产生重大影响。

让我们开始吧。

文档分块的重要性

文档分块发生在将数据加载为文档格式之后,但在存入向量数据库之前。这看似非常简单,例如,你可以简单地根据字符长度来分割文本。但为了说明这个过程为何既棘手又重要,让我们看一个例子。

我们有一段关于丰田凯美瑞及其规格的句子。如果我们进行简单的分割,可能会导致句子的前半部分在一个块中,后半部分在另一个块中。那么,当我们后续尝试回答“凯美瑞的规格是什么?”这个问题时,实际上没有任何一个块包含完整的信息,信息被分割开了。因此,我们将无法正确回答这个问题。

所以,如何分割块以获得语义相关的片段,这其中有很多细节和重要性。

分块的基本原理

LangChain中所有文本分割器的基础都涉及:按照某个块大小进行分割,并保留一定的块重叠。

下图展示了这个概念:

  • 块大小 对应一个块的长度,可以通过几种不同的方式来衡量(我们将在课程中讨论几种)。我们允许传入一个长度函数来衡量块的大小,通常是字符数或令牌数。
  • 块重叠 通常是在两个块之间保留一点重叠,就像从一个块滑动到下一个块的滑动窗口。这允许同一段上下文出现在一个块的末尾和下一个块的开头,有助于保持一定的连贯性。

LangChain中的文本分割器都有 create_documentssplit_documents 方法。它们内部的逻辑相同,只是暴露的接口略有不同:一个接收文本列表,另一个接收文档列表。

分割器的类型

LangChain中有许多不同类型的分割器,我们将在本节课中介绍其中几种。但我鼓励你在空闲时间查看其余的分割器。

这些文本分割器在多个维度上有所不同:

  • 分割方式:它们如何分割块,使用哪些字符作为分隔符。
  • 长度衡量方式:是按字符、按令牌,还是其他方式。
  • 智能分割:有些分割器甚至使用较小的模型来确定句子的结尾,并以此作为分割块的依据。
  • 元数据处理:分块的另一个重要部分是元数据。需要在所有块中保持相同的元数据,同时在相关时添加新的元数据片段。因此,有些文本分割器专门关注这一点。

分块通常取决于我们正在处理的文档类型,这在分割代码时尤为明显。因此,我们有一个语言文本分割器,它针对多种不同语言(如Python、Ruby、C)有一系列不同的分隔符。在分割这些文档时,它会考虑这些不同的语言及其相关的分隔符。

实践:使用字符分割器

首先,我们将像之前一样设置环境,加载OpenAI API密钥。

接下来,我们将导入LangChain中最常见的两种文本分割器:递归字符文本分割器字符文本分割器

我们将首先使用几个简单的示例来感受一下它们的具体功能。

我们将设置一个相对较小的块大小(26)和一个更小的块重叠(4),以便观察它们的效果。

让我们初始化这两种不同的文本分割器,分别命名为 r_splitterc_splitter

然后,让我们看几个不同的用例。首先加载一个字符串“ABCD...Z”,看看使用不同分割器时会发生什么。

当我们用递归字符文本分割器分割它时,它仍然是一个字符串。这是因为它的长度正好是26个字符,而我们指定的块大小是26,所以实际上不需要进行任何分割。

现在,让我们对一个稍长的字符串进行操作,其长度超过了指定的26个字符的块大小。

这里我们可以看到创建了两个不同的块。第一个块以“Z”结尾,即26个字符。下一个块以“WXYZ”开头,这四个字符就是块重叠的部分,然后继续字符串的其余部分。

让我们看一个稍微复杂一点的例子,其中字符之间有很多空格。

现在我们可以看到它被分成了三个块。因为有空格,所以占用了更多空间。如果我们查看重叠部分,可以看到第一个块中有“L”和“M”,而“L”和“M”也出现在第二个块中。看起来只有两个字符,但由于“L”和“M”之间以及“L”之前和“M”之后的空格,实际上构成了四个字符的块重叠。

现在让我们尝试使用字符文本分割器。

我们可以看到,当我们运行它时,它实际上根本没有尝试分割。这是怎么回事?问题在于,字符文本分割器默认在单个字符(换行符)上进行分割,但这里没有换行符。

如果我们把分隔符设置为一个空格,看看会发生什么。

这里它以与之前相同的方式进行了分割。这是一个暂停视频的好时机,尝试一些新的例子。你可以用自己编造的不同字符串进行尝试,也可以更换分隔符,看看会发生什么。尝试调整块大小和块重叠也很有趣,通过几个简单的例子来感受一下底层发生了什么,这样当我们转向更真实的例子时,你就能对底层机制有很好的直觉。

实践:更真实的例子

现在让我们在一些更真实的例子上尝试。我们这里有这个长段落,可以看到这里有一个双换行符,这是段落之间典型的分隔符。让我们检查一下这段文本的长度。

我们可以看到它大约是500个字符。现在让我们定义我们的两个文本分割器。我们将像之前一样使用以空格为分隔符的字符文本分割器,然后初始化递归字符文本分割器。这里我们传入一个分隔符列表,这些是默认的分隔符,但我们把它们放在这个笔记本中是为了更好地展示发生了什么。我们可以看到我们有一个列表:双换行符、单换行符、空格,然后是空字符串。

这些意味着当你分割一段文本时,它会首先尝试用双换行符分割,如果仍然需要进一步分割单个块,它会继续使用单换行符,如果还需要,就使用空格,最后如果确实需要,它会逐个字符分割。

观察这些分割器在上述文本上的表现,我们可以看到字符文本分割器在空格处分割,因此我们在句子中间得到了奇怪的分割。

而递归文本分割器首先尝试在双换行符处分割。因此,这里它被分割成了两个段落,即使第一个段落的长度小于我们指定的450个字符。这可能是一个更好的分割,因为现在两个各自独立的段落分别位于各自的块中,而不是在句子中间被分割。

现在让我们把它分割成更小的块,以便更好地理解发生了什么。

我们还将添加一个句号分隔符。这是为了在句子之间进行分割。

如果我们运行这个文本分割器,可以看到它按句子进行了分割,但句号实际上放错了位置。这是因为底层使用了正则表达式。

为了解决这个问题,我们可以指定一个稍微复杂一点、带有“后顾断言”的正则表达式。现在,如果我们运行这个。

我们可以看到它被分割成了句子,并且分割正确,句号放在了正确的位置。

实践:处理真实文档

现在让我们在一个更真实的例子中应用这个方法,使用我们在第一个文档加载部分处理过的PDF之一。

让我们加载它,然后在这里定义我们的文本分割器。这里我们传递了长度函数,这是使用Python内置的len函数,这是默认设置,但我们指定它是为了更清楚地了解底层发生了什么,这是在计算字符的长度。因为我们现在想使用文档,所以我们使用split_documents方法,并传入一个文档列表。

如果我们比较这些文档的长度与原始页面的长度。

我们可以看到,由于这次分割,创建了更多的文档。

我们可以对第一讲中使用过的Notion数据库做类似的事情。

再次比较原始文档的长度与新分割文档的长度,我们可以看到,在完成所有分割后,我们现在有了更多的文档。

实践:基于令牌的分割

这是一个暂停视频并尝试一些新例子的好时机。到目前为止,我们都是基于字符进行分割,但还有另一种分割方式,即基于令牌。为此,让我们导入令牌文本分割器。

这种方式很有用,因为通常LLM的上下文窗口是由令牌数量指定的。因此,了解令牌是什么、它们出现在哪里很重要,然后我们可以基于它们进行分割,以便更准确地了解LLM会如何看待它们。

为了真正理解令牌和字符之间的区别,让我们用块大小为1、块重叠为0来初始化令牌文本分割器。这样会将任何文本分割成相关令牌的列表。让我们创建一个虚构的文本。

当我们分割它时,我们可以看到它被分割成了许多不同的令牌,它们在长度和字符数上都略有不同。第一个是“fo”,然后是一个空格和“bar”,然后是一个空格和“b”,接着是“az”、“zy”,然后是“fo”再次出现。这显示了基于字符分割和基于令牌分割之间的一些区别。

让我们以类似的方式将其应用到上面加载的文档中。

类似地,我们可以在页面上调用split_documents

如果我们查看第一个文档,我们有了新的分割文档,其页面内容大致是标题,然后我们有了来源和页码的元数据。

你可以看到,这里的来源和页码元数据与原始文档中的相同。为了确认,我们可以查看一下。第0页的元数据,我们可以看到它是对齐的。这很好,它正在将元数据适当地传递到每个块。

但也可能存在这样的情况:你实际上希望在分割块时向块添加更多元数据。这可以包含诸如块在文档中的位置、相对于文档中其他内容或概念的位置等信息。通常,这些信息可以在回答问题时用于提供关于这个块到底是什么的更多上下文。

为了看一个具体的例子,让我们看看另一种类型的文本分割器,它实际上会向每个块的元数据中添加信息。

你现在可以暂停并尝试一些你自己想出的例子。

这个文本分割器是Markdown标题文本分割器

它的作用是:根据标题或任何子标题来分割Markdown文件。然后,它会将这些标题作为内容添加到元数据字段中,这些元数据将传递给源自这些分割的任何块。

让我们先做一个简单的例子,玩转一个文档,其中有一个标题,然后是第1章的子标题,接着是一些句子,然后是另一个更小子标题的部分,然后我们跳回到第2章和一些句子。

让我们定义一个我们想要分割的标题列表以及这些标题的名称。首先,我们有一个单井号,我们称之为header1;然后有两个井号,header2;三个井号,header3。然后,我们可以用这些标题初始化Markdown标题文本分割器。

然后分割我们上面的简单示例。如果我们查看其中几个例子,可以看到第一个例子的内容是“Hi, this is Jim. Hi, this is Joe.”,在元数据中,我们有header1为“Title”,header2为“Chapter 1”,这来自于上面示例文档中的这里。

让我们看看下一个例子,我们可以看到我们已经跳到了一个更小的子部分,因此内容为“Hi this is Lance”,现在我们不仅有header1,还有header2header3。这同样来自于上面Markdown文档中的内容和名称。

实践:处理真实Markdown文档

让我们在一个真实世界的例子中尝试一下。之前我们使用Notion目录加载器加载了Notion目录,这会将文件加载为Markdown格式,这与Markdown标题分割器相关。

所以让我们加载这些文档,然后用header1作为单井号、header2作为双井号来定义Markdown分割器。

我们分割文本并得到分割结果。如果我们查看它们。

我们可以看到第一个的内容是某个页面,现在如果我们向下滚动到元数据,可以看到我们已将header1加载为“Bdle's Employee Handbook”。

总结

本节课中,我们一起学习了如何将文档分割成语义相关的块,并保留或添加适当的元数据。我们介绍了分块的基本原理、不同类型的分割器(如字符分割器、递归字符分割器、令牌分割器和Markdown标题分割器),并通过实践了解了它们在不同场景下的应用。

我们已经介绍了如何获得具有适当元数据的语义相关块。下一步是将这些数据块移动到向量数据库中,我们将在下一节中介绍。

004:嵌入与向量存储

在本节课中,我们将学习如何将分割好的文档块转换为数值表示(嵌入),并将其存储在向量数据库中,以便后续高效地检索与问题相关的信息。

上一节我们介绍了如何将文档分割成有意义的块。本节中,我们来看看如何将这些块存储起来,以便在回答问题时能快速找到它们。为此,我们将利用嵌入向量存储技术。

什么是嵌入与向量存储?

我们在之前的课程中简要介绍过,但这里需要更深入地探讨,因为它们是构建数据聊天机器人的核心。同时,我们也会讨论这种通用方法可能失效的边缘情况。

嵌入的作用是接收一段文本,并为其创建一个数值表示。内容相似的文本,其数值向量在向量空间中也相似。这意味着我们可以通过比较向量来找到语义相近的文本片段。

以下是完整的工作流程:

  1. 从文档开始。
  2. 将文档分割成较小的块。
  3. 为这些文档块创建嵌入向量。
  4. 将所有向量存储在向量存储中。

向量存储是一种数据库,可以方便地查找相似的向量。当我们试图找到与当前问题相关的文档时,这非常有用。我们可以将问题也转换为嵌入向量,然后与向量存储中的所有向量进行比较,并选出最相似的N个。最后,将这N个最相似的文本块与问题一起输入给大语言模型,从而获得答案。

实践:创建与比较嵌入

首先,我们需要设置环境变量。我们将使用CS 229讲座的文档,并特意复制了第一讲,以模拟存在重复数据的情况。

加载文档后,我们使用递归字符文本分割器来创建块。现在,我们有了超过200个不同的文本块,是时候为它们创建嵌入向量了。我们将使用OpenAI的模型来生成这些嵌入。

在进入真实示例之前,我们先通过几个简单的测试句子来理解其底层原理。以下是几个示例句子,前两个非常相似,第三个则无关。

# 示例:创建和比较嵌入
from langchain.embeddings import OpenAIEmbeddings
import numpy as np

embeddings = OpenAIEmbeddings()
sentences = [
    "I have a dog.",
    "I have a pet.",
    "I drive a car."
]
vec1 = embeddings.embed_query(sentences[0])
vec2 = embeddings.embed_query(sentences[1])
vec3 = embeddings.embed_query(sentences[2])

# 使用点积比较相似度,数值越高表示越相似
similarity_1_2 = np.dot(vec1, vec2)
similarity_1_3 = np.dot(vec1, vec3)

运行代码后,我们发现前两个句子的嵌入向量相似度很高(约0.96),而它们与第三个句子的相似度则低得多(约0.77)。这表明嵌入模型成功捕捉了语义相似性。

构建向量存储

现在回到真实示例。我们需要为所有PDF文本块创建嵌入,并将它们存储到向量存储中。本节课我们使用Chroma,因为它轻量且支持内存存储,易于上手。LangChain集成了超过30种向量存储,Chroma是其中之一。

以下是创建和保存向量存储的步骤:

# 创建并持久化向量存储
from langchain.vectorstores import Chroma

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-dt-chat/img/da0d6f0c6aa59e2544613bcd771d300a_5.png)

persist_directory = ‘./docs/chroma‘
# 确保目录干净(示例中为演示而清理)
vectorstore = Chroma.from_documents(
    documents=splits, # 之前创建的文本块
    embedding=OpenAIEmbeddings(),
    persist_directory=persist_directory
)
print(vectorstore._collection.count()) # 应输出209,与块数量一致

进行语义搜索

向量存储构建完成后,我们就可以开始使用它了。假设我们想问一个关于课程的问题:“如果有问题,可以联系哪个邮箱寻求帮助?”

我们使用similarity_search方法,传入问题并指定返回最相似的3个文档块(K=3)。

# 在向量存储中进行相似性搜索
question = “Is there an email I can ask for help if I need help with the course?”
docs = vectorstore.similarity_search(question, k=3)
print(len(docs)) # 输出 3
print(docs[0].page_content) # 查看最相关块的内容

搜索结果显示,最相关的文本块确实包含了课程助教的邮箱地址:cs229qa@cs.stanford.edu。这证明基本的语义搜索是有效的。

最后,记得持久化向量数据库以供后续课程使用:

vectorstore.persist()

当前方法的局限性

虽然基本搜索效果不错,但该方法并不完美。我们接下来探讨几个它可能失效的边缘情况。

1. 重复信息问题
当我们询问“关于Matlab他们说了什么?”并获取前5个结果(K=5)时,会发现前两个结果完全相同。这是因为我们在加载数据时故意引入了重复的文档。这会导致将重复信息传递给语言模型,浪费资源且无助于生成更好的答案。

2. 结构化信息缺失问题
另一个失效情况出现在询问需要结合结构化信息的查询时。例如,问题:“在第三讲中,他们关于回归说了什么?”直觉上,我们希望所有返回的文档都来自第三讲。

但当我们检查返回文档的元数据时,会发现结果混合了第一讲、第二讲和第三讲的内容。这是因为当前的语义搜索主要基于整个句子的嵌入相似度,它捕捉到了“回归”这个关键词,但未能有效捕捉“仅限第三讲”这个结构化过滤条件。

小结与下节预告

本节课中我们一起学习了:

  1. 嵌入的概念:将文本转换为数值向量,语义相似的文本其向量也相似。
  2. 向量存储的作用:存储嵌入向量,以便快速进行相似性检索。
  3. 基本的语义搜索流程:将问题嵌入,在向量库中查找最相似的文档块。
  4. 当前方法的两个主要局限性:重复信息难以处理结构化查询条件

现在我们已经掌握了语义搜索的基础及其一些失效模式,下一节课我们将探讨如何解决这些问题,并增强我们的检索能力。

005:高级检索技术

在本节课中,我们将深入学习检索环节,并探讨几种更先进的方法来克服上一课中提到的边缘情况。检索是查询时的关键步骤,当用户提出问题时,我们需要找到最相关的文本片段。上一课我们介绍了语义相似性搜索,本节课我们将介绍几种不同的、更高级的检索方法。

最大边际相关性 (MMR)

上一节我们介绍了语义搜索的基础,本节中我们来看看如何通过最大边际相关性来提升检索结果的多样性。如果我们总是选择在嵌入空间中最相似查询的文档,可能会错过多样化的信息,正如我们在一个边缘案例中看到的那样。

以下是MMR的工作原理:

  1. 首先,根据语义相似性获取一组初始文档(数量由 fetch_k 参数控制)。
  2. 然后,在这组较小的文档中,同时优化相关性多样性
  3. 最后,从这组文档中选择最终 k 个文档返回给用户。

其核心思想是避免返回内容高度重复的文档。

自查询检索器

接下来,我们探讨自查询检索器。这种方法适用于用户问题不仅包含需要语义查找的内容,还包含需要基于元数据进行过滤的条件的情况。

例如,对于问题:“1980年有哪些关于外星人的电影?”,它包含两个部分:

  • 语义部分:关于“外星人”。
  • 元数据部分:年份等于“1980”。

自查询检索器会利用语言模型将原始问题拆分为过滤条件搜索词。大多数向量数据库都支持元数据过滤,因此可以轻松筛选出符合年份等条件的记录。

上下文压缩

最后,我们来了解上下文压缩。这种方法有助于从检索到的长文档中提取出最相关的部分。

例如,当回答一个问题时,你可能会得到整个存储的文档,即使只有前一两句话是相关的。通过上下文压缩,你可以将所有文档通过一个语言模型运行,提取出最相关的片段,然后只将这些最相关的片段传递给最终的语言模型调用。这样做的代价是需要更多次调用语言模型,但能确保最终答案聚焦于最重要的信息,是一种权衡。

让我们看看这些不同技术的实际应用。

实践:最大边际相关性 (MMR)

我们首先像往常一样加载环境变量,并导入之前使用过的Chroma和OpenAI。可以看到,我们的集合中包含了之前加载的209个文档。

现在,我们回顾一下关于蘑菇的例子。我们加载示例文本,其中包含蘑菇的信息。为了演示,我们创建一个小型数据库作为示例。

我们有一个问题:“所有白色的蘑菇是什么?”。现在运行相似性搜索,设置 k=2 只返回两个最相关的文档。我们可以看到,返回的文档中没有提到它是有毒的。

现在使用MMR运行检索。我们设置 k=2(最终返回2个文档),但 fetch_k=3(最初获取3个文档)。现在我们可以看到,返回的文档中包含了“有毒”的信息。

让我们回到上一课中关于“MATLAB”的例子,当时我们得到了包含重复信息的文档。运行MMR后,我们可以看到第一个结果与之前相同(因为它最相似),但第二个结果不同了,它引入了多样性。

实践:自查询检索器

这是关于“他们在第三讲中关于回归说了什么?”的例子。之前,它返回的结果不仅来自第三讲,还来自第一和第二讲。

如果手动修复,我们会指定一个元数据过滤器,要求来源等于第三讲的PDF。这样,检索到的文档都将来自那堂课。

我们可以使用语言模型自动完成这个任务。为此,我们导入语言模型(OpenAI)、自查询检索器以及属性信息。我们只有两个元数据字段:sourcepage。我们为每个属性填写名称、描述和类型。这些信息将传递给语言模型,因此尽可能描述清楚非常重要。

然后,我们指定文档存储中的内容信息,初始化语言模型和自查询检索器。设置 verbose=True 可以让我们看到语言模型在推断查询和元数据过滤器时的内部过程。

当我们用这个问题运行自查询检索器时,得益于 verbose=True,我们可以看到内部过程:我们得到了一个关于“回归”的查询(语义部分),以及一个过滤器,要求 source 属性等于第三讲机器学习的路径。这意味着在语义空间中查找“回归”,同时过滤出源值为该路径的文档。遍历文档并打印元数据,可以看到它们都来自第三讲。

实践:上下文压缩

最后,我们来讨论上下文压缩。我们导入相关模块:上下文压缩检索器和LLM链提取器。这将从每个文档中提取仅相关的部分,然后将这些部分作为最终返回的响应。

我们定义一个函数来漂亮地打印文档,因为它们通常很长且混乱。然后,我们用LLM链提取器创建一个压缩器,再用这个压缩器和向量数据库的基础检索器创建上下文压缩检索器。

当我们问“关于MATLAB他们说了什么?”并查看压缩后的文档时,可以看到两点:一是它们比正常文档短得多;二是仍然存在一些重复内容。这是因为底层仍然在使用语义搜索算法,而这是我们之前用MMR解决的问题。这是一个结合多种技术以获得最佳结果的好例子。

为此,在从向量数据库创建检索器时,我们可以将搜索类型设置为MMR。重新运行后,我们可以看到返回了一组经过过滤、不包含重复信息的结果。

其他检索技术

到目前为止,我们提到的所有额外检索技术都建立在向量数据库之上。值得注意的是,还有其他类型的检索根本不使用向量数据库,而是使用其他更传统的NLP技术。

这里,我们将用两种不同类型的检索器重新创建检索流程:SVM检索器和TF-IDF检索器。如果你从传统NLP或机器学习中认出了这些术语,那很好;如果不认识,也没关系。这只是其他现有技术的一个例子,除了这些还有很多,我鼓励你去探索一下。

我们可以快速完成加载和分割文本的常规流程。然后,这两种检索器都暴露了一个 from_texts 方法。SVM检索器需要一个嵌入模块,而TF-IDF检索器直接接收分割后的文本。

现在我们可以使用这些检索器了。将“关于MATLAB他们说了什么?”传递给SVM检索器,查看返回的顶部文档,可以看到它提到了很多关于MATLAB的内容,效果不错。我们也可以在TF-IDF检索器上尝试,可以看到结果看起来稍差一些。

现在是一个很好的时机,可以暂停并尝试所有这些不同的检索技术。你会发现其中一些技术在不同方面比其他技术更好。我鼓励你在各种各样的问题上尝试。特别是自查询检索器是我的最爱,我建议尝试使用更复杂的元数据过滤器,甚至可以创建一些具有嵌套元数据结构的场景,尝试让LLM推断出来。我认为这非常有趣,也是一些更高级的内容,我很高兴能与你们分享。

总结

本节课中,我们一起学习了三种高级检索技术:

  1. 最大边际相关性:用于在返回结果时平衡相关性与多样性,避免信息冗余。
  2. 自查询检索器:利用语言模型自动将用户问题分解为语义查询和元数据过滤条件,实现精准检索。
  3. 上下文压缩:通过额外的语言模型调用,从长文档中提取最相关的片段,使最终答案更聚焦。

我们还了解到,这些技术可以组合使用(例如,MMR + 上下文压缩),并且除了基于向量的方法,还存在如SVM、TF-IDF等传统检索技术。掌握这些方法将帮助你构建更强大、更灵活的问答系统。

006:基于检索的问答

在本节课中,我们将学习如何利用检索到的相关文档,结合语言模型来回答问题。我们将介绍几种不同的方法,并分析它们的优缺点。

我们已经介绍了如何为给定问题检索相关文档。下一步是获取这些文档和原始问题,将它们一起传递给语言模型,并要求它回答问题。本节课将介绍这个过程,以及完成此任务的几种不同方法。

环境与数据准备

首先,我们像往常一样加载环境变量。

import os
from dotenv import load_dotenv
load_dotenv()

接着,我们加载之前持久化保存的向量数据库。

from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

embedding = OpenAIEmbeddings()
vectordb = Chroma(persist_directory="./chroma_db", embedding_function=embedding)

我们检查数据库是否正确加载,确认它包含之前相同的209个文档。

print(f"文档数量: {vectordb._collection.count()}")

为了确保检索功能正常工作,我们对问题“这门课的主要主题是什么?”进行一个快速的相似性搜索测试。

docs = vectordb.similarity_search("这门课的主要主题是什么?", k=3)
for doc in docs:
    print(doc.page_content[:100])

初始化语言模型与问答链

现在,我们初始化用于回答问题的语言模型。我们将使用ChatOpenAI模型(GPT-3.5),并将温度设置为0。这在需要事实性答案时非常有效,因为它具有较低的可变性,通常能提供最高保真度和最可靠的答案。

from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

然后,我们导入RetrievalQA链。这个链执行基于检索步骤的问答任务。我们可以通过传入语言模型和作为检索器的向量数据库来创建它。

from langchain.chains import RetrievalQA
qa_chain = RetrievalQA.from_chain_type(llm, retriever=vectordb.as_retriever())

我们可以用我们想问的问题来调用它。

question = "这门课的主要主题是什么?"
result = qa_chain.run({"query": question})
print(result)

返回的答案可能是:“这门课的主要主题是机器学习。此外,课程可能涵盖统计学和代数作为讨论部分的复习内容。在本季度后期,讨论部分还将涵盖主讲座中教授内容的扩展。”

理解底层机制与自定义提示

为了更好地理解底层发生了什么,并展示一些可以调整的参数,我们需要关注所使用的提示词。这个提示词接收文档和问题,并将它们传递给语言模型。

以下是定义提示模板的示例:

from langchain.prompts import PromptTemplate

template = """请使用以下上下文信息来回答问题。如果你不知道答案,就说你不知道,不要试图编造答案。请使用三句话以内回答,并保持答案简洁。
上下文:{context}
问题:{question}
有帮助的答案:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)

现在,我们创建一个新的RetrievalQA链。我们将使用相同的语言模型和向量数据库,但传入一些新参数。我们将return_source_documents设置为True,以便轻松检查检索到的文档。同时,我们传入上面定义的QA_CHAIN_PROMPT作为提示词。

qa_chain_custom = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    return_source_documents=True,
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

让我们尝试一个新问题:“概率是课程主题吗?”

result = qa_chain_custom({"query": "概率是课程主题吗?"})
print("答案:", result['result'])
print("\n--- 来源文档片段 ---")
for i, doc in enumerate(result['source_documents'][:2]):
    print(f"文档 {i+1}: {doc.page_content[:200]}...")

返回的结果可能是:“是的,概率被假定为这门课程的先修知识。讲师假设学生熟悉基本的概率和统计学,并将在讨论部分复习一些先修知识作为复习课程。谢谢提问。” 模型甚至友好地回应了我们。

为了更好地了解答案的数据来源,我们可以查看一些返回的源文档。浏览它们,你会发现所有回答中的信息都包含在这些源文档之一中。

现在是暂停的好时机,尝试一些不同的问题或你自己的不同提示模板,看看结果如何变化。

超越“Stuff”方法:MapReduce与Refine

到目前为止,我们一直使用默认的“stuff”技术,即简单地将所有文档塞入最终的提示词中。这种方法很好,因为它只涉及一次语言模型调用。然而,它有一个限制:如果文档太多,它们可能无法全部放入上下文窗口中。

我们可以使用另一种类型的技术来对文档进行问答,即“MapReduce”技术。在这种技术中,每个单独的文档首先被单独发送到语言模型以获得初始答案,然后这些答案通过最后一次语言模型调用被组合成最终答案。这涉及更多次的语言模型调用,但它的优点是可以处理任意多的文档。

当我们通过这个链运行之前的问题时,我们可以看到这种方法的另一个限制:首先,它慢得多;其次,结果实际上更差。例如,它可能回答:“根据给定的文档部分,这个问题没有明确的答案。” 这可能是因为它是基于每个文档单独回答的。因此,如果信息分布在两个文档中,它就无法在同一上下文中获得所有信息。

这是一个使用LangChain平台更好地了解这些链内部情况的好机会。我们将在这里演示,如果你想自己使用它,课程材料中会有关于如何获取API密钥的说明。

设置好环境变量后,我们可以重新运行MapReduce链,然后切换到UI界面查看底层发生了什么。从那里,我们可以找到刚刚运行的记录,点击进入,可以看到输入和输出。然后,我们可以查看子运行记录,以很好地分析底层发生的情况。首先,我们有“MapReduceDocumentsChain”。这实际上涉及四次单独的语言模型调用。点击其中一个调用,我们可以看到输入和输出是针对每个文档的。返回后,我们可以看到在处理完每个文档后,它在一个最终链——“StuffedDocumentsChain”中组合,将所有响应塞入最终调用中。点击进入,我们可以看到系统消息,其中包含来自先前文档的四个摘要,然后是用户问题,接着就是答案。

我们也可以做类似的事情,将链类型设置为“refine”。这是一种新型的链,让我们看看它在底层是什么样子。在这里,我们可以看到它调用了“RefineDocumentsChain”,这涉及对LLM链的四次顺序调用。让我们看看这个链中的第一次调用,以了解发生了什么。在这里,我们有了发送给语言模型之前的提示词。我们可以看到由几部分组成的系统消息。“以下是上下文信息”这部分是系统消息的一部分,是我们事先定义的提示模板的一部分。接下来的这一大段文本,这是我们检索到的一个文档。然后,我们在这里有用户问题,接着答案就在这里。如果我们返回,可以查看下一次语言模型调用。这里,我们发送给语言模型的最终提示词是一个序列,它将先前的响应与新数据结合起来,然后要求一个改进的响应。所以我们可以看到,这里有原始的用户问题,然后是答案(和之前一样)。接着我们说“我们有机会在需要时用以下更多上下文来完善现有答案”,这是提示模板的一部分,是指令的一部分。其余部分是我们检索到的文档,即列表中的第二个文档。然后,在最后,我们可以看到更多的指令:“根据新的上下文,完善原始答案以更好地回答问题。” 然后在下面,我们得到一个最终答案。但这只是第二个“最终”答案,所以这个过程运行四次,遍历所有文档后才得出最终答案。而这个最终答案就在这里:“该课程假设学生熟悉基本的概率和统计学,但将有复习部分来温习先修知识。” 你会注意到,这个结果比MapReduce链的结果更好,因为使用Refine链确实允许你(尽管是顺序地)组合信息,并且它实际上比MapReduce链更能鼓励信息的传递。

这是一个暂停的好机会,尝试一些问题、不同的链、不同的提示模板,看看在UI中是什么样子,这里有很多可以探索的内容。

对话的挑战:引入记忆

聊天机器人如此受欢迎的一个伟大之处在于,你可以提出后续问题,可以要求澄清先前的答案。让我们在这里尝试一下。让我们创建一个QA链,就使用默认的“stuff”方法。问它一个问题:“概率是课程主题吗?” 然后问一个后续问题,它提到概率应该是先修知识,那么让我们问“为什么需要这些先修知识?” 然后我们得到一个答案:“该课程的先修知识被假定为计算机科学的基础知识和基本的计算机技能与原理。” 这和我们之前问概率时的答案完全无关。

这里发生了什么?基本上,我们正在使用的链没有任何状态概念,它不记得之前的问题或答案。为此,我们需要引入记忆,这将是我们下一节要介绍的内容。

总结

在本节课中,我们一起学习了如何利用检索到的文档进行问答。我们首先介绍了基础的“stuff”方法,它简单高效,但受限于上下文窗口的长度。接着,我们探讨了“MapReduce”和“Refine”这两种能够处理大量文档的方法,并通过LangChain平台可视化了它们的内部工作流程。最后,我们指出了当前简单链在对话场景中的局限性——缺乏记忆能力,为下一节课引入记忆机制做好了铺垫。

007:构建带记忆的对话机器人

在本节课中,我们将学习如何为我们的问答机器人添加对话记忆功能,使其能够处理后续问题,实现真正的对话交互。


我们已经非常接近构建一个功能完整的聊天机器人了。我们首先学习了如何加载文档,然后对文档进行分割。接着,我们创建了向量存储库,并讨论了不同类型的检索方法。我们已经展示了如何回答问题,但还无法处理后续提问,无法进行真正的对话。好消息是,我们将在本节课中解决这个问题。让我们来看看具体如何实现。

添加对话历史记忆

上一节我们介绍了基础的检索式问答链,本节中我们来看看如何为其添加记忆功能,使其能够理解对话上下文。

我们将创建一个问答聊天应用。这个应用看起来与之前类似,但我们会加入“聊天历史”这个概念。聊天历史指的是你与链交换过的所有先前对话或消息。这使得链在尝试回答问题时,能够将聊天历史作为上下文考虑进去。因此,当你提出一个后续问题时,它能知道你指的是什么。

需要指出的是,我们之前讨论过的所有高级检索类型,如自查询检索、压缩检索等,都可以在这里使用。我们讨论的所有组件都是模块化的,可以很好地组合在一起。我们现在只是加入聊天历史这个概念。

实现步骤与代码

让我们看看具体如何实现。首先,和往常一样,我们需要加载环境变量。如果你设置了平台,最好从一开始就打开它,这样可以看到很多底层运行的细节。

我们将加载包含所有课程资料嵌入向量的向量存储库。我们可以对向量库运行基础的相似性搜索。接着,初始化我们将用作聊天机器人的语言模型。然后,初始化提示模板,创建检索式问答链,传入问题并获取结果。这些都是之前的内容,所以快速带过。

现在,让我们添加一些记忆功能。我们将使用“对话缓冲记忆”。它的作用是简单地维护一个聊天消息历史缓冲区列表,并在每次提问时,将这些历史连同新问题一起传递给聊天机器人。

以下是关键代码实现:

# 导入必要的库
from langchain.memory import ConversationBufferMemory

# 创建对话缓冲记忆
memory = ConversationBufferMemory(
    memory_key="chat_history",  # 与提示模板中的输入变量对齐
    return_messages=True  # 以消息列表形式返回历史,而非单个字符串
)

我们指定 memory_key 为“chat_history”,这是为了与提示模板中的输入变量对齐。然后指定 return_messages=True,这将把聊天历史作为消息列表返回,而不是单个字符串。这是最简单的记忆类型。

现在,让我们创建一个新类型的链:对话检索链

from langchain.chains import ConversationalRetrievalChain

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-dt-chat/img/460d4f013c8fde0d0c3445255dee57b7_6.png)

# 创建对话检索链
qa = ConversationalRetrievalChain.from_llm(
    llm,  # 语言模型
    retriever=vectordb.as_retriever(),  # 检索器
    memory=memory  # 记忆
)

对话检索链在检索式问答链的基础上增加了一个新步骤:它不仅包含记忆,还增加了一个步骤,该步骤将历史和新问题合并,重新组织成一个独立的、完整的问题,再传递给向量存储库以查找相关文档。我们稍后会在用户界面中查看这个效果。

现在让我们试试看。我们可以先问一个问题(此时没有任何历史),看看返回的结果。然后,我们可以针对那个答案提出一个后续问题。

例如:

  • 第一个问题:“概率是课程主题吗?” 我们得到一个答案:“讲师假设学生已具备概率论与数理统计的基础知识。”
  • 第二个问题(后续):“为什么需要这些先决条件?” 我们得到的答案会引用“基础概率论与数理统计”作为先决条件并进行阐述,而不会像之前那样与计算机科学混淆。

理解底层机制

让我们通过用户界面看看底层发生了什么。我们可以看到,现在链的输入不仅包含问题,还有聊天历史。聊天历史来自记忆,在链被调用之前就已应用并记录在日志系统中。

如果我们查看追踪记录,会发现有两个独立的过程:首先是对一个项目的调用,然后是对“StuffDocuments”链的调用。

查看第一个调用,我们可以看到一段提示词,其指令类似于:“给定以下对话和一个后续问题,请将后续问题重新表述为一个独立的问题。” 这里包含了之前的历史(第一个问题和助手答案),然后输出一个独立的问题,例如:“为什么这门课程要求基础概率论与数理统计作为先决条件?”

这个独立的问题随后被传递给检索器,我们检索到指定数量的文档(例如3或4个)。然后,这些文档被传递给“StuffDocuments”链,以尝试回答原始问题。在这个链中,我们有系统指令:“使用以下上下文片段来回答用户的问题。” 接着是一堆上下文,下面是独立的问题,最后我们得到一个与当前问题相关的答案。

尝试与定制

这是一个很好的时机,可以暂停并尝试这个链的不同选项。你可以传入不同的提示模板,不仅用于回答问题,也用于将问题重新表述为独立问题。你还可以尝试不同类型的记忆。这里有很多不同的选项可以探索。

整合到用户界面

之后,我们将把所有功能整合到一个漂亮的用户界面中。创建这个界面会有很多代码,但核心部分就在这里。具体来说,这是对整个课程内容的一个完整演练。

以下是核心流程代码:

# 加载数据库和检索链
def load_db(file, chain_type, k):
    # 使用PDF加载器加载文件
    loader = PyPDFLoader(file)
    documents = loader.load()
    # 分割文档
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
    docs = text_splitter.split_documents(documents)
    # 创建嵌入向量并存入向量存储库
    embeddings = OpenAIEmbeddings()
    vectordb = Chroma.from_documents(docs, embeddings)
    # 将向量库转为检索器,使用相似性搜索,设置检索数量k
    retriever = vectordb.as_retriever(search_type="similarity", search_kwargs={"k": k})
    # 创建对话检索链(注意:此处未传入memory,将在外部管理)
    qa = ConversationalRetrievalChain.from_llm(
        llm=llm,
        chain_type=chain_type,
        retriever=retriever,
        return_source_documents=True
    )
    return qa

这里需要注意的一点是,我们没有在链中传入记忆。为了下面图形用户界面的便利,我们将在外部管理记忆。这意味着聊天历史必须在链的外部进行管理。

后续还有很多代码,我们不花太多时间讲解,但需要指出的是,在这里我们将聊天历史传入链中(再次强调,因为链本身没有附加记忆),然后我们用得到的结果来扩展聊天历史。最后,我们可以将所有部分组合起来运行,得到一个可以通过其进行交互的漂亮用户界面。

与聊天机器人互动

现在,我们可以向聊天机器人提问了。例如:“讲师是谁?” 它会回答:“讲师是Paul Bumt和Katie Chin。”

你会注意到这里有几个可以点击的标签页。例如,如果我们点击“数据库”,可以看到我们向数据库提出的最后一个问题,以及从查找中返回的来源。这些文档是分割后我们检索到的每个文本块。

我们还可以看到包含输入和输出的聊天历史。此外,还有一个配置区域,你可以在那里上传文件。我们也可以问后续问题,例如:“他们的专业是什么?” 我们会得到一个关于之前提到的讲师的答案,可以看到Paul研究机器学习和计算机视觉,而Katie实际上是一位神经科学家。

课程总结

本节课中,我们一起学习了如何利用LangChain构建一个功能完整的、具备对话记忆的端到端聊天机器人。

我们首先介绍了如何从多种文档源加载数据,使用了LangChain的80多种不同的文档加载器。接着,我们将文档分割成块,并讨论了在此过程中出现的许多细微差别。之后,我们获取这些文本块,为它们创建嵌入向量,并将其放入向量存储库,展示了这如何轻松实现语义搜索。但我们也讨论了语义搜索的一些缺点,以及在特定边缘情况下它可能失败的地方。

接下来我们涵盖了检索部分,这可能是本课程中最喜欢的部分。我们讨论了许多新颖、先进且非常有趣的检索算法,用于克服那些边缘情况。在下一部分中,我们将其与大型语言模型结合,获取检索到的文档和用户问题,将其传递给LLM,从而生成原始问题的答案。但还缺少一个东西,那就是对话的交互性。

而这正是我们完成本课程的地方:通过创建一个在你的数据上完全运行的端到端聊天机器人。


总结:本节课中,我们一起学习了如何为LangChain问答链添加“对话缓冲记忆”,从而构建出能够理解上下文、处理后续问题的智能对话机器人。我们探讨了ConversationalRetrievalChain的工作原理,它通过将聊天历史和新问题重构成独立问题来改进检索,并最终将所有组件集成到一个交互式用户界面中。现在,你可以上传自己的文档,与你的数据进行真正的对话了。

008:课程总结 🎯

在本节课中,我们将回顾整个《LangChain:与你的数据对话》课程的核心内容与学习路径。课程涵盖了从数据加载到构建完整对话式应用的完整流程。


课程内容回顾

上一节我们介绍了如何构建一个端到端的聊天机器人。本节中,我们来对整个课程进行总结。

课程从如何使用LangChain加载数据开始。我们介绍了利用LangChain提供的80多种不同的文档加载器,从各种文档源加载数据。

以下是数据加载后的关键处理步骤:

  1. 文档分块:将加载的文档分割成较小的片段。我们深入探讨了执行此操作时可能出现的许多细微差别。
  2. 向量化与存储:为这些文档块创建嵌入向量,并将其存入向量数据库。这轻松实现了语义搜索功能。
  3. 检索策略:我们讨论了语义搜索的一些缺点及其在某些边缘情况下可能失效的问题。因此,课程介绍了许多新颖、先进且实用的检索算法来克服这些边缘情况。
  4. 与大语言模型结合:在检索到相关文档后,结合用户问题,将其传递给大语言模型,从而生成对原始问题的答案。
  5. 构建对话应用:最后,我们通过创建一个功能完整的端到端聊天机器人,为课程收尾,实现了数据的对话式交互。

致谢与展望

我十分享受教授这门课程,也希望你们喜欢学习它。我要感谢开源社区中的每一个人,他们贡献了许多使这门课程成为可能的内容,例如所有的提示词和你们看到的许多功能。

当你们使用LangChain进行构建,并发现新的方法、技巧和技术时,我希望你们能在Twitter上分享所学,甚至在LangChain中提交一个PR。这是一个快速发展的领域,也是一个激动人心的时刻。我真的很期待看到你们如何应用在本课程中学到的一切。


总结

本节课中,我们一起学习了《LangChain:与你的数据对话》的完整知识体系。从数据加载、分块、嵌入存储,到高级检索、与大模型结合生成答案,最终构建出交互式聊天机器人。希望这门课程能为你开启利用LangChain探索数据对话能力的大门。

posted @ 2026-03-26 08:11  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报