第 07 期 | 切分艺术:Text Splitters 与上下文保全

⏱ 预计阅读 20 分钟 更新于 2026/5/7
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)

🎯 本期学习目标

各位未来的 AI 架构师们,欢迎回到《LangChain 全栈大师课》!上一期我们初探了 LangChain 的骨架,这期我们就要开始往这个骨架上填充真正的“智慧”了。想象一下,一个优秀的客服小哥,他的核心能力是什么?不是能说会道,而是“知道得多,记得住,找得快”。我们这期就是要让我们的智能客服小助手具备这样的能力!

学完本期,你将:

  1. 深入理解 RAG (检索增强生成) 的核心思想,以及它在解决大型语言模型(LLM)“一本正经地胡说八道”和“知识过时”等痛点中的关键作用。
  2. 掌握 LangChain 文档加载器 (Document Loaders) 的使用,能够将各种格式(PDF、网页、本地文件等)的非结构化数据高效地导入到我们的智能客服系统中。
  3. 学会利用 LangChain 检索器 (Retrievers) 从海量知识库中精准地提取相关信息,确保客服小助手总能找到最匹配用户问题的答案。
  4. 能够将文档加载与检索器无缝结合,为智能客服构建起一个可扩展、高效率的知识检索基础,让你的 AI 真正拥有“记忆”和“查找”的能力。

📖 原理解析

RAG:让LLM告别“幻觉”与“失忆”

还记得我们之前说的吗?LLM 固然强大,但它也有自己的“阿喀琉斯之踵”:

  1. 幻觉 (Hallucination): 它们可能一本正经地编造事实。
  2. 知识时效性 (Knowledge Cutoff): 训练数据有截止日期,无法了解最新的信息。
  3. 私有数据缺失 (Proprietary Data): 它们不知道你公司的内部规章、产品手册、历史工单。

这对于我们的“智能客服知识库”项目来说,简直是致命的!一个客服如果张口就来,或者对自家产品一问三不知,那用户不投诉你才怪。

RAG (Retrieval-Augmented Generation),检索增强生成,就是解决这些问题的“银弹”。 它的核心思想很简单:在 LLM 生成答案之前,先去一个外部的、权威的、实时的知识库里找到最相关的参考资料,然后把这些资料和用户的问题一起喂给 LLM,让 LLM 基于这些资料来生成答案。

想象一下你的智能客服小助手:

当用户问:“你们最新的产品XX功能怎么用?”

没有 RAG 的 LLM 可能回答:“很抱歉,我不知道您说的XX功能。”或者更糟糕,“XX功能是用来泡咖啡的。”(瞎编!)

有了 RAG 的 LLM 会怎么做?

  1. 检索: 小助手会先去公司的产品手册、FAQ 页面、技术文档里,搜索所有关于“XX功能”的资料。
  2. 提炼: 找到几段最相关的文档片段。
  3. 生成: 然后,把这些文档片段和用户的问题一起,交给 LLM:“用户问XX功能怎么用,这是我找到的相关资料,请你根据这些资料生成一个简洁明了的答案。”

这样,LLM 就有了“参考书”,它的回答会更准确、更及时,也更符合我们公司的实际情况。

智能客服项目中的 RAG 工作流

在我们的“智能客服知识库”项目中,RAG 的完整工作流可以拆解为以下几个核心阶段,本期我们重点关注前两个:数据摄取检索

graph TD
    subgraph 数据摄取 (Ingestion)
        A[原始知识库: PDF, DOCX, Web页面, 数据库] --> B(LangChain Document Loaders - 文档加载器)
        B --> C(Documents - 文档对象)
        C --> D(Text Splitters - 文本分割器)
        D --> E(Text Chunks - 文本块)
        E --> F[Embedding Model - 嵌入模型]
        F --> G[Vector Store - 向量知识库]
    end

    subgraph 检索与生成 (Retrieval & Generation)
        H[用户提问] --> I(Embedding Model - 嵌入模型)
        I --> J(Query Vector - 查询向量)
        J --> K[LangChain Retrievers - 检索器]
        K -- 检索相似向量 --> G
        G -- 返回Top-K相关文本块 --> K
        K --> L(Top-K 相关文档块)
        L --> M[LLM - 大语言模型]
        M -- 结合提问与文档块生成 --> N(最终答案)
        N --> H
    end

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#ccf,stroke:#333,stroke-width:2px
    style D fill:#ddf,stroke:#333,stroke-width:2px
    style E fill:#eef,stroke:#333,stroke-width:2px
    style F fill:#ffc,stroke:#333,stroke-width:2px
    style G fill:#cfc,stroke:#333,stroke-width:2px

    style H fill:#f9f,stroke:#333,stroke-width:2px
    style I fill:#ffc,stroke:#333,stroke-width:2px
    style J fill:#eef,stroke:#333,stroke-width:2px
    style K fill:#bbf,stroke:#333,stroke-width:2px
    style L fill:#ddf,stroke:#333,stroke-width:2px
    style M fill:#fcc,stroke:#333,stroke-width:2px
    style N fill:#efe,stroke:#333,stroke-width:2px

