LangChain 应用开发(十六):RAG 检索增强生成与文档处理
发布时间:2026/9/4 6:59:54来源:尧图网络
目录一、为什么需要 Retrieval 模块1. 大模型的天然局限二、什么是 RAG三、RAG 的完整工作流程1. 流程图四、环境准备与 Document1. 什么是 Document2. Document 生命周期五、Document Loader 文档加载器1. Loader 的特性2. 经典格式加载TXT 文本加载PDF 格式加载3. 其他常见格式加载CSV 数据加载JSON 结构化数据加载Word 文档加载Markdown 文档加载HTML 网页加载4. 文件夹批量加载六、Text Splitter 文档切分1. 为什么不能对整篇文档做 Embedding2. chunk_size 与 chunk_overlap3. Chunk 的选择七、不同 Text Splitter 与切分策略1. CharacterTextSplitter2. RecursiveCharacterTextSplitter3. 语义分块4. 结构感知分块总结一、为什么需要 Retrieval 模块在探讨具体实现之前有必要先回答一个问题既然大模型已经具备了强大的理解与推理能力为什么我们还需要额外的 Retrieval检索模块回顾 Agent 体系的能力演进每一个模块都在解决大模型的不同短板Model依赖参数内部存储的通用知识进行推理Tools赋予 Agent 调用外部 API、执行代码与计算的能力Memory解决对话历史与跨会话上下文的状态保留Retrieval解决 Agent 如何从外部知识库中精准提取与当前问题相关的知识1. 大模型的天然局限1. 知识边界与静态冻结大模型的知识源于预训练阶段的公开数据。一旦训练完成其知识库便处于冻结状态大模型参数知识 │ ├── 公开知识 (历史、科学、通用语言规则) └── 历史数据 (截止到模型训练完成之日) × 公司的私有业务文档 × 昨天刚更新的接口规范 × 内部数据库中的实时数据2. 私有知识屏障当用户询问我们公司今年最新的差旅报销标准是多少这类涉及企业内部制度、私有项目代码或客户敏感数据的提问大模型由于从未学习过这些非公开信息单凭参数内部的知识无法直接回答3. 幻觉风险大模型的底层逻辑是基于概率预测下一个 Token 的文本接龙。当遇到超出其知识边界的问题时模型为了补全句子极其容易产生看似逻辑严密、实则完全捏造的伪答案。在财务、法律、医疗等严肃业务中这种胡说八道是不可接受的4. 上下文窗口与注意力稀释有人会提出质疑现在的 LLM 不是支持 100 万甚至更高的 Context Window 吗把整个 500 页的《员工手册》直接粘贴到 Prompt 里不就行了直接塞入全量文档会带来三个致命缺陷成本飙升单次 API 调用消耗数十万 Token极度昂贵延迟陡增处理超长 Prompt 会显著拉长首 Token 延迟影响交互体验信息淹没过长的无关背景会稀释模型的注意力导致隐藏在中间的关键信息被大模型忽略Retrieval 的核心定义针对上述瓶颈Retrieval检索模块给出的解题思路非常清晰不要试图把所有知识一次性全部塞给大模型而是在用户提问时只把与当前问题最相关的极少片段找出来提供给模型二、什么是 RAG基于 Retrieval 的核心思路RAG应运而生RAG 的本质并不是重新训练或微调一个大模型而是在模型生成答案之前增加一个 先查阅相关外部资料再结合资料进行推导 的置前步骤传统 LLM全凭脑子里的记忆回答问题记错了或者没学过就容易瞎编RAG 模式则遇到问题时先翻阅资料把提炼出来的相关段落摆在桌上再根据资料撰写答案RAG 的优缺点优势局限支持私有知识可直接使用企业内部文档与私有数据系统复杂度提高增加了文档解析、向量化与存储组件知识实时更新更新文档库即可生效无需重新训练模型存在检索延迟多了一步外部数据库的查询开销降低幻觉强迫模型基于检索到的上下文回答答案质量受制于检索结果若没查到对的片段模型仍会答错答案可追溯可附带引用的文档来源调优成本切分粒度与检索策略需要针对性调优知识与模型解耦无微调昂贵成本可自由切换模型底座文档预处理工程量大非结构化 PDF/Word 解析容易丢格式RAG并不是彻底消除幻觉而是通过给模型提供高可靠性的外部上下文将模型的回答收敛在给定资料的范围内从而极大降低产生幻觉的概率三、RAG 的完整工作流程一个端到端的 RAG 系统并非单点工具而是一个典型的双阶段流水线Pipeline。它解耦为离线索引与在线检索生成1. 流程图两个阶段的核心职责1. 离线索引阶段该阶段的目标是将文档转化为向量存储库为后续快速检索打下基础文档加载将各种不同格式的原始文档PDF、Markdown、Word、数据库表格清洗并统一读取为标准格式文档切分将动辄数万字的庞大文档按特定策略切碎为语义相对独立的小片段Chunks向量化调用 Embedding 模型将每一个 Chunk 的文本转化为包含深层语义信息的向量向量存储将 [文本片段 向量特征 元数据] 建立索引并写入向量数据库中2. 在线检索与生成阶段当真实用户发起提问时实时流水线随之触发问题向量化将用户的自然语言提问使用相同的 Embedding 模型转化为向量相似度检索在向量数据库中计算余弦相似度等指标筛选出与问题语义最贴近的 Top-K 个文本 ChunkPrompt 组装将原始问题与检索出的 Top-K Chunk 拼接填充到 RAG 模板中构成上下文增强 Prompt大模型生成大模型根据输入的高相关上下文回答用户问题并给出参考依据本篇重点 了解了 RAG 的整体脉络后本篇博客将重点攻克 Indexing Pipeline 的前半程——文档加载与文本切分的细节四、环境准备与 Document在动手编写 RAG 之前我们需要先准备基础依赖并理解 LangChain 处理文档的核心数据结构——Document环境准备安装 LangChain 核心库以及处理常用文档的社区扩展包pip install langchain langchain-community langchain-core1. 什么是 Document无论你加载的是 PDF、Word、HTML 还是 MarkdownLangChain 在完成解析后统一返回的都是Document对象Document 是 LangChain 中贯穿整个 RAG 接入层与检索层的核心基类它主要由两个属性组成class Document: page_content: str # 文档的正文文本 metadata: dict # 描述该文档元数据的字典其结构定义如下Document │ ├── page_content (str) │ └── 这里是实际提取出来的正文文本内容... │ └── metadata (dict) ├── source: 2026_annual_report.pdf (来源文件名) ├── page: 12 (页码) ├── title: 财务报表摘要 (标题)① page_content存储从原始数据源解析出来的纯文本内容。后续的文本切分、向量化以及投喂给大模型的上下文都直接基于此字段② metadata以 Key-Value 字典形式存储与该段文本相关的上下文元数据如来源路径、页码、创建时间等在后续的 RAG 优化中metadata 扮演着极其关键的角色它允许我们在向量检索时进行元数据过滤。例如指定 只在 sourcexxx.pdf 且 year2026 的文档范围内进行相似度检索从而大幅收敛搜索范围、提升检索精准度2. Document 生命周期理解了 Document就抓住了 LangChain RAG 模块的数据主线。在整个 RAG 流程中Document 会像流水线上的零件一样被加工与传递Loader (加载器) ──► 解析生成全量 [Document] │ Splitter (切分器) ─► 切割为微型 [Document] (保持 metadata 继承) │ VectorStore ──────► 索引落盘 [Document] (向量 page_content metadata) │ Retriever (检索器) ─► 查出匹配的 Top-K [Document] 并交给 LLM五、Document Loader 文档加载器在现实的企业场景中私有知识可能散落在各种格式的文件中财务报表是PDF、项目需求是Word、技术规范是Markdown、数据导出是CSV网页抓取是HTMLDocument Loader的核心职责就是屏蔽异构数据源的格式差异统一将其解析并转换为 LangChain 的 Document对象结构1. Loader 的特性无论使用何种 Loader调用 .load() 方法后返回的统一都是 List[Document] 列表docs loader.load() # 类型: List[Document]文件 ≠ Document在实际解析中Loader 往往会根据文件格式的特性将其拆分为多个 Document 对象PDF Loader通常以 页 为单位解析一份 10 页的 PDF 加载后会生成 10 个 Document 对象metadata 中会自动附带 {page: 0} 等页码信息CSV Loader通常以 行 为单位解析一行数据对应生成一个 Document 对象TXT Loader通常将全量文件直接解析为一个完整的 Document 对象2. 经典格式加载TXT 文本加载最简单的加载方式将纯文本文件的内容全量读取至一个 Document 中from langchain_community.document_loaders import TextLoader # 1. 实例化 TextLoader建议明确指定字符编码 loader TextLoader(./knowledge_base/company_intro.txt, encodingutf-8) # 2. 执行加载 docs loader.load() # 3. 检查解析结果 print(f解析得到的 Document 数量: {len(docs)}) print(正文前 50 字:, docs[0].page_content[:50]) print(元数据:, docs[0].metadata) # 输出元数据: {source: ./knowledge_base/company_intro.txt}PDF 格式加载处理 PDF 最常用的工具是基于 pypdf 的 PyPDFLoader它会自动按页生成 Document 列表并提取页码元数据# 需预先安装依赖: pip install pypdf from langchain_community.document_loaders import PyPDFLoader # 1. 初始化 PDF 加载器 loader PyPDFLoader(./knowledge_base/annual_report_2026.pdf) # 2. 加载文档按页拆分 pages loader.load() # 3. 验证分页结果 print(fPDF 总页数: {len(pages)}) # 查看第一页Index 0的内容与元数据 first_page pages[0] print(f第一页内容片段: {first_page.page_content[:100]}) print(f第一页元数据: {first_page.metadata}) # 输出元数据: {source: ./annual_report_2026.pdf, page: 0}3. 其他常见格式加载为了避免代码冗余其他文件格式的调用模式与上面保持高度一致主要区别在于引入对应的 Loader 扩展库CSV 数据加载以行为单位加载表格可将每行的列名与对应值自动拼装为 Key-Value 文本格式# 需预先安装依赖: pip install pandas (或内置 csv) from langchain_community.document_loaders import CSVLoader # 可通过 source_column 参数指定将哪一列设为 metadata 的来源标识 loader CSVLoader(file_path./data/products.csv, source_columnproduct_name) docs loader.load() # 每一行记录转化为一个 DocumentJSON 结构化数据加载对于结构化 JSON可通过 jq 表达式灵活提取指定的字段作为 page_content# 需预先安装依赖: pip install jq from langchain_community.document_loaders import JSONLoader # 提取 JSON 数组中每个对象的 text 字段作为正文 loader JSONLoader( file_path./data/user_feedback.json, jq_schema.messages[].text, text_contentTrue ) docs loader.load()Word 文档加载提取 Word 文档中的段落文字# 需预先安装依赖: pip install docx2txt from langchain_community.document_loaders import Docx2txtLoader loader Docx2txtLoader(./docs/project_spec.docx) docs loader.load()Markdown 文档加载直接加载 Markdown 格式文件from langchain_community.document_loaders import UnstructuredMarkdownLoader loader UnstructuredMarkdownLoader(./docs/README.md) docs loader.load()HTML 网页加载解析 HTML 页面自动过滤 HTML 标签与样式仅提取纯文本# 需预先安装依赖: pip install beautifulsoup4 from langchain_community.document_loaders import BSHTMLLoader loader BSHTMLLoader(./webpages/about_us.html) docs loader.load()4. 文件夹批量加载在实际工程项目中知识库往往是一个包含上百个文件的文件夹。使用DirectoryLoader可以实现文件夹的多文件递归匹配与批量加载from langchain_community.document_loaders import DirectoryLoader, TextLoader # 批量加载 knowledge_base 目录下所有的 .txt 文本 loader DirectoryLoader( path./knowledge_base, glob**/*.txt, # 通配符匹配规则包含子目录 loader_clsTextLoader, # 指定底层对单个文件生效的 Loader 类型 loader_kwargs{encoding: utf-8} ) # 获取目录下所有文件解析后的全量 Document 列表 all_docs loader.load() print(f成功批量加载 {len(all_docs)} 个文档)六、Text Splitter 文档切分在成功将各种格式的文档加载为 Document 之后我们不能直接把这些文档扔给 Embedding 模型去生成向量文本切分是 RAG 中最核心、对后续检索准确率影响最大的工程环节1. 为什么不能对整篇文档做 Embedding假设我们使用 PyPDFLoader 加载了一本 300 页的企业员工手册并将其全量内容作为一个巨大的 Document 进行向量化300 页 PDF ──► 1 个庞大 Document ──► 转化为 1 个高维向量 ──► 检索粒度极其粗糙当用户提问“公司的年假规定是什么需要提前几天申请”如果直接匹配这个庞大文档的向量语义稀释年假规定只占这 300 页里的两段话。在全量向量中年假信息的语义已经被财务制度、考勤规范、绩效考核等海量无关内容淹没检索精准度极差数据库无法定位到具体哪一页、哪一段包含了答案因此我们需要使用Text Splitter对庞大文档进行切碎处理庞大 Document ──► Text Splitter ──► Chunk 1, Chunk 2, ... Chunk 37 (恰好是年假规定) │ ▼ 精准匹配 Top-K 检索单元在 RAG 体系中Chunk 是知识检索与投喂给大模型的基本单元。切分粒度的好坏直接决定了 RAG 系统的性能上限2. chunk_size 与 chunk_overlap在 LangChain 提供的所有文本切分器中最基础且最关键的两个控制参数是chunk_size块大小和chunk_overlap块重叠为了直观理解这两个参数的作用假设有一段连续的原始文本原始文本 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z① 无重叠切分chunk_size 10, chunk_overlap 0单纯按照固定长度切断隐患如果一个关键词或重要句式正好跨越了 J 和 K例如 不 和 可以 分别落在 Chunk 1 和 Chunk 2 的边缘句子的完整语义会被硬生生砍断导致两边的 Chunk 包含的都是不完整的破碎信息② 带重叠切分chunk_size 10, chunk_overlap 3为了防止边界处的语义断裂我们在相邻的 Chunk 之间保留一部分共同的滑动窗口通过引入 chunk_overlap相邻两个片段共享了边缘的上下文。即便切分点刚好落在关键句子中间该句子的上下文依然能够在其中一个 Chunk 中保持完整3. Chunk 的选择在实际项目中并没有一个万能的数值。选择 chunk_size 本质上是在上下文完整度与检索精准度之间做选择Chunk 尺寸过大Chunk 尺寸过小优点上下文完整不易断句检索非常精准噪音少缺点包含太多无关废话稀释向量语义上下文碎片化容易丢失前因后果Chunk 过大例如 2000 Tokens缺点检索粒度粗查出来的片段里绝大部分都是无关废话消耗大量 LLM 上下文窗口导致成本激增、耗时变长Chunk 过小例如 50 Tokens缺点语义严重碎片化。例如 根据上文规定此条款不适用 被单独切出来如果没有上一段的背景这个 Chunk 对大模型来说完全是不可理解的废料七、不同 Text Splitter 与切分策略了解了切分原理后我们来看 LangChain 提供的具体切分器以及在实践中如何做出正确的选型1. CharacterTextSplitterCharacterTextSplitter 是最直观的字符切分器。它的核心思想是按照指定的单一分隔符字符切割文本# 1.导入相关依赖 from langchain_text_splitters import CharacterTextSplitter # 2.示例文本 text LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象使开发者 能够更容易地构建复杂的应用程序。 splitter CharacterTextSplitter( chunk_size30, chunk_overlap3, separator, ) chunks splitter.split_text(text) for i, chunk in enumerate(chunks): print(f块 {i 1}:长度{len(chunk)}) print(chunk) print(* * 30)输出结果优点逻辑简单、计算开销小缺点缺乏对文本结构的通用适应能力。如果文档中某些段落很长且缺乏连续的分隔符CharacterTextSplitter 可能会因为找不到分隔符而强制切断文本或者产生体积远超 chunk_size 的巨型 Chunk2. RecursiveCharacterTextSplitter为了解决单一分隔符容易失效的问题LangChain 提出了RecursiveCharacterTextSplitter递归字符切分器。它也是官方推荐处理通用文本的默认首选工具它的核心思想在于优先尽量保持较大的自然文本结构只有当前片段仍然过大时才递归使用更细粒度的分隔方式默认的分隔符优先级列表RecursiveCharacterTextSplitter 内部维护了一个按语义粒度由大到小排序的分隔符数组separators [\n\n, \n, , ]假设我们有一篇包含多个段落与句子的文档切分逻辑如下这种层层下放的递归设计最大限度地保留了自然语言的完整段落与句子结构# 1.导入相关依赖 from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter # 2.定义RecursiveCharacterTextSplitter分割器对象 text_splitter RecursiveCharacterTextSplitter( chunk_size20, chunk_overlap0, add_start_indexTrue, ) # 3.定义拆分的内容 textLangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例需要处理PDF/Word等格式。 # 4.拆分器分割 paragraphs text_splitter.split_text(text) for i,chunk in enumerate(paragraphs): print(f块{i 1},长度{len(chunk)}) print(chunk) print(- * 50)输出结果3. 语义分块无论是按字符还是递归切分本质上都是基于 文本长度 和 固定符号 的机械切分。机械切分无法理解上下文语义的变化例如一段文本前 5 句话在探讨 LangChain 的架构第 6 句话突然转折开始介绍向量数据库。机械切分器可能会把这两部分强行切在同一个 Chunk 里语义分块Semantic Chunking改变了切分思路基于语义的变化幅度来决定切分位置工作机制首先将文本按基础句子如句号打散计算相邻句子之间的 Embedding 向量相似度/距离当相邻两句之间的语义距离高于设定的阈值时判定此处发生了话题转移并在该处放置切分点# 需预先安装依赖: pip install langchain-experimental from langchain_experimental.text_splitter import SemanticChunker from langchain_openai import OpenAIEmbeddings # 基于 Embedding 计算语义边界 embeddings OpenAIEmbeddings() semantic_splitter SemanticChunker( embeddings, breakpoint_threshold_typepercentile # 根据语义距离的分位数决定切分点 ) semantic_chunks semantic_splitter.split_documents(docs)优点切分出来的每一个 Chunk 在语义主题上高度内聚缺点切分阶段需要对所有句子批量调用 Embedding 模型耗时较长且会产生额外的 API Token 开销4. 结构感知分块并非所有文档都是纯文本。对于 HTML 网页、Markdown 文档或源代码丢弃格式标签进行纯字符切分会破坏原本天然存在的树状逻辑层级结构感知分块强调利用文档自带的标记符如 h1~h3或 Markdown 的 #~###进行结构化解构以 HTMLHeaderTextSplitter 为例h1LangChain 快速入门/h1 h21. Model 模块/h2 pModel 是大模型的核心封装.../p h22. Agent 模块/h2 pAgent 具备自主执行能力.../p按 HTML 标题标签切分后不仅能够精准剥离各章节还能把层级标题自动注入到切分出的每个 Chunk 的 metadata 中from langchain_text_splitters import HTMLHeaderTextSplitter html html body h1Python 教程/h1 h2基础语法/h2 pPython 是一种简单易学的编程语言。/p p它支持变量、函数、类等基本语法。/p h2数据分析/h2 pPython 可以使用 NumPy 和 Pandas 进行数据分析。/p h3Pandas/h3 pPandas 主要用于处理表格数据。/p h3NumPy/h3 pNumPy 主要用于高性能数值计算。/p /body /html # 指定哪些标题参与切分以及保存到 metadata 时叫什么 headers_to_split_on [ (h1, 一级标题), (h2, 二级标题), (h3, 三级标题), ] splitter HTMLHeaderTextSplitter( headers_to_split_onheaders_to_split_on print(html_header_splits[0].metadata) ) documents splitter.split_text(html)保留这种层级元数据后检索阶段不仅能匹配正文还能匹配到属于哪个大标题极大地增强了召回的精度各切分策略对比切分策略切分依据特点适用场景CharacterTextSplitter固定字符 / 单一分隔符简单直接格式高度规范且统一的简短文本RecursiveCharacterTextSplitter多级层级分隔符递归处理尽量保持自然段落结构通用文本默认首选HTML / Markdown Splitter文档结构标签保留文档层级与逻辑关系网页 HTML、Markdown 文档、项目代码Semantic Chunking上下文句子之间的语义距离按话题/语义变化切分语义完整度最高逻辑复杂、无明显格式分隔的深度知识文档总结本章正式进入RAG检索增强生成从大模型存在的知识边界、私有数据无法直接访问以及幻觉等问题出发理解了 Retrieval 模块与 RAG 的设计意义并梳理了 RAG 从知识库构建到检索生成的完整工作流程随后我们重点学习了 RAG 数据预处理阶段的两个核心环节Document Loader 与 Text Splitter。通过文档加载器可以将 PDF、HTML、TXT 等不同数据源统一转换为 Document通过文档切分则可以将大型文档进一步处理为适合检索的 Chunk最后我们学习了 CharacterTextSplitter、RecursiveCharacterTextSplitter、语义分块以及 HTMLHeaderTextSplitter 等不同切分策略并理解了一个重要原则文档切分的目的不是单纯把文本切小而是在合适的检索粒度下尽可能保留完整的语义和文档结构下一篇将继续进入Embedding 向量模型与 Vector Store 向量存储看看这些 Chunk 如何被转换成向量以及系统如何根据语义相似度找到与用户问题最相关的知识
网站建设高端定制企业官网