【LangChain 1.0】 语义搜索实战:从 PDF 文档到向量知识库的完整 RAG 链路

一、背景介绍

在 AI 应用开发中,大语言模型(LLM)虽然具备强大的理解和生成能力,但其知识存在两个固有局限:知识截止日期的边界无法访问私有数据。检索增强生成(RAG, Retrieval-Augmented Generation)正是为解决这两个问题而生的架构范式------它让模型在回答问题时,能够先检索相关的外部知识,再基于检索到的上下文进行推理和生成。

本文将完整演示一个 RAG 系统的核心链路:从 PDF 文档读取 → 文本分割 → 向量化 → 向量库存储 → 语义检索。我们会使用 LangChain 1.0 的抽象接口,配合 Ollama 本地嵌入模型和 Chroma 轻量级向量数据库,全程无需联网即可完成索引构建与检索。


二、方案分析:RAG 的核心组件与数据流

RAG 系统的本质是一个"先查后答"的流水线。要让电脑"读懂"一本 PDF 并建立可检索的知识库,需要经过四个关键步骤,类比于给实体书建立图书馆检索系统:

步骤 技术动作 类比理解
1. 读 PDF 将 PDF 按页解析为 Document 对象 把整本书拆成一页一页的纸
2. 分文本 将每页内容切成有重叠的小段落 把每页纸剪成若干小纸条(相邻纸条有重叠,避免信息断裂)
3. 向量化 将小段落转为数学向量 给每个小纸条贴上书架定位标签(数字列表)
4. 存向量库 把向量存入数据库,供后续搜索 把所有小纸条按标签整理到图书馆的检索柜

2.1 核心概念

Document(文档)

LangChain 的 Document 是文本数据的基本单元,包含三个属性:

  • page_content:文本内容字符串
  • metadata:元数据字典(来源、页码、创建时间等)
  • id(可选):文档唯一标识

单个 Document 通常代表较大文档的一个片段,而非整本书。

Embeddings(嵌入)

嵌入是将文本映射为高维数值向量的技术。核心直觉是:语义相近的文本,在向量空间中的距离也相近。例如:

  • "苹果很好吃" → [0.23, -0.56, 0.89, ...](768 维)
  • "苹果是一家公司" → [0.11, -0.32, 0.55, ...](数字接近,因为共享"苹果"一词)
  • "香蕉味道不错" → [-0.78, 0.23, -0.41, ...](数字差异大,语义不同)

向量的维度由模型决定(如 nomic-embed-text 输出 768 维),与输入文本长度无关------无论"你好"还是一万字长文,都输出固定维度的向量。

Vector Stores(向量存储)

向量数据库负责存储向量并提供相似度检索能力。LangChain 集成了数十种向量数据库,从云端托管服务到本地轻量级方案均有覆盖。本文选用 Chroma,它是一个类似 SQLite 的本地文件型向量数据库,适合开发和轻量级场景。

Retrievers(检索器)

检索器是 LangChain 的 Runnable 子类,实现了标准接口(invokebatchstream 等)。它可以从向量存储构建,也可以对接非向量数据源(如外部 API)。检索器的价值在于将"找相关文档"这个动作封装为可复用的组件,便于在 LangChain 的链式调用中集成。


三、实操步骤

3.1 环境准备

安装依赖
bash 复制代码
# 使用 uv(推荐)
uv add langchain langchain-community langchain-chroma langchain-ollama pypdf

# 或使用 pip
pip install langchain langchain-community langchain-chroma langchain-ollama pypdf
拉取嵌入模型

通过 Ollama 本地部署 nomic-embed-text(开源免费,输出 768 维向量):

bash 复制代码
ollama pull nomic-embed-text

验证模型已就绪:

bash 复制代码
ollama list
# 应显示 nomic-embed-text:latest

3.2 第一阶段:构建索引(让电脑"读懂"PDF)

索引构建是 RAG 的"离线"阶段,只需执行一次,后续反复检索。

步骤 1:读取 PDF

使用 PyPDFLoader 按页解析 PDF,每页生成一个 Document 对象:

python 复制代码
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("data/nke-10k-2023.pdf")
docs = loader.load()

# 验证输出
print(len(docs))        # 107 ------ PDF 共 107 页
print(type(docs[0]))    # <class 'langchain_core.documents.base.Document'>
print(docs[0].page_content[:200])  # 第一页的文本内容
print(docs[0].metadata)  # {'source': 'data/nke-10k-2023.pdf', 'page': 0, 'total_pages': 107, ...}

PyPDFLoader 为 LangChain 提供的 PDF 读取器。Docs 返回一个列表,每个元素是一个 Document 对象,代表 PDF 的一页。Document 对象包含 page_content、metadata。page_content 表示这一页的纯文本内容。Metadata 表示元数据(页码、来源、创建时间等)。

步骤 2:分割文本

一页 PDF 可能很长(如财报一页 3000 字),而嵌入模型有输入长度限制(通常 512-8192 tokens)。过长的文本会导致信息截断,因此需要有策略地切分

