第 3 章:文档预处理与切分
能在本机用 30 行代码切完一份 PDF,并讲清切分如何决定一个 RAG 系统的天花板
📊 学习时长:60-75 分钟 🎯 完成后能力:能在本机用 30 行代码切完一份 PDF,并讲清切分如何决定一个 RAG 系统的天花板 🔗 关联面试题:5 道(覆盖 5 个不同角度,见 Part 3)
这一章你会学到什么
- ✓ 零基础也能跟着做:用 langchain 跑通你的第一份文档切分
- ✓ 看懂 3 个核心概念:什么是切分、什么是召回率、什么是 top-k
- ✓ 亲手重现一个切分失败演练,理解"切分为什么是地基"
- ✓ 看懂基础方案可能遇到哪些坑,并学会用自己的评估集比较方案
- ✓ 学会用 QA 评估集量化切分质量,而不是靠感觉
本章按三段式组织,不同基础的读者各取所需:
段落 给谁看 内容 占比 🚀 Part 1 · 主线实战 零基础,想动手做出来 概念白话 + 跟着敲的代码 ~50% 🎯 Part 2 · 面试深度 想讲出深度、备战面试 失败模式 + 测量方法 + 取舍 ~40% 🏆 Part 3 · 验收串题 检验自己学到位没 关联面试题 + 自检清单 ~10% 只想先做出东西?读完 Part 1 就够动手了。想讲透?继续 Part 2。
🚀 Part 1 · 主线实战 | ~50% 这部分零基础也能跟着做。读完你能在本机用 30 行代码切完一份 PDF,并亲手重现一个教学用的失败场景。
在开始之前
你需要:
- ✅ 装好 Python(3.9+)
- ✅ 能
pip install - ✅ 能看懂基础 Python(变量、函数、for 循环)
- ✅ 一份测试 PDF,用你手头任意一份 PDF 都行(保险条款、产品说明书、论文都可以)
完全没接触过 RAG?→ 回看第 1 章:为什么做 RAG?
先做一个失败演练(这是本章的"为什么")
下面的保险条款、用户问题和后果都是为讲解切分风险而构造的教学场景,不是作者或学员的线上事故。
有一条保险条款是这么写的:"本保险承保意外伤害导致的身故或残疾,但以下情况除外:(1)战争 (2)核辐射……"
演练先使用最朴素的"固定长度切分":不管内容,每到长度上限就切一刀。结果这句话正好被劈成两半:"本保险承保……身故或残疾,但以下情况除外:"进了一个块,"战争、核辐射……"进了另一个块。
假设用户问:"核辐射在保障范围内吗?" 系统只找回前半块,那块读起来像"本保险承保"。模型如果没有拒答约束,就可能据此回答:"在保障范围内。"
这个演练要说明的不是某次真实损失,而是因果链:一句话被切断 → 检索只返回残缺证据 → 生成阶段依据错误上下文作答。因此,切分策略是 RAG 系统的隐形地基;上层模型无法补回未被检索到的证据。
这个失败路径,用一张图就能看明白:
别急着记结论。本章后半会告诉你怎么避免它,但在那之前,我们先把最基础的东西跑起来。
3 个核心概念(先白话,再术语)
概念 1:什么是"切分"?
切分(chunking),英文 chunk 就是"块"的意思,说白了就是把一份大文档剪成多个小段,每段几百字。
为什么要剪?因为大模型一次能"读"的文字有上限(就像一口能吃的饭量有限),你不可能把一本 500 页的保险条款整本塞给它。所以要先剪成小块,用户提问时只挑最相关的几块送进去。
这里的"量"通常用 token 计:一个 token 你可以粗略理解成 1 个汉字或 0.5 个英文单词,512 token 大约对应 400–500 字中文。后面代码里的
chunk_size=512,就是"每块大约 512 token"的意思。
这步看着简单,却决定了整个 RAG 系统的天花板,后面你会反复看到,很多上层优化,其实都是在弥补"切分没切对"留下的窟窿。
概念 2:什么是"召回率"?
召回率(recall),你可以理解成**"该找回来的,到底有没有找回来"的比例**。
举个例子:
- 你准备 100 道用户问题,每道题"正确答案应该来自第几个 chunk",事先人工标好;
- 系统跑完检索,"成功找回正确 chunk"的题数 ÷ 100 = 召回率。
例如,同一批人工标注问题里,命中正确 chunk 的题数除以总题数,就是召回率。具体数值由你的评估集和检索结果计算,不要沿用教程示例作为项目结果。
召回率是评估 RAG 检索好不好最重要的指标,本章会反复出现这个词。
概念 3:什么是 "top-k" 检索?
top-k:每次检索返回最相似的 K 个 chunk。比如 top-3,就是返回相似度最高的 3 个块。
- 为什么不只返回 1 个?因为"最相似"不一定就是"正确答案",多取几个让模型综合判断,更稳;
- 为什么不返回 100 个?因为太多模型消化不过来,而且大量无关信息会干扰生成。
实务上,top-3 到 top-10 比较常用。
🛠️ 跟着做:10 分钟切完你的第一份文档
Step 1:装环境
pip install langchain langchain-community pypdf
langchain 是大模型应用最常用的开发框架,这里我们只用它的文档加载和切分两个功能,不用怕它体量大。
Step 2:准备测试 PDF
随便找一份 PDF 放到当前目录,改名 your_doc.pdf(任意保险条款、说明书、论文都行,几页到几十页都可以)。
Step 3:写第一段切分代码
新建 chunk_demo.py(完整文件也在 code-snippets/chunk_demo.py):
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 1. 读 PDF
loader = PyPDFLoader("your_doc.pdf")
docs = loader.load()
text = "\n".join(d.page_content for d in docs)
print(f"原文档总长度:{len(text)} 字")
# 2. 切分
splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 每个 chunk 大约 512 token(约 400–500 字中文,见上文)
chunk_overlap=50, # 相邻 chunk 重叠 50 字(防止切断关键信息)
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""],
)
chunks = splitter.split_text(text)
# 3. 看看切完的结果
print(f"切完共 {len(chunks)} 个 chunk\n")
print("第一个 chunk:")
print(chunks[0])
print("\n第二个 chunk(开头约 50 字和上一块重叠):")
print(chunks[1])
Step 4:运行
python chunk_demo.py
你应该看到类似这样的输出(具体数字取决于你的 PDF):
原文档总长度:35280 字
切完共 79 个 chunk
第一个 chunk:
第一章 总则
第一条 本保险合同……(后续约 510 字)
第二个 chunk(开头约 50 字和上一块重叠):
……承担保险责任。
第二条 投保人应当如实告知……(后续约 510 字)
🎉 你做出了第一份切分!
Step 5:动手实验,改 chunk_size
把 chunk_size=512 改成 1024,再跑一次,观察:
- chunk 数量大约减半;
- 每个 chunk 变长了:信息更全,但一个块里塞的主题多了,单次检索的精度可能下降。
再改成 128 跑一次,观察:
- chunk 数量翻好几倍;
- 每块很短,容易把一段完整的话切散。
这是你第一次直观感受到 chunk_size 的取舍:大了信息全但不精,小了精但易碎。Part 2 会讲这个值到底怎么选。
Step 6:Bonus,亲手重现"核辐射切分失败"
新建 reproduce-disaster.py(完整文件见 code-snippets/reproduce-disaster.py),设一个极小的 chunk_size:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text = "本保险承保意外伤害导致的身故或残疾,但以下情况除外:(1)战争 (2)核辐射"
# 极小的 chunk_size,并让它在冒号/句号处断开,演示开篇那个"从中间切断"的风险
splitter = RecursiveCharacterTextSplitter(
chunk_size=30, chunk_overlap=0, separators=[":", "。", ""]
)
chunks = splitter.split_text(text)
for i, c in enumerate(chunks):
print(f"Chunk {i}: {c}")
输出:
Chunk 0: 本保险承保意外伤害导致的身故或残疾,但以下情况除外
Chunk 1: :(1)战争 (2)核辐射
Chunk 0 只到"但以下情况除外"就断了:到底除外哪些(战争、核辐射)全留在了 Chunk 1。用户问"核辐射在不在保障范围",检索命中 Chunk 0,模型看不到"核辐射属于除外项",就可能回答"在"。你刚复现的是一个可运行的失败演练,不是生产事故记录。
🎉 你不只跑通了代码,还亲手重现了切分错误如何沿链路放大。
这一节你掌握了什么?
- ✅ chunk 是什么、为什么要切
- ✅ 召回率、top-k 是什么意思
- ✅ 用 langchain 跑通了第一份切分代码
- ✅ 看到了
chunk_size的取舍,还亲手复现了"切错了"的后果
跑通这 6 步,你已经掌握了切分的"概念骨架"。
但复杂文档没那么乖。PDF 里可能有图、表、印章和跨页公式,你刚才跑的 RecursiveCharacterTextSplitter 无法保留这些结构。
下面 Part 2,我们看基础方案会遇到哪些坑,以及怎样用同一套评估集比较不同切分策略。正文不预设提升幅度,结果以你自己的实验为准。
🎯 Part 2 · 面试深度 | ~40% 这部分讲常见失败模式、测量方法和面试官想听的取舍。Part 1 跑通了能让你做出 demo,Part 2 学完能让你讲清"为什么 demo 还不能直接用于生产"。
2.1 解析阶段的 4 大坑
在切分之前,还有一步更靠前的"解析",把 PDF/Word/PPT/扫描件变成干净、带结构的文本。Part 1 里 PyPDFLoader 那一行就是在做这件事,但它只是入门级的。真实文档会用四种方式坑你。
专业解析不是"读取文本",而是一条 pipeline:先搞清版面,再分类,再针对性处理:
坑 1:多栏排版被"按行拼接"搅烂
- 现象:一份双栏的理赔指南,左栏写"理赔流程",右栏写"需要的材料"。普通解析器按行从左读到右,变成"流程第 1 句 → 材料第 1 句 → 流程第 2 句 → 材料第 2 句……"彻底乱套。
- 后果:用户问"理赔要交哪些材料",检索回来的是流程和材料交错的块,模型分不清谁是谁。
- 应对:用带"版面分析"的专业工具(如 Deepdoc、MinerU),它们先搞清每个文字块在页面的位置,按版面逻辑结构解析,而不是按文本行顺序。
坑 2:无边框表格识别率暴跌
- 现象:靠空格对齐、没有边框线的表格,解析器很难分清行列。
- 后果:费率表的行列对应一乱,"30 岁男性的费率"可能被错配成别的数字,这种错比丢几个字严重得多。
- 应对:对无边框/半结构化表格,先按文档类型分别标注并测量。只有当普通方案在你的样本上暴露明显错误时,再比较更强的表格识别模型、版面分析或人工复核方案。
- 失败演练:准备一张无边框费率表,故意去掉表头或打乱单元格读取顺序,观察"年龄、性别、费率"如何错配。这里不预设金额或业务后果,重点是验证解析结果能否保持行列关系。
坑 3:印章/签名压在文字上
- 现象:合同的红章常盖在关键文字上,OCR(光学字符识别,把扫描件/图片里的文字提取出来变成可编辑文本)直接把那片糊成一团。
- 后果:被印章覆盖的恰恰常是金额、日期这类关键字段。
- 应对:先做遮挡检测,把印章层和文字层分离,再对文字层单独 OCR。
- 失败演练:准备一张数字被印章遮挡的合成页面,观察 OCR 是否漏字或误读。测试时保存原图、识别结果和人工标注,再比较去章、增强或人工复核策略;不要把演练数字写成客户损失。
坑 4:扫描件模糊
- 现象:低分辨率扫描件(尤其老合同),字迹发虚。
- 后果:OCR 错字可能污染知识库,实际错误率取决于扫描质量、版式与识别模型。
- 应对:上图像增强(去噪、锐化、超分),并按模糊程度分档处理;是否改善、改善多少,必须在同一批人工标注页面上测量。
业内行话:这里有个非常值钱的工程思想:分类器前置 + 动态调度。不是所有文档都用最贵最慢的模型,而是先判断"这页是不是无边框表格 / 是不是模糊扫描件",只对那一小撮难 case 上重型方案,其余走轻量快速通道。既保住准确率,又不让平均延迟爆掉。面试时讲这个,比"我全用最强模型"成熟得多。
2.2 切分策略的三代演进
开篇的核辐射失败演练可以用来比较三类切分方案。它们切出来的 chunk"形状"完全不同,但优劣必须在同一评估集上验证:
| 代次 | 做法 | 被推翻的原因 | 召回率 |
|---|---|---|---|
| V1 固定长度 | 每 512 token 一刀,overlap 50 | 句子/条款可能被切碎 | <填写实测> |
| V2 句子级 | 按句号切,累积到长度上限 | 句子完整,但可能丢失章节层级 | <填写实测> |
| V3 结构感知 | 按文档结构 + 语义完整性切 | 需要更复杂的解析与规则 | <填写实测> |
注:表格只给出比较维度,不提供作者、学员或客户项目的固定结果。请记录数据来源、样本量、标注规则和运行配置,再填写自己的实测值;不同业务的趋势也可能不同。
业内行话:面试官经常会追问"你切分怎么做的"。比起只报算法名,更有说服力的是讲清楚**"问题 → 假设 → 验证 → 推翻 → 再优化"**这条完整的迭代链路。
2.3 V3 结构感知切分的 5 个招式
V3 不是某个神奇算法,而是五个工程招式的组合。这里讲思路,完整可运行的实现都在配套 GitHub 仓库。
招式 1:识别文档层级
- 白话:让程序知道"这是第几章第几条",而不是把整篇文档当成一坨大文本。
- 为什么需要:有了层级,大表格切开能补回章节标签,用户问"第 3 条第 2 款"能精确定位。
- 做法:中文编号特别杂(
1.1.1/第一条/(一)/ 纯靠字号加粗,还经常跳号重号)。纯正则不够,实务是多策略融合:正则匹配常见编号 + 利用解析得到的样式(字号、加粗、缩进)+ 一个轻量分类器兜底。 - 怎么验证:先抽样标注一批文档标题,分别测纯正则和"正则 + 样式/分类器"的识别结果。是否需要分类器,应由你自己的错误分布决定,不能从示例直接推出。
招式 2:超长章节递归切分
- 白话:一章太长放不下,就先按小节切;小节还太长,再按段落切;段落还长,才按句子切。
- 为什么需要:在上下文预算允许时,优先保住更大的语义单元;超过预算再逐级拆分,并用评估集验证。
- 做法:一个递归函数,逐级降级(章 → 节 → 段 → 句),每级先判断是否超
max_size。
招式 3:表格单独处理、每块都带表头
- 白话:大表格按行切开后,每一小块都复制一份表头。
- 为什么需要:一个 50 行的费率表,只给模型第 30–40 行而不带表头,它根本不知道每列是什么,没法回答"30 岁男性的费率是多少"。
- 做法:小表格整体成块;大表格按语义分组或固定行数切,每块前面都拼上表头。
招式 4:智能 overlap
- 白话:相邻块之间留一点重叠,防止关键信息正好卡在边界被漏掉;但重叠要对齐到完整句子,别带半截话进来。
- 为什么需要:固定长度的 overlap 会在句子中间截断,带进来半句话反而是噪音。
- 做法:取上一块结尾的一段,裁到最近的句号再带过去。把 0、较小、中等、较大 overlap 作为候选范围,固定同一批文档、切分器、评估集、
top_k和检索/重排配置,同时记录 Recall@K、MRR、重复率、上下文 token、索引体积与延迟,再选择满足质量门槛且代价可接受的配置。不同语料与 chunk 大小可能得到不同结果,不能预设某个固定数值就是拐点。
招式 5:chunk 元数据
- 白话:每个 chunk 除了正文,还贴几个"标签"。
- 为什么需要:标签是真正拉开差距的地方。
- 做法:带上
section_path(章节路径,如第3条 > 3.2 > (1),用于答案溯源)、content_type(text/table/image)、is_key_clause(是不是责任/免责/费率这类关键条款,检索可加权)、prev/next_chunk_id(语义不完整时自动拉前后块补全)。 - 工程提示:在开篇演练里,如果 chunk 带有
is_key_clause标记,并保证免责条款完整成块,就能降低只召回半句证据的风险;仍需通过评估集验证。
2.4 怎么量化评估切分质量
业内行话:没有评估集的切分优化都是自嗨。
什么意思?你说"我换了语义切分,效果更好了":面试官一句"好多少?怎么量化的?"就把你问住了。
最实用的方法是构造 QA 评估集:
- 选代表性文档:按目标资料类型、难度与风险分层抽样;
- 人工写问题 + 标 ground-truth chunk(ground-truth chunk:你已经知道答案应该来自哪个 chunk,就像标准答案):题量由查询分布、风险与期望覆盖决定;
- 跑检索看召回率:用当前切分方案建库,对每个问题检索 top-k,看 ground-truth chunk 有没有被找到 → 算出召回率;
- 横向对比:换切分策略/参数,重复跑,看哪个方案召回率更高。
每种策略都应在同一评估集上运行并记录结果。正文只提供比较方法,不提供可直接写进简历的固定提升数字。
讲师建议:亲手构造一套与你项目资料和查询分布相符的评估集,记录样本来源、标注规则、基线、配置和运行结果。面试被问"切分你怎么优化的"时,可复查的证据比固定题数更有说服力。
🏆 Part 3 · 验收串题 | ~10% 学到这一步,做几道题验证一下,知道自己学到位没。
关联面试题(5 道,覆盖 5 个角度)
从预处理一路到评估,刻意避免全是"切分策略"换皮。
- 【预处理/清洗】 面向专业领域的 RAG,如何对原始文档做清洗和预处理以提升检索与生成质量?
- 【策略对性能的影响】 文本分块策略如何影响 RAG 系统的性能?常见分块方法及适用场景?
- 【粒度与重叠】 你是如何设计分块策略的?说明粒度选择、重叠处理以及向量库写入。
- 【上下文 vs 精度权衡】 分块策略如何设计才能平衡上下文完整性与检索精度?
- 【策略横向比较】 chunk 划分有哪些常见方法?固定长度 / 语义边界 / 滑动窗口等如何取舍?
学完这一章你应该能干嘛
跟着做(Part 1):
- 能用 30 行 Python 跑通基础切分,看到 chunk 输出
- 能改
chunk_size看到 chunk 数量和长度的变化 - 能复现开篇的"核辐射条款切分失败"
讲深度(Part 2):
- 能用核辐射失败演练讲清"切分为什么是地基",并明确它不是个人事故经历
- 能说出解析阶段至少 3 个真实的坑及应对
- 能讲清切分三代演进、每一代为什么被推翻
- 能讲清结构感知切分的 5 个招式
- 能讲清怎么用 QA 评估集量化切分质量
5 项以下 → 回去 Part 1 重做;5–7 项 → Part 2 再看一遍;8 项以上 → 进入下一章。
完整代码 + 数据集
本章所有代码 + 测试样本 + 评估数据,都在配套 GitHub 仓库:
🔗 github.com/MisterBooo/rag-from-zero
- 跟着教程 clone 下来就能跑
- 包含本章用到的所有完整可运行代码
- 包含测试 PDF 样本 + 评估集
- 欢迎 Star ⭐ / Issue 反馈 / PR 贡献
导航