这次我们认真聊一个 RAG 落地时绕不开的问题:文档预处理阶段的切片与重叠区设计。很多人做 RAG 知识库时,文档一进来就直接按固定长度切成 chunk,再塞给 embedding 模型,结果检索效果不稳定,追问起来才发现是预处理阶段埋了雷。尤其是 PDF 和 PPT 这类强结构文档,到底按页切还是按段落切,重叠区(overlap)到底该设多少,网上说法很多,实际一测差距又很大。这篇文章不谈玄学,把 PDF、PPT 按页分割的适用场景和重叠区设计思路拆开讲清楚。
先说结论:重叠区不是越大越好,按页分割也不是万能方案。它适合“一页一个主题单元”的文档,不适合“连续叙述型长文”。重叠区的本质是补偿切分时丢失的上下文,它的最优值取决于文档结构、embedding 模型能力、检索策略这三个变量,必须用测试集去验证,不能拍脑袋定一个数。本文会从文档加载规则、按页分割适用场景、重叠区设计、功能验证、批量处理与接口调用几个方面展开,适合正在做 RAG 知识库、遇到底层文档切片效果不稳定的开发者阅读。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 解决的问题 | RAG 文档加载、预处理、切片粒度、重叠区设计 |
| 主要文档类型 | PDF、PPT、TXT、Word 等常见办公文档 |
| 关键处理环节 | 格式解析、内容提取、结构化切分、重叠区设置、向量化 |
| 核心难点 | 跨页内容断裂、扫描件 OCR、表格/图示跨页、重叠区噪音 |
| 适用框架 | LangChain、LlamaIndex 均可,也可纯手写解析管线 |
| 硬件要求 | 解析本身 CPU 足够,向量化取决于 embedding 模型部署方式 |
| 是否支持批量 | 支持,建议按目录扫描 + 任务队列 + 失败重试 |
| 是否支持 API | 可封装为 HTTP 服务,供上游系统调用 |
| 适合场景 | 企业内部知识库、课程课件解析、产品文档问答、合同/报告检索 |
表格里这些能力项,本质上都是围绕一个问题:让切片后的文本块既保持语义完整,又能被检索系统稳定命中。所以接下来先从文档加载和预处理规则说起。
2. 文档加载与预处理:不是所有文件都该用同一种切法
RAG 的文档预处理不是“读文本 -> 切分 -> 向量化”这么简单。不同格式的文档,内容组织方式完全不同,预处理规则必须跟着文档结构走。
2.1 TXT / Markdown 类纯文本
这类文件没有版面信息,只能依靠段落、标题、空行来判断语义边界。常见做法是:
- 按空行分段落。
- 按 Markdown 标题层级识别章节。
- 对无结构的纯文本,才退化为固定 token 切分 + 重叠区。
纯文本的切分自由度最高,也是重叠区设计讨论最多的场景。
2.2 Word 文档
Word 除了段落和标题,还有表格、智能目录、批注、页眉页脚等附加信息。预处理时应先提取正文结构:
- 优先读取内嵌标题样式,而不是纯文本换行。
- 表格建议单独提取,作为独立的切片单元。
- 页眉页脚默认丢弃,避免检索噪音。
2.3 PDF 文档
PDF 是 RAG 预处理中情况最复杂的格式。同样是 PDF,可能是「数字生成型」或「扫描图片型」:
| PDF 类型 | 特征 | 预处理方式 |
|---|---|---|
| 数字生成型 PDF | 文本可选中、可复制 | 直接用 pypdf、pdfplumber 提取文本 |
| 扫描图片型 PDF | 本质是图片,文本不可选中 | 先 OCR 再走文本切分流程 |
| 图文混排 PDF | 有图表、多栏排版 | 需要版面分析或按页抽取 |
| 幻灯片导出 PDF | 每页内容相对独立 | 适合按页分割 + 页面元数据 |
PDF 按页分割在这里是一个「候选策略」,不是默认策略。后面单独展开讲。
2.4 PPT 文档
PPT 的特殊性在于它的最小语义单元天然是「页面」。每一页 PPT 通常包含标题、正文、图片、表格、备注,一页往往表达一个完整主题。处理时建议:
- 优先提取「标题 + 正文 + 备注」三部分。
- 页眉页脚、装饰性占位符要去除。
- 如果一页中同时包含多个并列要点,可以考虑按“标题级别”二次拆分。
PPT 按页分割的收益通常比 PDF 更明显,因为 PPT 的版式本身就是信息块。
3. 重叠区设计:先理解它补偿的是什么
重叠区被称为“玄学”是因为很多人把它当成一个固定参数。但想让重叠区发挥价值,必须理解它到底在补偿什么。
3.1 为什么需要重叠区
假设一份文档被切分成:
chunk1: A B C D chunk2: E F G H如果检索问题涉及「C D E」这个语义片段,而切分点刚好落在 D 和 E 之间,那么 chunk1 和 chunk2 各自都不完整。加上重叠区后:
chunk1: A B C D E chunk2: D E F G HD 和 E 同时出现在两个 chunk 中,即使检索命中了其中一个,也能拿到相对完整的上下文。这是重叠区最核心的作用。
3.2 重叠区大小由什么决定
从实践角度看,重叠区大小主要受三个变量约束:
- 文档结构强度:如果文档本身有明确的章节、标题、列表,切分边界更容易落在语义边界上,重叠区可以小;如果文档是连续叙述型长文,重叠区需要适当增大。
- Embedding 模型窗口:Embedding 模型对文本长度有上限,重叠区过大会挤占主体内容的 token 预算,导致有效信息被稀释。
- 检索策略:如果后面接 rerank 模型,重叠区可以压缩;如果只用向量相似度直接取 top-k,重叠区要更谨慎,避免多个高度相似的 chunk 同时命中导致内容重复。
3.3 重叠区不是越大越好
大重叠区容易造成两个问题:
- 向量存储冗余,相同内容出现多次,增加存储成本和检索噪音。
- 检索结果集中度下降,Top5 结果可能全是同一段话的不同切法,真正需要的信息反而排不到前面。
所以重叠区设计应当遵循一个原则:刚好覆盖切分边界丢失的上下文,不给检索制造重复噪音。
4. PDF 按页分割的适用场景与设计思路
PDF 按页分割是一个很容易被过度使用的方法。不是所有 PDF 都应按页切,但特定场景下按页切效果很好。
4.1 适合按页分割的 PDF 类型
| 文档类型 | 为什么适合按页切 |
|---|---|
| 企业年报、财报、投研报告 | 每页通常是一个独立的分析模块 |
| 产品宣传 PPT 导出的 PDF | 页面本身就是设计好的语义单元 |
| 合同、发票、简历 | 一页一个文档或一页一个关键部分 |
| 课程课件 PDF | 每页标题 + 要点,内容密度适中 |
4.2 不适合按页分割的 PDF 类型
| 文档类型 | 问题 |
|---|---|
| 技术手册、操作说明 | 一个操作步骤可能跨两页,按页切会切断流程 |
| 学术论文 | 段落和公式可能跨页,按页切严重破坏语义 |
| 多栏排版 PDF | 一页里有两个或三个内容栏,按页切会导致一页多个并列主题混合 |
4.3 按页分割时的增强设计
按页分割不是简单地把每页文本变成一个 chunk,还需要做三件事:
- 保留页面元数据:记录
page_number、source_path,检索结果返回时能定位到具体页面。 - 跨页粘连处理:如果上一页末尾是不完整句子,尝试与下一页开头合并,或通过重叠区覆盖。
- 窄文本合并:有些页面只有一行标题或一个图,单独成 chunk 没有检索价值,可以和相邻页合并。
一个兼顾「按页分割」和「语义完整性」的通用思路是:先按页切,再对每页内容做二次判断——如果页面内容太少,合并;如果页面内容超长,再细分。
5. PPT 按页分割的适用场景与设计思路
PPT 按页分割比 PDF 更自然,因为 PPT 的页面本身就承载了「一个页面一个主题」的设计逻辑。
5.1 适合按页分割的 PPT 类型
| 文档类型 | 为什么适合按页切 |
|---|---|
| 培训课件 | 每页讲一个知识点 |
| 产品方案 | 每页介绍一个模块或功能 |
| 项目汇报 | 每页一个议题 |
| 技术分享 | 每页一个技术点 |
这类 PPT 的标题、正文、备注往往构成完整的信息单元,按页切后可以直接作为 RAG 的检索单元。
5.2 按页分割时的内容组装
PPT 页面的文本分布在不同的文本框中,直接按阅读顺序拼接可能让语义乱掉。推荐组装顺序:
页面标题 正文要点(按排版顺序) 备注内容备注往往包含讲解者补充的信息,是 RAG 检索中的高价值内容。但如果备注是演讲稿风格的流水账,需要适当截断,避免 chunk 过长。
5.3 PPT 按页分割注意事项
- 显式识别「标题占位符」和「正文占位符」,优先用 pptx 文件中的 XML 结构,而不是 OCR。
- 图片中的文字如果参与检索,需要 OCR 后合并到文本中,否则信息丢失。
- 每页的“第 X 页 / 共 Y 页”这类装饰文本要去除。
6. RAG 重叠区与按页分割的工程实现
到这里,设计思路已经清楚了,接下来看具体实现。以下示例使用通用 Python 库,实际项目需要根据你的文件路径和环境调整。
6.1 环境准备
建议最小验证环境:
- Python 3.9 及以上。
- 安装以下依赖:
pip install pypdf python-pptx pdfplumber langchain-text-splitters如果要做 OCR,再安装 OCR 相关依赖,具体以你的源文档类型为准。
6.2 PDF 按页分割 + 重叠区示例
from pypdf import PdfReader def split_pdf_by_page_with_overlap(pdf_path, overlap_chars=80): reader = PdfReader(pdf_path) pages = [] for page_num, page in enumerate(reader.pages, start=1): text = page.extract_text() if text: pages.append({ "page_number": page_num, "text": text.strip() }) chunks = [] for i, page in enumerate(pages): text = page["text"] if i > 0: prev_tail = pages[i - 1]["text"][-overlap_chars:] text = prev_tail + "\n" + text chunks.append({ "page_number": page["page_number"], "source": pdf_path, "text": text }) return chunks这段代码做的事情是:按页提取文本,再引入前一页末尾的 overlap_chars 字符,保证跨页上下文不断裂。实际项目中,overlap_chars需要根据测试结果调整。
6.3 PPT 按页分割示例
from pptx import Presentation def split_ppt_by_slide(pptx_path): prs = Presentation(pptx_path) slides = [] for idx, slide in enumerate(prs.slides, start=1): parts = [] for shape in slide.shapes: if shape.has_text_frame: for para in shape.text_frame.paragraphs: line = "".join(run.text for run in para.runs).strip() if line: parts.append(line) notes_text = "" if slide.has_notes_slide: notes_text = slide.notes_slide.notes_text_frame.text.strip() content = "\n".join(parts) if notes_text: content += f"\n备注:{notes_text}" slides.append({ "slide_number": idx, "source": pptx_path, "text": content }) return slidesPPT 按页分割后,每一页的标题、正文、备注作为一个整体 chunk,特别适合课件的 RAG 检索。
6.4 通用文本切分 + 重叠区示例
对于已经提取出来的纯文本,推荐使用langchain-text-splitters的RecursiveCharacterTextSplitter:
from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=512, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ". ", "! ", "? "] ) chunks = text_splitter.split_text(long_text)这里的chunk_size和chunk_overlap是通用建议值,不是固定标准。实际项目中,应该先跑一组候选参数,再通过检索测试选择最优组合。
7. 功能测试与效果验证
预处理管线搭好之后,不能只看「能跑通」,要看检索效果。下面给出一套通用验证流程。
7.1 准备测试集
建议手工构造 10 到 30 个测试问题,覆盖:
- 明确出现在某个页面/段落的问题。
- 跨页才能回答的问题。
- 需要结合 PPT 备注才能回答的问题。
- 容易混淆的相似主题问题。
每个问题标注对应的标准答案片段,最好精确到页码或 slide 编号。
7.2 对比测试维度
| 参数 | 测试候选值 |
|---|---|
| chunk_size | 256 / 512 / 768 |
| chunk_overlap | 0 / 64 / 128 / 256 |
| 分割策略 | 按页 / 按段落 / 固定长度 |
运行同一组测试问题,记录每个配置下的检索命中率和答案完整性。
7.3 判断标准
- 命中率:正确答案片段是否出现在检索结果 Top5 中。
- 上下文完整性:命中的 chunk 是否包含足够上下文支撑回答。
- 冗余度:Top5 结果中有多少重复内容。
- 定位准确性:是否能够定位到源文档的准确页面或 slide。
这种验证方法不需要复杂框架,几行脚本就能跑出来,但价值远高于凭感觉调参数。
8. 接口 API 与批量任务设计
文档预处理做完了,下一步是工程化接入。推荐把预处理 + 切分 + 向量化封装成服务,方便上游系统调用。
8.1 FastAPI 接口示例
from fastapi import FastAPI, UploadFile, File import tempfile app = FastAPI() @app.post("/documents/process") async def process_document(file: UploadFile = File(...)): suffix = file.filename.rsplit(".", 1)[-1].lower() with tempfile.NamedTemporaryFile(suffix=f".{suffix}", delete=False) as tmp: tmp.write(await file.read()) tmp_path = tmp.name if suffix == "pdf": chunks = split_pdf_by_page_with_overlap(tmp_path) elif suffix in ("pptx", "ppt"): chunks = split_ppt_by_slide(tmp_path) else: return {"error": "unsupported file type"} return { "filename": file.filename, "chunk_count": len(chunks), "chunks": chunks }这个接口接收 PDF 或 PPT 文件,返回按页分割后的 chunk 列表,方便上层系统直接对接向量库。
8.2 批量任务设计
批量处理文档时,建议按以下思路组织:
- 目录扫描:输入一个总目录,递归找出所有 PDF、PPTX 文件。
- 任务队列:每个文件作为一个任务写入队列,记录处理状态。
- 失败重试:对解析失败的文件单独记录日志,不阻塞整个队列。
- 结果输出:每个文件生成独立的 JSON 输出,包含 chunk 内容和元数据。
{ "input_dir": "./docs", "output_dir": "./outputs", "file_types": ["pdf", "pptx"], "overlap_chars": 80, "retry_count": 3 }批量任务最重要的是可观测。每个文件处理完成后输出“文件名、页数、chunk 数、耗时、状态”,有问题时能快速定位到具体文件。
9. 资源占用与性能观察
文档预处理阶段的资源占用容易被忽略,实际跑批量任务时往往在这里卡住。
9.1 解析阶段的资源占用
- PDF 文本提取和 PPT 文本框提取都是 CPU 密集操作,单个文件通常不会占用太多内存,但几百个文件并发处理时,CPU 会成为瓶颈。
- 如果 PDF 是扫描件需要 OCR,资源占用会明显上升,OCR 对 CPU、内存的需求都更高。
- 大批量文档处理建议用队列控制并发数,不要一次性全部载入内存。
9.2 向量化阶段的资源占用
- 如果 embedding 模型是本地部署,显存占用取决于模型尺寸和 batch size。
- 如果 embedding 服务通过 API 调用,批量任务要注意速率限制和超时重试。
- 向量库写入时,批量插入的 batch size 不宜设置过大,避免内存峰值。
9.3 性能观察方法
- 用
time记录每个文件处理耗时。 - 观察
CPU / 内存 / 显存三个维度。 - 批量任务增加「每 100 个文件输出一次统计」的日志。
这种观察方式不依赖特定平台,在实际项目中能帮你快速识别预处理管线的瓶颈。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| PDF 提取出的文本乱序 | PDF 是多栏排版或图层复杂 | 打印每页文本顺序检查 | 改用 pdfplumber 按坐标提取,或人工指定栏顺序 |
| PDF 提取不到任何文本 | 扫描版或图片型 PDF | 查看 PDF 是否可选择文本 | 接入 OCR 流程后再切片 |
| PPT 按页切后检索效果差 | 只提取了正文,忽略了标题/备注 | 检查 chunk 内容是否完整 | 按“标题 + 正文 + 备注”组装页面内容 |
| 设置了重叠区但检索噪音变大 | 重叠区过大或文档结构强无需重叠 | 对比不同 overlap 值的命中率 | 减小 overlap 或对强结构页面关闭重叠 |
| 跨页表格被切断 | 按页分割时表格跨了两页 | 定位到具体页数查看内容 | 对表格区域做完整块提取,再覆盖到相邻页 |
| 中文文档切分后语义碎片化 | 切分边界落在中文句子中间 | 检查 separator 是否包含中文字符 | 在分隔符中加入“。”“!”和换行符 |
| 批量任务处理中途卡住 | 单个文件解析异常导致线程阻塞 | 查看日志定位到具体文件 | 增加超时控制和失败跳过机制,单文件异常不阻塞队列 |
| 向量库中重复内容过多 | 重叠区跨多个 chunk 造成高度相似 | 检索 Top 结果相似度对比 | 加重叠区对检索结果做去重,或改用 rerank |
| 接口调用超时 | 大文件解析耗时过长 | 查看服务日志和文件大小 | 接口层增加异步任务,文件解析完成后回调通知 |
排查时建议先看“单文件是否能解析”,再看“解析结果是否合理”,最后看“检索效果是否达标”。大多数问题在第二阶段就能暴露出来。
11. 最佳实践与使用建议
把前面所有内容收敛成几条可操作的实践准则。
- 先小批量验证再全量处理。不要一上来就处理上千个文件,先拿 10 个代表性文件跑通全流程,确认解析结果和切分效果。
- 按文档类型分组配置。PDF 按页分割、PPT 按页分割、TXT 按段落切分,不要用同一套参数处理所有格式。
- 把重叠区当成可调参数,而不是固定值。每一批新文档都要重新验证 chunk_size 和 overlap 的组合。
- 保留元数据。每个 chunk 都记录来源文件、页码、slide 编号,否则检索到内容后无法溯源。
- 建立检索评测集。哪怕只是 10 个问题,也比没有评测集靠感觉调参强。
- 接口和批量任务要加日志和失败重试。文档解析不可控因素多,尤其是 PDF,一个坏文件就能卡住整条流水线。
- 关注版权与授权边界。企业内部文档、课程 PPT、合同、报告在投入 RAG 前要确认文档来源和复制范围是否符合授权要求,涉及个人隐私或商业机密的内容要做脱敏和访问控制。
- 发布前做效果复核。RAG 问答系统对事实性要求高,检索召回不准会导致回答误导,关键业务场景必须人工复核后再上线。
12. 总结与下一步
回到开头的问题:重叠区是玄学吗?不是。它是对切分边界信息丢失的补偿,是一个可以量化、可以用测试集评估的设计参数。PDF 按页分割和 PPT 按页分割也一样,核心判断标准是“这一页是否构成一个独立的语义单元”。是,就按页切;不是,就按段落或定长切,再用重叠区补偿边界。
最容易踩的坑有三个:一是所有文档统一用一套切分参数,二是只要设了重叠区就以为能解决所有跨页问题,三是只看“能跑通”不看真实检索效果。先找 10 个代表性文档,建立小测试集,跑一遍不同配置下的命中率对比,几分钟就能得到比凭感觉调参可靠得多的结论。
下一步你可以做三件事:把本文的 PDF、PPT 按页分割脚本接到你的文档目录上跑一版;构造一个 10 到 30 条问题的评测集;对比「固定长度切分 + 重叠区」与「按页切分 + 重叠区」的召回差异。做完这三步,你对 RAG 文档预处理的把控能力会比大多数现成框架默认配置更扎实。建议收藏备用,等实际跑完再回来对照参数。