更关键的是重叠设计:相邻切片共享部分文本,避免在切分边界处切断完整语义。例如"苹果公司发布财报"若被切成"苹果公司发"和"布财报",重叠 200 字可确保完整语义至少被一个 chunk 保留。

python 复制代码
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,      # 每个 chunk 包含约 1000 个字符
    chunk_overlap=200,    # 相邻 chunk 共享 200 个字符,保证语义连续性
    add_start_index=True  # 记录每个 chunk 在原文档中的起始位置,便于溯源
)

all_splits = text_splitter.split_documents(docs)

# 验证输出
print(len(all_splits))     # 516 ------ 107 页被切分为 516 个文本段
print(all_splits[0].metadata)  # 包含 'start_index': 0,标记在原文中的位置
步骤 3:向量化

所谓向量化是指把一段文本转成一组数组(这组数组被称为向量)。相似含义的文本,它们的向量在数学空间中也相近。例如,"苹果很好吃" → 0.12, -0.34, 0.56, ... (768 个数字);"苹果是一家公司" → 0.11, -0.32, 0.55, ... (注意数字很接近,因为"苹果"这个词);"香蕉味道不错" → -0.78, 0.23, -0.41, ... (数字差异很大,因为语义不同)。向量化需要使用嵌入模型,这里使用的是nomic-embed-text 模型。这个模型是开源免费向量模型,输出 768 维向量(768 个数字),通过 Ollama 本地运行,不需要联网。执行 uv add chromadbuv add langchain-chroma 安装向量数据库。向量的长度是固定的(这里是768),和输入文本的长度无关,和模型有关。例如"你好"和"一篇一万字的文章"都输出 768 个数字。

使用 Ollama 本地运行的 nomic-embed-text 模型,将每个文本段转为 768 维向量:

python 复制代码
from langchain_ollama import OllamaEmbeddings

embeddings = OllamaEmbeddings(model="nomic-embed-text")

# 验证向量维度
vector_0 = embeddings.embed_query(all_splits[0].page_content)
print(len(vector_0))     # 768 ------ 维度由模型决定,与文本长度无关
print(type(vector_0))    # <class 'list'>
# 输出示例:[-0.027, 0.033, -0.195, -0.084, 0.040, -0.030, ...]
步骤 4:存入向量库

使用 Chroma 持久化存储。collection_name 类似数据库中的表名,persist_directory 指定磁盘存储路径,下次启动可直接加载无需重建索引:

python 复制代码
from langchain_chroma import Chroma

vector_store = Chroma(
    collection_name="example_collection",      # 集合名称
    embedding_function=embeddings,            # 向量化函数
    persist_directory="./chroma_langchain_db" # 本地持久化路径
)

# 批量添加文档,自动向量化并存储
ids = vector_store.add_documents(documents=all_splits)

print(len(ids))     # 516 ------ 与 all_splits 数量一致
print(ids[:10])     # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ------ 每个 chunk 的唯一 ID

add_documents 对每个 all_splits 中的 Document 调用 embeddings 模型,得到向量后存储到 Chroma,最后返回每个向量的 ID(0, 1, 2, ...)。存储后的目录结构如下图所示:


3.3 第二阶段:执行检索(从知识库"找答案")

索引构建完成后,进入"在线"检索阶段。给定一个问题,从知识库中找出最相关的文本片段。LangChain 提供了四种查询方式,覆盖不同使用场景:

查询方式 核心特点 适用场景
similarity_search 传入文本,返回最相似的 Document 列表 最常用,直接用自然语言查询
similarity_search_with_score 返回 Document + 相似度分数 需要设置置信度阈值,过滤低质量结果
similarity_search_by_vector 传入预计算的向量,跳过文本嵌入步骤 性能优化、非文本查询(如图片向量)、缓存场景
Retriever 封装 @chain 装饰器包装为 Runnable LangChain 链式调用中复用,统一接口
方式一:相似度查询(最常用)
python 复制代码
results = vector_store.similarity_search(
    query="What is the company's business?",
    k=3  # 返回最相似的 3 个结果
)

for i, doc in enumerate(results):
    print(f"{i}: {doc.page_content[:150]}...")

内部机制:将查询文本通过嵌入模型转为向量 → 计算与所有 chunk 向量的余弦相似度 → 返回相似度最高的 3 个 Document。

方式二:带分数的相似度查询
python 复制代码
results = vector_store.similarity_search_with_score(
    query="What is the company's business?",
    k=3
)

for doc, score in results:
    print(f"Score: {score:.4f}")           # Chroma 返回距离,越小越相似(0 为完全相同)
    print(f"Content: {doc.page_content[:150]}...")
    print("-" * 50)

输出示例