从上图可以看出,文档加载器 (Document Loaders) 负责将我们散落在各处的知识(PDF 产品手册、HTML FAQ 页面、Markdown 格式的技术文档,甚至数据库记录)统一加载成 LangChain 认可的 Document 对象。这些 Document 对象包含 page_content(文档内容)和 metadata(元数据,如来源、页码等)。

加载进来的文档通常很大,不适合直接喂给 LLM,所以需要通过文本分割器 (Text Splitters) 将它们切分成更小、管理起来更方便的文本块 (Text Chunks)

然后,这些文本块会被嵌入模型 (Embedding Model) 转换成向量 (Vector),存储到向量知识库 (Vector Store) 中。这一步非常关键,它将文本的语义信息编码成了数字形式,为后续的语义检索打下基础。

当用户提出问题时,用户的查询也会被嵌入模型转换成一个查询向量 (Query Vector)检索器 (Retrievers) 登场了!它会拿着这个查询向量,去向量知识库里寻找语义上最相似的文本块。这就是我们说的“找得快、找得准”。

最后,这些被检索到的相关文本块,会和用户的原始问题一起,作为上下文喂给大语言模型 (LLM),让它生成最终的答案。

LangChain 文档加载器 (Document Loaders) 深度解析

LangChain 提供了海量的 DocumentLoader,它们的作用就是统一接口,将不同来源、不同格式的数据转化为 LangChain 的 Document 对象。这就像一个超级翻译官,无论你的知识是 PDF 格式的“天书”,还是网页上的“散文”,它都能翻译成 LLM 能理解的“白话文”。

常用的加载器包括:

  • PyPDFLoader: 从 PDF 文件加载文档。
  • WebBaseLoader: 从网页 URL 加载内容。
  • DirectoryLoader: 从本地目录加载各种文件(结合 Glob 模式和 Loader 类型)。
  • CSVLoader, JSONLoader: 加载结构化数据。
  • EvernoteLoader, NotionLoader, ConfluenceLoader: 从各种笔记或协作平台加载。

它们的共同方法是 load(),返回一个 List[Document]

LangChain 检索器 (Retrievers) 深度解析

检索器是 RAG 的“眼睛”和“手”,它负责根据用户的问题,在你的知识库中“看”到并“抓取”出最相关的资料。

最核心、最常用的检索器是 VectorStoreRetriever 它的工作原理是:

  1. 接收一个用户查询字符串。
  2. 将这个查询字符串通过嵌入模型转换为一个向量。
  3. 在底层的 VectorStore 中,通过向量相似度搜索算法(如余弦相似度)找到与查询向量最相似的 k 个文档块。
  4. 返回这些文档块。

关键参数:

  • vectorstore: 必须关联一个已经存储了文档向量的 VectorStore 实例。
  • search_type: 检索类型,默认为 similarity(相似度搜索),也可以是 mmr(最大边际相关性,用于在保持相关性的同时增加检索结果的多样性)。
  • search_kwargs: 传递给底层 VectorStore 搜索方法的额外参数,最常用的是 k(返回多少个文档块)。

💻 实战代码演练 (客服项目中的具体应用)

好了,原理讲得再天花乱坠,不如撸起袖子干一场!现在,我们将把这些概念应用到我们的“智能客服知识库”项目。

场景设定: 我们的智能客服需要能够回答关于公司产品的问题。这些产品信息分散在:

  1. 产品说明书 (PDF): product_manual.pdf
  2. 常见问题解答 (FAQ 网页): https://www.example.com/faq (这里我们用一个模拟的URL)

