LangChain 中的关键 RAG 组件

159 阅读37分钟

本章深入探讨了我们之前讨论过的关键技术组件,重点是它们在 LangChain 和检索增强生成(RAG)中的应用。回顾一下,我们的 RAG 系统的关键技术组件按使用顺序依次是向量存储、检索器和大型语言模型(LLMs)。我们将逐步介绍最新版本的代码,该版本在第 8 章的代码实验 8.3 中首次展示。我们将重点讨论这些核心组件,并通过 LangChain 展示每个组件的不同选项。显然,很多讨论将着重于不同选项之间的差异,并讨论在不同场景下哪种选项可能更优。

我们将从一个代码实验开始,概述向量存储的不同选项。

技术要求

本章的代码托管在以下 GitHub 仓库中:GitHub Repository

代码实验 10.1 – LangChain 向量存储

本章所有代码实验的目标是帮助你更熟悉 LangChain 平台中每个关键组件的选项,如何增强你的 RAG 系统。我们将深入探讨每个组件的功能、可用的函数、能够带来差异的参数,最终展示你可以利用的所有选项,从而实现更好的 RAG 实现。从第 8 章的代码实验 8.3 开始(跳过第 9 章的评估代码),我们将按照它们在代码中的出现顺序逐一讲解这些元素,首先从向量存储开始。你可以在 GitHub 上的第 10 章代码文件夹中找到完整的代码,文件夹标签为 10.1。

向量存储、LangChain 和 RAG

向量存储在 RAG 系统中发挥着至关重要的作用,能够高效地存储和索引知识库文档的向量表示。LangChain 提供了与多种向量存储实现的无缝集成,如 Chroma、Weaviate、FAISS(Facebook AI 相似度搜索)、pgvector 和 Pinecone。在本代码实验中,我们将展示如何将数据添加到 Chroma、Weaviate 和 FAISS,打下基础,以便你能够将 LangChain 提供的任何向量存储集成进来。这些向量存储提供了高性能的相似度搜索功能,能够基于查询向量快速检索相关文档。

LangChain 的向量存储类充当了与不同向量存储后端交互的统一接口。它提供了将文档添加到向量存储、执行相似度搜索和检索已存储文档的方法。这种抽象使得开发人员可以轻松地在不同的向量存储实现之间切换,而无需修改核心的检索逻辑。

在构建 RAG 系统时,你可以利用向量存储类来高效地存储和检索文档向量。向量存储的选择取决于诸如可扩展性、搜索性能和部署要求等因素。例如,Pinecone 提供了一个完全托管的向量数据库,具有高度的可扩展性和实时搜索功能,非常适合生产级的 RAG 系统。而 FAISS 提供了一个高效的开源相似度搜索库,适用于本地开发和实验。Chroma 是开发人员在构建首个 RAG 管道时常用的选择,因其易于使用并且与 LangChain 的集成非常有效。

如果你回顾之前讨论的代码,你会发现我们已经在使用 Chroma。以下是展示我们使用 Chroma 的代码片段,你也可以在本代码实验中的代码中找到这部分:

chroma_client = chromadb.Client()
collection_name = "google_environmental_report"
vectorstore = Chroma.from_documents(
               documents=dense_documents,
               embedding=embedding_function,
               collection_name=collection_name,
               client=chroma_client
)

LangChain 将其称为集成,因为它与名为 Chroma 的第三方进行了集成。LangChain 还提供了许多其他的集成选项。

在 LangChain 网站上,目前可以在网站顶部导航中找到一个“Integrations”链接,点击后,你将看到一个左侧菜单,其中包含“Providers”和“Components”两个主要类别。正如你可能猜到的,这让你可以按提供者或组件查看所有集成。如果点击“Providers”,你会首先看到“Partner Packages”和“Featured Community Providers”列表。Chroma 当前不在这两个列表中,但如果你想了解 Chroma 作为提供者的更多信息,可以点击页面底部的“Click here to see all providers”链接。列表按字母顺序排列,滚动到 C 部分,你可以找到 Chroma。这将为你展示与 Chroma 相关的有用 LangChain 文档,特别是在创建向量存储和检索器时。

另一种有用的方法是点击“Components”下的“Vector stores”选项。当前有 49 种向量存储选项!你可以通过以下链接查看版本 0.2.0,但也请关注未来的版本: python.langchain.com/v0.2/docs/i…

我们强烈建议你查看 LangChain 向量存储文档: api.python.langchain.com/en/latest/c…

我们已经在过去的章节中深入讨论了当前的向量存储和 Chroma,但让我们回顾一下 Chroma,并讨论它最适用的场景。

Chroma

Chroma 是一个开源的 AI 原生向量数据库,旨在提高开发人员的生产力和易用性。它提供了快速的搜索性能,并通过其 Python SDK 实现了与 LangChain 的无缝集成。Chroma 支持多种部署模式,包括内存部署、持久化存储和使用 Docker 的容器化部署。

Chroma 的一个关键优势是它的简洁性和开发者友好的 API。它提供了简单的方法,用于添加、更新、删除和查询向量存储中的文档。Chroma 还支持基于元数据的动态过滤集合,从而使搜索更加精准。此外,Chroma 提供了内置的文档切分和索引功能,使得处理大规模文本数据集变得更为便捷。

