本章的代码实验室为本书其余部分的代码奠定基础。我们将在整个章节中提供一个完整的检索增强生成(RAG)流水线。接下来,在本书的各个章节中,我们将逐步查看代码的不同部分,随着学习的深入,我们会在代码中添加增强功能,让你全面理解代码如何演变,解决越来越复杂的问题。
在本章中,我们将逐步讲解 RAG 流水线的每个组件,包括以下方面:
- 没有用户界面
- 设置 OpenAI 的大型语言模型(LLM)账户
- 安装所需的 Python 包
- 通过网页抓取、拆分文档并嵌入文段来对数据进行索引
- 使用向量相似度搜索检索相关文档
- 通过将检索到的上下文集成到 LLM 提示中生成回答
在我们逐步讲解代码的过程中,你将通过使用像 LangChain、Chroma DB 和 OpenAI API 等工具,全面理解 RAG 流程的每个步骤。这样,你将获得坚实的基础,后续章节中我们会在此基础上进行扩展和增强,解决更为复杂的问题。
在后续的章节中,我们将探讨一些技术,可以帮助改善和定制流水线以适应不同的用例,并克服在构建 RAG 驱动的应用时常见的挑战。让我们开始吧,着手构建!
技术要求
本章的代码可以在此链接找到:github.com/PacktPublis…
你需要在已经设置好的 Jupyter notebook 环境中运行本章代码。使用 Jupyter notebook 的经验是使用本书的前提,且短时间内无法在文本中全面讲解。设置 notebook 环境有多种方法,既有在线版本,也有可下载版本,还有大学为学生提供的 notebook 环境,以及不同的界面。如果你在公司工作,他们可能已有一个环境供你使用,值得熟悉。每种方法都有不同的设置说明,并且这些说明常常会发生变化。如果你需要重新学习这类环境的知识,可以从 Jupyter 网站开始:docs.jupyter.org/en/latest/。… LLM 请求更多帮助,以便设置环境。
我使用什么?
当我在旅行时使用 Chromebook 时,我常常在其中一个云环境中设置 notebook。我更喜欢 Google Colab 或者它们的 Colab Enterprise notebook,可以在 Google Cloud Platform 的 Vertex AI 部分找到。但这些环境需要收费,如果使用频繁,通常每月超过 20 美元。如果我像我一样频繁使用,它可能会超过每月 1000 美元!
为了在频繁使用时降低成本,我在我的 Mac 上使用 Docker Desktop,托管一个本地 Kubernetes 集群,并在该集群中设置我的 notebook 环境。所有这些方法都有一些不断变化的环境要求。最好做一些研究,找出最适合你情况的解决方案。Windows 系统也有类似的解决方案。
最终,主要的要求是找到一个可以运行 Jupyter notebook 并使用 Python 3 的环境。我们提供的代码将指示你需要安装哪些其他包。
注意
所有这些代码假设你在 Jupyter notebook 中工作。你也可以直接在 Python 文件(.py)中运行,但可能需要做一些修改。在 notebook 中运行代码可以让你逐单元格地执行,并观察每个步骤发生的情况,更好地理解整个流程。
无界面!
在下面的编码示例中,我们不会使用界面,我们将在第 6 章中处理界面相关内容。与此同时,我们将简单地创建一个字符串变量,表示用户将输入的提示,并将其用作一个完整界面输入的替代。
设置大型语言模型(LLM)账户
对于普通用户来说,OpenAI 的 ChatGPT 模型目前是最流行和最知名的 LLM。然而,市场上还有许多其他适合不同用途的 LLM。你并不总是需要使用最昂贵、最强大的 LLM。有些 LLM 专注于某一领域,比如 Meditron LLM,它是 Llama 2 的医学研究聚焦版。如果你从事医疗领域,可能会想使用这个 LLM,因为它在你的领域内的表现可能比通用的 LLM 更好。通常,LLM 还可以用来交叉验证其他 LLM,所以在这种情况下,你可能需要多个 LLM。我强烈建议你不要仅仅使用你第一次接触的 LLM,而是要寻找最适合你需求的 LLM。但为了保持本书初期的简洁,我将以设置 OpenAI 的 ChatGPT 为例:
- 访问 OpenAI 网站的 API 部分:openai.com/api/。
- 如果你还没有设置账户,请立即注册。页面可能会经常变化,但请注意查找注册的位置。
警告
使用 OpenAI 的 API 是收费的!请谨慎使用!
-
注册后,访问文档:platform.openai.com/docs/quicks… API 密钥。
-
创建 API 密钥时,为它起个容易记住的名字,并选择你想要实施的权限类型(全部、受限或只读)。如果你不确定该选择哪个选项,建议先选择“全部”。但要注意其他选项——你可能希望与其他团队成员共享不同的权限,同时限制某些类型的访问:
- 全部:该密钥将具有对所有 OpenAI API 的读写访问权限。
- 受限:会显示可用的 API 列表,提供对每个 API 的 granular(精细)控制。你可以为每个 API 设置仅读或仅写的访问权限。确保至少启用了你将在本书示例中使用的模型和嵌入 API。
- 只读:此选项仅提供对所有 API 的只读访问权限。
-
复制提供的密钥。稍后你将在代码中使用它。与此同时,请记住,如果将此密钥共享给他人,任何获得此密钥的人都可以使用它,并且你会被收费。所以,这是一个你需要将其视为机密并采取适当预防措施以防止未经授权使用的密钥。
-
OpenAI API 需要你提前购买积分才能使用。购买你觉得合适的额度,然后为了安全起见,确保关闭“启用自动充值”选项。这将确保你只花费你打算花费的金额。
至此,你已经设置好了 RAG 流水线中的关键组件:LLM!接下来,我们将设置你的开发环境,以便连接到 LLM。
安装必要的包
确保这些包已经安装在你的 Python 环境中。在你的笔记本的第一个单元格中添加以下代码:
%pip install langchain_community
%pip install langchain_experimental
%pip install langchain-openai
%pip install langchainhub
%pip install chromadb
%pip install langchain
%pip install beautifulsoup4
以上代码通过 pip 包管理器安装了几个 Python 库,这是你运行我提供的代码所需要的。以下是每个库的简要说明:
- langchain_community: 这是 LangChain 库的社区驱动版本,LangChain 是一个开源框架,用于构建基于 LLM 的应用。它提供了一组工具和组件,用于与 LLM 配合工作,并将它们集成到各种应用中。
- langchain_experimental: 这个库提供了 LangChain 核心库之外的额外功能和工具,这些功能可能尚未完全稳定或生产就绪,但仍可用于实验和探索。
- langchain-openai: 该包提供了 LangChain 和 OpenAI 语言模型之间的集成。它允许你轻松地将 OpenAI 的模型(如 ChatGPT 4 或 OpenAI 嵌入服务)纳入你的 LangChain 应用。
- langchainhub: 这个包提供了一些预构建的组件和模板,用于 LangChain 应用。它包括各种代理、内存组件和实用函数,可以加速 LangChain 应用的开发。
- chromadb: 这是 Chroma DB 的包名,一个高性能的嵌入/向量数据库,设计用于高效的相似度搜索和检索。
- langchain: 这是 LangChain 核心库,提供了一个框架和一组抽象,用于构建基于 LLM 的应用。LangChain 包含了一个有效 RAG 流水线所需的组件,包括提示、内存管理、代理以及与外部工具和服务的集成。
- beautifulsoup4: 这是一个流行的库,用于网页抓取和解析 HTML 或 XML 文档。我们将在网页上进行操作,它可以帮助我们提取标题、内容和头部等信息。
安装完这些包后,你需要重新启动内核才能访问刚才安装的新包。根据你所使用的环境,这可以通过多种方式完成。通常,你会看到一个刷新按钮或一个重启内核的选项。
如果你找不到重新启动内核的方法,可以添加并运行以下代码:
import IPython
app = IPython.Application.instance()
app.kernel.do_shutdown(True)
这段代码用于在 IPython 环境(如 Jupyter notebook)中重启内核。你通常不需要它,但为了防止意外,提供它以备不时之需!
安装完这些包并重启内核后,你就可以开始编码了!让我们从导入刚刚安装的库开始。
导入
现在,我们将导入执行与 RAG 相关任务所需的所有库。我在每组导入的顶部提供了注释,指出这些导入与 RAG 的哪些方面相关。结合以下列表中的描述,这为你构建第一个 RAG 流水线提供了基本介绍:
import os
from langchain_community.document_loaders import WebBaseLoader
import bs4
import openai
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
import chromadb
from langchain_community.vectorstores import Chroma
from langchain_experimental.text_splitter import SemanticChunker
以下是每个导入的详细说明:
- import os: 提供与操作系统交互的方式,用于执行访问环境变量和操作文件路径等操作。
- from langchain_community.document_loaders import WebBaseLoader:
WebBaseLoader
类是一个文档加载器,用于抓取并加载网页内容作为文档。 - import bs4:
bs4
(Beautiful Soup 4)是一个流行的网页抓取和 HTML/XML 文档解析库。我们将使用它从网页中提取标题、内容和头部等信息。 - import openai: 提供与 OpenAI 语言模型和 API 交互的接口。
- from langchain_openai import ChatOpenAI, OpenAIEmbeddings: 导入
ChatOpenAI
(LLM)和OpenAIEmbeddings
(嵌入),这两个是使用 OpenAI 模型的 LangChain 特定实现。 - from langchain import hub:
hub
组件提供了访问 LangChain 组件和实用工具的方式。 - from langchain_core.output_parsers import StrOutputParser: 该组件用于解析语言模型生成的输出,并提取相关信息。在这里,它假设语言模型的输出是字符串,并按原样返回。
- from langchain_core.runnables import RunnablePassthrough: 该组件将问题或查询直接传递,不做任何修改。它允许问题在链的后续步骤中按原样使用。
- import chromadb: 导入 Chroma DB 向量存储,这是一个高性能的嵌入/向量数据库,设计用于高效的相似度搜索和检索。
- from langchain_community.vectorstores import Chroma: 提供了使用 LangChain 与 Chroma 向量数据库进行交互的接口。
- from langchain_experimental.text_splitter import SemanticChunker:
SemanticChunker
是一种文本分割工具,用于将长文本拆分为较小的部分,同时保持每个块的语义连贯性和上下文。
这些导入提供了建立 RAG 流水线所需的核心 Python 包。下一步是将你的环境连接到 OpenAI 的 API。
OpenAI 连接
以下代码演示了如何将你的 API 密钥传入系统。但这不是一个安全的使用方式。实际上,有很多更安全的方法。如果你有偏好的方法,可以现在实现,但我们将在第 5 章中介绍一种常用的安全方法。
你需要将 sk-###################
替换为你实际的 OpenAI API 密钥:
os.environ['OPENAI_API_KEY'] = 'sk-###################'
openai.api_key = os.environ['OPENAI_API_KEY']
重要提醒
这只是一个简单的示例;请使用安全方法隐藏你的 API 密钥!
你可能已经猜到,这个 OpenAI API 密钥将用于连接 ChatGPT LLM。但 ChatGPT 不是我们将从 OpenAI 使用的唯一服务。这个 API 密钥还将用于访问 OpenAI 的嵌入服务。在接下来的章节中,我们将在 RAG 流程的索引阶段使用 OpenAI 嵌入服务,将你的内容转换为向量嵌入,这是 RAG 流水线的关键部分。
索引
接下来的几个步骤代表了索引阶段,我们需要获取目标数据、对其进行预处理,并将其向量化。这些步骤通常是在离线环境下进行的,也就是说它们是为稍后使用应用程序做准备。但在某些情况下,将这些步骤实时进行也是有意义的,比如在数据快速变化的环境中,使用的数据量较小。在本例中,步骤如下:
- 网络加载与爬取
- 将数据分割成适合 Chroma DB 向量化算法处理的小块
- 对这些小块进行嵌入和索引
- 将这些小块和嵌入添加到 Chroma DB 向量存储中
我们从第一个步骤开始:网络加载与爬取。
网络加载与爬取
首先,我们需要拉取数据,当然,数据可以是任何内容,但我们需要从某个地方开始!
在本例中,我提供了一个基于第一章部分内容的网页示例。我采用了 LangChain 在 lilianweng.github.io/posts/2023-… 提供的原始结构。
如果你在阅读时该网页仍然可用,你也可以尝试访问该页面,但请确保将你查询内容的问题更改为更适合该页面内容的问题。如果更改了网页,你需要重启内核;否则,如果你重新运行加载器,它将包含两个网页的内容。也许这正是你想要的,但我只是提醒你一下!
我还鼓励你尝试用其他网页进行实验,看看这些网页会带来哪些挑战。与大多数网页相比,本例涉及的是非常干净的数据,很多网页充满了广告和其他你不想展示的内容。但也许你可以找到一篇相对干净的博客文章并将其拉取?你也可以自己创建一个!尝试不同的网页,看看效果如何!
loader = WebBaseLoader(
web_paths=("https://kbourne.github.io/chapter1.html",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
docs = loader.load()
前面的代码首先使用 langchain_community.document_loaders
模块中的 WebBaseLoader
类来将网页加载为文档。让我们分解一下这段代码:
创建 WebBaseLoader 实例
WebBaseLoader
类通过以下参数实例化:
-
web_paths
:包含要加载的网页 URL 的元组。在这个例子中,它只包含一个 URL:https://kbourne.github.io/chapter1.html
。 -
bs_kwargs
:一个传递给 BeautifulSoup 解析器的关键字参数字典。parse_only
:一个bs4.SoupStrainer
对象,指定要解析的 HTML 元素。在本例中,它设置为仅解析具有post-content
、post-title
和post-header
CSS 类的元素。
WebBaseLoader
实例会启动一系列步骤,表示将文档加载到你的环境中:调用 load
方法,它会拉取并加载指定的网页作为文档。在内部,loader
进行很多工作!
以下是它仅根据这段小代码执行的步骤:
- 向指定的 URL 发起 HTTP 请求,获取网页内容。
- 使用 BeautifulSoup 解析网页的 HTML 内容,只考虑
parse_only
参数中指定的元素。 - 从解析的 HTML 元素中提取相关的文本内容。
- 为每个网页创建
Document
对象,包含提取的文本内容以及来源 URL 等元数据。
最终生成的 Document
对象被存储在 docs
变量中,以便我们后续使用。
我们传递给 bs4
的类(如 post-content
、post-title
和 post-header)
是 CSS 类。如果你使用的 HTML 页面没有这些 CSS 类,这段代码就无法正常工作。所以,如果你使用不同的 URL 且没有获取到数据,请检查你正在爬取的 HTML 页面中使用的 CSS 类。许多网页确实使用这种模式,但并非所有网页都如此!爬取网页时会面临很多这样的挑战。
分割
在你从数据源收集到文档之后,你需要对它们进行预处理。在本例中,预处理的步骤是数据的分割。
如果你使用的是提供的 URL,你将只会解析具有 post-content
、post-title
和 post-header
CSS 类的元素。这会提取主文章正文(通常由 post-content
类标识)、博客文章的标题(通常由 post-title
类标识)以及任何头部信息(通常由 post-header
类标识)。
如果你感到好奇,这就是该文档在网页上的展示效果(如图 2.1):
这篇文档也非常长!内容很多,LLM 直接处理会有点过多。所以,我们需要将文档分割成可消化的小块:
text_splitter = SemanticChunker(OpenAIEmbeddings())
splits = text_splitter.split_documents(docs)
在 LangChain 中有很多文本分割器,但我选择了一个实验性的但非常有趣的选项,叫做 SemanticChunker
。正如我在之前谈到导入时提到的,SemanticChunker
侧重于将长文本拆分成更易处理的块,同时保留每个块的语义一致性和上下文。
其他的文本分割器通常会采用一个任意的块大小,这种方式不考虑上下文,会在一些重要内容被分割时造成问题。解决这个问题的方法我们将在第11章讨论,但现在只需知道,SemanticChunker
聚焦于考虑上下文,而不仅仅是块的任意长度。需要注意的是,它仍然被认为是实验性的,正在持续开发中。在第11章中,我们将把它与可能是另一个最重要的文本分割器——RecursiveCharacter TextSplitter
——进行对比,看看哪个分割器在这个内容上效果更好。
还要指出的是,代码中使用的 SemanticChunker
分割器使用了 OpenAIEmbeddings
,这会产生费用。根据所使用的模型,OpenAI 的嵌入模型当前每百万个 token 的费用在 0.13 之间。撰写本文时,如果没有指定嵌入模型,OpenAI 会默认使用 text-embedding-ada-002
模型,每百万个 token 的费用为 $0.02。如果你想避免费用,可以退回到 RecursiveCharacter TextSplitter
,我们将在第11章进行讲解。
我鼓励你尝试不同的分割器,看看效果如何!例如,你认为使用 RecursiveCharacter TextSplitter
比 SemanticChunker
处理效果更好吗?也许在你特定的情况下,速度比质量更重要——哪个分割器更快呢?
一旦将内容切分后,接下来的步骤是将其转换成我们已经讨论过的向量嵌入!
嵌入和索引切分后的内容
接下来的几步代表了检索和生成步骤,在这个过程中我们将使用 Chroma DB 作为向量数据库。正如之前多次提到的,Chroma DB 是一个非常棒的向量存储!我选择它作为向量存储,是因为它很容易在本地运行,也适合像这样做演示,但它也是一个功能强大的向量存储。正如我们之前谈到的词汇表,以及向量存储与向量数据库的区别,Chroma DB 实际上是两者兼具!不过,Chroma 只是众多向量存储选项之一。在第7章中,我们将讨论许多向量存储选项,以及为什么选择其中的一个而不是另一个。有些选项甚至提供免费的向量嵌入生成服务。
我们这里也使用了 OpenAI 嵌入,它将使用你的 OpenAI 密钥将你的数据切分发送到 OpenAI API,转化为嵌入,并返回其数学表示。需要注意的是,这会产生费用!每个嵌入的费用是几分钱,但值得注意。如果你预算紧张,请谨慎使用这段代码!在第7章中,我们将回顾一些使用免费的向量化服务生成这些嵌入的方法:
vectorstore = Chroma.from_documents(
documents=splits,
embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
首先,我们通过 Chroma.from_documents
方法创建了 Chroma 向量存储,这是从切分文档创建 Chroma 向量存储的一种方法。这是我们创建 Chroma 数据库的众多方法之一。通常这取决于数据源,但对于这个特定方法,它需要以下参数:
documents
:从之前代码片段获得的切分文档列表 (splits
)embedding
:一个OpenAIEmbeddings
类的实例,用来为文档生成嵌入
在内部,该方法执行以下几项操作:
- 遍历
splits
列表中的每个Document
对象。 - 对于每个
Document
对象,使用提供的OpenAIEmbeddings
实例生成嵌入向量。 - 将文档文本和其对应的嵌入向量存储在 Chroma 向量数据库中。
此时,你已经有了一个名为 vectorstore
的向量数据库,里面充满了嵌入,代表了…?没错——你刚刚抓取的网页的所有内容的数学表示!太酷了!
但是接下来的部分是什么?检索器?是狗狗吗?当然不是。这里的“检索器”是指你将用来对新创建的向量数据库执行向量相似性搜索的机制。你可以直接在 vectorstore
实例上调用 as_retriever
方法来创建检索器。检索器是一个对象,提供了一个方便的接口,用于执行相似性搜索并从向量数据库中检索相关文档。
如果你只是想执行文档检索过程,也是可以的。这部分代码不是正式的一部分,但如果你想测试,可以在一个额外的单元中加入并运行:
query = "How does RAG compare with fine-tuning?"
relevant_docs = retriever.get_relevant_documents(query)
relevant_docs
输出应该是我在后续代码中所列的内容,这些内容实际上是从 vectorstore
向量数据库中最与查询相似的文档。
是不是很厉害?当然,这只是一个简单的例子,但这是你可以用来访问数据并为你的组织提供强大支持的生成式 AI 应用的基础!不过,在应用的这个阶段,你只创建了检索器,还没有在 RAG 流水线中使用它。接下来我们将审视如何将它纳入 RAG 流程!
检索与生成
在代码中,检索和生成阶段被合并在我们设置的链中,以表示整个 RAG 过程。这利用了来自 LangChain Hub 的预构建组件,例如提示模板,并将其与选定的 LLM 结合使用。我们还将利用 LangChain 表达式语言(LCEL)定义一个操作链,该链基于输入问题检索相关文档,格式化检索到的内容,并将其输入 LLM 以生成响应。总体而言,检索和生成的步骤如下:
- 获取用户查询。
- 对用户查询进行向量化。
- 对向量存储执行相似性搜索,找到与用户查询向量最相似的向量以及它们相关的内容。
- 将检索到的内容传入提示模板,这个过程称为“填充”。
- 将填充后的提示传递给 LLM。
- 一旦从 LLM 收到响应,将其呈现给用户。
从编码的角度来看,我们将首先定义提示模板,以便在接收到用户查询时能够进行填充。我们将在下一节中详细介绍。
LangChain Hub 中的提示模板
LangChain Hub 是一个包含预构建组件和模板的集合,可以轻松集成到 LangChain 应用程序中。它提供了一个集中式的库,用于共享和发现可重用的组件,如提示、代理和工具。在这里,我们调用了来自 LangChain Hub 的提示模板,并将其分配给变量 prompt
,该模板表示我们将传递给 LLM 的内容:
prompt = hub.pull("jclemens24/rag-prompt")
print(prompt)
这段代码通过使用 hub
模块的 pull
方法从 LangChain Hub 检索一个预构建的提示模板。提示模板通过 jclemens24/rag-prompt
字符串来标识。这个标识符遵循了仓库/组件的命名惯例,其中仓库表示托管组件的组织或用户,组件表示要获取的具体组件。rag-prompt
组件表明这是为 RAG 应用设计的提示模板。
如果你使用 print(prompt)
输出,你可以看到使用的模板以及它的输入变量:
input_variables=['context', 'question']
messages=[HumanMessagePromptTemplate( prompt=PromptTemplate( input_variables=['context', 'question'],
template="You are an assistant for question-answering tasks. Use the following pieces of retrieved-context to answer the question. If you don't know the answer, just say that you don't know.\nQuestion: {question} \nContext: {context} \nAnswer:"))]
这是传递给 LLM 的提示的初步部分,它告诉 LLM:
"You are an assistant for question-answering tasks. Use the following pieces of retrieved-context to answer the question. If you don't know the answer, just say that you don't know.
Question: {question}
Context: {context}
Answer:"
稍后,你会添加问题和上下文变量以填充提示,但从这个格式开始可以优化它,使其在 RAG 应用中更有效。
注意
jclemens24/rag-prompt
字符串是预定义起始提示的一个版本。访问 LangChain Hub 可以找到更多提示模板——你甚至可能找到一个更适合你需求的:LangChain Hub 搜索。
你也可以使用自己的模板!截至本文写作时,我可以列举超过30个选项!
提示模板是 RAG 流水线中的关键部分,因为它代表了你与 LLM 之间的交互方式,以获取你所需要的响应。但在大多数 RAG 流水线中,获取提示并使其与提示模板兼容的过程并不像直接传递一个字符串那么简单。在这个例子中,context
变量表示我们从检索器获得的内容,而这些内容还不是字符串格式!接下来,我们将详细介绍如何将检索到的内容转换为我们所需的字符串格式。
格式化函数,使其与下一步的输入匹配
首先,我们将设置一个函数,接受检索到的文档列表(docs
)作为输入:
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
在这个函数内部,使用了一个生成器表达式 (doc.page_content for doc in docs)
,它从每个文档对象中提取 page_content
属性。page_content
属性表示每个文档的文本内容。
注意
在这里,文档并不是你之前抓取的整个文档。它只是其中的一个小部分,但我们通常称这些为文档。
join
方法在字符串 \n\n
上被调用,用于将每个文档的 page_content
连接起来,每个文档内容之间用两个换行符分隔。格式化后的字符串由 format_docs
函数返回,并表示字典中将传递给提示对象的 context
键。
这个函数的目的是将检索器的输出格式化为下一步所需的字符串格式,确保其能够与后续操作兼容。稍后我们将进一步解释这一点,但像这样的简短函数通常是 LangChain 链中必要的,它们帮助整个链条的输入和输出对接。
接下来,我们将回顾创建 LangChain 链之前的最后一步——那就是定义我们将在该链中使用的 LLM。
定义你的 LLM
让我们设置你将使用的 LLM 模型:
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
上述代码创建了一个 ChatOpenAI
类的实例,它来自 langchain_openai
模块,作为 OpenAI 语言模型的接口,特别是 GPT-4o mini 模型。尽管这个模型较新,但它以显著低于旧模型的价格发布。使用这个模型可以帮助你降低推理成本,同时仍然能使用到较新的模型!如果你想尝试不同版本的 ChatGPT,比如 gpt-4,你只需更改模型名称即可。你可以在 OpenAI API 网站上查找最新的模型——他们经常更新!
使用 LCEL 设置 LangChain 链
这个链是 LangChain 特有的代码格式,称为 LCEL。你将会在接下来的代码中看到我使用 LCEL。它不仅使代码更易读、更简洁,还打开了通过优化 LangChain 代码的速度和效率的全新技术。
如果你逐步走过这个链,你会看到它很好地表示了整个 RAG 过程:
rag_chain = (
{"context": retriever | format_docs,
"question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
这些组件已经在之前被描述过,但为了总结一下,rag_chain
变量代表了一个使用 LangChain 框架的操作链。让我们逐步分析这个链的每个步骤,深入了解每个环节发生了什么:
检索:链中的第一个环节是检索,因为它处理检索相关的操作。然而,它有自己的子链。因此,让我们进一步分解这个步骤:
当我们稍后调用 rag_chain
变量时,我们将传入一个“问题”。如前面的代码所示,这个链从一个定义了两个键的字典开始:“context”和“question”。问题部分非常直观,但“context”从哪里来呢?“context”键赋值的是 retriever | format_docs
操作的结果。
format_docs
听起来很熟悉吧?没错!因为我们之前刚刚设置了这个函数。这里,我们将 format_docs
函数与 retriever
一起使用。|
运算符,也称为管道,将 retriever
和 format_docs
链接在一起。这意味着,retriever
对象的输出被传递到 format_docs
函数。我们在这里运行的是检索操作,即向量相似度搜索。相似度搜索应返回一组匹配项,这些匹配项将作为参数传递给 format_docs
函数。正如前面描述的那样,format_docs
函数会将检索器提供的内容格式化成一个字符串。这个格式化后的字符串将赋值给 context
,正如你可能记得的那样,context
是我们提示中的一个变量。下一步的输入格式应该是一个包含两个键的字典——即“context”和“question”。赋给这些键的值应该是字符串。所以,我们不能直接传递 retriever
输出的对象列表,这就是为什么我们使用 format_docs
函数——将 retriever
结果转换为下一个步骤所需的字符串格式。
回到传递到链中的问题,它已经是我们所需的字符串格式,不需要任何格式化!所以,我们使用 RunnablePassthrough()
对象,让这个输入(已格式化的查询)直接传递下去。这个对象接收传递给 rag_chain
变量的查询,并原样返回,而不进行任何修改。至此,我们在链中完成了第一步,定义了提示中接收的两个变量。
接下来,我们看到另一个管道(|
)紧跟着 prompt
对象,我们将这些变量(以字典的形式)传递给这个提示对象。这就是“填充”提示的过程。如前所述,prompt
对象是一个提示模板,定义了我们将传递给 LLM 的内容,通常包括需要首先填充/填充的输入变量(context
和 question
)。第二步的结果是一个完整的提示文本字符串,变量填充了上下文和问题的占位符。然后,链中又出现了一个管道(|
)和我们之前定义的 llm
对象。正如我们已经看到的,这一步将上一步的输出(包含所有先前步骤信息的提示字符串)传递给语言模型。llm
对象表示我们设置的语言模型,在本例中是 ChatGPT 4o。格式化后的提示字符串被传递给语言模型,语言模型根据提供的上下文和问题生成响应。
这似乎就够了,但使用 LLM API 时,返回的不仅仅是你在 ChatGPT 中输入时看到的文本。它是一个 JSON 格式的响应,还包含了很多其他数据。为了保持简单,我们将 LLM 的输出传递到下一步,并使用 LangChain 的 StrOutputParser()
对象。请注意,StrOutputParser()
是 LangChain 中的一个实用类,用于将语言模型的关键输出解析为字符串格式。它不仅会去除所有你目前不想处理的信息,还确保生成的响应以字符串的形式返回。
让我们花一点时间来欣赏我们刚刚做的工作。通过 LangChain 创建的这个链代表了我们整个 RAG 流水线的核心代码,而这仅仅是几行代码!
当用户使用你的应用时,整个过程将从用户查询开始。但是从编程的角度来看,我们设置了其他所有内容,以便我们能够正确处理查询。到目前为止,我们已经准备好接受用户查询了,让我们回顾一下代码中的最后一步。
提交一个问题进行 RAG
到目前为止,你已经定义了链,但还没有运行它。那么,让我们用以下一行代码来运行整个 RAG 流水线,并传入你要查询的问题:
rag_chain.invoke("What are the advantages of using RAG?")
如前所述,当我们走过链的各个步骤时,"What are the advantages of using RAG?"
是我们将传递到链中的字符串,作为问题传入链的第一步。链中的第一步将这个字符串作为我们之前提到的“问题”变量之一进行处理。在某些应用中,这个问题可能不是我们所期望的格式,可能需要额外的函数来进行准备,但在本应用中,它已经是我们期望的字符串格式,所以我们直接将它传递给 RunnablePassthrough()
对象。
将来,这个提示将包含来自用户界面的查询,但现在,我们将其表示为这个变量字符串。请记住,这不仅仅是 LLM 会看到的文本;你之前已经定义了一个更强大的提示,并由 context
和 question
变量进行填充。
最终输出
最终的输出看起来像这样:
The advantages of using Retrieval Augmented Generation (RAG) include:
1. **Improved Accuracy and Relevance:** RAG enhances the accuracy and relevance of responses generated by large language models (LLMs) by fetching and incorporating specific information from databases or datasets in real time. This ensures outputs are based on both the model's pre-existing knowledge and the most current and relevant data provided.
2. **Customization and Flexibility:** RAG allows for the customization of responses based on domain-specific needs by integrating a company's internal databases into the model's response generation process. This level of customization is invaluable for creating personalized experiences and for applications requiring high specificity and detail.
3. **Expanding Model Knowledge Beyond Training Data:** RAG overcomes the limitations of LLMs, which are bound by the scope of their training data. By enabling models to access and utilize information not included in their initial training sets, RAG effectively expands the knowledge base of the model without the need for retraining. This makes LLMs more versatile and adaptable to new domains or rapidly evolving topics.
这有一些基础的格式化,因此当它显示时,会像这样(包括项目符号和加粗文本):
The advantages of using Retrieval Augmented Generation (RAG) include:
- Improved Accuracy and Relevance: RAG 增强了大语言模型(LLM)生成的回答的准确性和相关性,通过实时从数据库或数据集中提取并整合特定信息。这确保了输出不仅基于模型的预先知识,还基于提供的最新和最相关的数据。
- Customization and Flexibility: RAG 允许根据特定领域的需求定制回答,通过将公司的内部数据库整合到模型的响应生成过程中。这种定制化对于创建个性化体验和对细节要求很高的应用非常宝贵。
- Expanding Model Knowledge Beyond Training Data: RAG 克服了 LLM 的局限性,因为 LLM 受其训练数据范围的限制。通过使模型能够访问和利用初始训练集中未包含的信息,RAG 有效地扩展了模型的知识库,而无需重新训练。这使得 LLM 更具通用性,能够适应新的领域或快速发展的主题。
进一步思考
在你的应用场景中,你可能需要通过提问来做出决策,例如:“使用一个更便宜的模型是否能够以显著降低的成本完成足够好的工作?”,或者“我是否需要花更多的钱来获得更强大的响应?”如果你的提示要求非常简洁,那么即使是一个便宜的模型也可能会产生相同的较短响应,那为什么还要花更多的钱呢?这是使用这些模型时一个常见的考虑因素,在许多情况下,最大的、最昂贵的模型并不总是满足应用需求的最佳选择。
LLM 将看到的内容
当你将这个与之前 RAG 相关的提示结合时,LLM 会看到这样的内容:
vbnet
复制代码
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know.
Question: What are the Advantages of using RAG?
Context: Can you imagine what you could do with all of the benefits mentioned above, but combined with all of the data within your company, about everything your company has ever done, about your customers and all of their interactions, or about all of your products and services combined with a knowledge of what a specific customer's needs are? You do not have to imagine it, that is what RAG does! Even smaller companies are not able to access much of their internal data resources very effectively. Larger companies are swimming in petabytes of data that are not readily accessible or are not being fully utilized. Before RAG, most of the services you saw that connected customers or employees with the data resources of the company were just scratching the surface of what is possible compared to if they could access ALL of the data in the company. With the advent of RAG and generative AI in general, corporations are on the precipice of something really, really big. Comparing RAG with Model Fine-Tuning#
Established Large Language Models (LLM), what we call the foundation models, can be learned in two ways:
Fine-tuning - With fine-tuning, you are adjusting the weights and/or biases that define the model's intelligence based
[TRUNCATED FOR BREVITY!]
Answer:
如你所见,context
是非常大的,它返回了来自原始文档的所有最相关的信息,以帮助 LLM 确定如何回答这个新问题。这个 context
是通过向量相似度搜索返回的内容,稍后我们将在第 8 章中深入探讨这个过程。
完整代码
以下是完整代码:
%pip install langchain_community
%pip install langchain_experimental
%pip install langchain-openai
%pip install langchainhub
%pip install chromadb
%pip install langchain
%pip install beautifulsoup4
# 在运行以下代码前请重启内核:
import os
from langchain_community.document_loaders import WebBaseLoader
import bs4
import openai
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
import chromadb
from langchain_community.vectorstores import Chroma
from langchain_experimental.text_splitter import SemanticChunker
os.environ['OPENAI_API_KEY'] = 'sk-###################'
openai.api_key = os.environ['OPENAI_API_KEY']
#### 索引步骤 ####
loader = WebBaseLoader(
web_paths=("https://kbourne.github.io/chapter1.html",),
bs_kwargs=dict(parse_only=bs4.SoupStrainer(
class_=("post-content",
"post-title",
"post-header")
)
),
)
docs = loader.load()
text_splitter = SemanticChunker(OpenAIEmbeddings())
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(
documents=splits,
embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
#### 检索与生成 ####
prompt = hub.pull("jclemens24/rag-prompt")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
llm = ChatOpenAI(model_name="gpt-4o-mini")
rag_chain = (
{"context": retriever | format_docs,
"question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
rag_chain.invoke("What are the Advantages of using RAG?")
总结
本章提供了一个完整的代码实验室,展示了如何实现完整的 RAG 流水线。以下是关键步骤的回顾:
- 安装必要的 Python 包:包括 LangChain、Chroma DB,以及其他相关扩展。
- 设置 OpenAI API 密钥:使用 WebBaseLoader 从网页加载文档,并结合 BeautifulSoup 预处理 HTML 内容以提取相关部分。
- 切分文档:使用 LangChain 的实验模块
SemanticChunker
将加载的文档分割成可管理的小块。 - 嵌入文档:通过 OpenAI 的嵌入模型将文档转换为向量表示,并存储在 Chroma DB 向量数据库中。
- 构建检索器:通过检索器基于查询对嵌入文档执行向量相似度搜索。
- RAG 检索与生成阶段:结合 LCEL,将检索与生成整合到一个 LangChain 链中。链包括来自 LangChain Hub 的预构建提示模板、选定的 LLM,以及用于格式化检索文档和解析 LLM 输出的实用函数。
- 提交查询并接收生成的响应:我们通过 RAG 流水线生成了一个响应,该响应结合了检索到的上下文。