我们将演示如何加载这些文档,分割它们,存储到向量数据库,并最终通过检索器获取相关信息。

1. 环境准备与依赖安装

首先,确保你的 Python 环境就绪,并安装必要的库。

pip install -q langchain-community langchain-openai pypdf beautifulsoup4 chromadb tiktoken
  • langchain-community: 包含各种 Document Loaders 和 Vector Stores。
  • langchain-openai: 用于 OpenAI 的嵌入模型和 LLM。
  • pypdf: 用于处理 PDF 文件。
  • beautifulsoup4: 用于解析 HTML 网页内容(WebBaseLoader 依赖)。
  • chromadb: 一个轻量级的本地向量数据库,非常适合学习和原型开发。
  • tiktoken: OpenAI token 计算工具。

2. 设置 OpenAI API Key

确保你已经设置了 OPENAI_API_KEY 环境变量。

import os
# 请替换为你的实际 API 密钥,或者确保已设置环境变量
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

3. 创建模拟文档 (或使用真实文档)

为了演示,我们先创建一个模拟的 PDF 文件。对于网页,我们直接使用一个公共 URL。

a. 创建 product_manual.pdf

你可以手动创建一个简单的 PDF 文件,内容如下:

# product_manual.pdf (内容示例)
## 产品 A 使用指南

产品 A 是一款创新的智能家居设备,旨在提升您的生活品质。

### 安装步骤
1. 打开包装,取出产品 A 主机及配件。
2. 将产品 A 放置在平稳的表面。
3. 连接电源线,并确保指示灯亮起。
4. 下载并安装“智能管家”App。
5. 按照 App 指引完成设备配对。

### 常见问题
- **Q: 产品 A 无法开机怎么办?**
  A: 请检查电源连接是否牢固,或尝试更换电源插座。如果问题依旧,请联系客服。
- **Q: 如何重置产品 A?**
  A: 在设备通电状态下,长按设备背面的重置按钮 5 秒,直到指示灯闪烁。

## 产品 B 功能介绍

产品 B 是一款高效的办公助手,提升您的工作效率。

### 主要功能
- 智能日程管理
- 会议纪要自动生成
- 任务提醒与协作

### 故障排除
- **Q: 产品 B 无法连接网络?**
  A: 检查网络设置,确保 Wi-Fi 密码正确。重启设备和路由器后重试。

将上述内容保存为 product_manual.txt,然后用任何文本转 PDF 工具转换为 product_manual.pdf,并确保放在你的 Python 脚本同目录下。

b. 模拟 FAQ 网页 (使用一个真实的公共页面作为示例)

我们将使用 LangChain 官方文档的一个页面作为示例,以演示 WebBaseLoader 的能力。 faq_url = "https://www.langchain.com/blog/rag-is-all-you-need" (这个页面内容丰富,适合演示)

4. 实战代码:文档加载、分割、嵌入与检索

from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document

import os

# 确保你的 OpenAI API 密钥已设置
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
if not os.getenv("OPENAI_API_KEY"):
    raise ValueError("OPENAI_API_KEY 环境变量未设置。请设置你的API密钥。")

print("--- 1. 文档加载阶段 ---")

# --- 1.1 使用 PyPDFLoader 加载 PDF 文档 ---
pdf_path = "product_manual.pdf"
try:
    pdf_loader = PyPDFLoader(pdf_path)
    pdf_docs = pdf_loader.load()
    print(f"成功加载 {len(pdf_docs)} 页 PDF 文档。")
    # 打印第一页内容预览
    if pdf_docs:
        print(f"PDF 第一页内容预览: \n{pdf_docs[0].page_content[:200]}...")