在考虑将 Chroma 作为 RAG 应用的向量存储时,重要的是要评估其架构和选择标准。Chroma 的架构包括一个用于快速向量检索的索引层、一个用于高效数据管理的存储层以及一个用于实时操作的处理层。Chroma 与 LangChain 无缝集成,使得开发人员可以在 LangChain 生态系统内充分利用其功能。Chroma 客户端可以轻松实例化并传递给 LangChain,从而实现高效的文档向量存储和检索。Chroma 还支持高级检索选项,如最大边际相关性(MMR)和元数据过滤,以优化搜索结果。

总的来说,Chroma 是一个很好的选择,适合那些寻求开源、易用的向量数据库,并且希望与 LangChain 进行良好集成的开发人员。它的简洁性、快速的搜索性能以及内置的文档处理功能使其成为构建 RAG 应用的理想选择。事实上,这也是我们在本书中多次选用 Chroma 的原因之一。然而,评估你的具体需求,并将 Chroma 与其他向量存储选项进行对比,以确定最适合你的项目的解决方案,是非常重要的。让我们来看一下代码,并讨论一些其他的可用选项,从 FAISS 开始。

FAISS

让我们从如何使用 FAISS 作为向量存储开始。如果你想使用 FAISS,你首先需要安装 FAISS:

%pip install faiss-cpu

安装新包后(因为你安装了新包),重新启动内核并运行所有代码直到与向量存储相关的部分,接着将 Chroma 相关的代码替换为 FAISS 向量存储的实例化代码:

from langchain_community.vectorstores import FAISS
vectorstore = FAISS.from_documents(
               documents=dense_documents,
               embedding=embedding_function
)

我们用 FAISS 替换了 Chroma 的 from_documents() 方法调用。collection_nameclient 参数不适用于 FAISS,因此它们已从方法调用中移除。我们重复了一些与 Chroma 向量存储相关的代码,例如文档生成,这使得我们能够在代码中清楚地展示两种向量存储选项的完全等价实现。通过这些更改,代码现在可以使用 FAISS 作为向量存储,而不是 Chroma。

FAISS 是由 Facebook AI 开发的一个开源库,提供高性能的搜索功能,并能够处理可能无法完全加载到内存中的大数据集。与其他提到的向量存储一样,FAISS 的架构包括一个用于快速检索的索引层、高效的数据管理存储层以及一个可选的实时操作处理层。FAISS 提供了多种索引技术,如聚类和量化,以优化搜索性能和内存使用。它还支持 GPU 加速,以进一步提升相似度搜索的速度。

如果你有 GPU 可用,可以安装 GPU 版本的 FAISS:

%pip install faiss-gpu

使用 FAISS 的 GPU 版本可以显著加速相似度搜索过程,特别是对于大规模数据集。GPU 可以并行处理大量的向量比较,从而加速 RAG 应用中相关文档的检索。如果你在处理大量数据并且需要比我们目前使用的(Chroma)更大的性能提升,强烈建议你测试 FAISS GPU,并查看它对你工作的影响。

FAISS 的 LangChain 文档提供了详细的示例和说明,帮助开发人员更好地理解如何将 FAISS 集成到 LangChain 项目中。

Weaviate

Weaviate 提供了多种使用和访问方式。我们将展示嵌入式版本,它通过应用程序代码启动 Weaviate 实例,而不是通过独立的 Weaviate 服务器安装。

当嵌入式 Weaviate 首次启动时,它会在 persistence_data_path 设置的位置创建一个永久的数据存储。当客户端退出时,嵌入式 Weaviate 实例也会退出,但数据会保留下来。下次客户端运行时,它会启动一个新的嵌入式 Weaviate 实例。新的嵌入式 Weaviate 实例会使用保存在数据存储中的数据。

如果你熟悉 GraphQL,你可能会发现 Weaviate 的代码中有 GraphQL 的影子。查询语言和 API 受到 GraphQL 的启发,但 Weaviate 并没有直接使用 GraphQL。Weaviate 使用的是一个 RESTful API,查询语言在结构和功能上类似于 GraphQL。Weaviate 在架构定义中使用预定义的数据类型,类似于 GraphQL 的标量类型。Weaviate 支持的数据类型包括字符串(string)、整数(int)、数值(number)、布尔值(Boolean)、日期(date)等。

Weaviate 的一个优势是它支持批量操作,可以在一次请求中创建、更新或删除多个数据对象。这类似于 GraphQL 的变更操作,你可以在一次请求中进行多个修改。Weaviate 使用 client.batch 上下文管理器将多个操作组合成一个批次,稍后我们将演示这一点。

让我们从如何使用 Weaviate 作为向量存储开始。如果我们要使用 Weaviate,首先需要安装 FAISS:

%pip install weaviate-client
%pip install langchain-weaviate

在重新启动内核(因为你安装了新包)后,运行直到与向量存储相关的代码,并更新代码以初始化 FAISS 向量存储:

import weaviate
from langchain_weaviate.vectorstores import WeaviateVectorStore
from weaviate.embedded import EmbeddedOptions
from langchain.vectorstores import Weaviate
from tqdm import tqdm

如你所见,需要导入许多额外的包。我们还安装了 tqdm,它与 Weaviate 并不特定相关,但它是必要的,因为 Weaviate 使用 tqdm 来显示加载进度条。

首先,我们必须声明 weaviate_client 为 Weaviate 客户端:

weaviate_client = weaviate.Client(
    embedded_options=EmbeddedOptions())

与我们之前使用 Chroma 向量存储的代码相比,使用 Weaviate 的变化要复杂一些。使用 Weaviate 时,我们通过 WeaviateClient 客户端初始化,并通过嵌入选项启用嵌入模式,如前面所见。

