概述

本教程将让您熟悉 LangChain 的文档加载器嵌入向量存储抽象。这些抽象旨在支持从(向量)数据库和其他来源检索数据——以便与 LLM 工作流集成。对于在模型推理过程中获取数据以进行推理的应用程序来说,它们很重要,如检索增强生成或 RAG 的情况。 在这里,我们将构建一个基于 PDF 文档的搜索引擎。这将允许我们检索与输入查询相似的 PDF 中的段落。本指南还包括在搜索引擎之上的最小 RAG 实现。

概念

本指南专注于文本数据的检索。我们将涵盖以下概念:

设置

安装

本教程需要 langchain-communitypypdf 包:
pip install langchain-community pypdf
有关更多详细信息,请参阅我们的安装指南

LangSmith

您使用 LangChain 构建的许多应用程序都包含多个步骤和多次 LLM 调用。随着这些应用程序变得越来越复杂,能够检查链或代理内部发生的事情变得至关重要。做到这一点的最佳方法是使用 LangSmith 在上述链接注册后,确保设置您的环境变量以开始记录追踪:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."
或者,如果在笔记本中,可以使用以下方式设置它们:
import getpass
import os

os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = getpass.getpass()

1. 文档和文档加载器

LangChain 实现了文档抽象,旨在表示一个文本单元和关联的元数据。它有三个属性:
  • page_content:表示内容的字符串;
  • metadata:包含任意元数据的字典;
  • id:(可选)文档的字符串标识符。
metadata 属性可以捕获有关文档来源、与其他文档的关系以及其他信息。请注意,单独的文档对象通常代表较大文档的一个块。 我们可以在需要时生成示例文档:
from langchain_core.documents import Document

documents = [
    Document(
        page_content="狗是很棒的伙伴,以其忠诚和友好而闻名。",
        metadata={"source": "mammal-pets-doc"},
    ),
    Document(
        page_content="猫是独立的宠物,通常喜欢自己的空间。",
        metadata={"source": "mammal-pets-doc"},
    ),
]
但是,LangChain 生态系统实现了与数百个常见源集成的文档加载器。这使得将这些来源的数据整合到您的 AI 应用程序中变得容易。

加载文档

让我们将 PDF 加载到一系列文档对象中。这是示例 PDF——Nike 2023 年的 10-K 报告。我们可以查阅 LangChain 文档了解可用的 PDF 文档加载器
from langchain_community.document_loaders import PyPDFLoader

file_path = "../example_data/nke-10k-2023.pdf"
loader = PyPDFLoader(file_path)

docs = loader.load()

print(len(docs))
107
PyPDFLoader 为每个 PDF 页面加载一个文档对象。对于每个,我们可以轻松访问:
  • 页面的字符串内容;
  • 包含文件名和页码的元数据。
print(f"{docs[0].page_content[:200]}\n")
print(docs[0].metadata)
目录
美国
证券交易委员会
华盛顿特区 20549
表格 10-K
(标记一)
☑ 根据 1934 年证券交易法第 1315(d) 条的年度报告

{'source': '../example_data/nke-10k-2023.pdf', 'page': 0}

分割

对于信息检索和下游问答目的,页面可能太粗糙而无法表示。我们的最终目标将是检索回答输入查询的文档对象,进一步分割我们的 PDF 将有助于确保文档相关部分的含义不会被周围文本”冲淡”。 我们可以使用文本分割器来实现此目的。在这里,我们将使用基于字符的简单文本分割器。我们将把文档分割成 1000 个字符的块,块之间有 200 个字符的重叠。重叠有助于减轻将陈述与其相关的重要上下文分离的可能性。我们使用 RecursiveCharacterTextSplitter,它将递归地使用常见分隔符(如新行)分割文档,直到每个块达到合适的大小。这是用于通用文本用例的推荐文本分割器。 我们设置 add_start_index=True,以便每个分割文档在初始文档中开始的字符索引被保留为元数据属性”start_index”。
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)

print(len(all_splits))
514

2. 嵌入

向量搜索是存储和搜索非结构化数据(如非结构化文本)的常用方式。其思想是存储与文本关联的数值向量。给定查询,我们可以嵌入它为相同维度的向量,并使用向量相似性度量(如余弦相似性)来识别相关文本。 LangChain 支持来自数十个提供商的嵌入。这些模型指定了如何将文本转换为数值向量。让我们选择一个模型:
pip install -U "langchain-openai"
import getpass
import os

if not os.environ.get("OPENAI_API_KEY"):
    os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter API key for OpenAI: ")

from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
vector_1 = embeddings.embed_query(all_splits[0].page_content)
vector_2 = embeddings.embed_query(all_splits[1].page_content)