except Exception as e:
    print(f"加载 PDF 文档失败: {e}")
    # 如果 PDF 文件不存在,创建一个空的 Document 列表,避免后续报错
    pdf_docs = []
    # 为了演示,如果 PDF 不存在,我们创建一个模拟的 Document
    if not os.path.exists(pdf_path):
        print(f"警告:'{pdf_path}' 不存在,将使用模拟 PDF 内容。")
        pdf_docs.append(Document(page_content="""
        ## 产品 A 使用指南
        产品 A 是一款创新的智能家居设备,旨在提升您的生活品质。
        ### 安装步骤
        1. 打开包装,取出产品 A 主机及配件。
        2. 将产品 A 放置在平稳的表面。
        3. 连接电源线,并确保指示灯亮起。
        4. 下载并安装“智能管家”App。
        5. 按照 App 指引完成设备配对。
        ### 常见问题
        - Q: 产品 A 无法开机怎么办?A: 请检查电源连接是否牢固,或尝试更换电源插座。
        ## 产品 B 功能介绍
        产品 B 是一款高效的办公助手,提升您的工作效率。
        ### 主要功能
        - 智能日程管理
        - 会议纪要自动生成
        - 任务提醒与协作
        ### 故障排除
        - Q: 产品 B 无法连接网络?A: 检查网络设置,确保 Wi-Fi 密码正确。重启设备和路由器后重试。
        """, metadata={"source": "simulated_product_manual.pdf"}))


# --- 1.2 使用 WebBaseLoader 加载网页文档 ---
faq_url = "https://www.langchain.com/blog/rag-is-all-you-need" # 示例URL
try:
    web_loader = WebBaseLoader(faq_url)
    web_docs = web_loader.load()
    print(f"成功加载 {len(web_docs)} 个网页文档。")
    # 打印第一个网页内容预览
    if web_docs:
        print(f"网页内容预览: \n{web_docs[0].page_content[:200]}...")
except Exception as e:
    print(f"加载网页文档失败: {e}")
    web_docs = []
    # 如果加载失败,也创建一个模拟的 Document
    print(f"警告:无法从 '{faq_url}' 加载,将使用模拟网页内容。")
    web_docs.append(Document(page_content="""
    智能客服系统 FAQ:
    Q: 如何联系客服?A: 您可以通过电话 400-123-4567 或在线聊天联系我们。
    Q: 订单状态查询?A: 请登录您的账户,在“我的订单”中查询。
    Q: 退换货政策?A: 购买后7天内可无理由退换货,详情请参考官网政策。
    """, metadata={"source": "simulated_faq_webpage"}))


# 合并所有加载的文档
all_docs = pdf_docs + web_docs
if not all_docs:
    print("没有可用于处理的文档,请检查PDF文件和网络连接。")
    exit() # 如果没有任何文档,直接退出

print("\n--- 2. 文本分割阶段 ---")

# --- 2.1 初始化文本分割器 ---
# RecursiveCharacterTextSplitter 尝试按不同字符(如段落、句子、单词)递归分割,效果较好
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,      # 每个文本块的最大长度
    chunk_overlap=200,    # 文本块之间的重叠长度,有助于保留上下文
    length_function=len   # 长度计算函数,默认为 len
)

# --- 2.2 分割文档 ---
chunks = text_splitter.split_documents(all_docs)
print(f"原始文档被分割成 {len(chunks)} 个文本块。")
if chunks:
    print(f"第一个文本块内容预览: \n{chunks[0].page_content[:200]}...")


print("\n--- 3. 嵌入模型与向量存储阶段 ---")

# --- 3.1 初始化嵌入模型 ---
# 我们使用 OpenAI 的 text-embedding-ada-002 模型
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")

# --- 3.2 初始化 Chroma 向量存储并添加文本块 ---
# persist_directory 参数会把向量存储持久化到本地文件系统,下次可以直接加载
persist_directory = './chroma_db_for_copilot'
# 如果目录不存在,Chroma 会自动创建
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory=persist_directory
)
# 持久化存储
vectorstore.persist()
print(f"文本块已成功嵌入并存储到 Chroma 向量数据库 ({persist_directory})。")


print("\n--- 4. 检索器应用阶段 ---")

# --- 4.1 初始化检索器 ---
# 从向量存储创建一个检索器
# search_kwargs={"k": 3} 表示检索最相似的 3 个文档块
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
print("检索器已初始化,将返回最相似的 3 个文档块。")

# --- 4.2 模拟用户查询并进行检索 ---
user_query_1 = "产品 A 怎么安装?"
retrieved_docs_1 = retriever.invoke(user_query_1)
print(f"\n用户查询: '{user_query_1}'")
print(f"检索到 {len(retrieved_docs_1)} 个相关文档:")
for i, doc in enumerate(retrieved_docs_1):
    print(f"--- 文档 {i+1} (来源: {doc.metadata.get('source', '未知')}, 页面: {doc.metadata.get('page', '未知')}) ---")
    print(doc.page_content[:300] + "...") # 打印部分内容

