LangChain 之 【RAG简介、文档加载器、文本分割器】
目录
1.RAG(Retrieval-Augmented Generation,检索增强生成)
2.数据载体:Document 对象
3.文档加载器
3.1. 加载 PDF
3.2. 加载 Markdown
4.文本分割器(Text Splitters)
4.1 基于字符的软约束(CharacterTextSplitter)
4.2 基于 Token 的软约束(适配 GPT)
4.3 硬性约束(RecursiveCharacterTextSplitter)
4.4 特殊代码分割(PythonCodeTextSplitter)
5. 一些现象
1.RAG(Retrieval-Augmented Generation,检索增强生成)
两种信息搜索方式
- AI 搜索:利用搜索引擎索引的公网数据,结合大模型进行总结。它擅长回答实时、公开、泛领域的问题(如天气、新闻),但无法涉足企业内部未公开的私域数据。
- 企业级 RAG:将本地文件、数据库等私域语料预先离线处理,构建成向量索引。当用户提问时,系统在私有向量库中进行语义检索,将检索到的“上下文证据”注入提示词,引导大模型生成基于事实的回答。(本质上是AI搜索在私域场景下的移植)
| 对比维度 | AI 搜索 | RAG | 技术本质 |
|---|---|---|---|
| 数据来源 | 公网(实时爬取) | 私有/本地(离线构建) | 这是两者最根本的分水岭 |
| 核心能力 | 搜索引擎检索 + 大模型总结 | 向量语义检索 + 大模型生成 | 都用了“检索+生成”,但检索对象不同 |
| 典型场景 | 查天气、查新闻 | 查公司制度、查内部技术文档 | 私密性 vs 公开性 |
RAG 的核心两大阶段:
| 阶段 | 做的事 | 工程目标 |
|---|---|---|
| 离线数据处理 | 加载 → 分割 → 向量化 → 存入向量库 | 将非结构化文本转化为 可语义检索的数学向量 |
| 在线检索 | 问题向量化 → 相似度召回 → 上下文注入 → LLM 生成 | 动态增强 prompt,压制模型幻觉 |
2.数据载体:Document 对象
LangChain 最终都会把加载的 PDF、Markdown、数据库等数据封装成Document 对象
Document 是 LangChain 中用来表示文本块及其元数据的核心数据结构,在 RAG(检索增强生成)等流程中,作为加载器、分割器、向量库和检索器之间统一的数据接口。
Document 对象贯穿整个 RAG 流程:
- 加载 (Load):文档加载器(如 PyPDFLoader)从不同数据源读取数据,并将其封装成 Document 对象列表
- 转换 (Transform):文本分割器(如 RecursiveCharacterTextSplitter)接收 Document 列表,将长文档切分成更小的 Document 块
- 存储与检索 (Store & Retrieve):向量库(如 FAISS)接收 Document 列表,将其文本向量化并存储。当用户提问时,检索器会根据语义相似度从向量库中找出最相关的 Document 对象
- Document 参数
| 属性 | 类型 | 说明 |
|---|---|---|
page_content | str | 核心文本内容。通常是分割后的一个块(Chunk) |
metadata | dict | 极其关键。存储来源路径(source)、页码、以及层级关系(parent_id) |
id | str(可选) | 全局唯一标识符,用于去重 |
加载 Markdown 使用 mode="elements" 时,元数据里会出现 category: "Title" 或 "ListItem",同时附带 parent_id。这允许我们还原整篇文档的树状结构,但也意味着文档数量会暴增(见下文)。如果你的业务不需要细粒度结构,请用 mode="single" 保持为一个整体。
3.文档加载器
LangChain 提供了上百种加载器,这里我们聚焦最常用的 PDF 和 Markdown
3.1. 加载 PDF
在使用 PyPDFLoader 前,需要先安装必要的包:
pip install -qU langchain-community pypdfPyPDFLoader 是 LangChain 社区版中用于加载 PDF 文档的核心工具,其默认行为就是按页拆分
| 属性名 | 类型 | 默认值 | 是否必需 | 描述 |
|---|---|---|---|---|
file_path | str或PurePath | 无 | 是 | 要加载的 PDF 文件的路径。 |
mode | Literal['single', 'page'] | 'page' | 否 | 决定切分粒度的核心参数。 - 'page'(默认):按页拆分,PDF 的每一页变成一个独立的Document对象。- 'single': 合并全文,将整个 PDF 的所有页面合并成一个Document对象。 |
password | Optional[Union[str, bytes]] | None | 否 | 用于打开加密 PDF 的密码。 |
extract_images | bool | False | 否 | 是否尝试提取 PDF 中的图片,通常用于多模态 RAG 场景。 |
headers | Optional[Dict] | None | 否 | 当从 Web 路径下载文件时,可选的 HTTP 请求头。 |
pages_delimiter | str | 预定义 | 否 | 在mode="single"模式下,用于分隔各页内容的字符串。 |
extraction_mode | Literal['plain', 'layout'] | 'plain' | 否 | 提取模式,'plain'为纯文本,'layout'会尝试保留更多布局信息。 |
核心方法
- load():继承自 BaseLoader 的标准方法。它会直接执行加载逻辑(根据 mode 参数决定是否按页拆分),并一次性返回所有 Document 对象的列表。这是最通用的入口方法
- load_and_split(text_splitter=None):这是一个增强型方法。它的主要设计目的是方便你传入一个文本分割器(TextSplitter),在按页拆分后,进一步将过长的单页文本切分成更小的逻辑块。如果你不传 text_splitter,它内部实际上也是调用 load() 并返回相同结果
- lazy_load():返回一个生成器(Generator),实现懒加载(Lazy Loading)。对于页数很多的 PDF,使用此方法可以避免一次性将所有页面载入内存,极大优化性能
| 方法 | 返回类型 | 内存占用 | 是否支持进一步切块 | 推荐场景 |
|---|---|---|---|---|
load() | List[Document] | 一次性全载入 | 需手动再切 | 小文件、需要反复访问所有页 |
lazy_load() | 生成器Iterator[Document] | 逐页加载,极低 | 需手动再切 | 超大文件、流式处理 |
load_and_split() | List[Document](块) | 取决于切块后总数量 | 通过text_splitter | 需要控制最终块大小以适应模型输入 |
from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 准备加载器(mode='page' 默认按页拆分) loader = PyPDFLoader(file_path="./demo.pdf") # 方法一:load() — 一次性全量加载 pages = loader.load() print(f"总页数:{len(pages)}") print(f"第1页片段:{pages[0].page_content[:80]}...") print(f"第1页元数据:{pages[0].metadata}\n") # 含 page 和 source # 方法二:lazy_load() — 生成器逐页产出 for idx, doc in enumerate(loader.lazy_load()): if idx >= 3: break print(f"第{idx+1}页片段:{doc.page_content[:80]}...") # 方法三:load_and_split() — 加载后进一步切块 # 自定义分割器:每块200字符,重叠20字符 text_splitter = RecursiveCharacterTextSplitter( chunk_size=200, chunk_overlap=20, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = loader.load_and_split(text_splitter=text_splitter) print(f"切分后总块数:{len(chunks)}") print(f"第1块片段:{chunks[0].page_content[:80]}...") print(f"第1块来源页码:{chunks[0].metadata['page'] + 1}") # metadata中保留原页码 print(f"第1块元数据:{chunks[0].metadata}")常见问题与注意事项
- 扫描版 PDF(图片型PDF):PyPDFLoader 默认无法提取扫描版 PDF 中的文字。如需处理此类文件,应使用支持 OCR 的加载器,如 UnstructuredPDFLoader
- 中文支持:PyPDFLoader 对中文的支持取决于 PDF 文件本身的编码。如果遇到乱码,可能需要检查 PDF 的字体嵌入情况,或考虑使用其他解析引擎
- 提取图片:通过设置 extract_images=True 可以提取图片,但提取的图片通常以 Base64 编码的字符串形式存在于元数据中,需要进一步处理
3.2. 加载 Markdown
依赖库:使用前需安装 langchain_community 和 unstructured 包。
为了更好支持 Markdown,推荐安装 "unstructured[md]"
pip install "unstructured[md]" langchain_community # 2. 安装 NLTK 库 pip install nltk # 3. 下载 NLTK 所需的必要数据包 python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger')"| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
file_path | str或List[str] | 必填 | 要加载的 Markdown 文件的路径。 |
mode | str | 'single' | 核心参数。决定加载模式,可选'single'或'elements'。 |
strategy | str | 'hi_res' | 解析策略。'hi_res'(高精度,速度慢)或'fast'(速度快,可能损失细节)。 |
**unstructured_kwargs | Any | - | 可传入其他unstructured库的配置参数。 |
两种加载模式详解
1. mode="single" (默认模式),整个 Markdown 文件会被合并成一个 Document 对象
- 输出:List[Document],列表长度为1
- page_content:包含合并后的全部文本内容,但会丢失标题、列表等结构信息
- 适用场景:当你不需要保留文档结构,或者打算自行进行文本分割时
2. mode="elements",unstructured 库会将文档解析为不同的语义元素(如标题、段落、列表项等),每个元素都成为一个独立的 Document 对象
- 输出:List[Document],列表长度等于文档中的元素数量
- page_content:每个 Document 包含一个独立元素的文本内容
- 元数据 (metadata):每个 Document 的 metadata 中会包含一个 category 字段,标明元素类型,如 'Title', 'NarrativeText', 'ListItem' 等
- 适用场景:当你需要保留并利用文档的层级结构时。例如,只提取所有标题,或按标题对内容进行分
主要方法
| 方法名 | 描述 |
|---|---|
load() | 同步加载,根据mode设置返回Document列表。 |
lazy_load() | 返回一个生成器,实现懒加载,适合处理大型文件以节省内存。 |
load_and_split() | 在加载后,可选的TextSplitter进行进一步切分 |
from langchain_community.document_loaders import UnstructuredMarkdownLoader loader = UnstructuredMarkdownLoader( "./README.md", mode="elements", # 启用元素模式 strategy="fast" # 使用快速解析策略 ) docs = loader.load() print(f"共解析出 {len(docs)} 个元素") # 查看不同类型元素及其内容 for doc in docs[:3]: print(f"元素类型: {doc.metadata.get('category')}") print(f"内容: {doc.page_content[:50]}...\n")4.文本分割器(Text Splitters)
为什么不能直接把整本书丢给 LLM?因为:
- 上下文窗口有限(哪怕是 100 万 token 的模型,检索精度也会随上下文变长而急剧下降)。
- 检索颗粒度:如果块太大(比如 2000 字),用户问“Redis 缓存策略”,向量检索可能因为块内包含太多“MySQL”内容而召回失败。
4.1 基于字符的软约束(CharacterTextSplitter)
核心逻辑:根据指定的字符序列(默认为 "\n\n")来切分文本,并以字符数来衡量每个块(Chunk)的大小,但如果某个段落实在过长且找不到分隔符,为了不破坏语义完整性,它宁可保留整个长块,并仅打印 Created a chunk of size xxx...作为提醒(这不是报错!)
from langchain_text_splitters import CharacterTextSplitter text_splitter = CharacterTextSplitter( separator="\n\n", # 优先按段落切 chunk_size=100, # 目标大小 chunk_overlap=20, # 重叠防止切断关键句 length_function=len, )| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
separator | str | "\n\n" | 分隔符。文本将首先在此字符序列处被分割 |
chunk_size | int | 4000 | 块大小。每个文本块的最大字符数 |
chunk_overlap | int | 200 | 块重叠。相邻两个文本块之间重叠的字符数 |
length_function | callable | len | 长度计算函数。用于计算文本长度的函数,默认是 Python 内置的len,即计算字符数 |
is_separator_regex | bool | False | 分隔符正则。若设为True,则separator将被当作正则表达式来使用 |
核心方法
| 方法名 | 参数 | 返回值 | 核心规则与特点 |
|---|---|---|---|
split_text | text: str | List[str] | ①按separator(默认\n\n)切分② 每块≤ chunk_size字符③ 相邻块重叠 chunk_overlap字符④ 返回纯字符串列表,无元数据 |
split_documents | documents: | List[Document] | ①批量处理多个Document对象 ② 每个Document独立切分成多个子文档 ③ 自动复制原元数据 ④ 新增 page或index等分割位置信息 |
create_documents | texts: List[str]metadatas: List[dict] = None | List[Document] | ①将多个纯文本分别切割 ② 为每个块附加对应的元数据 ③ 元数据数量需与文本数量匹配 ④ 未提供元数据则生成空字典 |
工作原理
- 按分隔符初次分割:根据你指定的 separator(如 "\n\n")将文本切分成多个小块
- 合并成块:从第一个小块开始,不断将后续小块合并到一起,直到总字符数接近 chunk_size
- 处理重叠:下一个块从上一个块的末尾开始,保留 chunk_overlap 个字符
- 超长块处理(伪递归):如果某一块本身长度超过 chunk_size,分割器会尝试用同一个 separator 再次切分。如果切不动(即块内不包含该分隔符),则直接保留为一个大块,不强制截断
from langchain_text_splitters import CharacterTextSplitter # 1. 读取长文本 with open("document.txt") as f: long_text = f.read() # 2. 创建分割器实例 text_splitter = CharacterTextSplitter( separator="\n\n", # 以两个换行符作为段落分隔 chunk_size=1000, # 每块最多1000个字符 chunk_overlap=200, # 块之间重叠200个字符 length_function=len, # 使用字符数计算长度 ) # 3. 执行分割,得到 Document 对象列表 docs = text_splitter.create_documents([long_text]) # 或直接得到字符串列表 # texts = text_splitter.split_text(long_text) print(f"生成了 {len(docs)} 个文本块") print(docs[0].page_content) # 查看第一个块的内容4.2 基于 Token 的软约束(适配 GPT)
from_tiktoken_encoder主要用来创建能按 Token 数量来度量文本块大小的分割器。它尤其适合处理 OpenAI 的 GPT 系列模型,因为能更准确地估算 Token 消耗
- from_tiktoken_encoder 的关键特性是:使用 tiktoken 来计算长度,但分割操作本身仍然基于字符(如 CharacterTextSplitter 的 separator)
- 因此它提供的是软约束:它会尽力让每个块不超过设定的 chunk_size,但无法 100% 保证。如果一个独立的语义单元(如一个很长的段落)本身 Token 数就超过了限制,它会被整个保留下来,导致该块的实际大小超标
参数说明
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
encoding_name | str | 'gpt2' | tiktoken编码的名称,如'cl100k_base'(用于GPT-4等)。注意:默认是较旧的'gpt2'编码。 |
model_name | Optional[str] | None | 模型名称,如'gpt-4'。如果提供,会覆盖encoding_name的设置。 |
allowed_special | Optional[...] | None | 允许在编码中出现的特殊 token。 |
disallowed_special | Union[...] | 'all' | 禁止在编码中出现的特殊 token。 |
**kwargs | Any | {} | 传递给分割器构造函数(如CharacterTextSplitter)的其他参数,例如chunk_size,chunk_overlap,separators等。 |
from langchain_text_splitters import CharacterTextSplitter # 使用 encoding_name text_splitter = CharacterTextSplitter.from_tiktoken_encoder( encoding_name="cl100k_base", # 为GPT-4等模型指定编码 chunk_size=100, chunk_overlap=0 ) # 或者使用 model_name # text_splitter = CharacterTextSplitter.from_tiktoken_encoder( # model_name="gpt-4", # chunk_size=100, # chunk_overlap=0 # ) texts = text_splitter.split_text(your_long_text)4.3 硬性约束(RecursiveCharacterTextSplitter)
RecursiveCharacterTextSplitter 是 LangChain 官方文档推荐的通用文本分割器,它通过按优先级顺序尝试不同的分隔符,在保证块大小(chunk_size)的同时,尽可能维持段落、句子等语义单元的完整性
初始化 RecursiveCharacterTextSplitter 时,最常用的参数如下:
| 参数名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
separators | List[str] | ["\n\n", "\n", " ", ""] | 核心参数。一个按优先级排列的分隔符列表。 |
chunk_size | int | 4000 | 每个文本块的最大尺寸,其衡量方式由length_function决定。 |
chunk_overlap | int | 200 | 相邻两个文本块之间重叠的字符数,用于缓解上下文在切割处丢失的问题。 |
length_function | Callable | len | 用于计算文本长度的函数,默认为计算字符数。 |
is_separator_regex | bool | False | 决定separators列表中的元素是否被当作正则表达式处理。 |
工作原理解析
(1)递归分割
- 按序尝试:它首先使用 separators 列表中的第一个分隔符(如 \n\n)来分割整个文本
- 检查大小:分割后,检查每个片段的大小
- 递归处理:如果某个片段的尺寸仍然超过 chunk_size,分割器会自动使用列表中的下一个分隔符(如 \n)来递归地分割这个片段
- 直到达标:这个过程会一直持续,直到所有片段都符合尺寸要求,或者用尽所有分隔符
这种机制确保了更大的语义单元(如段落)会优先被保留在一起。只有当段落太长时,才会退而求其次,尝试按句子或词语来分割。
(2)合并块
- 在递归分割后,分割器会进行合并:它会从第一个片段开始,不断将后续片段合并到一起,直到总尺寸接近 chunk_size,以此来生成最终的文本块
from langchain_text_splitters import RecursiveCharacterTextSplitter # 初始化分割器 text_splitter = RecursiveCharacterTextSplitter( chunk_size=100, # 每块最大100个字符 chunk_overlap=20, # 块之间重叠20个字符 length_function=len, is_separator_regex=False, ) # 示例长文本 long_text = """这是第一段。它包含一些句子。 这是第二段。它也有一些句子。 这是第三段,但它非常长,以至于可能超过我们设定的 chunk_size 限制,所以它会被进一步分割。""" # 执行分割,返回字符串列表 texts = text_splitter.split_text(long_text) # 或者,直接从文档列表创建 Document 对象 # docs = text_splitter.create_documents([long_text]) for i, chunk in enumerate(texts): print(f"块 {i+1}: {chunk}\n")RecursiveCharacterTextSplitter 提供了 from_language 类方法,可以为特定编程语言使用预定义的分隔符列表,这在分割代码时非常有用
from langchain_text_splitters import RecursiveCharacterTextSplitter, Language # 为 Python 代码创建分割器 python_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, # 也支持 Language.JS, Language.JAVA 等 chunk_size=2000, chunk_overlap=200 ) python_code = "def hello():\n print('Hello, world!')\n" chunks = python_splitter.split_text(python_code)支持 Language.PYTHON, Language.JS, Language.JAVA, Language.GO, Language.RUST 等多种语言
4.4 特殊代码分割(PythonCodeTextSplitter)
PythonCodeTextSplitter 是 RecursiveCharacterTextSplitter 的一个子类
它的特别之处在于,初始化时会自动加载一套针对 Python 优化的分隔符列表
它的参数与 RecursiveCharacterTextSplitter 基本一致。
| 参数名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
chunk_size | int | 4000 | 每个文本块的最大尺寸,衡量方式由length_function决定。 |
chunk_overlap | int | 200 | 相邻两个文本块之间重叠的字符数。 |
length_function | Callable | len | 用于计算文本长度的函数,默认为计算字符数。 |
separators | List[str] | (自动设置) | 由类自动根据Language.PYTHON填充,一般无需手动设置。 |
from langchain_text_splitters import PythonCodeTextSplitter python_code = """ class MyClass: def method_one(self): print("Hello") def method_two(self): print("World") def top_level_function(): return True """ # 初始化分割器 splitter = PythonCodeTextSplitter( chunk_size=50, # 每块最大50个字符 chunk_overlap=0 # 块之间无重叠 ) # 方法1:分割文本,返回字符串列表 chunks = splitter.split_text(python_code) for i, chunk in enumerate(chunks): print(f"--- Chunk {i+1} ---\n{chunk}\n") # 方法2:创建Document对象 # docs = splitter.create_documents([python_code])5. 一些现象
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
控制台不停打印Created a chunk of size 150... | 分割器的软约束机制在保护语义完整性。它找不到合适的分隔符,保留了长块 | 增大chunk_size(如 100→300);或在separators中增加中文标点"。", "," |
| Markdown 加载后文档数量暴涨 | 误用mode="elements",每个标题、每个列表项都被独立成 Document | 若只需全局问答,切回mode="single";若需结构,利用parent_id做后处理合并 |
| 模型回答明显偏离文档内容(幻觉) | chunk_size过大导致检索出的块包含太多噪声,或Top-K设置过小 | 调小chunk_size(建议 200~400),适当增加Top-K(如 5~6),并设置相似度阈值过滤低分结果 |
| 中文词组被割裂(如“分布式”变“分布”+“式”) | 默认分隔符列表针对英文空格设计,无中文感知 | 重写separators或引入Jieba等分词器预处理后再传入 LangChain |
