RAG文档预处理与切片策略:重叠区、按页分割与工程实践

发布时间:2026/9/7 11:39:28
RAG文档预处理与切片策略:重叠区、按页分割与工程实践 做 RAGRetrieval-Augmented Generation检索增强生成项目很多团队把精力花在选模型、调 prompt、做 rerank 上结果检索效果还是不稳定。问题往往不在模型而在文档预处理阶段就埋了雷。这次我们聊一个几乎所有 RAG 项目都会遇到的话题重叠区chunk overlap到底是不是玄学PDF、PPT 是不是直接按页切割就行先说结论重叠区不是玄学它解决的是“切分边界处语义断裂”这个具体问题按页分割也不是万能方案它只适用于语义单元和页面边界基本重合的文档。本文会用工程视角拆解一套可落地的文档加载预处理与切片流程覆盖 PDF、PPT 的适用场景、重叠区和 chunk_size 的设计思路、批量处理与检索评测方法。适合读者正在搭建 RAG 知识库的工程师、需要处理 PDF/PPT 语料的产品和技术负责人、对“文档切片到底该怎么切”有疑问的开发者。1. 核心能力速览先给一张表把本文方案的关键信息列出方便你判断这套思路是否适合你的项目。能力项说明方案定位RAG 文档加载预处理与切片策略覆盖格式PDF、PPT、Word、TXT 等常见文本类文档切片方式按页分割 / 按标题结构分割 / 定长滑动窗口重叠区支持支持chunk_size 的 10%~25% 通常作为起点输出形态带文档名、页码、章节号等元数据的 chunk 列表批量能力可通过脚本批量处理本地目录接口扩展可封装为 HTTP 接口供上层知识库调用合规提醒处理前需确认素材版权与授权范围这套方案不依赖特定 GPU文档预处理本身是 CPU 任务主要的算力消耗在后面的 embedding 和问答推理环节。下面逐块讲清楚设计思路和实现方式。2. 直接回答重叠区不是玄学它解决的是切点断裂问题重叠区有明确的工程作用。假设一段文本是 1000 个字chunk_size 取 200overlap 取 40。滑动窗口每步前进 160 个字也就是说每个 chunk 会保留上一个 chunk 末尾的 40 个字作为上下文衔接。这样做最直接的价值是当一个知识点刚好落在两个 chunk 的切点上时至少有一个 chunk 保留了足够多的上下文不至于让检索和生成阶段完全丢失切点处的信息。举个例子原文有一句话是“该模型的参数量为 700 亿训练数据规模约为 2TB”。如果切点刚好落在“700 亿”和“训练数据”之间前一个 chunk 只知道参数量后一个 chunk 只知道训练数据规模两个 chunk 都没有完整描述这句话。加上 overlap 之后后一个 chunk 会带着前一句的尾部进入从而保留“参数量为 700 亿”这个信息。那重叠区是不是越大越好不是。overlap 过大带来三类问题Token 和存储成本增加索引膨胀检索耗时变长。向量检索时过多的重叠会让相邻 chunk 在向量空间里高度相似top-k 结果容易被同一段内容占据召回覆盖度下降。Embedding 模型有输入长度限制overlap 挤占有效内容空间。但如果文档本身结构非常独立比如每条 FAQ 都是完整的问答对段落之间没有强依赖overlap 的价值就明显降低。这种情况下与其纠结重叠区不如先做好按语义块切分。所以重叠区不是玄学它是可以直接评测和调整的参数。关键在于先用一套固定的测试问题集去测再决定重叠区设多少而不是凭感觉拍脑袋。3. PDF、PPT 按页分割的适用场景“按页分割”是最容易被误解的操作。按页切不代表每一页必须等于一个 chunk更合理的说法是先把页面文本抽取出来作为中间结构再根据语义边界决定如何组装成最终 chunk。3.1 PPT 按页切先当默认选项再处理边界PPT 是文档切片里最适合按页处理的格式。原因很简单PPT 的每一页通常是演讲者设计好的一个语义单元由“标题 要点”构成天然适合作为 RAG 的检索单元。但 PPT 按页切有几个坑要处理装饰性文字。很多 PPT 的母版里会带固定的 logo 文案、页脚、装饰字符需要清洗掉。文本框乱序。页面上的多个文本框在读取时不一定按视觉顺序返回最好记录每个 shape 的位置坐标按坐标排序后再拼接文本。一个页面塞了太多内容。有些页面是密集的架构图或对比表格一页之内还能继续拆分。跨页连续观点。比如结论在第一页末尾解释在第二页开头这种跨页内容在按页切时会断裂需要额外把上一页的末尾部分拼进下一页。用 python-pptx 读取 PPT 文本是一个常见做法。下面的代码演示了如何按页抽取文本from pptx import Presentation def extract_slides(pptx_path: str) - list[dict]: prs Presentation(pptx_path) slides [] for idx, slide in enumerate(prs.slides, start1): 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) if line.strip(): parts.append(line) if shape.has_table: for row in shape.table.rows: cells [cell.text.strip() for cell in row.cells] parts.append( | .join(cells)) slides.append({page: idx, text: \n.join(parts)}) return slides这段代码会把每一页的文本框和表格内容拼接成一个文本块。实际项目中还可以在parts.append(line)之前判断当前 shape 是否属于固定的母版元素从而跳过装饰性内容。3.2 PDF 按页切先区分文字版和扫描版PDF 是所有文档格式里最容易“看似简单、实际复杂”的类型。按页切之前先要判断 PDF 是文字版还是扫描版文字版 PDF文本可以被直接提取按页切出来的内容基本可读。扫描版 PDF页面是图片直接抽取文本大概率得到空字符串必须先接 OCR。对于文字版 PDF按页切是否合适取决于文档类型产品手册、操作指南、答辩 PPT 导出的 PDF通常按页切可用。合同、招标文件、政府公文、规章制度按页切会切断条款和章节逻辑应该优先按标题结构切。学术论文推荐按章节、段落切而不是按物理页切。数据手册、规格书页面里表格密集按页切之前最好先定位表格区域按表格结构单独抽取。下面用 pdfplumber 做一个文字版 PDF 的按页抽取与基础清洗import pdfplumber import re def extract_clean_pages(pdf_path: str) - list[dict]: pages [] with pdfplumber.open(pdf_path) as pdf: for page_num, page in enumerate(pdf.pages, start1): text page.extract_text() or # 清理多余空行 text re.sub(r\n{3,}, \n\n, text) # 清理页眉页脚等全局重复内容按实际文档调整 lines [line.strip() for line in text.splitlines() if line.strip()] clean_text \n.join(lines) pages.append({page: page_num, text: clean_text}) return pages这段代码只做了两层清洗合并多余空行、去掉空白行。真正的页眉页脚过滤需要根据文档样本单独写规则比如识别每页重复出现的公司名、文档编号、版本号。3.3 什么时候不要按页切如果文档的语义边界和物理页面边界明显不一致就不要按页切。典型情况包括学术论文的结论可能跨页分布按页切会把一个完整结论拆到多个 chunk。长篇小说一个章节往往跨多页按页切会把叙事逻辑打散。表格密集的 PDF按页切会把表格结构切断导致检索结果残缺。用户问题通常是以知识点为单位提出的不是以页为单位提出的。所以“按页分割”更准确的定位是它是文档结构化过程中的一个中间产物不是终点。4. RAG 文档加载与预处理的完整流程把视野拉高一点重叠区和按页切都只是“文档加载预处理”链路中的一环。一个完整的 RAG 预处理流程通常包含六个阶段文件收集与格式识别确定来源目录识别 PDF、PPT、Word、TXT 等格式。文本抽取从不同格式中提取出可读文本扫描版 PDF 走 OCR。文本清洗移除页眉页脚、水印、装饰性文字、多余空行统一换行符。结构化切分按页、按标题、按滑动窗口等方式切出候选 chunk。元数据标注给每个 chunk 标记文档名、页码、章节号、切片方式等。向量化与入库用 embedding 模型生成向量写入向量库同时保存原始文本。下面是一个批量预处理脚本的骨架它会遍历 input 目录下的文件按格式调用不同的解析函数输出带元数据的 chunkimport json import logging from pathlib import Path INPUT_DIR Path(data/raw) OUTPUT_DIR Path(data/chunks) logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) def extract_pdf_pages(pdf_path: str) - list[dict]: # 按 3.2 节的思路实现 pass def extract_pptx_pages(pptx_path: str) - list[dict]: # 按 3.1 节的思路实现 pass def split_pages_to_chunks(pages: list[dict], chunk_size: int, overlap: int) - list[dict]: # 按第 5 节的思路实现 pass def process_one_file(path: Path): if path.suffix.lower() .pdf: pages extract_pdf_pages(str(path)) elif path.suffix.lower() .pptx: pages extract_pptx_pages(str(path)) else: logging.warning(funsupported format: {path.suffix}) return chunks split_pages_to_chunks(pages, chunk_size500, overlap100) out_file OUTPUT_DIR / f{path.stem}.json with open(out_file, w, encodingutf-8) as f: json.dump(chunks, f, ensure_asciiFalse, indent2) logging.info(fprocessed {path.name}, chunks: {len(chunks)}) for path in INPUT_DIR.glob(*.*): try: process_one_file(path) except Exception as e: logging.error(ffailed: {path} - {e})这个脚本的关键点是每个文件单独用 try/except 包住单个文件解析失败不会中断整个目录的批量任务每个文件输出一个独立 JSON便于定位问题。5. 重叠区与 chunk_size 的设计思路5.1 chunk_size 怎么定chunk_size 的取值主要受三个因素影响Embedding 模型的最大输入长度。不同模型的上限不同如果 chunk 长度接近或超过模型上限向量质量会明显下降。LLM 的上下文窗口。chunk 太长塞进 prompt 后留给回答生成的空间就小chunk 太短单条上下文信息量不足。问答场景的粒度。短问题、明确的知识点适合短 chunk跨多页的综合问题适合长 chunk。中文场景下一个常见起点是从 256 或 512 开始试。这里的单位可能是字符数也可能是 token 数取决于你的切分器和 embedding 模型的计量方式。实际调优时不要只看参数绝对值要看“检索召回率”和“问答正确率”两条指标。5.2 overlap 怎么定overlap 的经验起点是 chunk_size 的 10%~25%。比如 chunk_size 为 500 时overlap 取 50~125。但这只是起点。更合理的做法是让 overlap 和“句子边界”对齐而不是机械地截断字符。一个句子很长、超过 overlap 长度时机械截断很可能把句子从中间切开产生语义不完整的文本。比较好的顺序是先把文本按句子切分。再按 chunk_size 将句子组装成 chunk。组装时让新 chunk 携带上一个 chunk 末尾的一句话或几句话代替固定字符数 overlap。这里有一个可以落地的滑动窗口切分实现它先按中英文句末标点切句再用滑动窗口组装import re def split_sentences(text: str) - list[str]: raw re.split(r(?[。!?]), text) return [s.strip() for s in raw if s.strip()] def sliding_window_chunk(sentences: list[str], chunk_size: int 500, overlap: int 100) - list[str]: chunks [] current for sent in sentences: if len(current) len(sent) chunk_size: current sent else: if current: chunks.append(current) if overlap 0 and current: tail current[-overlap:] current tail sent else: current sent if current: chunks.append(current) return chunks text 这是第一句话。这是第二句话介绍模型参数。这是第三句话讨论训练数据。 sentences split_sentences(text) chunks sliding_window_chunk(sentences, chunk_size30, overlap10) for i, chunk in enumerate(chunks): print(fchunk {i}: {chunk})这个函数有一个明显的边界问题如果某个句子本身超过 chunk_size这个句子会单独成为一个 chunk。实际项目中还要进一步处理超长句子的截断以及避免 overlap 截断到半个词。需要根据业务语料调整。5.3 重叠区包含的内容要清洗重叠区本质上是从前文复制到后文的一段文本。如果前文里包含页眉、页脚、固定的文档编号这些内容会跟着 overlap 一起进入多个 chunk造成重复 embedding。所以重叠区的设计必须放在文本清洗之后而不是之前。6. 按页分割与重叠区的组合设计按页分割和重叠区不是两个互相排斥的方案而是可以组合使用的两个层级。整体思路是第一级切分按页面粒度抽取文本保留每页的独立结构。第二级切分在页内或跨页边界处用重叠区缓解边界断裂。对 PPT 而言一页通常是一个完整语义单元。如果页与页之间有连续逻辑可以在下一页的 chunk 顶部拼接上一页的最后一句话。比如第一页末尾是“因此我们决定采用双塔模型”第二页开头讲解双塔模型结构检索“为什么选双塔模型”时如果第二页的 chunk 不携带上页结尾就无法回答“因此”指代的是什么。对 PDF 而言如果按页切每页的页眉和页脚要提前清洗。清洗后可以在相邻页之间做少量重叠取上一页最后一段文本作为下一页 chunk 的前缀。如果文档是按标题结构切的重叠区的必要性会降低很多因为标题本身就是很好的边界信号。无论哪种方式chunk 的元数据都要保留页面信息。推荐的数据结构如下{ doc_id: 2024-RAG-guide, file_name: RAG实践指南.pdf, page_range: [1, 2], section: 3.2 重叠区设计, chunk_index: 12, text: 重叠区的设计必须放在文本清洗之后... }元数据能让检索到结果后直接定位到原文页码方便用户溯源。这对企业知识库、政务知识库这类对溯源要求高的场景尤其重要。7. 批量预处理与工程化实践真实业务里不会有单独一个 PDF 等你处理通常是一整个目录甚至是一条持续更新的数据管道。工程化实践需要注意四个点目录分层、增量处理、失败重试、日志可追踪。一套推荐的目录结构data/ raw/ # 原始文件 extracted/ # 抽取出的页级文本 chunks/ # 最终 chunk json logs/ # 处理日志增量处理的思路是记录每个源文件的 hash。文件内容没变就不重新处理文件内容变了才更新对应 chunk。这样即使知识库新增文件也不会触发全量重建。import hashlib import json from pathlib import Path STATE_FILE Path(data/process_state.json) def file_hash(path: Path) - str: return hashlib.md5(path.read_bytes()).hexdigest() def load_state() - dict: if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text(encodingutf-8)) return {} def save_state(state: dict): STATE_FILE.write_text(json.dumps(state, ensure_asciiFalse, indent2), encodingutf-8) state load_state() for path in Path(data/raw).glob(*.*): current_hash file_hash(path) if state.get(path.name) current_hash: continue # 这里执行处理逻辑 state[path.name] current_hash save_state(state)如果希望把文档预处理封装成服务可以加一层 HTTP 接口输入文件路径和切片参数输出 chunk 列表。给一个 FastAPI 的骨架实际接口路径和参数需要按项目调整。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class PreprocessRequest(BaseModel): file_path: str chunk_size: int 500 overlap: int 100 app.post(/preprocess) def preprocess(req: PreprocessRequest): # 按实际项目实现解析文件 - 清洗 - 切分 - 返回 chunk return {status: ok, file_path: req.file_path}接口化的好处是上游可以把 PDF、PPT 直接传给预处理服务下游拿到 chunk 后统一写入向量库解耦整个知识库构建流程。8. 检索质量验证与评测思路重叠区设多少、按页切还是按标题切最终要回到一个问题上检索质量是否达标。推荐用三层评测法验证切分效果。第一层切片完整性抽查。人工阅读一批 chunk看每个 chunk 是否包含一个相对完整的知识点是否出现句子被拦腰截断的情况。这一层成本最低适合切分规则刚确定时先跑一遍。第二层检索召回测试。准备一批测试问题每个问题对应原文中正确答案所在的页码或 chunk ID跑检索看 top-k 是否命中正确答案。这一层直接反映切分和 embedding 组合的效果。第三层端到端问答测试。把检索到的 chunk 喂给 LLM看最终回答是否正确。这一层最接近用户真实体验但耗时也最高。下面是一个召回率测试脚本的骨架使用 sentence-transformers 做向量化用 numpy 计算余弦相似度import numpy as np from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3) def cosine_similarity(a, b): return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) def recall_at_k(question: str, doc_chunks: list[dict], correct_ids: set, k: int 5) - bool: q_vec model.encode(question) scored [] for chunk in doc_chunks: score cosine_similarity(q_vec, chunk[vector]) scored.append((score, chunk[id])) scored.sort(reverseTrue) return any(chunk_id in correct_ids for _, chunk_id in scored[:k])对比实验设计可以参考三组变量chunk_size256 / 512 / 1024overlap0 / 64 / 128切分方式按页切 / 按标题切 / 滑动窗口按句切每组实验使用同一套测试问题集和同一个 embedding 模型记录 Recall1 和 Recall5输出到 CSV 做横向对比chunk_size,overlap,split_method,recall1,recall5 256,0,page,0.42,0.68 256,64,page,0.45,0.71 512,128,sliding,0.51,0.78注意表格里的数字只是列名示例不代表任何真实实验结论。实际数值必须以你的语料和问题集为准。评测要做成可重复的固定流程否则每次换参数都靠感觉问题很难定位。9. 常见问题与排查方法文档预处理阶段的问题通常不会报明显的错误而是以“检索不到内容”的形式出现。下表覆盖了最常见的几类问题。问题现象可能原因排查方式解决方案PDF 抽取不到文本扫描版 PDF 或加密 PDF用 PDF 阅读器打开确认页面是否为图片先接 OCR或确认文件权限PPT 文本顺序混乱文本框按创建顺序返回未按坐标排序打印每个 shape 的 left/top 坐标按坐标排序后再拼接文本切分后句子断裂切分规则没有考虑句末标点抽查 chunk 首尾字符先按句子边界切再滑动窗口重叠区导致大量重复内容页眉页脚进入重叠区打印 overlap 包含的文本片段先清洗页眉页脚再做 overlap检索结果集中在同一段overlap 过大向量相似度过高查看 top-k 结果对应的页码分布减小 overlap或检索后做去重批量处理中途中断某个文件格式异常查看日志定位失败文件单文件 try/except失败重试换 embedding 模型后效果变差chunk 长度与新模型输入限制不匹配查看新模型文档和 max_seq_length重新评估 chunk_size10. 最佳实践与使用建议结合多个 RAG 知识库项目的常见问题这里整理几条适合起步阶段的实践建议。先做小规模人工切片检查再进入自动化批量流程。不要一开始就全量灌入数百个文件先挑 5~10 个有代表性的 PDF、PPT跑完切片后人工读一遍确认没有明显的文本乱序和语义断裂。保留原始文本、清洗后文本、最终 chunk 三级中间结果。后续发现问题时可以逐级回溯判断问题出在抽取、清洗还是切分环节。把切片配置记录下来。chunk_size、overlap、切分方式、清洗规则、使用的 embedding 模型这些参数组成了知识库的“配方”。换模型或调参数时可以直接对比新旧配置的效果。涉及人脸、声音、版权素材时必须确认授权。PDF、PPT 可能是出版物、内部文档或商业资料在企业知识库和政务知识库中要特别关注素材来源、敏感信息脱敏和发布边界。用真实业务数据做评测时建议先脱敏再入库。对政务、企业规章制度这类标题层级严格的文档不要机械按页切。先解析标题层级建立文本的标题树再按章节决定 chunk 边界效果通常比固定大小滑动窗口更好。检索策略要和切片策略一起考虑。如果业务中允许多条 chunk 同时进入 LLM重叠区可以适当小一些如果每次都只取单条 chunk 生成答案重叠区的价值会更高。11. 总结与下一步这篇文章最想传递的一个观点是重叠区和按页分割都不是独立的“玄学参数”它们服务于同一个目标——让每个 chunk 尽量成为一个完整的语义单元同时保留切点附近的上下文。如果你正要开始调 RAG 文档预处理建议第一件事不是调 overlap而是把一份典型的 PDF 和一份 PPT 跑通“抽取 - 清洗 - 切分 - 人工检查”这条链路先把中间产物看清。最容易踩的坑是把 PDF 的物理页码当成语义边界扫描版 PDF 不接 OCR 就灌入知识库。后续可以考虑扩展的方向包括自建标题检测模型、版面分析、表格结构化抽取以及把评测问题集做成自动回归测试每次改参数后自动比对 Recall 指标。