在继续之前,我们需要确保没有已有的 Weaviate 客户端实例,否则代码会失败:

try:
    weaviate_client.schema.delete_class(collection_name)
except:
    pass

对于 Weaviate,你需要确保清除掉过去迭代中可能残留的模式,因为它们可能会在后台持续存在。

接下来,我们使用 weaviate 客户端通过类似于 GraphQL 的定义架构来创建数据库:

weaviate_client.schema.create_class({
    "class": collection_name,
    "description": "Google Environmental Report",
    "properties": [
        {
            "name": "text",
            "dataType": ["text"],
            "description": "Text content of the document"
        },
        {
            "name": "doc_id",
            "dataType": ["string"],
            "description": "Document ID"
        },
        {
            "name": "source",
            "dataType": ["string"],
            "description": "Document source"
        }
    ]
})

这段代码提供了一个完整的模式类,稍后你将在向量存储定义中将其作为 weaviate_client 对象的一部分传递。你需要使用 client.collections.create() 方法为你的集合定义此模式。模式定义包括指定类名、属性及其数据类型。属性可以具有不同的数据类型,例如字符串、整数和布尔值。正如你所看到的,Weaviate 强制执行了比我们在之前使用 Chroma 时更严格的模式验证。

尽管这种类似于 GraphQL 的模式为建立向量存储增添了一些复杂性,但它也为你提供了更多控制数据库的方式,尤其是对于如何定义模式,你可以更细粒度地控制。

接下来,你可能会发现下面的代码与你之前定义的 dense_documentssparse_documents 变量很像,但如果仔细观察,你会发现有一个微小的差别,这对于 Weaviate 很重要:

dense_documents = [Document(page_content=text, metadata={"doc_id": str(i), "source": "dense"}) for i, text in enumerate(splits)]
sparse_documents = [Document(page_content=text, metadata={"doc_id": str(i), "source": "sparse"}) for i, text in enumerate(splits)]

我们在预处理文档时稍微修改了元数据,使用 doc_id 而不是 id。这是因为 id 是 Weaviate 内部使用的,因此无法直接使用。在稍后的代码中,当你从元数据中提取 ID 时,需要更新代码,使用 doc_id

接下来,我们定义向量存储,类似于我们过去使用 Chroma 和 FAISS 时的做法,但这里使用了 Weaviate 特有的参数:

vectorstore = Weaviate(
    client=weaviate_client,
    embedding=embedding_function,
    index_name=collection_name,
    text_key="text",
    attributes=["doc_id", "source"],
    by_text=False
)

对于向量存储初始化,Chroma 使用 from_documents 方法直接从文档创建向量存储,而 Weaviate 是先创建向量存储,然后再添加文档。Weaviate 还要求额外的配置,如 text_keyattributesby_text。一个重要的区别是 Weaviate 的使用模式。

最后,我们用实际内容加载 Weaviate 向量存储实例,这也会在过程中应用嵌入函数:

weaviate_client.batch.configure(batch_size=100)
with weaviate_client.batch as batch:
    for doc in tqdm(dense_documents, desc="Processing documents"):
        properties = {
            "text": doc.page_content,
            "doc_id": doc.metadata["doc_id"],
            "source": doc.metadata["source"]
        }
        vector = embedding_function.embed_query(doc.page_content)
        batch.add_data_object(
            data_object=properties,
            class_name=collection_name,
            vector=vector
        )

总结来说,Chroma 提供了更简单且灵活的数据模式定义方法,专注于嵌入存储和检索,且可以轻松嵌入到你的应用程序中。另一方面,Weaviate 提供了一个更结构化、更丰富的向量数据库解决方案,具有显式的模式定义、多个存储后端以及内建支持各种嵌入模型。它可以作为独立的服务器部署,也可以托管在云中。选择 Chroma、Weaviate 或其他向量存储取决于你的具体需求,比如模式灵活性、部署偏好以及是否需要嵌入存储之外的附加功能。

请注意,你可以使用这些向量存储中的任何一个,并且剩下的代码都可以在加载数据时正常工作。这是使用 LangChain 的优势之一,它允许你轻松地替换组件。这在生成性 AI 领域尤其重要,因为新技术和显著改进的技术层出不穷。采用这种方法,如果你遇到一种更新且更好的向量存储技术,可以相对轻松地进行切换。接下来,我们将讨论 LangChain 工具链中的另一个关键组件——检索器,它是 RAG 应用中的核心部分。

代码实验 10.2 – LangChain 检索器

在本次代码实验中,我们将展示在检索过程中最重要的组件之一:LangChain 检索器。与 LangChain 向量存储一样,LangChain 提供了多种检索器选项,无法一一列举。我们将重点介绍一些特别适用于 RAG 应用的流行选项,并鼓励你查看其他选项,以便根据你的特定需求选择最佳解决方案。就像我们讨论向量存储一样,LangChain 网站上有丰富的文档,能够帮助你找到最佳方案:LangChain 官方文档
检索器包的文档可以在这里找到:LangChain API 文档
现在,让我们开始编码检索器!

检索器、LangChain 和 RAG

检索器负责查询向量存储,并根据输入查询返回最相关的文档。LangChain 提供了一系列检索器实现,可以与不同的向量存储和查询编码器一起使用。

在到目前为止的代码中,我们已经看到了三种不同版本的检索器;让我们先回顾一下这些与原始 Chroma 向量存储相关的版本。

基本检索器(密集嵌入)

我们从密集检索器开始。这是我们在多个代码实验中使用过的代码:

dense_retriever = vectorstore.as_retriever(
    search_kwargs={"k": 10})

密集检索器是通过 vectorstore.as_retriever 函数创建的,指定要检索的前 10 个最相关结果(k=10)。在这个检索器的实现中,Chroma 使用文档的密集向量表示,并通过余弦距离或欧几里得距离执行相似度搜索,从而根据查询嵌入检索最相关的文档。

这使用了最简单的检索器类型 —— 向量存储检索器,它只是为每段文本创建嵌入,并使用这些嵌入进行检索。该检索器本质上是对向量存储的一个包装器。通过这种方法,你可以访问向量存储的内建检索/搜索功能,但以一种与 LangChain 生态系统集成的方式进行。它是向量存储类的轻量级包装器,提供了一个一致的接口,适用于 LangChain 中所有检索器选项。因此,一旦构建了一个向量存储,构建一个检索器就变得非常简单。如果你需要更改向量存储或检索器,也同样很容易做到。

这些检索器主要提供两种搜索功能,直接源自它们包装的向量存储:相似度搜索和 MMR。

相似度得分阈值检索

默认情况下,检索器使用相似度搜索。如果你希望设置一个相似度阈值,你只需将搜索类型设置为 similarity_score_threshold,并在传递给检索器对象的 kwargs 函数中设置相似度得分阈值。代码如下:

dense_retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.5}
)

这是对默认相似度搜索的一个有用升级,在许多 RAG 应用中非常有用。然而,相似度搜索并不是这些检索器唯一支持的搜索类型,还有 MMR。

MMR(最大化多样性和相关性)

MMR 是一种在避免冗余的同时从查询中检索相关项的技术。它平衡了检索项的相关性和多样性,而不仅仅是检索最相关的项,因为这些项可能非常相似。MMR 通常用于信息检索,也可以用于通过计算文本各部分之间的相似度来总结文档。为了设置你的检索器使用这种类型的搜索,而不是相似度搜索,你可以在定义检索器时添加 search_type="mmr" 参数,如下所示:

dense_retriever = vectorstore.as_retriever(
    search_type="mmr"
)

将此添加到任何基于向量存储的检索器中,将使其利用 MMR 搜索类型。

相似度搜索和 MMR 可以通过支持这些搜索技术的任何向量存储来实现。

接下来,我们来讨论第 8 章介绍的稀疏搜索机制 —— BM25 检索器。

BM25 检索器

BM25 是一种用于稀疏文本检索的排名函数,BM25Retriever 是 LangChain 中的 BM25 表示,它可以用于稀疏文本检索。

你也已经看到过这个检索器,因为我们在第 8 章中用它将基本搜索转换为混合搜索。在我们的代码中可以看到如下设置:

sparse_retriever = BM25Retriever.from_documents(
    sparse_documents, k=10)

BM25Retriever.from_documents() 方法被调用来创建一个稀疏检索器,从稀疏文档中指定要检索的前 10 个最相关的结果(k=10)。

BM25 通过计算每个文档的相关性得分,该得分基于查询词和文档的词频及逆文档频率(TF-IDF)。它使用概率模型来估计文档与给定查询的相关性。检索器返回具有最高 BM25 得分的前 k 个文档。

集成检索器

集成检索器结合了多种检索方法,并使用额外的算法将它们的结果合并为一个集合。使用这种类型的检索器的理想情况是,当你想要将密集检索器和稀疏检索器结合起来,支持混合检索方法时,就像我们在第8章的代码实验8.3中所创建的那样:

ensemble_retriever = EnsembleRetriever(
    retrievers=[dense_retriever, sparse_retriever],
    weights=[0.5, 0.5], c=0, k=10)

在我们的案例中,集成检索器将Chroma密集检索器和BM25稀疏检索器结合起来,以实现更好的检索性能。它是通过EnsembleRetriever类创建的,该类接受检索器列表及其对应的权重。在这里,密集检索器和稀疏检索器的权重各自为0.5。

c参数是一个重排序参数,用于控制原始检索得分和重排序得分之间的平衡。它用于调整重排序步骤对最终检索结果的影响。在这个例子中,c参数设置为0,表示不执行重排序。当c设置为非零值时,集成检索器会对检索到的文档进行额外的重排序步骤。重排序步骤会根据一个独立的重排序模型或函数对检索到的文档进行重新打分。重排序模型可以考虑额外的特征或标准来评估文档与查询的相关性。

在RAG应用中,检索到文档的质量和相关性直接影响生成的输出。通过利用c参数和合适的重排序模型,你可以优化检索结果,以更好地满足RAG应用的特定需求。例如,你可以设计一个重排序模型,考虑文档的相关性、与查询的一致性或领域特定的标准。通过为c设置适当的值,你可以在原始检索得分和重排序得分之间找到平衡,在需要时赋予重排序模型更多的权重。这可以帮助优先选择那些对RAG任务更相关、更有信息量的文档,从而改善生成的输出。

当查询传递给集成检索器时,它将查询传递给密集和稀疏检索器。然后,集成检索器根据它们分配的权重将两个检索器的结果合并,并返回前k个文档。在内部,集成检索器利用了密集和稀疏检索方法的优势。密集检索通过密集向量表示捕获语义相似性,而稀疏检索则依赖于关键词匹配和词频。通过结合它们的结果,集成检索器旨在提供更准确、更全面的搜索结果。

代码片段中使用的具体类和方法可能会因所使用的库或框架而有所不同。但密集检索(使用向量相似性搜索)、稀疏检索(使用BM25)和集成检索(结合多个检索器)的基本概念是相同的。