user_query_2 = "如何联系客服或者查询订单状态?"
retrieved_docs_2 = retriever.invoke(user_query_2)
print(f"\n用户查询: '{user_query_2}'")
print(f"检索到 {len(retrieved_docs_2)} 个相关文档:")
for i, doc in enumerate(retrieved_docs_2):
    print(f"--- 文档 {i+1} (来源: {doc.metadata.get('source', '未知')}, 页面: {doc.metadata.get('page', '未知')}) ---")
    print(doc.page_content[:300] + "...")

user_query_3 = "RAG是什么?"
retrieved_docs_3 = retriever.invoke(user_query_3)
print(f"\n用户查询: '{user_query_3}'")
print(f"检索到 {len(retrieved_docs_3)} 个相关文档:")
for i, doc in enumerate(retrieved_docs_3):
    print(f"--- 文档 {i+1} (来源: {doc.metadata.get('source', '未知')}, 页面: {doc.metadata.get('page', '未知')}) ---")
    print(doc.page_content[:300] + "...")

# --- 5. (可选) 结合 LLM 形成一个简单的 RAG 链 ---
# 这一步是下一期的重点,这里只是一个简单的预览
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

print("\n--- 5. (可选) 结合 LLM 进行生成 ---")

# 定义一个 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

# 定义一个提示模板
template = """
你是一个专业的智能客服助手。请根据提供的上下文信息,简洁、准确地回答用户的问题。
如果上下文没有提到相关信息,请礼貌地说明你无法回答。

上下文信息:
{context}

用户问题:
{question}

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

# 构建 RAG 链
# retriever | format_docs | prompt | llm | output_parser
# format_docs 辅助函数将 Document 对象转换为字符串
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()
)

print(f"\n--- 使用 RAG 链回答问题: '{user_query_1}' ---")
response_1 = rag_chain.invoke(user_query_1)
print(response_1)

print(f"\n--- 使用 RAG 链回答问题: '{user_query_2}' ---")
response_2 = rag_chain.invoke(user_query_2)
print(response_2)

print(f"\n--- 使用 RAG 链回答问题: '{user_query_3}' ---")
response_3 = rag_chain.invoke(user_query_3)
print(response_3)

代码解析:

  1. PyPDFLoaderWebBaseLoader: 分别加载本地 PDF 文件和远程网页内容。它们都返回一个 Document 对象列表,每个 Document 包含 page_content(实际文本)和 metadata(元数据,如文件路径、URL、页码等)。注意我们增加了错误处理和模拟内容,以防文件或网络不可用。
  2. RecursiveCharacterTextSplitter: 这是文本分割的利器。它会尝试以多种字符(如 \n\n, \n, )递归地分割文本,直到每个块都小于 chunk_sizechunk_overlap 参数非常重要,它让相邻的文本块之间有重叠部分,这有助于 LLM 在处理跨越块边界的上下文时不会丢失信息。
  3. OpenAIEmbeddings: 我们使用 OpenAI 的 text-embedding-ada-002 模型将文本块转换为高维向量。这个模型在语义理解方面表现出色,是构建 RAG 的基石。
  4. Chroma.from_documents: 这是将文本块、嵌入模型和向量存储连接起来的关键一步。它会遍历所有文本块,使用 embeddings 模型生成它们的向量,然后将这些向量和原始文本块一起存储到 Chroma 向量数据库中。persist_directory 确保数据可以被保存到本地,下次无需重新生成。
  5. vectorstore.as_retriever(search_kwargs={"k": 3}):Chroma 向量存储创建一个检索器。k=3 意味着每次查询时,检索器会返回语义上最相似的 3 个文档块。这个 k 值需要根据实际应用场景进行调整,过大可能引入噪音,过小可能信息不足。
  6. retriever.invoke(user_query): 这是检索器的核心方法。当你传入一个用户查询时,它会执行上述的向量相似度搜索,并返回一个 Document 对象列表,这些就是我们从知识库中找到的“参考资料”。
  7. RAG 链 (可选): 最后,我们简单演示了如何将检索器与 LLM 结合。RunnablePassthrough() 允许用户问题直接传递给 question 槽位。retriever | format_docs 这一部分首先通过检索器获取相关文档