text 复制代码
Score: 0.6309
the Company's financial position or results of operations. In the ordinary course of business, the C...
--------------------------------------------------
Score: 0.6848
Table of Contents PART I ITEM 1. BUSINESS GENERAL NIKE, Inc. was incorporated in 1967 under the laws...
--------------------------------------------------
Score: 0.7150
Because NIKE is a consumer products company, the relative popularity and availability of various spo...

分数可用于设置阈值过滤:如只接受 score < 0.7 的结果,避免召回低质量内容。

方式三:向量直接查询

适用于已预计算查询向量的场景,避免重复嵌入:

python 复制代码
# 预计算查询向量(可从缓存获取)
query_vector = embeddings.embed_query("What is the company's business?")

results = vector_store.similarity_search_by_vector(
    vector=query_vector,
    k=3
)

for i, doc in enumerate(results):
    print(f"{i}: {doc.page_content[:150]}...")
方式四:检索器封装(LangChain 风格)

@chain 装饰器将检索逻辑包装为标准的 Runnable,便于在复杂链路中复用:

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=3)

# 像调用函数一样使用,但具备 LangChain 的标准接口(invoke、batch、stream 等)
results = retriever.invoke("What is the company's business?")

for i, doc in enumerate(results):
    print(f"{i}: {doc.page_content[:150]}...")

四、验证效果:完整数据流回顾

整个 RAG 索引与检索的数据流可以用下图概括:

复制代码
PDF 文件(107 页)
    ↓ PyPDFLoader
107 个 Document 对象(每页一个)
    ↓ RecursiveCharacterTextSplitter(chunk_size=1000, overlap=200)
516 个 Document 对象(每个 chunk 一个,含 start_index 溯源信息)
    ↓ OllamaEmbeddings(nomic-embed-text)
516 个向量(每个 768 维)
    ↓ Chroma.add_documents
向量数据库(持久化到 ./chroma_langchain_db)
    ↑
    └── 后续检索:query → embedding → similarity_search → top-k Documents

关键验证点:

检查项 预期结果 意义
len(docs) 107 PDF 页数正确解析
len(all_splits) 516 文本分割粒度合理(约 1000 字符/chunk)
len(vector_0) 768 嵌入模型输出维度正确
len(ids) 516 所有 chunk 均已入库
检索结果 score 0.6-0.8 相似度合理,与查询语义相关

五、总结与延伸

本文演示了 RAG 系统最核心的一环------检索(Retrieval)。完整的 RAG 链路还应包括:

  1. 检索(本文已覆盖):从知识库召回相关文档;
  2. 增强:将检索到的文档拼接为上下文,注入 Prompt;
  3. 生成:让 LLM 基于上下文回答问题。

LangChain 1.0 的 init_chat_model 与检索器可以无缝衔接,例如:

python 复制代码
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate

# 检索相关文档
docs = retriever.invoke("耐克 2023 年的营收是多少?")
context = "\n\n".join([d.page_content for d in docs])

# 构建增强 Prompt
prompt = ChatPromptTemplate.from_template("""
基于以下上下文回答问题。如果上下文中没有相关信息,请说"我不知道"。

上下文:
{context}

问题:{question}
""")

# 生成回答
model = init_chat_model(model="ollama:deepseek-r1:latest", base_url="http://localhost:11434")
chain = prompt | model
response = chain.invoke({"context": context, "question": "耐克 2023 年的营收是多少?"})
print(response.content)

从 PDF 到向量库,从语义搜索到增强生成,LangChain 1.0 提供了一整套统一且可组合的抽象。掌握这套工具链,就能快速构建数据驱动的 AI 应用。


六、参考文献

  1. LangChain 1.0 文档 - Retrieval
  2. LangChain 文档 - Text Splitters
  3. LangChain 文档 - Vector Stores
  4. Chroma 官方文档
  5. Ollama 嵌入模型
  6. RAG Survey Paper: Retrieval-Augmented Generation for Large Language Models
相关推荐
qq_5469372714 小时前
内置 AI 搜索、换肤、PDF 工具、100G 网盘,这款浏览器有点东西
人工智能·pdf
诸葛大钢铁14 小时前
知网CAJ格式文件如何转为Word/PDF?CAJ转Word的三个免费方法
pdf·word·知网·caj·caj转word
Komorebi_99991 天前
LangChain Day5 课程:Agent 智能代理
前端·langchain·大模型
꧁꫞꯭零꯭点꯭꫞꧂1 天前
LangChain 提示词模板与链式调用笔记
人工智能·笔记·langchain
wtsolutions1 天前
QMT 知识库 XtQuant知识库 使用文档 pdf
pdf·知识库·文档·qmt
AI周红伟1 天前
Agent Skills生产级Skills 案例实操-周红伟
前端·chrome·react.js·langchain
海盗12342 天前
C#中PDF操作-QuestPDF介绍和使用教程
pdf·c#
半月夏微凉2 天前
win11下不能预览pdf的问题解决方法
windows·pdf
Irissgwe2 天前
九、LangChain之核心组件--(7)文本向量(下)
langchain·检索器·向量存储·rag·langgraph