这部分内容涵盖了我们之前在代码中看到的所有检索器,它们都来自我们在索引阶段访问和处理的数据。LangChain网站上有许多其他类型的检索器,可以与从文档中提取的数据一起使用,您可以根据需求进行探索。然而,并非所有检索器都设计用来从您正在处理的文档中提取数据。接下来,我们将回顾一个基于公共数据源(Wikipedia)的检索器示例。

Wikipedia检索器

正如LangChain网站上Wikipedia检索器的创建者所描述的那样(www.langchain.com/):

“维基百科是历史上最大和最广泛阅读的参考书籍,作为一个多语言的免费在线百科全书,由一群志愿者编写和维护。”

这听起来像是一个非常适合在RAG应用中提取有用知识的资源!我们将在现有的检索器单元后添加一个新单元,在其中使用这个Wikipedia检索器来从wikipedia.org获取维基百科页面,并将其转换为下游使用的文档格式。

首先,我们需要安装几个新包:

%pip install langchain_core
%pip install --upgrade --quiet wikipedia

和往常一样,当你安装新包时,别忘了重启你的内核!

有了WikipediaRetriever检索器,我们现在就有了一个可以根据用户查询从维基百科获取数据的机制,类似于我们使用的其他检索器,但它背后有整个维基百科的数据支持:

from langchain_community.retrievers import WikipediaRetriever
retriever = WikipediaRetriever(load_max_docs=10)
docs = retriever.get_relevant_documents(query=
    "What defines the golden age of piracy in the Caribbean?")
metadata_title = docs[0].metadata['title']
metadata_summary = docs[0].metadata['summary']
metadata_source = docs[0].metadata['source']
page_content = docs[0].page_content
print(f"First document returned:\n")
print(f"Title: {metadata_title}\n")
print(f"Summary: {metadata_summary}\n")
print(f"Source: {metadata_source}\n")
print(f"Page content:\n\n{page_content}\n")

在这段代码中,我们从langchain_community.retrievers模块中导入了WikipediaRetriever类。WikipediaRetriever是一个专门设计用于根据给定查询从维基百科检索相关文档的检索器类。然后我们使用WikipediaRetriever类实例化一个检索器对象,并将其赋值给变量retrieverload_max_docs参数设置为10,表示检索器最多加载10个相关文档。这里的用户查询是“什么定义了加勒比海海盗黄金时代?”,我们可以查看响应,了解维基百科检索到哪些文章来帮助回答这个问题。

我们调用检索器对象的get_relevant_documents方法,将查询字符串作为参数传入,并将返回的第一个文档作为响应:

返回的第一个文档:

Title: Golden Age of Piracy
Summary: The Golden Age of Piracy is a common designation for the period between the 1650s and the 1730s, when maritime piracy was a significant factor in the histories of the North Atlantic and Indian Oceans.
Histories of piracy often subdivide the Golden Age of Piracy into three periods:
The buccaneering period (approximately 1650 to 1680)…

你可以通过以下链接查看匹配的内容: Golden Age of Piracy

此链接是检索器提供的源。

总结来说,这段代码演示了如何使用langchain_community.retrievers模块中的WikipediaRetriever类,根据给定查询从维基百科检索相关文档。然后,它提取并打印了第一个检索文档的特定元数据(标题、摘要、来源)和页面内容。

WikipediaRetriever内部处理了查询维基百科的API或搜索功能,检索相关文档,并将它们作为文档对象列表返回。每个文档对象包含元数据和实际的页面内容,可以根据需要进行访问和利用。还有许多其他检索器可以访问类似的公共数据源,但它们专注于特定的领域。例如,对于科学研究,存在PubMedRetriever;对于其他领域的研究,如数学和计算机科学,则有ArxivRetriever,它访问超过200万篇关于这些学科的开放存取学术文章;在金融领域,还有一个叫KayAiRetriever的检索器,它可以访问美国证券交易委员会(SEC)的文件,其中包含上市公司需要提交的财务报表。

对于处理非大规模数据的项目,我们还有一个检索器需要强调:kNN检索器。

kNN 检索器

到目前为止,我们一直在使用近似最近邻(ANN)算法来查找与用户查询最相关的内容。然而,除了 ANN 之外,还有一种更传统且历史更久远的算法作为替代,那就是 k 最近邻(kNN)算法。kNN 算法的历史可以追溯到1951年,那么,既然有更先进、更强大的 ANN 算法,为什么还要使用 kNN 呢?答案是,kNN 仍然比任何后来的算法更有效。没错,这并不是笔误。kNN 仍然是寻找最近邻的最有效方法。它比 ANN 更好,尽管许多数据库、向量数据库以及信息检索公司都把 ANN 视为解决方案。虽然 ANN 可以接近 kNN 的效果,但 kNN 仍然被认为是更优秀的选择。

那为什么 ANN 会被推崇为解决方案呢?因为 kNN 无法像 ANN 那样扩展到这些大企业所面临的规模。不过,这一切都是相对的。假设你有百万级的数据点,看似很多,每个数据点有 1,536 个维度的向量,但在全球企业的规模下,这样的数据量还是算比较小的,kNN 完全能够轻松处理!许多使用 ANN 的小型项目可能会从使用 kNN 中受益。kNN 的理论极限取决于多种因素,如开发环境、数据量、数据的维度、如果使用 API 还涉及互联网连接等。所以我们无法给出一个具体的数据点数目,你需要自己测试。如果你的项目规模小于我刚才描述的那个例子(百万级数据点,每个数据点有 1,536 个维度的向量),并且开发环境相对强大,那么你应该考虑使用 kNN!当你发现处理时间显著增加,且等待时间过长,影响了应用的实用性时,可以切换到 ANN。但在此之前,务必充分利用 kNN 的卓越搜索能力。

幸运的是,kNN 可以通过一个易于配置的检索器 KNNRetriever 实现。这个检索器将使用我们在其他算法中也使用的密集嵌入,因此我们可以用基于 kNN 的 KNNRetriever 替换掉原先的 dense_retriever。下面是实现代码,在我们定义了之前版本的 dense_retriever 之后,代码将紧接着进行替换:

from langchain_community.retrievers import KNNRetriever

dense_retriever = KNNRetriever.from_texts(splits, OpenAIEmbeddings(), k=10)
ensemble_retriever = EnsembleRetriever(
    retrievers=[dense_retriever, sparse_retriever],
    weights=[0.5, 0.5], c=0, k=10
)

运行代码时,kNN 检索器将取代之前的 dense_retriever,并执行相应的功能。在此特定情况下,数据集非常有限,很难评估 kNN 是否比之前的基于 ANN 的算法更好。但随着项目的扩展,我们强烈建议在其扩展问题变得过于繁重之前,利用这种方法。

这就是我们对支持 RAG 的检索器的探讨。除了这些检索器之外,LangChain 网站上还有其他类型的检索器以及支持这些检索器的向量存储集成。例如,存在一个时间加权向量存储检索器,可以在检索过程中加入时效性因素。还有一个叫做 Long-Context Reorder 的检索器,专注于改善长文本模型在检索文档时处理中间信息的能力。务必浏览一下这些内容,它们可能会对你的 RAG 应用产生重大影响。接下来,我们将讨论生成阶段的“大脑”——LLMs(大型语言模型)。

Code lab 10.3 – LangChain LLMs

现在,我们将注意力转向 RAG(检索增强生成)中的最后一个关键组件:LLM(大型语言模型)。就像在检索阶段使用检索器一样,如果没有用于生成阶段的 LLM,那么 RAG 就无法实现。检索阶段只是从数据源中检索数据,通常是 LLM 不知道的数据。然而,这并不意味着 LLM 在我们的 RAG 实现中不起重要作用。通过将检索到的数据提供给 LLM,我们迅速让 LLM 了解我们希望它讨论的内容,这使得 LLM 能够发挥它的优势,根据这些数据提供回应,回答用户提出的原始问题。

LLM 和 RAG 系统之间的协同作用源于这两种技术的互补优势。RAG 系统通过引入外部知识源增强了 LLM 的能力,使生成的回应不仅在上下文上相关,而且在事实准确性和时效性上也得到保证。反过来,LLM 为 RAG 提供了对查询上下文的精确理解,从而有效地帮助检索相关信息。这种共生关系显著提高了 AI 系统在需要深入语言理解和访问广泛事实信息的任务中的表现,利用每个组件的优势,打造更强大、更灵活的系统。

在本次代码实验中,我们将展示一些生成阶段最重要组件——LangChain LLM 的示例。

LLMs, LangChain, and RAG

与之前的关键组件一样,我们首先提供与这个重要组件(LLMs)相关的 LangChain 文档链接:LangChain LLMs 文档
另一个有用的资源是关于将 LLMs 与 LangChain 结合的 API 文档:LangChain API 文档

让我们从我们已经使用过的 API 开始:OpenAI。

OpenAI

我们已经有了这段代码,但让我们通过逐步讲解代码中的关键部分,来刷新这部分代码的内部工作原理:

首先,我们必须安装 langchain-openai 包:

%pip install langchain-openai

langchain-openai 库提供了 OpenAI 语言模型与 LangChain 的集成。

接下来,我们导入 openai 库,这是官方的 Python 库,用于与 OpenAI 的 API 进行交互,主要在此代码中用于将 API 密钥应用到模型上,以便我们可以访问付费 API。然后,我们从 langchain_openai 库中导入 ChatOpenAIOpenAIEmbeddings 类:

import openai

from langchain_openai import ChatOpenAI, OpenAIEmbeddings

ChatOpenAI 用于与 OpenAI 的聊天模型进行交互,OpenAIEmbeddings 用于从文本生成嵌入。

接下来,我们通过 load_dotenv 函数加载名为 env.txt 的文件中的环境变量:

_ = load_dotenv(dotenv_path='env.txt')

我们使用 env.txt 文件来存储敏感信息(如 API 密钥),这样我们可以将其隐藏在版本控制系统之外,实践更安全的密钥管理。

然后,我们通过以下代码将 API 密钥传递给 OpenAI:

os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY')

openai.api_key = os.environ['OPENAI_API_KEY']

我们首先将 API 密钥设置为名为 OPENAI_API_KEY 的环境变量。然后,我们使用从环境变量中获取的值设置 openai 库的 API 密钥。此时,我们可以使用 LangChain 与 OpenAI 集成,调用托管在 OpenAI 上的 LLM,并获得适当的访问权限。

在代码的后续部分,我们定义了要使用的 LLM:

llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)

这行代码创建了一个 ChatOpenAI 类的实例,指定模型名称为 gpt-4o-mini,并将 temperature 设置为 0。temperature 控制生成响应的随机性,较低的值会生成更加集中和确定性的输出。目前,gpt-4o-mini 是 GPT-4 系列中最新且最强大的模型,同时也是最具成本效益的模型。但即便如此,这个模型的成本也比 gpt-3.5-turbo 高出 10 倍,而 gpt-3.5-turbo 其实是一个相对强大的模型。

