资讯动态

RAG文档预处理实战:PDF/PPT按页分割与重叠区设计

发布时间:2026/9/7 11:39:17 来源:尧图企业网站定制
之前在一套 RAG 知识库里排查问题时遇到一个很典型的场景用户提问“这份合同里违约金的计算基数是什么”文档原文其实写得清清楚楚但知识库始终答非所问。后来把文档捞出来人工看了一遍发现答案被切分到了两个 Chunk 的边界处前半段只留下“违约金以合同总金额为基数”后半段才是“按日万分之五计算”检索时谁都不是完整答案。这类问题在 RAG 项目里非常普遍根源就在文档预处理。尤其是分块Chunking和重叠区Overlap的设置很多初学同学把它当成“玄学”要么完全不设重叠区要么把重叠区拉得很大结果要么检索不全要么向量库里全是重复片段。本文不打算把重叠区讲成一种“调参玄学”而是从实际文档类型出发重点聊聊 PDF、PPT 这类文件按页分割的适用场景以及重叠区到底该怎么设计。不管你是刚开始搭 RAG 知识库还是已经被脏数据折腾到怀疑人生这篇文章都能给你一套可以落地的思路和代码。1. 为什么说文档预处理决定了 RAG 的天花板很多同学第一次接触 RAGRetrieval-Augmented Generation检索增强生成时注意力都放在 Embedding 模型和 LLM 的选型上文档预处理反而被忽略。实际跑过一轮问答评测后会发现大多数 Bad Case 不是模型不行而是文档在进入向量库之前就已经“坏”了。RAG 的完整链路可以简化成下面几条文档加载把 PDF、Word、PPT、TXT 等格式读入程序。文档解析将原始文件中的目录、页眉页脚、表格、图片、文本块还原成可用的纯文本。文本清洗去掉无意义空行、乱码、重复水印、页眉页脚。文本分块把长文本切成若干固定大小或语义完整的片段。向量化与入库对每个 Chunk 生成 Embedding写入向量数据库。检索与生成根据用户问题召回 TopK 相关片段拼进 Prompt 后交给 LLM 生成回答。其中最影响检索质量的是“文本分块”。分块太大向量化后语义容易被稀释分块太小单个块又装不下完整信息。更麻烦的是切分位置一旦落在关键句子的中间原本完整的一句话、一组条件、一条规则就会被“拦腰斩断”。重叠区就是在切分时让相邻两个 Chunk 共享一部分文本希望用这种“内容重复”来补偿切分造成的边界损失。但要注意重叠区解决的是“文本边界割裂”问题不是“语义不完整”问题。如果你的切分粒度本身就不合理比如把一段 3000 字的连续合同条款强行切成 500 字一块那重叠区再怎么调也救不回来。所以在讨论重叠区之前先要把文档类型和切分策略的适配关系想清楚。PDF 和 PPT 的页面结构差异巨大它们适用的切分方式当然也不一样。2. 重叠区是玄学吗它补偿的到底是什么先说结论重叠区不是玄学它是一个有明确物理意义的参数。理解它为什么有效、为什么失效比记住“设 10% 还是设 20%”重要得多。2.1 重叠区解决的问题边界割裂假设有一段文本被切成下面两个 ChunkChunk 1: 甲方应在收到乙方通知后 15 个工作日内支付违约金 Chunk 2: 若逾期未支付乙方有权暂停提供服务如果用户的提问是“违约金支付逾期有什么后果”那么 Chunk 1 和 Chunk 2 各有半句相关内容。单独召回的 Chunk 1 没有“后果”单独召回的 Chunk 2 没有“触发条件”LLM 基于任意一个片段都很难回答完整。加入重叠区后切分结果可能变成Chunk 1: 甲方应在收到乙方通知后 15 个工作日内支付违约金 Chunk 2: 支付违约金若逾期未支付乙方有权暂停提供服务Chunk 2 里保留了“支付违约金”这个上下文检索时更容易把条件和后果关联起来。这就是重叠区最基本的补偿作用。2.2 重叠区为什么有时候看起来像“玄学”网络上很多教程直接告诉你“重叠区设 50 个 token”“重叠率 10%-20%”但很少解释背后的约束条件。于是大家照着配置效果好了觉得有用效果差了觉得没用最终得出“靠感觉调参”的结论。重叠区效果不稳定通常是因为下面几个变量在同时起作用文本本身的语义边界是否清晰。如果原文本来就是按段落拆分的重叠区几乎没有存在感。切分方式是否匹配文档结构。PPT 按页切分时关键内容天然集中在一页内重叠区的价值就小PDF 中一段跨两页很常见重叠区价值就大。检索的 TopK 设置。如果一次召回 10 个块边界割裂问题会被海量候选掩盖如果只召回 1-2 个块重叠区的影响就会被放大。Embedding 模型对上下文长度的敏感度。部分模型对相邻句子关系敏感重叠区带来的“冗余上下文”有助于编码但也可能引入噪音。所以重叠区不是一个独立参数它必须和分块大小、文档结构、检索策略一起评估。脱离了具体评测谈重叠区确实容易变成玄学。2.3 重叠区过大的副作用不是重叠区越大越好。过大的重叠区会带来三个可量化的副作用向量库膨胀。假设每个 Chunk 是 500 字重叠 250 字那么每个有效内容会平均存储 1.5 次向量数量明显上升存储和检索成本随之增加。检索结果冗余。用户提问时可能同时召回多个高度相似的块TopK 里有效信息密度反而下降甚至挤掉了真正应该被召回的内容。上下文污染。如果一个 Chunk 里塞了太多重复片段Embedding 在表征时可能被重复文本“带偏”相似度得分失真。实践中的建议是重叠区的长度一般控制在分块长度的 10%-20%并且优先保证不跨越明显的语义边界。接下来我们重点看 PDF、PPT 按页分割时重叠区应该怎么配合。3. PDF、PPT 按页分割的适用场景与设计思路按页分割就是直接把每一页提取出的文本作为一个基础单元然后再决定是否对页面做合并或重叠。这个思路听起来很“偷懒”但在不少场景下比复杂的语义切分更实用。3.1 为什么会有“按页分割”这种方案PDF 和 PPT 有一个共同特点版面天然按页组织。无论是设计软件导出的 PPT还是排版工具生成的 PDF每一页通常对应一个相对完整的知识单元。对这类文档做解析时直接按页提取文本可以保留页面顺序、版面关系和内容边界减少在段落中途“随意下刀”的概率。另外按页分割还有一个隐藏好处页与页之间的索引关系是明确的。比如用户需要引用“来自第 12 页的内容”如果你做的是任意位置切分很难回溯到原始页面而按页切分后每个 Chunk 可以直接保存页码元数据检索结果里也能顺带返回来源页码。3.2 PDF 按页分割的两个适用场景PDF 的排版方式差别很大不能一概而论。我通常会把 PDF 分成两类来看第一类是“报告、招股书、合同、规章制度”这类以文字段落为主、段落可能跨页的文档。这类文档按页切分时经常把一个段落切到两页里。单纯按页切分会破坏段落完整性所以更适合先按页提取再通过段落编号、缩进或行距信息把跨页段落拼接起来。如果懒得做段落拼接也可以通过“前后页重叠”来补救也就是构建 Chunk 时把当前页和上一页末尾、下一页开头的一部分内容合并进来。第二类是“产品手册、技术方案、PPT 导出的 PDF、论文”这类版式规整、每页相对独立的文档。这类文档按页切分的效果通常不错甚至可以直接把一页当成一个 Chunk。尤其当页面内含有大量图表、流程图时文字和图片是一体的按页切分能保留图片与文字的对应关系方便后续做多模态检索。3.3 PPT 按页分割几乎是最自然的单元PPT 和 Word/PDF 不一样它的核心表达单位就是“页”也就是一张幻灯片。一张幻灯片内部通常包含一个完整观点标题给出主题正文给出论据配图给出示意。这种情况下把一页作为一个基础 Chunk 是最省力也最合理的方案。用 python-pptx 提取 PPT 文本时要注意PPT 里的文本并不全在普通文本框里。SmartArt、图表、组合形状中的文本都需要递归提取否则会出现“某页明明有字但提取出来为空”的情况。下面会给出完整的递归提取代码。PPT 适合按页分割但并不意味着“一页就是一个最终 Chunk”。如果一页内容太多比如塞满了密密麻麻的要点和备注单页字符数超过 Embedding 模型的最佳长度就需要在页内继续细分如果一页内容太少比如只有一行标题则需要把连续两到三页合并后再作为 Chunk。3.4 不适合按页分割的场景不是所有文档都适合按页分割下面几种情况要特别小心扫描版 PDF。页面本质是图片直接提取不到文本必须先走 OCR。代码文档。代码跨页、缩进敏感按页切分容易切断代码块更适合按代码块、函数或 Markdown 标题切分。电子书、小说。这类连续叙述型文本没有明显页面边界按页切分会产生大量残缺语句更适合按章节、段落或固定长度切分。版式复杂的杂志、报纸。页面内是多栏结构文字阅读顺序不遵循物理坐标顺序直接 get_text 提取出来的顺序可能是乱的。所以按页分割不是一个万能的默认方案它是“当页面边界恰好等于语义边界”时才正确的策略。这也是我在这篇文章里反复强调的核心观点先看文档结构再定切分策略。4. 完整实战写一个按页分割 重叠区的预处理脚本下面用 Python 写一个可以落地的文档预处理脚本。它支持 PDF 和 PPTX核心流程是解析文件、按页提取文本、按页粒度合并、再叠加字符级重叠切分。4.1 项目结构与依赖建议在独立虚拟环境中操作用到的库只有两个pip install pymupdf python-pptxPyMuPDFfitz负责读取 PDF按页提取文本。python-pptx 负责读取 PPTX按幻灯片提取文本。项目建议按下面结构组织rag-doc-preprocess/ ├── requirements.txt ├── doc_preprocess.py └── sample_files/ ├── demo.pdf └── demo.pptx4.2 完整代码# 文件路径rag-doc-preprocess/doc_preprocess.py import re from typing import List from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE import fitz # PyMuPDF def extract_pdf_pages(pdf_path: str) - List[str]: 按页提取 PDF 文本返回每一页的文本列表。 pages_text [] doc fitz.open(pdf_path) try: for page_num in range(len(doc)): page doc.load_page(page_num) text page.get_text(text) pages_text.append(text.strip()) finally: doc.close() return pages_text def _iter_shape_text(shapes): 递归提取 PPT 中所有形状的文本包括组合形状里的文本。 for shape in shapes: if shape.shape_type MSO_SHAPE_TYPE.GROUP: # 组合形状需要进入内部继续找 yield from _iter_shape_text(shape.shapes) elif hasattr(shape, text_frame): for paragraph in shape.text_frame.paragraphs: text paragraph.text.strip() if text: yield text def extract_ppt_pages(ppt_path: str) - List[str]: 按幻灯片提取 PPT 文本返回每一页的文本列表。 pages_text [] prs Presentation(ppt_path) for slide_idx, slide in enumerate(prs.slides, start1): texts list(_iter_shape_text(slide.shapes)) if texts: pages_text.append(\n.join(texts)) else: # 没有任何文本时保留占位标记方便排查问题 pages_text.append(f[第{slide_idx}页无文本内容]) return pages_text def split_pages_with_overlap( pages_text: List[str], pages_per_chunk: int 2, overlap_pages: int 1, ) - List[str]: 以页为粒度合并 Chunk并允许前后页重叠。 场景PPT 或 PDF 中单页内容太短时把连续几页合并成一个 Chunk。 overlap_pages 表示相邻 Chunk 之间共享的页数。 if overlap_pages pages_per_chunk: raise ValueError(overlap_pages 必须小于 pages_per_chunk) chunks [] i 0 while i len(pages_text): chunk \n.join(pages_text[i: i pages_per_chunk]) if chunk.strip() and chunk.strip() ! [无文本内容]: chunks.append(chunk) if i pages_per_chunk len(pages_text): break i pages_per_chunk - overlap_pages return chunks def split_text_with_overlap( text: str, chunk_size: int 500, overlap_size: int 50, ) - List[str]: 按字符长度切分文本并保留重叠区。 场景页面内容本身很长时在页内继续细分。 重叠区长度一般建议控制在 chunk_size 的 10%-20%。 if overlap_size chunk_size: raise ValueError(overlap_size 必须小于 chunk_size) # 将三个以上连续换行压缩为两个减少空行干扰 text re.sub(r\n{3,}, \n\n, text.strip()) chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) chunk text[start:end].strip() if chunk: chunks.append(chunk) if end len(text): break start end - overlap_size return chunks def build_chunks_for_file(file_path: str) - List[str]: 对外主流程自动识别 PDF / PPTX返回最终 Chunk 列表。 file_lower file_path.lower() if file_lower.endswith(.pdf): pages extract_pdf_pages(file_path) elif file_lower.endswith(.pptx): pages extract_ppt_pages(file_path) else: raise ValueError(仅支持 PDF 和 PPTX 文件) print(f共提取到 {len(pages)} 页文本) for i, text in enumerate(pages, 1): print(f第 {i} 页字符数{len(text)}) # 第一步按页粒度先合并一次解决单页内容过短问题 page_level_chunks split_pages_with_overlap( pages, pages_per_chunk2, overlap_pages1, ) print(f按页粒度合并后得到 {len(page_level_chunks)} 个 Chunk) # 第二步对每个页级 Chunk再按字符大小做精细切分 final_chunks [] for chunk in page_level_chunks: sub_chunks split_text_with_overlap( chunk, chunk_size500, overlap_size50, ) final_chunks.extend(sub_chunks) print(f最终得到 {len(final_chunks)} 个可用于向量化的 Chunk) return final_chunks if __name__ __main__: # 实际运行时替换为自己的文件路径 chunks build_chunks_for_file(sample_files/demo.pptx) for idx, chunk in enumerate(chunks, 1): print(f\n Chunk {idx} ) print(chunk[:200])4.3 代码关键点讲解这段代码里有两个设计值得留意。第一个是_iter_shape_text这个递归函数。PPT 的文本可能嵌套在 Group 形状里直接用shape.text只能拿到一层组合形状里的文字会丢。代码先判断shape.shape_type MSO_SHAPE_TYPE.GROUP是组合形状就递归遍历内部形状否则读取文本帧这样能最大限度避免“页面文本提取为空”的情况。第二个是两层切分策略。先按页粒度合并再在页内做字符级重叠切分。这样既保留了“页”的文档边界又能在单页内容过长时继续细分。两层的重叠参数可以独立控制调起来比单层更灵活。4.4 运行与验证把build_chunks_for_file的路径换成你自己的文件后直接运行python doc_preprocess.py预期输出类似共提取到 5 页文本 第 1 页字符数124 第 2 页字符数312 ... 按页粒度合并后得到 4 个 Chunk 最终得到 9 个可用于向量化的 Chunk这里页级合并把 5 页变成了 4 个页级 Chunk是因为第 1 页和第 2 页合并后第 2 页又被作为下一段的起始页复用了一次。这种重叠能保证跨页的语义连接不被切断。4.5 入库前的元数据建议实际项目中不建议只把文本丢进向量库。每个 Chunk 最好带上来源文件、页码、原始索引等元数据。例如chunk_metadata { source_file: demo.pptx, page_range: 2-3, chunk_index: 3, }有了页码元数据检索结果里可以回显来源用户确认信息时也会方便很多。向量数据库一般都以 JSON 形式存储元数据可以在写入时一并保存。5. 常见问题与排查思路按页分割 重叠区的方案在实际落地时会遇到一些高频问题。下面用表格整理一下现象、可能原因和解决思路。问题现象常见原因解决思路按页切分后答案内容被截断段落跨页恰好落在两页边界增加前后页重叠或先做跨页段落拼接PDF 提取后没有文本扫描件、图片型 PDF页面没有文本层接入 OCR如 PaddleOCR、Tesseract后再提取PPT 某页提取为空文本在组合形状或图片中使用递归提取函数图片内容尝试 OCR 或多模态模型描述重叠区导致大量重复召回overlap_size 过大或重叠比例不合理重叠比例降到 10%-20%观察 TopK 结果去重后再送入 LLMChunk 数量膨胀向量库占用过大chunk 切得太碎、重叠太多适度增大 chunk_size减少重叠过滤无信息页面同一问题多次回答结果不稳定检索到了不同位置的相似片段加入重排Rerank模型去掉低相关度片段排查时我一般建议从“文本内容本身”开始先把预处理后的 Chunk 打印出来人工检查确认文本有没有丢、顺序对不对再去看向量化参数和检索效果。不要一上来就调重叠区那是用战术上的勤奋掩盖战略上的懒惰。6. 最佳实践与工程建议6.1 先把文档分类再定切分策略完整的生产级 RAG 项目不会用一套参数处理所有文件。一个比较推荐的做法是把文档按结构分成几类规整型文档PPT、产品手册、论文按页分割为主重叠区作为辅助。段落型文档合同、规章制度、报告按段落或章节切分必要时做跨页段落拼接。叙述型文档电子书、小说、长篇博客按章节或固定长度切分重叠区可以保守一些。代码类文档按代码块、函数、Markdown 标题切分。配置层面可以通过文件后缀、目录、甚至前几行文本的特征来自动分流避免人工维护一份庞大的文件清单。6.2 建立一个小规模评测集用数据替代“感觉”如果你觉得重叠区调参像玄学说明缺少一个可量化的评测集。搭建评测集不需要很复杂从知识库里挑 30-50 个典型问题。对每个问题记录“标准答案所在的页码或段落”。运行检索程序统计 TopK 内是否包含标准答案。用命中率RecallK来评估切分策略的好坏。有了这个评测过程你可以大胆去试不同的chunk_size、overlap_size、pages_per_chunk用数据判断哪个组合更接近最优解而不是靠“这次好像行了”的模糊感觉。6.3 重叠区从 10%-20% 开始不要盲目加大如果采用字符级切分overlap_size建议从chunk_size的 10%-20% 起步。如果采用页级重叠overlap_pages一般取 1 页即可。只有在明显出现跨页语义割裂时才考虑增加重叠的页数。同时要监控向量库的存储增长曲线。如果发现重叠区让 Chunk 数量增加了 50% 以上而检索命中率没有明显提升那说明重叠区已经超出补偿边界需要回退参数。6.4 不要轻易丢弃图片与表格PDF 和 PPT 里很多时候关键信息在图片或表格里文本只负责描述。如果你只提取文本等于把答案本身扔掉了。针对这些内容可以考虑两条路一是接入 OCR 或目标检测模型把图片里的文字识别出来作为文本补充二是做多模态向量化让图片和文本一起入库。资源有限时至少给图片生成一段描述性文本作为检索的辅助信号。6.5 生产环境加入增量更新与版本管理文档预处理不是一次性的。文档更新后旧 Chunk 需要清理新 Chunk 需要写入。建议在 Chunk 元数据中带上文档版本号或更新时间更新时先按source_file version删除旧数据再写入新数据避免新旧版本混用导致检索结果自相矛盾。6.6 先验证成熟框架再决定要不要自研如果你想快速验证“按页分割 重叠区”这个方案是否适合自己的业务可以先在 Dify、LangChain、LlamaIndex 这类成熟工具里配置对应的 Loader 和 Splitter跑一轮评测看效果。成熟框架能帮你把链路快速串起来等确认瓶颈确实在切分策略上再针对 PDF、PPT 写自定义预处理也不迟。这样既节省开发时间又能用真实数据验证设计思路。7. 回到重叠区一套可以直接使用的起步参数最后把本文的思路压缩成一套可以直接上手的起步配置方便你在新项目里快速开始文档解析阶段PDF 用 PyMuPDF 按页提取PPT 用 python-pptx 递归提取组合形状和文本框都要覆盖。页级切分PPT 默认 1 页 1 个基础单元内容太短时 2 页合并 1 个 Chunk重叠 1 页。字符级切分单页内容超过 500 字时在页内继续切分重叠区 50 字左右。向量化与检索TopK 设置 5 左右配合 Rerank 模型进一步精排。评估方式准备 30 个典型问题计算 Recall5持续迭代参数。这组参数不一定是最优解但它是一个符合多数文档实际情况的起点。RAG 文档预处理没有一劳永逸的万能配置真正重要的是你能根据文档结构和评测结果找到当前场景下的“够用参数”而不是死记某一个数值。如果你在 PDF 跨页段落拼接、PPT 图文信息提取或者重叠区调优上有更好的思路欢迎在评论区交流。文档预处理这个环节看起来不起眼却往往是知识库问答效果差异最大的地方值得每一个做 RAG 实战的开发者认真对待。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价