assert len(vector_1) == len(vector_2)
print(f"生成了长度为 {len(vector_1)} 的向量\n")
print(vector_1[:10])
生成了长度为 1536 的向量

[-0.008586574345827103, -0.03341241180896759, -0.008936782367527485, -0.0036674530711025, 0.010564599186182022, 0.009598285891115665, -0.028587326407432556, -0.015824200585484505, 0.0030416189692914486, -0.012899317778646946]
有了用于生成文本嵌入的模型,我们接下来可以将它们存储在支持高效相似性搜索的专门数据结构中。

3. 向量存储

LangChain VectorStore 对象包含用于向存储添加文本和文档对象的方法,以及使用各种相似性度量查询它们的方法。它们通常使用嵌入模型初始化,这些模型决定如何将文本数据转换为数值向量。 LangChain 包含与不同向量存储技术的一系列集成。一些向量存储由提供商托管(例如各种云提供商),需要特定凭证才能使用;一些(如 Postgres)在可以本地运行或通过第三方运行的单独基础设施中运行;其他可以在内存中运行以处理轻量级工作负载。让我们选择一个向量存储:
pip install -U "langchain-core"
from langchain_core.vectorstores import InMemoryVectorStore

vector_store = InMemoryVectorStore(embeddings)
实例化向量存储后,我们现在可以为文档编制索引。
ids = vector_store.add_documents(documents=all_splits)
请注意,大多数向量存储实现将允许您连接到现有向量存储——例如,通过提供客户端、索引名称或其他信息。请参阅特定集成的文档了解更多详情。 一旦我们实例化了一个包含文档的向量存储,我们就可以查询它。向量存储包括用于查询的方法:
  • 同步和异步;
  • 通过字符串查询和通过向量;
  • 带和不带返回相似性分数;
  • 通过相似性和最大边际相关性(以平衡检索结果中与查询的相似性和多样性)。
这些方法通常在其输出中包含文档对象列表。 用法 嵌入通常将文本表示为”密集”向量,使得具有相似含义的文本在几何上接近。这让我们可以仅通过传入问题来检索相关信息,而无需了解文档中使用的任何特定关键词。 根据与字符串查询的相似性返回文档:
results = vector_store.similarity_search(
    "Nike 在美国有多少个配送中心?"
)

print(results[0])
page_content='直接面向消费者的运营通过以下数量的美国零售店销售产品:
美国零售店数量
耐克品牌工厂店 213
耐克品牌直营店(包括员工专用店)74
匡威商店(包括工厂店)82
总计 369
在美国,耐克有八个重要的配送中心。请参阅第 2"财产"了解更多详情。
2023 年 表格 10-K 2' metadata={'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}
异步查询:
results = await vector_store.asimilarity_search("Nike 什么时候成立的?")

print(results[0])
page_content='目录
第一部分
1 项。业务
概览
耐克公司于 196 7 年根据俄勒冈州法律成立。在本年度报告(本"年度报告")中,"我们""我们""我们的""耐克""公司"指的是耐克公司及其前身、子公司和关联公司,除非上下文另有说明。
我们的主要业务活动是设计、开发、全球营销和销售运动鞋、服装、设备、配件和服务。耐克是全球最大的运动鞋和服装销售商。我们通过耐克直销业务(包括耐克自有零售店和通过我们的数字平台销售,也称为"耐克品牌数字")、零售账户以及独立分销商、许可人和销售的组合来销售我们的产品' metadata={'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}
返回分数:
# 请注意,提供商实现不同的分数;这里的分数
# 是一个与相似性成反比的距离度量。

results = vector_store.similarity_search_with_score("Nike 2023 年的收入是多少?")
doc, score = results[0]
print(f"分数:{score}\n")
print(doc)
page_content='耐克截至 2023 年 5 月 31 日的财政年度收入为约 510 亿美元。' metadata={'page': 2, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}

4. 检索器

虽然向量存储是存储和搜索文档的核心,但 LangChain 定义了检索器接口,您可以使用它来封装从向量存储检索的逻辑。 LangChain 的向量存储集成自动为您创建一个检索器。
retriever = vector_store.as_retriever(search_kwargs={"k": 1})

使用检索器

检索器可以用作链或代理的一部分:
from langchain.agents import create_agent
from langchain.tools import tool

@tool
def search_documents(query: str) -> str:
    """Search for information in the document."""
    docs = retriever.invoke(query)
    return "\n\n".join([doc.page_content for doc in docs])

agent = create_agent(
    model="openai:gpt-4",
    tools=[search_documents],
    system_prompt="你是一个有帮助的助手。使用提供的搜索工具来回答用户的问题。"
)