OpenAI 最贵的模型 gpt-4-32k 的速度和能力都不如 gpt-4o-mini,但其上下文窗口是后者的 4 倍。未来可能会有更新的模型,如 gpt-5,可能更具成本效益且功能更强大。从中可以得出一个结论:你不应该仅仅假设最新的模型是最贵的,市面上总有一些新的、更具成本效益的版本不断推出。始终保持关注最新模型的发布,并根据每个版本的成本、LLM 能力以及其他相关属性,评估是否需要更换模型。

不过,在这个过程中,你不必仅限于使用 OpenAI。使用 LangChain 可以轻松切换 LLM,并拓宽你的搜索范围,寻找 LangChain 社区内的最佳解决方案。接下来,我们将逐步了解一些你可能考虑的其他选项。

Together AI

Together AI 提供了一套对开发者友好的服务,允许你访问多个模型。它们托管的 LLMs 的定价极具竞争力,并且经常提供 $5.00 的免费积分,用于测试不同的模型。

如果你是 Together API 的新用户,可以使用以下链接来设置你的 API 密钥,并像之前使用 OpenAI API 密钥那样将其添加到 env.txt 文件中:设置 API 密钥。当你访问这个网页时,它会在你点击“开始使用”按钮后提供 5.00的积分。你无需提供信用卡即可获得这5.00 的积分。你无需提供信用卡即可获得这 5.00 的积分。

确保将你的新 API 密钥添加到 env.txt 文件中,并命名为 TOGETHER_API_KEY

登录后,你可以在这里查看每个 LLM 的当前费用:模型费用。例如,Meta Llama 3 70B Instruct(Llama-3-70b-chat-hf)目前的费用为每百万个 token 0.90。这是一个被证明可以与ChatGPT4竞争的模型,但TogetherAI的推理成本远低于OpenAI收取的费用。另一个非常强大的模型是Mixtralmixtureofexperts模型,费用为每百万token0.90。这是一个被证明可以与 ChatGPT 4 竞争的模型,但 Together AI 的推理成本远低于 OpenAI 收取的费用。另一个非常强大的模型是 Mixtral mixture of experts 模型,费用为每百万 token 1.20。

设置和使用 Together AI

首先,我们安装需要的包来使用 Together API:

%pip install --upgrade langchain-together

接着,我们使用 LangChain 与 Together API 进行集成:

from langchain_together import ChatTogether

_ = load_dotenv(dotenv_path='env.txt')

这导入了 LangChain 中需要使用的 ChatTogether 集成,并加载了 API 密钥(在运行这行代码之前不要忘了将其添加到 env.txt 文件中!)。

和之前使用 OpenAI API 密钥的做法一样,我们将拉取 TOGETHER_API_KEY 以访问你的帐户:

os.environ['TOGETHER_API_KEY'] = os.getenv('TOGETHER_API_KEY')

我们将使用 Llama 3 Chat 模型和 Mixtral 的 Mixtral 8X22B Instruct 模型,但你可以在 文档中找到超过 50 个模型,你也许能找到更适合你需求的模型!

定义模型

这里,我们定义了两个模型:

llama3llm = ChatTogether(
    together_api_key=os.environ['TOGETHER_API_KEY'],
    model="meta-llama/Llama-3-70b-chat-hf",
)

mistralexpertsllm = ChatTogether(
    together_api_key=os.environ['TOGETHER_API_KEY'],
    model="mistralai/Mixtral-8x22B-Instruct-v0.1",
)

在上面的代码片段中,我们定义了两个不同的 LLM,可以在剩下的代码中运行它们并查看结果。

接下来,我们更新了使用 Llama 3 模型的最终代码:

llama3_rag_chain_from_docs = (
    RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
    | RunnableParallel(
        {"relevance_score": (
            RunnablePassthrough()
            | (lambda x: relevance_prompt_template.format(question=x['question'], retrieved_context=x['context']))
            | llama3llm
            | StrOutputParser()
        ), "answer": (
            RunnablePassthrough()
            | prompt
            | llama3llm
            | StrOutputParser()
        )}
    )
    | RunnablePassthrough().assign(final_answer=conditional_answer)
)

这应该看起来很熟悉,因为它是我们之前使用过的 RAG 链,但这次是使用 Llama 3 LLM。

最终的 RAG 链

llama3_rag_chain_with_source = RunnableParallel(
    {"context": ensemble_retriever, "question": RunnablePassthrough()}
).assign(answer=llama3_rag_chain_from_docs)

这是我们最终使用的 RAG 链,更新了 Llama 3 版本。

运行 RAG 管道

我们将运行类似于之前的代码,这次是用 Llama 3 LLM 替代 ChatGPT-4o-mini 模型:

llama3_result = llama3_rag_chain_with_source.invoke(user_query)

llama3_retrieved_docs = llama3_result['context']

print(f"Original Question: {user_query}\n")
print(f"Relevance Score: {llama3_result['answer']['relevance_score']}\n")
print(f"Final Answer:\n{llama3_result['answer']['final_answer']}\n\n")
print("Retrieved Documents:")
for i, doc in enumerate(llama3_retrieved_docs, start=1):
    print(f"Document {i}: Document ID: {doc.metadata['id']} source: {doc.metadata['source']}")
    print(f"Content:\n{doc.page_content}\n")

