RAG 实战:数据处理没做好,再强的模型也是“巧妇难为无米之炊“

张开发
2026/4/10 10:54:16 15 分钟阅读

分享文章

RAG 实战:数据处理没做好,再强的模型也是“巧妇难为无米之炊“
导读 上一篇我们搭好了 RAG 的骨架但很多人跑起来效果稀烂——问题往往不在模型在数据。本文带你拿下 RAG 数据处理最核心的两关文档加载 和 文本分块。含 4 种分块策略详解 可直接运行的代码含动图讲解。一、“垃圾进垃圾出”——被忽视的数据处理有一个铁律在数据领域流传已久放到 RAG 里同样成立“Garbage In, Garbage Out”垃圾进垃圾出很多人搭完 RAG 系统发现回答质量差第一反应是换更大的模型、调整 Prompt、优化检索策略……但往往忽视了一个最根本的问题喂给 AI 的食材本身是不是好的RAG 的数据处理流程分为两大关卡1. 文档加载把 PDF、Word、网页等原材料变成程序能读的格式2. 文本分块把整篇文档切成合适大小的食材块这两步做不好后面的向量化、检索、生成全是白费功夫。今天我们就来把这两关彻底讲透。二、第一关文档加载2.1 加载器做了什么文档加载器Document Loader是 RAG 数据管道的入口负责完成三件事任务说明示例解析格式把 PDF/Word/HTML 里的内容提取为纯文本去掉 PDF 的排版代码只留文字抽取元数据同步记录来源、页码、作者等信息{source: 手册.pdf, page: 3}统一结构输出标准的Document对象Document(page_content..., metadata{...}) 一句话理解加载器就像一个翻译官把五花八门的文档格式统统翻译成 RAG 流水线能看懂的通用语言。文档加载流程动图动图PDF / Word / HTML / CSV 经过各自的加载器统一输出为 Document 对象2.2 主流加载器速查表LangChain 提供了覆盖几乎所有格式的加载器以下是最常用的格式加载器类安装依赖适用场景纯文本.txtTextLoader无需额外安装日志、纯文本语料PDFPyPDFLoaderpip install pypdf报告、论文、手册Word.docxUnstructuredWordDocumentLoaderpip install python-docx企业内部文档网页 HTMLWebBaseLoaderpip install bs4爬取的网页内容CSVCSVLoader无需额外安装结构化表格数据MarkdownUnstructuredMarkdownLoaderpip install unstructured技术文档、WikiJSONJSONLoaderpip install jqAPI 返回数据YouTubeYoutubeLoaderpip install youtube-transcript-api视频字幕2.3 加载器代码示例# 步骤1根据文件格式选择对应加载器 from langchain_community.document_loaders import ( TextLoader, PyPDFLoader, WebBaseLoader, CSVLoader, ) # 加载纯文本 text_loader TextLoader(data/knowledge.txt, encodingutf-8) text_docs text_loader.load() # 加载 PDF自动按页分割 pdf_loader PyPDFLoader(data/manual.pdf) pdf_docs pdf_loader.load() # 加载网页 web_loader WebBaseLoader(https://example.com/docs) web_docs web_loader.load() # 步骤2查看加载结果 doc text_docs[0] print(f文本长度{len(doc.page_content)} 字符) print(f元数据{doc.metadata}) # 输出 # 文本长度12384 字符 # 元数据{source: data/knowledge.txt}加载完成后所有文档都变成了统一的Document对象可以直接送入下一环节——文本分块。三、第二关文本分块3.1 为什么要分块拿到一整篇文档后能不能直接扔给 AI不能。 原因有两个硬限制限制一Embedding 模型的输入上限把文本变成向量的嵌入模型Embedding Model有严格的 token 上限。比如常用的bge-base-zh-v1.5上限是 512 个 token。超出部分会被直接截断损失信息。限制二LLM 的上下文窗口检索到的文档片段最终要塞进 LLM 的 Prompt而 LLM 的上下文窗口虽然比 Embedding 模型大得多但也有上限。块太大能放进去的参考内容就少了。 类比就像你去图书馆查资料把整本书都复印过来太费纸你只需要复印最相关的那几页。分块就是事先把每一页切好备用。3.2 块不是越大越好“那我把块切得接近上限尽量大不就能包含更多信息吗”很遗憾块越大效果往往越差。原因有三原因一向量被稀释了嵌入模型把一段文本压缩成一个向量通常 768 维来表示语义。文本越长包含的主题越多这个向量就得同时表达所有含义结果谁也没表达清楚——就像用一张照片同时记录 100 个地方每个地方都模糊不清。原因二大海捞针效应Lost in the Middle研究表明当 LLM 处理很长的上下文时会更关注开头和结尾中间的内容容易被忽视。塞进去一大块文字关键信息可能就淹没在中间了。原因三主题被稀释检索失败举个栗子 假设有一份《企业员工手册》包含入职流程、年假制度、报销规定三个部分。❌ 糟糕的做法把三部分合成一个大块。员工问我能休几天年假这个大块因为同时掺杂了入职、年假、报销三个主题向量语义被稀释相关性得分很低可能根本召不回来。✅ 正确的做法三部分分成三个独立的小块。年假制度那个块与问题高度相关精准命中。分块策略对比动图动图同一篇文档大块 vs 小块检索时的召回效果对比四、四种分块策略详解4.1 固定大小分块CharacterTextSplitter这是最简单的方法按字符数切割超过chunk_size就开一个新块。# 固定大小分块 from langchain.text_splitter import CharacterTextSplitter from langchain_community.document_loaders import TextLoader loader TextLoader(data/蜂医.txt, encodingutf-8) docs loader.load() text_splitter CharacterTextSplitter( chunk_size200, # 每个块的目标大小字符数 chunk_overlap20 # 相邻块的重叠字符数避免语义断裂 ) chunks text_splitter.split_documents(docs) print(f文档被切分为 {len(chunks)} 个块) for i, chunk inenumerate(chunks[:3]): print(f/n--- 块 {i1}长度 {len(chunk.page_content)}---) print(chunk.page_content[:80] ...)运行结果文档被切分为 47 个块 --- 块 1长度 198--- 第一章 蜂医的传说 在深山老林里有一位以蜂疗闻名的老大夫人称蜂医... --- 块 2长度 203--- 疗闻名的老大夫人称蜂医。他用蜂针、蜂蜜、蜂毒治愈了无数疑难杂症...注意看块 1 的结尾和块 2 的开头——有 20 个字符的重叠这就是chunk_overlap的作用防止一句话被切断后语义丢失。⚠️ 注意LangChain 的CharacterTextSplitter实际上是段落感知的自适应分块——它优先在/n/n段落符处切割只有段落本身超过chunk_size才强制截断。所以块的实际大小会根据段落边界动态浮动。适用场景日志文件、结构松散的文本、对速度要求高的场景。4.2 递归字符分块RecursiveCharacterTextSplitter⭐ 推荐这是 RAG 实践中最常用的分块方式也是上一篇代码中用到的方法。它的核心思想是分隔符有优先级大的切不了就换小的。优先级段落/n/n 换行/n 句号。 逗号 空格 单字符递归分块过程动图动图递归字符分块的层级切割过程——优先按段落段落太大就按句子句子太大再往下# 递归字符分块中文优化版 from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( separators[ /n/n, # 段落最高优先级 /n, # 换行 。, # 中文句号 , , , # 逗号最后才用 , # 空格和单字符兜底 ], chunk_size200, chunk_overlap20, ) chunks text_splitter.split_documents(docs) print(f切分为 {len(chunks)} 个块)与固定大小分块的核心区别对比项固定大小分块递归字符分块切割方式固定按字符数截断按分隔符层级递归切割语义完整性可能截断句子尽量在句子/段落边界切割超长段落处理发出警告强制保留继续用更细粒度分隔符切割推荐程度一般场景可用⭐ 大多数场景首选4.3 语义分块SemanticChunker前两种方法都是基于字符/符号的机械切割不管内容说的是什么。语义分块则更智能——它看的是内容本身的语义是否发生了转变。 类比就像一个聪明的编辑读完整篇文章后在话题切换的地方自然地划一道线——不是数字符而是真正理解了内容。工作原理5步1. 把文档拆成一个个句子2. 用 Embedding 模型对每个句子及其前后邻句生成向量3. 计算相邻句子之间的语义距离余弦距离4. 找出距离突然变大的位置——这就是话题断点5. 在断点处切割合并同一主题的句子为一个块语义分块插图# 语义分块 from langchain_experimental.text_splitter import SemanticChunker from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.document_loaders import TextLoader # 加载本地 Embedding 模型 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) # 初始化语义分块器 text_splitter SemanticChunker( embeddings, breakpoint_threshold_typepercentile, # 断点识别方法 breakpoint_threshold_amount95 # 只有最显著的 5% 差异才算断点 ) loader TextLoader(data/蜂医.txt, encodingutf-8) documents loader.load() chunks text_splitter.split_documents(documents) print(f语义分块结果{len(chunks)} 个块) for i, chunk inenumerate(chunks[:3]): print(f/n--- 语义块 {i1}长度 {len(chunk.page_content)}---) print(chunk.page_content[:100])四种断点识别方法对比方法逻辑适用场景percentile默认超过第 N 百分位的差异才算断点通用场景推荐默认standard_deviation超过均值 N 倍标准差主题跳跃明显的文档interquartile基于四分位距的异常值检测内容分布均匀的文档gradient差异值变化率的百分位法法律、医疗等语义紧密文档⚠️ 注意语义分块需要调用 Embedding 模型速度比字符分块慢很多适合对质量要求极高、离线处理的场景。实时入库不推荐。4.4 结构感知分块MarkdownHeaderTextSplitter对于有明确结构的文档Markdown、HTML、LaTex最优雅的切法是按文档结构切。 类比按目录章节切书每一块天然带着第几章第几节的标签AI 检索时不仅知道内容还知道来源。# Markdown 结构分块 from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter markdown_text # 鲁班七号攻略 ## 技能介绍 鲁班七号拥有三个主动技能…… ## 推荐出装 核心装备无尽之刃、影刃…… 出装思路优先提升暴击率…… ## 英雄故事 在遥远的机械王国有一个天才工程师…… # 第一步按 Markdown 标题结构切割 headers_to_split_on [ (#, H1), (##, H2), ] header_splitter MarkdownHeaderTextSplitter( headers_to_split_onheaders_to_split_on ) header_chunks header_splitter.split_text(markdown_text) print( 第一步按标题切割结果 ) for chunk in header_chunks: print(f内容: {chunk.page_content[:40]}...) print(f元数据: {chunk.metadata}/n) # 输出 # 内容: 鲁班七号拥有三个主动技能…… # 元数据: {H1: 鲁班七号攻略, H2: 技能介绍} # 第二步如果某个章节内容过长再用递归分块细切 recursive_splitter RecursiveCharacterTextSplitter( chunk_size200, chunk_overlap20 ) final_chunks recursive_splitter.split_documents(header_chunks) # 最终每个小块都继承了来自第一步的标题元数据 print(f最终分块数: {len(final_chunks)})这种两阶段分块的优势每个小块都携带完整的地址元数据来自哪个章节LLM 不仅能看到内容还能知道这段话属于哪个章节是处理技术文档、产品手册的最佳实践五、四种策略怎么选分块策略选型象限图一句话选型建议文档类型推荐策略普通文章、小说、报告⭐ 递归字符分块首选Markdown / HTML 技术文档结构感知分块两阶段对质量要求极高、离线处理语义分块日志、结构极松散的文本固定大小分块六、进阶其他框架的分块思路Unstructured先理解文档再切割Unstructured 的特别之处在于它会先把文档解析成一系列带语义标签的元素Title、NarrativeText、ListItem 等再基于这些元素进行分块——相当于先让 AI 读懂文档结构再智能组合。对 PDF 扫描件、版式复杂的报告效果远优于普通方法。LlamaIndex面向节点的处理流LlamaIndex 把分块抽象为对节点Node的转换。它有一个特别有趣的方法——SentenceWindowNodeParser把文档切成单个句子但每个句子节点的元数据里保存了它前后 N 个句子的内容“窗口”。检索时用单句做精准匹配送给 LLM 时用带上下文的完整窗口——检索精度和生成质量两不误。七、总结总结图今天我们拿下了 RAG 数据处理的两大关卡文档加载选对加载器把各种格式统一成Document对象元数据完整保留文本分块理解三大原则满足 token 限制、避免稀释、保持语义聚焦按场景选对策略记住这个选型口诀普通文档用递归有结构用标题切极致质量用语义简单场景固定切。下一篇我们进入 RAG 的核心环节——向量化与向量数据库。同样一批分好的文本块用什么 Embedding 模型、存入什么向量库对最终的检索效果有多大影响答案会让你有点意外。这里给大家精心整理了一份全面的AI大模型学习资源包括AI大模型全套学习路线图从入门到实战、精品AI大模型学习书籍手册、视频教程、实战学习、面试题等资料免费分享扫码免费领取全部内容1. 成长路线图学习规划要学习一门新的技术作为新手一定要先学习成长路线图方向不对努力白费。这里我们为新手和想要进一步提升的专业人士准备了一份详细的学习成长路线图和规划。可以说是最科学最系统的学习成长路线。2. 大模型经典PDF书籍书籍和学习文档资料是学习大模型过程中必不可少的我们精选了一系列深入探讨大模型技术的书籍和学习文档它们由领域内的顶尖专家撰写内容全面、深入、详尽为你学习大模型提供坚实的理论基础。书籍含电子版PDF3. 大模型视频教程对于很多自学或者没有基础的同学来说书籍这些纯文字类的学习教材会觉得比较晦涩难以理解因此我们提供了丰富的大模型视频教程以动态、形象的方式展示技术概念帮助你更快、更轻松地掌握核心知识。4. 2026行业报告行业分析主要包括对不同行业的现状、趋势、问题、机会等进行系统地调研和评估以了解哪些行业更适合引入大模型的技术和应用以及在哪些方面可以发挥大模型的优势。5. 大模型项目实战学以致用当你的理论知识积累到一定程度就需要通过项目实战在实际操作中检验和巩固你所学到的知识同时为你找工作和职业发展打下坚实的基础。6. 大模型面试题面试不仅是技术的较量更需要充分的准备。在你已经掌握了大模型技术之后就需要开始准备面试我们将提供精心整理的大模型面试题库涵盖当前面试中可能遇到的各种技术问题让你在面试中游刃有余。7. 资料领取全套内容免费抱走学 AI 不用再找第二份不管你是 0 基础想入门 AI 大模型还是有基础想冲刺大厂、了解行业趋势这份资料都能满足你现在只需按照提示操作就能免费领取扫码免费领取全部内容

更多文章