LangChain 之 【RAG简介、文档加载器、文本分割器】

📅 2026/7/21 7:37:25
LangChain 之 【RAG简介、文档加载器、文本分割器】
目录1.RAGRetrieval-Augmented Generation检索增强生成2.数据载体Document 对象3.文档加载器3.1. 加载 PDF3.2. 加载 Markdown4.文本分割器Text Splitters4.1 基于字符的软约束CharacterTextSplitter4.2 基于 Token 的软约束适配 GPT4.3 硬性约束RecursiveCharacterTextSplitter4.4 特殊代码分割PythonCodeTextSplitter5. 一些现象1.RAGRetrieval-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_contentstr核心文本内容。通常是分割后的一个块Chunkmetadatadict极其关键。存储来源路径(source)、页码、以及层级关系(parent_id)idstr(可选)全局唯一标识符用于去重加载 Markdown 使用 modeelements 时元数据里会出现 category: Title 或 ListItem同时附带 parent_id。这允许我们还原整篇文档的树状结构但也意味着文档数量会暴增见下文。如果你的业务不需要细粒度结构请用 modesingle 保持为一个整体。3.文档加载器LangChain 提供了上百种加载器这里我们聚焦最常用的 PDF 和 Markdown3.1. 加载 PDF在使用 PyPDFLoader 前需要先安装必要的包pip install -qU langchain-community pypdfPyPDFLoader 是 LangChain 社区版中用于加载 PDF 文档的核心工具其默认行为就是按页拆分属性名类型默认值是否必需描述file_pathstr或PurePath无是要加载的 PDF 文件的路径。modeLiteral[single, page]page否决定切分粒度的核心参数。-page(默认):按页拆分PDF 的每一页变成一个独立的Document对象。-single: 合并全文将整个 PDF 的所有页面合并成一个Document对象。passwordOptional[Union[str, bytes]]None否用于打开加密 PDF 的密码。extract_imagesboolFalse否是否尝试提取 PDF 中的图片通常用于多模态 RAG 场景。headersOptional[Dict]None否当从 Web 路径下载文件时可选的 HTTP 请求头。pages_delimiterstr预定义否在modesingle模式下用于分隔各页内容的字符串。extraction_modeLiteral[plain, layout]plain否提取模式plain为纯文本layout会尝试保留更多布局信息。核心方法load()继承自 BaseLoader 的标准方法。它会直接执行加载逻辑根据 mode 参数决定是否按页拆分并一次性返回所有 Document 对象的列表。这是最通用的入口方法load_and_split(text_splitterNone)这是一个增强型方法。它的主要设计目的是方便你传入一个文本分割器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. 准备加载器modepage 默认按页拆分 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第{idx1}页片段{doc.page_content[:80]}...) # 方法三load_and_split() — 加载后进一步切块 # 自定义分割器每块200字符重叠20字符 text_splitter RecursiveCharacterTextSplitter( chunk_size200, chunk_overlap20, separators[\n\n, \n, 。, , , , ] ) chunks loader.load_and_split(text_splittertext_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图片型PDFPyPDFLoader 默认无法提取扫描版 PDF 中的文字。如需处理此类文件应使用支持 OCR 的加载器如 UnstructuredPDFLoader中文支持PyPDFLoader 对中文的支持取决于 PDF 文件本身的编码。如果遇到乱码可能需要检查 PDF 的字体嵌入情况或考虑使用其他解析引擎提取图片通过设置 extract_imagesTrue 可以提取图片但提取的图片通常以 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_pathstr或List[str]必填要加载的 Markdown 文件的路径。modestrsingle核心参数。决定加载模式可选single或elements。strategystrhi_res解析策略。hi_res高精度速度慢或fast速度快可能损失细节。**unstructured_kwargsAny-可传入其他unstructured库的配置参数。两种加载模式详解1. modesingle (默认模式)整个 Markdown 文件会被合并成一个 Document 对象输出List[Document]列表长度为1page_content包含合并后的全部文本内容但会丢失标题、列表等结构信息适用场景当你不需要保留文档结构或者打算自行进行文本分割时2. modeelementsunstructured 库会将文档解析为不同的语义元素如标题、段落、列表项等每个元素都成为一个独立的 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, modeelements, # 启用元素模式 strategyfast # 使用快速解析策略 ) 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_size100, # 目标大小 chunk_overlap20, # 重叠防止切断关键句 length_functionlen, )属性名类型默认值描述separatorstr\n\n分隔符。文本将首先在此字符序列处被分割chunk_sizeint4000块大小。每个文本块的最大字符数chunk_overlapint200块重叠。相邻两个文本块之间重叠的字符数length_functioncallablelen长度计算函数。用于计算文本长度的函数默认是 Python 内置的len即计算字符数is_separator_regexboolFalse分隔符正则。若设为True则separator将被当作正则表达式来使用核心方法方法名参数返回值核心规则与特点split_texttext: strList[str]①按separator默认\n\n切分② 每块≤chunk_size字符③ 相邻块重叠chunk_overlap字符④ 返回纯字符串列表无元数据split_documentsdocuments:List[Document]List[Document]①批量处理多个Document对象② 每个Document独立切分成多个子文档③ 自动复制原元数据④ 新增page或index等分割位置信息create_documentstexts: List[str]metadatas: List[dict] NoneList[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_size1000, # 每块最多1000个字符 chunk_overlap200, # 块之间重叠200个字符 length_functionlen, # 使用字符数计算长度 ) # 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 的软约束适配 GPTfrom_tiktoken_encoder主要用来创建能按 Token 数量来度量文本块大小的分割器。它尤其适合处理 OpenAI 的 GPT 系列模型因为能更准确地估算 Token 消耗from_tiktoken_encoder 的关键特性是使用 tiktoken 来计算长度但分割操作本身仍然基于字符如 CharacterTextSplitter 的 separator因此它提供的是软约束它会尽力让每个块不超过设定的 chunk_size但无法 100% 保证。如果一个独立的语义单元如一个很长的段落本身 Token 数就超过了限制它会被整个保留下来导致该块的实际大小超标参数说明参数类型默认值描述encoding_namestrgpt2tiktoken编码的名称如cl100k_base(用于GPT-4等)。注意默认是较旧的gpt2编码。model_nameOptional[str]None模型名称如gpt-4。如果提供会覆盖encoding_name的设置。allowed_specialOptional[...]None允许在编码中出现的特殊 token。disallowed_specialUnion[...]all禁止在编码中出现的特殊 token。**kwargsAny{}传递给分割器构造函数如CharacterTextSplitter的其他参数例如chunk_size,chunk_overlap,separators等。from langchain_text_splitters import CharacterTextSplitter # 使用 encoding_name text_splitter CharacterTextSplitter.from_tiktoken_encoder( encoding_namecl100k_base, # 为GPT-4等模型指定编码 chunk_size100, chunk_overlap0 ) # 或者使用 model_name # text_splitter CharacterTextSplitter.from_tiktoken_encoder( # model_namegpt-4, # chunk_size100, # chunk_overlap0 # ) texts text_splitter.split_text(your_long_text)4.3 硬性约束RecursiveCharacterTextSplitterRecursiveCharacterTextSplitter 是 LangChain 官方文档推荐的通用文本分割器它通过按优先级顺序尝试不同的分隔符在保证块大小chunk_size的同时尽可能维持段落、句子等语义单元的完整性初始化 RecursiveCharacterTextSplitter 时最常用的参数如下参数名类型默认值描述separatorsList[str][\n\n, \n, , ]核心参数。一个按优先级排列的分隔符列表。chunk_sizeint4000每个文本块的最大尺寸其衡量方式由length_function决定。chunk_overlapint200相邻两个文本块之间重叠的字符数用于缓解上下文在切割处丢失的问题。length_functionCallablelen用于计算文本长度的函数默认为计算字符数。is_separator_regexboolFalse决定separators列表中的元素是否被当作正则表达式处理。工作原理解析1递归分割按序尝试它首先使用 separators 列表中的第一个分隔符如 \n\n来分割整个文本检查大小分割后检查每个片段的大小递归处理如果某个片段的尺寸仍然超过 chunk_size分割器会自动使用列表中的下一个分隔符如 \n来递归地分割这个片段直到达标这个过程会一直持续直到所有片段都符合尺寸要求或者用尽所有分隔符这种机制确保了更大的语义单元如段落会优先被保留在一起。只有当段落太长时才会退而求其次尝试按句子或词语来分割。2合并块在递归分割后分割器会进行合并它会从第一个片段开始不断将后续片段合并到一起直到总尺寸接近 chunk_size以此来生成最终的文本块from langchain_text_splitters import RecursiveCharacterTextSplitter # 初始化分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size100, # 每块最大100个字符 chunk_overlap20, # 块之间重叠20个字符 length_functionlen, is_separator_regexFalse, ) # 示例长文本 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块 {i1}: {chunk}\n)RecursiveCharacterTextSplitter 提供了 from_language 类方法可以为特定编程语言使用预定义的分隔符列表这在分割代码时非常有用from langchain_text_splitters import RecursiveCharacterTextSplitter, Language # 为 Python 代码创建分割器 python_splitter RecursiveCharacterTextSplitter.from_language( languageLanguage.PYTHON, # 也支持 Language.JS, Language.JAVA 等 chunk_size2000, chunk_overlap200 ) 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 特殊代码分割PythonCodeTextSplitterPythonCodeTextSplitter 是 RecursiveCharacterTextSplitter 的一个子类它的特别之处在于初始化时会自动加载一套针对 Python 优化的分隔符列表它的参数与 RecursiveCharacterTextSplitter 基本一致。参数名类型默认值描述chunk_sizeint4000每个文本块的最大尺寸衡量方式由length_function决定。chunk_overlapint200相邻两个文本块之间重叠的字符数。length_functionCallablelen用于计算文本长度的函数默认为计算字符数。separatorsList[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_size50, # 每块最大50个字符 chunk_overlap0 # 块之间无重叠 ) # 方法1分割文本返回字符串列表 chunks splitter.split_text(python_code) for i, chunk in enumerate(chunks): print(f--- Chunk {i1} ---\n{chunk}\n) # 方法2创建Document对象 # docs splitter.create_documents([python_code])5. 一些现象现象根本原因解决方案控制台不停打印Created a chunk of size 150...分割器的软约束机制在保护语义完整性。它找不到合适的分隔符保留了长块增大chunk_size如 100→300或在separators中增加中文标点。, Markdown加载后文档数量暴涨误用modeelements每个标题、每个列表项都被独立成 Document若只需全局问答切回modesingle若需结构利用parent_id做后处理合并模型回答明显偏离文档内容幻觉chunk_size过大导致检索出的块包含太多噪声或Top-K设置过小调小chunk_size建议 200~400适当增加Top-K如 5~6并设置相似度阈值过滤低分结果中文词组被割裂如“分布式”变“分布”“式”默认分隔符列表针对英文空格设计无中文感知重写separators或引入Jieba等分词器预处理后再传入 LangChain