最终,得到的响应示例如下:

Google's environmental initiatives include:

  1. Empowering individuals to take action: Offering sustainability features in Google products, such as eco-friendly routing in Google Maps, energy efficiency features in Google Nest thermostats, and carbon emissions information in Google Flights…

使用 Mixture of Experts 模型

如果我们使用 Mixture of Experts 模型,代码如下:

mistralexperts_rag_chain_from_docs = (
    RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
    | RunnableParallel(
        {"relevance_score": (
            RunnablePassthrough()
            | (lambda x: relevance_prompt_template.format(question=x['question'], retrieved_context=x['context']))
            | mistralexpertsllm
            | StrOutputParser()
        ), "answer": (
            RunnablePassthrough()
            | prompt
            | mistralexpertsllm
            | StrOutputParser()
        )}
    )
    | RunnablePassthrough().assign(final_answer=conditional_answer)
)

与之前一样,这是一个熟悉的 RAG 链,但这次使用 Mixture of Experts LLM。

最终 RAG 管道

mistralexperts_rag_chain_with_source = RunnableParallel(
    {"context": ensemble_retriever, "question": RunnablePassthrough()}
).assign(answer=mistralexperts_rag_chain_from_docs)

更新最终的 RAG 管道,使用 Mixture of Experts 版本。

运行 Mixture of Experts 模型

mistralexperts_result = mistralexperts_rag_chain_with_source.invoke(user_query)

mistralexperts_retrieved_docs = mistralexperts_result['context']

print(f"Original Question: {user_query}\n")
print(f"Relevance Score: {mistralexperts_result['answer']['relevance_score']}\n")
print(f"Final Answer:\n{mistralexperts_result['answer']['final_answer']}\n\n")
print("Retrieved Documents:")
for i, doc in enumerate(mistralexperts_retrieved_docs, start=1):
    print(f"Document {i}: Document ID: {doc.metadata['id']} source: {doc.metadata['source']}")
    print(f"Content:\n{doc.page_content}\n")

得到的响应示例如下:

Google's environmental initiatives are organized around three key pillars:

  1. Empowering individuals: Google provides sustainability features like eco-friendly routing in Google Maps, energy efficiency features in Google Nest thermostats, and carbon emissions information in Google Flights. Their goal is to help individuals, cities, and other partners collectively reduce 1 gigaton of carbon equivalent emissions annually by 2030…

与之前的回答相比,Llama 3 和 Mixture of Experts 模型提供了更为丰富的响应,这些响应似乎更加完善,甚至可能比我们使用 OpenAI 的 gpt-4o-mini 模型所获得的原始响应更具优势,且费用远低于 OpenAI 更昂贵但更强大的模型。

扩展LLM能力

在你的RAG应用中,有一些LLM对象的功能可以更好地利用。如LangChain LLM文档中所描述的(python.langchain.com/v0.1/docs/m…

所有LLM都实现了Runnable接口,该接口提供了所有方法的默认实现,例如:ainvokebatchabatchstreamastream。这为所有LLM提供了基本的异步、流式处理和批处理支持。

这些是可以显著加速RAG应用处理的关键特性,特别是在同时处理多个LLM调用时。接下来,我们将介绍这些关键方法以及它们如何帮助你。

异步(Async)

默认情况下,异步支持会在一个独立线程中运行常规的同步方法。这允许你程序的其他部分在语言模型工作时继续运行。

流式处理(Stream)

流式处理支持通常会返回一个Iterator(或对于异步流式处理,返回AsyncIterator),它只包含一个项目:语言模型的最终结果。这并不提供逐词流式传输,但它确保你的代码可以与任何期望返回令牌流的LangChain语言模型集成工作。

批处理(Batch)

批处理支持可以同时处理多个输入。对于同步批处理,它使用多个线程;对于异步批处理,它使用asyncio.gather。你可以通过在RunnableConfig中设置max_concurrency来控制一次运行多少任务。

然而,并不是所有LLM都本地支持所有这些功能。对于我们讨论的两个实现以及更多其他实现,LangChain提供了一张详细的图表,具体可以参见这里:python.langchain.com/v0.2/docs/i…

总结

本章探讨了在LangChain上下文中构建RAG系统的关键技术组件:向量存储、检索器和LLM。我们深入了解了每个组件的不同选项,讨论了它们的优缺点,以及在不同场景下某个选项可能优于另一个的原因。

本章首先介绍了向量存储,它在高效存储和索引知识库文档的向量表示中起着至关重要的作用。LangChain与多种向量存储实现集成,如Pinecone、Weaviate、FAISS以及带有向量扩展的PostgreSQL。向量存储的选择取决于可扩展性、搜索性能和部署要求等因素。接着,本章讨论了检索器,它负责查询向量存储并根据输入查询检索最相关的文档。LangChain提供了多种检索器实现,包括密集检索器、稀疏检索器(如BM25)以及将多个检索器结果结合起来的集成检索器。

最后,本章讲解了LLM在RAG系统中的作用。LLM通过提供对查询上下文的深刻理解,帮助更有效地从知识库中检索相关信息。我们展示了LangChain与多个LLM提供商(如OpenAI和Together AI)的集成,并突出了不同模型的能力和成本考虑因素。还讨论了LangChain中LLM的扩展功能,如异步、流式处理和批处理支持,并对不同LLM集成所提供的原生实现进行了比较。

在下一章中,我们将继续讨论如何利用LangChain构建一个有能力的RAG应用,重点介绍那些可以支持本章所讨论的关键组件的较小组件。