5. 最小 RAG 实现

现在我们有了用于索引和检索文档的基础设施,让我们实现一个简单的 RAG(检索增强生成)系统。

设置

我们需要加载一个 LLM:
from langchain.chat_models import init_chat_model

llm = init_chat_model("gpt-4")

创建检索增强链

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain

# 创建文档组合链
combine_docs_chain = create_stuff_documents_chain(
    llm,
    prompt="""根据以下上下文回答用户的问题。

上下文:
{context}

问题:{input}

回答:
"""
)

# 创建检索链
retrieval_chain = create_retrieval_chain(
    retriever,
    combine_docs_chain
)

使用链

# 运行链
result = retrieval_chain.invoke({"input": "Nike 2023 年的收入是多少?"})

print(result["answer"])
我们可以创建一个简单的版本,而不需要子类化 Retriever。如果我们选择想要用来检索文档的方法,可以轻松地创建一个可运行对象。下面我们将围绕 similarity_search 方法构建一个:
from typing import List

from langchain_core.documents import Document
from langchain_core.runnables import chain


@chain
def retriever(query: str) -> List[Document]:
    return vector_store.similarity_search(query, k=1)


retriever.batch(
    [
        "Nike 在美国有多少个配送中心?",
        "Nike 什么时候成立的?",
    ],
)
[[Document(metadata={'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}, page_content='直接面向消费者的运营通过以下数量的美国零售店销售产品:
美国零售店数量
耐克品牌工厂店 213
耐克品牌直营店(包括员工专用店)74
匡威商店(包括工厂店)82
总计 369
在美国,耐克有八个重要的配送中心。请参阅第 2 项"财产"了解更多详情。
2023 年 表格 10-K 2')],
 [Document(metadata={'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}, page_content='目录
第一部分
第 1 项。业务
概览
耐克公司于 1967 年根据俄勒冈州法律成立。在本年度报告(本"年度报告")中,"我们"、"我们"、"我们的"、"耐克"和"公司"指的是耐克公司及其前身、子公司和关联公司,除非上下文另有说明。
我们的主要业务活动是设计、开发、全球营销和销售运动鞋、服装、设备、配件和服务。耐克是全球最大的运动鞋和服装销售商。我们通过耐克直销业务(包括耐克自有零售店和通过我们的数字平台销售,也称为"耐克品牌数字")、零售账户以及独立分销商、许可人和销售的组合来销售我们的产品')]]
向量存储实现了 as_retriever 方法,可以生成一个检索器,具体是 VectorStoreRetriever。这些检索器包含特定的 search_typesearch_kwargs 属性,用于标识要调用的底层向量存储方法以及如何对其进行参数化。例如,我们可以使用以下方法复制上述结果:
retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 1},
)

retriever.batch(
    [
        "Nike 在美国有多少个配送中心?",
        "Nike 什么时候成立的?",
    ],
)
[[Document(metadata={'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}, page_content='直接面向消费者的运营通过以下数量的美国零售店销售产品:
美国零售店数量
耐克品牌工厂店 213
耐克品牌直营店(包括员工专用店)74
匡威商店(包括工厂店)82
总计 369
在美国,耐克有八个重要的配送中心。请参阅第 2 项"财产"了解更多详情。
2023 年 表格 10-K 2')],
 [Document(metadata={'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}, page_content='目录
第一部分
第 1 项。业务
概览
耐克公司于 1967 年根据俄勒冈州法律成立。在本年度报告(本"年度报告")中,"我们"、"我们"、"我们的"、"耐克"和"公司"指的是耐克公司及其前身、子公司和关联公司,除非上下文另有说明。
我们的主要业务活动是设计、开发、全球营销和销售运动鞋、服装、设备、配件和服务。耐克是全球最大的运动鞋和服装销售商。我们通过耐克直销业务(包括耐克自有零售店和通过我们的数字平台销售,也称为"耐克品牌数字")、零售账户以及独立分销商、许可人和销售的组合来销售我们的产品')]]
VectorStoreRetriever 支持 "similarity"(默认)、"mmr"(最大边际相关性,如上所述)和 "similarity_score_threshold" 搜索类型。我们可以使用后者通过相似度分数对检索器输出的文档进行阈值过滤。 检索器可以轻松集成到更复杂的应用程序中,例如将给定问题与检索到的上下文结合到 LLM 提示中的检索增强生成 (RAG) 应用程序。要了解有关构建此类应用程序的更多信息,请查看 RAG 教程

下一步

您现在已经了解了如何构建基于 PDF 文档的语义搜索引擎。 更多关于文档加载器: 更多关于嵌入: 更多关于向量存储: 更多关于 RAG,请参阅: