Skip to content

使用 LangChain 构建语义搜索引擎

概述

本教程将带你熟悉 LangChain 的 EmbeddingVector Store 抽象。这些抽象旨在支持从(向量)数据库及其他来源检索数据,并与 LLM 工作流集成。它们对于在模型推理过程中获取数据进行推理的应用至关重要,例如检索增强生成(RAG)。

我们将基于一份 PDF 文档构建一个搜索引擎。这将使我们能够检索 PDF 中与输入查询相似的段落。本教程还包含一个在该搜索引擎之上构建的最小化 RAG 实现。

概念

本教程聚焦文本数据的检索。我们将涵盖以下概念:

环境搭建

安装依赖

本教程使用 pypdf 包读取 PDF:

bash
pip install pypdf
bash
conda install pypdf -c conda-forge
bash
uv add pypdf

更多详情请参阅安装指南

LangSmith

你使用 LangChain 构建的许多应用会包含多个步骤和对 LLM 的多次调用。随着这些应用越来越复杂,能够检查链或代理内部究竟发生了什么变得至关重要。最好的方式是使用 LangSmith

注册后,设置环境变量以开始记录追踪:

shell
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."

如果在笔记本中运行,可以通过代码设置:

python
import getpass
import os

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

1. Document(文档)

LangChain 实现了 Document 抽象,用于表示一个文本单元及其关联的元数据。它有三个属性:

  • page_content:表示内容的字符串;
  • metadata:包含任意元数据的字典;
  • id:(可选)文档的字符串标识符。

metadata 属性可以记录文档的来源、与其它文档的关系等信息。注意,单个 Document 对象通常代表更大文档的一个分块。

我们可以按需生成示例文档:

python
from langchain_core.documents import Document

documents = [
    Document(
        page_content="Dogs are great companions, known for their loyalty and friendliness.",
        metadata={"source": "mammal-pets-doc"},
    ),
    Document(
        page_content="Cats are independent pets that often enjoy their own space.",
        metadata={"source": "mammal-pets-doc"},
    ),
]

2. Embedding(嵌入)

向量搜索是一种常见的存储和搜索非结构化数据(如非结构化文本)的方式。其思路是将文本关联的数值向量存储起来。给定一个查询,我们可以将其嵌入为相同维度的向量,然后使用向量相似度指标(如余弦相似度)来识别相关文本。

LangChain 支持来自数十个供应商的嵌入模型。这些模型规定了如何将文本转换为数值向量。让我们选择一个模型:

OpenAI

bash
pip install -U "langchain-openai"
python
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")

Azure OpenAI

bash
pip install -U "langchain-openai"
python
import getpass
import os

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

from langchain_openai import AzureOpenAIEmbeddings

embeddings = AzureOpenAIEmbeddings(
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    azure_deployment=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"],
    openai_api_version=os.environ["AZURE_OPENAI_API_VERSION"],
)

Google Gemini

bash
pip install -qU langchain-google-genai
python
import getpass
import os

if not os.environ.get("GOOGLE_API_KEY"):
    os.environ["GOOGLE_API_KEY"] = getpass.getpass("Enter API key for Google Gemini: ")

from langchain_google_genai import GoogleGenerativeAIEmbeddings

embeddings = GoogleGenerativeAIEmbeddings(model="models/gemini-embedding-001")

Google Vertex AI

bash
pip install -qU langchain-google-vertexai
python
from langchain_google_vertexai import VertexAIEmbeddings

embeddings = VertexAIEmbeddings(model="text-embedding-005")

AWS Bedrock

bash
pip install -qU langchain-aws
python
from langchain_aws import BedrockEmbeddings

embeddings = BedrockEmbeddings(model_id="amazon.titan-embed-text-v2:0")

HuggingFace

bash
pip install -qU langchain-huggingface
python
from langchain_huggingface import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-mpnet-base-v2",
    encode_kwargs={"normalize_embeddings": True},
)

Ollama

bash
pip install -qU langchain-ollama
python
from langchain_ollama import OllamaEmbeddings

embeddings = OllamaEmbeddings(model="llama3")

Cohere

bash
pip install -qU langchain-cohere
python
import getpass
import os

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

from langchain_cohere import CohereEmbeddings

embeddings = CohereEmbeddings(model="embed-english-v3.0")

MistralAI

bash
pip install -qU langchain-mistralai
python
import getpass
import os

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

from langchain_mistralai import MistralAIEmbeddings

embeddings = MistralAIEmbeddings(model="mistral-embed")

Nomic

bash
pip install -qU langchain-nomic
python
import getpass
import os

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

from langchain_nomic import NomicEmbeddings

embeddings = NomicEmbeddings(model="nomic-embed-text-v1.5")

NVIDIA

bash
pip install -qU langchain-nvidia-ai-endpoints
python
import getpass
import os

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

from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings

embeddings = NVIDIAEmbeddings(model="NV-Embed-QA")

Voyage AI

bash
pip install -qU langchain-voyageai
python
import getpass
import os

if not os.environ.get("VOYAGE_API_KEY"):
    os.environ["VOYAGE_API_KEY"] = getpass.getpass("Enter API key for Voyage AI: ")

from langchain_voyageai import VoyageAIEmbeddings

embeddings = VoyageAIEmbeddings(model="voyage-3")

IBM watsonx

bash
pip install -qU langchain-ibm
python
import getpass
import os

if not os.environ.get("WATSONX_APIKEY"):
    os.environ["WATSONX_APIKEY"] = getpass.getpass("Enter API key for IBM watsonx: ")

from langchain_ibm import WatsonxEmbeddings

embeddings = WatsonxEmbeddings(
    model_id="ibm/slate-125m-english-rtrvr",
    url="https://us-south.ml.cloud.ibm.com",
    project_id="<WATSONX PROJECT_ID>",
)

测试嵌入

选定嵌入模型后,我们可以在示例文档上生成向量:

python
vector_1 = embeddings.embed_query(documents[0].page_content)
vector_2 = embeddings.embed_query(documents[1].page_content)

assert len(vector_1) == len(vector_2)
print(f"Generated vectors of length {len(vector_1)}\n")
print(vector_1[:10])

输出示例:

Generated vectors of length 1536

[-0.008586574345827103, -0.03341241180896759, -0.008936782367527485, -0.0036674530711025, ...]

有了生成文本嵌入的模型后,我们可以将它们存储在支持高效相似度搜索的特殊数据结构中。

3. Vector Store(向量存储)

LangChain 的 VectorStore 对象包含向存储中添加文本和 Document 对象的方法,以及使用各种相似度指标查询它们的方法。它们通常通过嵌入模型初始化,该模型决定文本数据如何转换为数值向量。

LangChain 包含一套与不同向量存储技术的集成。有些由供应商托管(如各种云供应商),需要特定的凭证;有些(如 Postgres)运行在独立的基础设施中,可以在本地或通过第三方运行;其他可以在内存中运行以处理轻量级工作负载。

InMemoryVectorStore(内存向量存储)

对于本教程,我们将使用最简单的内存向量存储:

bash
pip install -U "langchain-core"
python
from langchain_core.vectorstores import InMemoryVectorStore

vector_store = InMemoryVectorStore(embeddings)

其他向量存储选项

根据你的需求,也可以选择以下集成方案:

Chroma

bash
pip install -qU langchain-chroma
python
from langchain_chroma import Chroma

vector_store = Chroma(
    collection_name="example_collection",
    embedding_function=embeddings,
    persist_directory="./chroma_langchain_db",
)

Milvus

bash
pip install -qU langchain-milvus
python
from langchain_milvus import Milvus

URI = "./milvus_example.db"

vector_store = Milvus(
    embedding_function=embeddings,
    connection_args={"uri": URI},
    index_params={"index_type": "FLAT", "metric_type": "L2"},
)

Pinecone

bash
pip install -qU langchain-pinecone
python
from langchain_pinecone import PineconeVectorStore
from pinecone import Pinecone

pc = Pinecone(api_key=...)
index = pc.Index(index_name)

vector_store = PineconeVectorStore(embedding=embeddings, index=index)

Qdrant

bash
pip install -qU langchain-qdrant
python
from qdrant_client.models import Distance, VectorParams
from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient

client = QdrantClient(":memory:")

vector_size = len(embeddings.embed_query("sample text"))

if not client.collection_exists("test"):
    client.create_collection(
        collection_name="test",
        vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE),
    )

vector_store = QdrantVectorStore(
    client=client,
    collection_name="test",
    embedding=embeddings,
)

更多向量存储集成(AstraDB、PGVector、MongoDB Atlas 等)请参阅官方文档


向向量存储填充数据(Seeding)

接下来我们用 PDF 中的内容填充向量存储。这里使用一份示例 PDF —— Nike 2023 年的 10-K 年报文件。我们将直接使用一个小辅助函数读取 PDF,并在索引之前将其拆分为更小的分块。

python
import pypdf
from langchain_core.documents import Document

以下是用于演示的加载 PDF 页面的最小辅助函数:

python
def load_pdf_pages(file_path: str) -> list[Document]:
    reader = pypdf.PdfReader(file_path)
    return [
        Document(
            page_content=page.extract_text() or "",
            metadata={"source": file_path, "page": i},
        )
        for i, page in enumerate(reader.pages)
    ]

file_path = "../example_data/nke-10k-2023.pdf"
docs = load_pdf_pages(file_path)
print(len(docs))
107

整个页面作为检索和下游问答的单元可能粒度过粗。进一步分割有助于确保文档相关部分的含义不会被周围文本"稀释"。我们使用 RecursiveCharacterTextSplitter,它会递归地使用常见分隔符(如换行符)分割文档,直到每个分块达到合适的大小。这是通用文本用例推荐的文本分割器。

我们设置 add_start_index=True,以便每个分割后的 Document 在原始 Document 中起始的字符索引会以元数据属性 start_index 的形式保留。

python
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

现在我们就可以将分块索引到向量存储中:

python
ids = vector_store.add_documents(documents=all_splits)

注意: 大多数向量存储实现允许你连接到已有的向量存储 —— 例如,通过提供客户端、索引名称或其他信息。详细信息请参阅特定集成的文档。

查询向量存储

一旦实例化了一个包含文档的 VectorStore,我们就可以查询它。VectorStore 包含多种查询方法:

  • 同步和异步;
  • 按字符串查询和按向量查询;
  • 返回或不返回相似度分数;
  • 按相似度或最大边际相关性(在查询相似度与结果多样性之间取得平衡)。

这些方法通常会返回 Document 对象的列表。

按字符串查询相似文档

嵌入模型通常将文本表示为"密集"向量,使得含义相似的文本在几何上靠近。这意味着我们只需传入一个问句即可检索相关信息,无需了解文档中使用的任何特定关键词。

python
results = vector_store.similarity_search(
    "How many distribution centers does Nike have in the US?"
)

print(results[0])

输出:

page_content='direct to consumer operations sell products through the following number of retail stores in the United States:
U.S. RETAIL STORES NUMBER
NIKE Brand factory stores 213
NIKE Brand in-line stores (including employee-only stores) 74
Converse stores (including factory stores) 82
TOTAL 369
In the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information.
2023 FORM 10-K 2' metadata={'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}

异步查询

python
results = await vector_store.asimilarity_search("When was Nike incorporated?")

print(results[0])

输出:

page_content='Table of Contents
PART I
ITEM 1. BUSINESS
GENERAL
NIKE, Inc. was incorporated in 1967 under the laws of the State of Oregon. ...' metadata={'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}

返回相似度分数

不同供应商实现的分数含义不同;这里的分值是距离度量,与相似度成反比(值越小越相似)。

python
results = vector_store.similarity_search_with_score("What was Nike's revenue in 2023?")
doc, score = results[0]
print(f"Score: {score}\n")
print(doc)

输出:

Score: 0.23699893057346344

page_content='Table of Contents
FISCAL 2023 NIKE BRAND REVENUE HIGHLIGHTS
...NIKE, Inc. Revenues were $51.2 billion in fiscal 2023...' metadata={'page': 35, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}

按嵌入向量查询

python
embedding = embeddings.embed_query("How were Nike's margins impacted in 2023?")

results = vector_store.similarity_search_by_vector(embedding)
print(results[0])

4. Retriever(检索器)

LangChain 的 VectorStore 对象继承自 Runnable,而 LangChain 的 Retriever(检索器)是 Runnable,因此实现了标准的方法集(如同步/异步的 invokebatch 操作)。虽然我们可以从向量存储构建检索器,但检索器也可以与非向量存储的数据源(如外部 API)交互。

自定义检索器

我们可以自己创建简单的检索器实现,而无需子类化 Retriever。只需围绕 similarity_search 方法构建一个可运行对象:

python
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(
    [
        "How many distribution centers does Nike have in the US?",
        "When was Nike incorporated?",
    ],
)

内置检索器

VectorStore 实现了 as_retriever 方法,可生成一个 VectorStoreRetriever。这些检索器包含 search_typesearch_kwargs 属性,用于指定调用底层向量存储的哪些方法以及如何参数化。例如,我们可以用以下方式复现上面的逻辑:

python
retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 1},
)

retriever.batch(
    [
        "How many distribution centers does Nike have in the US?",
        "When was Nike incorporated?",
    ],
)

5. 在搜索之上构建最小化 RAG

有了检索器之后,我们可以在此之上构建一个最小化的 RAG(检索增强生成)应用。其核心流程是:用户提问 → 检索相关文档 → 将文档作为上下文注入提示词 → LLM 生成基于上下文的回答。

python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 定义提示词模板
template = """你是一个文档问答助手。请基于提供的上下文回答用户问题。
如果上下文中没有足够信息,请如实告知。

上下文:
{context}

问题:
{question}

回答:"""
prompt = ChatPromptTemplate.from_template(template)

# 初始化 LLM
llm = ChatOpenAI(model="gpt-4o-mini")

# 构建 RAG 链
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 执行查询
response = rag_chain.invoke("Nike 在美国有多少个配送中心?")
print(response)

这个最小化的 RAG 实现展示了 LangChain 的核心能力:将检索(retrieval)与生成(generation)无缝组合,构建知识密集型问答应用。

下一步

你已经学会了如何基于 PDF 文档构建语义搜索引擎。

有关 Embedding 的更多信息:

有关 Vector Store 的更多信息:

有关 RAG 的更多信息:


本页面基于 LangChain 官方文档 - Build a semantic search engine with LangChain 翻译整理。

本站为非官方中文学习站点,不代表 LangChain 官方。部分内容参考官方文档并重新整理为中文学习笔记。