ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

RAG文档预处理实战:PDF/PPT按页分割与重叠区设计详解 做过 RAG 知识库项目的同学应该都有同感真正决定检索效果上限的往往不是模型选得有多新而是文档预处理做得够不够细。早期我在做本地知识库时把大量时间花在调 embedding 模型和向量库参数上结果召回效果一直不稳定。后来把重心移到“文档加载—清洗—切片—重叠区”这一层问题才逐渐清晰起来。这篇文章就围绕 RAG 文档预处理中最容易让人纠结的两个问题展开PDF、PPT 到底该不该按页分割重叠区overlap到底设多少才算合理我会结合 txt、Word、PDF、PPT 四种常见格式给出完整的预处理设计思路和可运行的 Python 示例。文章适合正在搭建知识库的开发者阅读也适合准备 RAG 相关面试、需要系统梳理文档切片方案的同学。读完你会掌握以下内容txt、Word、PDF、PPT 各自的加载与清洗规则chunk size 和 chunk overlap 的设计原理按页分割的适用场景与反例一套完整的、可扩展的多格式文档预处理 Pipeline。1. 背景与核心概念1.1 文档预处理在 RAG 中的地位RAGRetrieval-Augmented Generation检索增强生成的标准流程可以简化为文档加载 - 文档解析 - 数据清洗 - 文档切片 - 向量化 - 召回 - 重排 - 生成。很多人在搭建 RAG 知识库时把精力集中在“向量库怎么选”“embedding 模型哪个好”“要不要上 rerank”但忽视了最前端的一个事实如果输入给 embedding 模型的文本本身就是破碎的、边界不合理的那么向量化后的质量一定不会好。文档预处理决定的是知识库质量的上限后面的模型和参数只是在逼近这个上限。这也是为什么像 Dify 这类工具在落地政务、企业知识库项目时文档加载和分段规则往往是实施团队花时间最多的地方。RAG 项目做得好不好不是看 Demo 效果而是看在混合格式文档、复杂表格、扫描件面前还能不能稳定工作。1.2 为什么“怎么切”比“怎么存”更影响效果向量检索的核心原理是把文本转换成高维向量通过余弦相似度等指标找到与查询最相近的文本块。这意味着一次检索的最小单元不是整个文档而是“文本块”chunk。这就带来两个直接后果如果文本块太大向量会包含太多无关信息查询向量与文本向量的相似度会被稀释检索精度下降如果文本块太小单个块可能无法表达一个完整语义单元关键实体和上下文关系容易丢失。更关键的是“切在哪”的问题。假设有一段会议纪要是这样写的本次会议决定将华东区销售目标调整为 3500 万元调整原因是一季度华东区新增客户数量超出预期。如果切片工具恰好把“3500 万元”和“调整原因是一季度华东区新增客户数量超出预期”切成两块那么当用户提问“华东区销售目标为什么调整”时两个块里单独看都不是完整的答案。这就是典型的“语义边界被切断”问题。重叠区只是在一定程度上缓解这种问题并不能完全取代“合理选择分隔符”和“判断语义边界”这两个动作。1.3 按页分割与重叠区的基本概念按页分割是指以 PDF 的物理页面或 PPT 的幻灯片为天然边界把每一页作为一个基础切片单元。重叠区是指在相邻两个文本块之间让后一个块重复包含前一个块尾部的一部分内容。例如 chunk_size 400chunk_overlap 80就表示每个块保留前一个块最后 80 个字符的文本。需要强调的是按页分割和重叠区不是二选一的关系。按页分割解决的是“边界来源”的问题重叠区解决的是“边界断裂”的问题。两者完全可以组合使用。2. 文档加载预处理规则txt、Word、PDF、PPT 应该怎么做2.1 先统一加载再统一清洗文档预处理的第一步不是切片而是把不同格式的文档统一加载成纯文本。这一步看似简单实际坑很多txt 文件有编码问题常见的是 UTF-8、GBK、GB18030Word 文件不仅有段落还有表格、页眉页脚、批注PDF 文件是“重灾区”有文本型 PDF、扫描型 PDF、表格型 PDF解析难度完全不同PPT 文件的文字分散在多个文本框中加载顺序可能和视觉顺序不一致。在设计预处理 Pipeline 时我建议把“加载器”做一层统一抽象# 加载器统一返回一个包含“文本 来源信息 结构信息”的对象 dataclass class RawDocument: source: str # 文件路径或来源标识 content: str # 提取出的纯文本 metadata: dict # 其他元信息如页码、章节、页数这样做的目的是后续切片、清洗、向量化逻辑只需要依赖 RawDocument 这个统一结构不用关心原始文件是 PDF 还是 PPT。2.2 txt 与 Word段落、表格与编码问题txt 文件的处理相对简单但要注意编码。推荐使用charset-normalizer或先尝试 UTF-8再回退到 GB18030# 文件路径loader_txt.py from pathlib import Path def load_txt(file_path: str) - RawDocument: path Path(file_path) raw_bytes path.read_bytes() # 先尝试 UTF-8失败后尝试 GB18030 try: content raw_bytes.decode(utf-8) except UnicodeDecodeError: content raw_bytes.decode(gb18030) return RawDocument( sourcestr(path), contentcontent, metadata{type: txt, charset: detected} )Word 文档建议使用python-docx库。需要注意两点Word 中的段落通过document.paragraphs遍历表格需要通过document.tables单独遍历并把表格转换为带分隔符的文本否则表格内容会在常规段落提取中丢失。# 文件路径loader_docx.py from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph def iter_block_items(parent): 按文档顺序遍历段落和表格 from docx.oxml.ns import qn parent_elm parent.element.body for child in parent_elm.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, parent) elif child.tag qn(w:tbl): yield Table(child, parent) def load_docx(file_path: str) - RawDocument: doc Document(file_path) parts [] for block in iter_block_items(doc): if isinstance(block, Paragraph): if block.text.strip(): parts.append(block.text.strip()) else: # 表格转为 Markdown 风格文本 for row in block.rows: cells [cell.text.strip().replace(\n, ) for cell in row.cells] parts.append( | .join(cells)) return RawDocument( sourcefile_path, content\n.join(parts), metadata{type: docx} )2.3 PDF难点在于“解析质量”而不是“切分”PDF 是所有格式中最复杂的。初次接触 PDF 解析的同学很容易把精力放在“如何切页”上实际上 PDF 解析最影响后续效果的是“这一页能不能提取出完整、有序的文字”。可以按下面的方式判断 PDF 类型文本型 PDF文字可以选择、复制直接用pypdf或PyMuPDF提取即可扫描型 PDF本质是图片需要 OCR光学字符识别常见方案有 PaddleOCR、Tesseract表格型 PDF文字可以提取但表格结构容易乱需要额外处理或用专门的 PDF 转 Word 工具辅助。需要注意的是OCR 涉及文档内容合规问题。如果是企业内部文档、政务文档需要确认文档本身有合法处理授权并且处理后要注意脱敏与权限控制。基础文本型 PDF 按页提取# 文件路径loader_pdf.py from pypdf import PdfReader def load_pdf_by_page(file_path: str) - list[RawDocument]: reader PdfReader(file_path) docs [] for i, page in enumerate(reader.pages): text page.extract_text() if text and text.strip(): docs.append(RawDocument( sourcefile_path, contenttext.strip(), metadata{type: pdf, page: i 1} )) return docs在实际处理时我会用PyMuPDF的场景更多一些因为它在处理带注释、双栏排版和文字位置信息时更灵活。不过pypdf足够演示核心思路下面实战案例也以它为主。2.4 PPT文字散落在线框里不能只 extract_textPPT 的解析和常规文档差别很大。常见的坑是一页幻灯片里有标题、正文、备注文字分布在多个文本框中如果只是简单拼接可能出现“第二块文字跑到了第一块文字前面”的问题。python-pptx是处理 PPTX 文件的标准库。处理规则是遍历每一页幻灯片slide遍历该页的所有形状shape只处理有文本框的形状默认按照形状在页面上的纵向位置top排序保证文本顺序接近视觉顺序把每页的文本框内容拼成一个文本块可选提取备注页notes slide内容与正文合并。# 文件路径loader_pptx.py from pptx import Presentation def _extract_shapes_text(slide) - str: 按纵向位置排序提取一页幻灯片中所有形状文字 texts [] for shape in slide.shapes: if shape.has_text_frame: text shape.text_frame.text.strip() if text: # top 属性用于保持视觉顺序 texts.append((shape.top or 0, text)) texts.sort(keylambda x: x[0]) return \n.join(t[1] for t in texts) def load_pptx_by_slide(file_path: str) - list[RawDocument]: prs Presentation(file_path) docs [] for i, slide in enumerate(prs.slides): body_text _extract_shapes_text(slide) notes_text if slide.has_notes_slide: notes_text slide.notes_slide.notes_text_frame.text.strip() content body_text if notes_text: content content \n[备注]\n notes_text docs.append(RawDocument( sourcefile_path, contentcontent.strip(), metadata{type: pptx, slide: i 1} )) return docs3. 分块策略与重叠区设计原理3.1 chunk size 怎么定chunk size文本块大小并没有一个万能值。它需要同时考虑三方面的约束约束来源影响Embedding 模型输入向量模型通常有最大 token 限制例如 512、1024 等大模型上下文窗口检索结果会拼进 Promptchunk 太大会挤占上下文空间业务问题类型精确问答需要小 chunk总结类任务需要大 chunk常见的经验范围如下chunk size按字符约算适用场景200 - 400短句问答、FAQ、精确检索500 - 800大多数企业内部知识库、文档问答800 - 1200总结、综述、长文本理解这里有一个更准确的建议如果你使用的 embedding 模型最大输入是 512 tokens那么单个 chunk 的 token 数不应超过 512通常建议控制在 256 - 400 tokens 之间。注意 token 数不完全等于字符数中文字符与 token 的换算关系大约是 1 个汉字约等于 1 到 2 个 token具体取决于分词器。3.2 为什么需要重叠区语义边界必须连续先看一个例子。假设原文是该项目二期工程将于 2025 年 6 月正式启动总投资金额为 2.3 亿元主要用于扩建生产线和升级数字化系统。如果切片刚好在“2.3 亿元”后面切断文本块 1 是该项目二期工程将于 2025 年 6 月正式启动总投资金额为 2.3 亿元用户提问“二期工程的投资预算用途是什么”时文本块 1 里没有“用途”信息文本块 2 里又没有“2.3 亿元”这个主体两个块单独召回都不是完整答案。重叠区的本质是以少量信息冗余换取语义完整性。它让相邻两个文本块在边界处共享一小段上下文降低了“跨块语义依赖”导致的召回丢失概率。3.3 重叠区该设多少重叠区的大小通常用 chunk_overlap 参数表示常见推荐值是 chunk_size 的 10% 到 20%。举个例子chunk_size 500 # 每个文本块目标长度字符数 chunk_overlap 80 # 与前一个块重叠的字符数但重叠区并不是越大越好。设计时要注意以下三个问题重叠区过大会导致相邻文本块的向量非常相似检索时可能召回多块内容几乎相同的文本造成信息冗余重叠区过大会使总体文本块数量膨胀索引占用增大写入和检索性能下降重叠区过大embedding 结果会趋向“平均化”反而可能模糊块的核心主题。我的建议是不要单独调 overlap而是结合分隔符优先级一起设计优先按段落、标题、列表项等自然语义边界切分只在自然边界切完后对仍超过 max_chunk_size 的大块做强制切分对强制切分产生的新边界再通过重叠区做补偿。3.4 按页分割 vs 滑动窗口按页分割和滑动窗口是两种不同的切块路径切分方式原理适用场景按页分割以 PDF 物理页 / PPT 幻灯片为边界页面本身是独立语义单元时滑动窗口按固定长度连续切分可加重叠区连续叙事型文档、书籍、论文混合策略先按页粗切再对超长页做滑动窗口综合型文档、手册后面两个章节会专门分析 PDF 和 PPT 各自的场景这里先把概念理清。4. PDF 按页分割的适用场景与实现4.1 适用场景分析我在项目中总结出一个判断方法随便打开 PDF 的某一页如果这一页离开前后页依然能表达一个相对完整的问题或结论那么它就适合按页分割。反之如果每一页都只是上一页的延续按页分割就会产生大量残缺句。适合按页分割的 PDF产品宣传册、操作手册通常一页就是一个功能模块规章制度、公告通知不同条款分布在页面上页边界基本对齐条款边界PPT 导出的 PDF每页本身就是一张幻灯片语义独立课件、培训材料一页讲一个知识点非常适合按页切片。不适合按页分割的 PDF论文正文一个段落跨两页的情况非常常见书籍、小说章节和段落连续性很强合同、标书条款之间有强关联按页切会破坏条款完整性。4.2 代码实现pypdf 按页提取并生成 chunk下面是一个更完整的实现按页提取 PDF 文本并把每个页面作为一个基础 chunk额外附带页码元数据# 文件路径split_pdf_by_page.py from pypdf import PdfReader def split_pdf_by_page(pdf_path: str, min_chars_per_page: int 20): 按页切分 PDF。 参数: pdf_path: PDF 文件路径 min_chars_per_page: 每页最少字符数低于阈值则跳过通常是空白页 返回: list[dict]: [{ text: str, metadata: {page: int} }] reader PdfReader(pdf_path) chunks [] for idx, page in enumerate(reader.pages): text page.extract_text() or text text.strip() if len(text) min_chars_per_page: continue chunks.append({ text: text, metadata: { source: pdf_path, page: idx 1, page_total: len(reader.pages) } }) return chunks if __name__ __main__: result split_pdf_by_page(demo.pdf) for item in result[:3]: print(f[第 {item[metadata][page]} 页]) print(item[text][:100]) print(---)运行后会看到每一页的文本被单独提取为一条记录并且带上了页码。这个结构可以直接对接下游的向量化逻辑。这里要注意page.extract_text()在不同 PDF 库中的效果差别较大。如果你的 PDF 提取出来是空字符串或者乱码首先怀疑 PDF 是扫描件或者使用了非标准字体编码。4.3 结合重叠区的 PDF 处理严格按页分割有一个天然问题某一页末尾的一句话可能和下一页开头组成完整语义。这时候可以设计一个“页级重叠”策略让后一页的文本块包含前一页末尾的若干字符。实现思路# 文件路径split_pdf_with_overlap.py from pypdf import PdfReader def split_pdf_with_page_overlap(pdf_path: str, overlap_chars: int 60): reader PdfReader(pdf_path) pages_text [] for page in reader.pages: text (page.extract_text() or ).strip() if len(text) 20: pages_text.append(text) chunks [] for i, text in enumerate(pages_text): # 前一页末尾的 overlap 内容拼接到当前页开头 prefix if i 0 and overlap_chars 0: prefix pages_text[i - 1][-overlap_chars:] \n chunks.append({ text: prefix text, metadata: { source: pdf_path, page: i 1, has_overlap: i 0 } }) return chunks这个方案适合“页面基本独立但偶尔有跨页句子”的 PDF。需要注意的是overlap 拼接的位置要放在“当前页文本”前面而不是后面这样才能保证检索到后半段内容时前文线索也在同一个块内。5. PPT 按页分割的适用场景与实现5.1 PPT 与 PDF 预处理的核心差异PPT 和 PDF 虽然都可以按页/按幻灯片分割但两者有本质区别PPT 每页信息密度更低一页可能只有几十个字直接单独作为 chunk语义信息往往不够PPT 的文字分布在多个文本框提取顺序和视觉顺序可能不一致PPT 的备注notes通常包含演讲者补充的信息对理解正文非常有价值不能忽略PPT 中经常有图片、图表如果不做多模态处理图片内的信息会直接丢失。因此PPT 的预处理策略通常不是“每页一个 chunk”而是“以页为最小单元按组聚合”。5.2 代码实现python-pptx 逐页提取文字与备注下面的代码会提取每页幻灯片的全部文字和备注返回一个按幻灯片编号排列的序列# 文件路径split_pptx_by_slide.py from pptx import Presentation def extract_pptx_by_slide(pptx_path: str): prs Presentation(pptx_path) slides_data [] for slide_idx, slide in enumerate(prs.slides, start1): shape_texts [] for shape in slide.shapes: if not shape.has_text_frame: continue text shape.text_frame.text.strip() if text: shape_texts.append(text) # 备注提取 notes_text if slide.has_notes_slide: notes_frame slide.notes_slide.notes_text_frame if notes_frame: notes_text notes_frame.text.strip() combined \n.join(shape_texts) if notes_text: combined combined \n[演讲备注]\n notes_text slides_data.append({ slide: slide_idx, text: combined, shape_count: len(shape_texts), has_notes: bool(notes_text) }) return slides_data if __name__ __main__: data extract_pptx_by_slide(demo.pptx) for item in data: print(f 第 {item[slide]} 页 ) print(item[text][:200]) print()这个函数的核心点是所有 shape 的文本被拼接进同一页的文本中备注被单独标记出来而不是无声无息地混入正文。5.3 PPT 适合的“按页合并”策略在实际知识库项目中PPT 每页通常只有不到 100 个字符。如果直接把每页作为向量检索单元检索到的信息量太少大模型无法基于该 chunk 给出高质量回答。推荐两种策略策略一按固定页数合并# 文件路径merge_pptx_slides.py def merge_slides(slides_data: list, group_size: int 3): 将幻灯片按 group_size 页合并为一个 chunk。 例如 group_size3则第 1-3 页合并第 4-6 页合并。 merged [] for i in range(0, len(slides_data), group_size): group slides_data[i:i group_size] text_parts [] for item in group: text_parts.append(f【第 {item[slide]} 页】\n{item[text]}) merged.append({ text: \n\n.join(text_parts), metadata: { type: pptx, start_slide: group[0][slide], end_slide: group[-1][slide] } }) return merged策略二语义合并先复用第 5.2 节的提取逻辑然后用“页主题是否相同”来判断是否合并。简单做法是检查相邻页是否有相同的关键词、是否有标题延续。如果两页都出现同一个专有名词大概率是同一个主题模块。我个人更推荐第二种思路但工程上固定页数合并更简单、更好维护。实际项目中可以先用固定页数合并再用评估集验证效果决定是否需要上语义合并。6. 完整实战多格式文档统一预处理 Pipeline本章给出一个完整的示例把 txt、Word、PDF、PPT 四种格式统一处理输出适合向量化的 chunk 列表。这个 Pipeline 可以直接作为 RAG 知识库的预处理脚本骨架。6.1 需求与设计输入一个目录里面混合存放多种格式文档。输出一个 JSON 文件每条包含id、text、metadatametadata 里记录来源文件、类型、页码/幻灯片编号、chunk 序号。整体流程输入目录 - 按文件扩展名分发加载器txt / docx / pdf / pptx - 统一转为 RawDocument 列表 - 选择合适的切片策略 - 生成 chunk 列表 - 输出 JSON6.2 项目结构rag_preprocess/ ├── main.py ├── loader_txt.py ├── loader_docx.py ├── loader_pdf.py ├── loader_pptx.py └── splitter.py6.3 加载器统一接口我们把前面几章的加载器集中到一个文件里并统一返回 RawDocument 列表。# 文件路径main.py部分内容后续连续补充 from dataclasses import dataclass, field from pathlib import Path import json from loader_txt import load_txt from loader_docx import load_docx from loader_pdf import load_pdf_by_page from loader_pptx import load_pptx_by_slide dataclass class RawDocument: source: str content: str metadata: dict field(default_factorydict) def load_document(file_path: str) - list[RawDocument]: 根据扩展名分派到对应加载器返回 RawDocument 列表 ext Path(file_path).suffix.lower() if ext .txt: return [load_txt(file_path)] if ext .docx: return [load_docx(file_path)] if ext .pdf: return load_pdf_by_page(file_path) if ext .pptx: return load_pptx_by_slide(file_path) raise ValueError(f不支持的格式: {ext})6.4 分割器与重叠区参数下面实现一个简单的滑动窗口分割器。它接收一篇长文本按照max_chunk_size和overlap_size切分。同时它先尝试按换行符和句号切保证切出来的块尽量完整。# 文件路径splitter.py import re def split_text_with_overlap( text: str, max_chunk_size: int 500, overlap_size: int 80 ) - list[str]: 滑动窗口分割文本并优先保留自然语义边界。 策略 1. 先将文本按段落换行拆开 2. 如果段落很短就累积到接近 max_chunk_size 3. 如果单个段落很长再按句号拆 4. 最终每个块之间保留 overlap_size 字符的重叠。 paragraphs [p.strip() for p in re.split(r\n, text) if p.strip()] chunks [] current_chunk for para in paragraphs: if len(para) max_chunk_size: if current_chunk: chunks.append(current_chunk) current_chunk # 长段落内部按句切 sentences re.split(r(?[。;]), para) buffer for sent in sentences: if not sent.strip(): continue if len(buffer) len(sent) max_chunk_size and buffer: chunks.append(buffer) # 保留重叠内容 buffer buffer[-overlap_size:] sent else: buffer sent if buffer: chunks.append(buffer) else: if len(current_chunk) len(para) max_chunk_size and current_chunk: chunks.append(current_chunk) current_chunk current_chunk[-overlap_size:] para \n else: current_chunk para \n if current_chunk: chunks.append(current_chunk) return [c.strip() for c in chunks if c.strip()] def split_documents(raw_docs: list[RawDocument], max_chunk_size: int 500, overlap_size: int 80) - list[dict]: 将 RawDocument 列表转为 chunk 字典列表 chunk_list [] chunk_id 0 for doc in raw_docs: parts split_text_with_overlap(doc.content, max_chunk_size, overlap_size) for part in parts: chunk_id 1 metadata dict(doc.metadata) metadata[source] doc.source metadata[chunk_index] chunk_id chunk_list.append({ id: fchunk_{chunk_id}, text: part, metadata: metadata }) return chunk_list这个分割器的关键点在于段落优先只要段落本身不超过 max_chunk_size就优先保持段落完整长段二次切分只有超过上限的段落才强制按句切重叠区兜底在所有新边界处用 overlap_size 补偿语义损失。6.5 主流程与运行验证现在编写 main.py 的主体部分# 文件路径main.py完整版示例 from pathlib import Path import json from dataclasses import dataclass, field from loader_txt import load_txt from loader_docx import load_docx from loader_pdf import load_pdf_by_page from loader_pptx import load_pptx_by_slide from splitter import split_documents dataclass class RawDocument: source: str content: str metadata: dict field(default_factorydict) def load_document(file_path: str) - list[RawDocument]: ext Path(file_path).suffix.lower() if ext .txt: return [load_txt(file_path)] if ext .docx: return [load_docx(file_path)] if ext .pdf: return load_pdf_by_page(file_path) if ext .pptx: return load_pptx_by_slide(file_path) raise ValueError(f不支持的格式: {ext}) def process_directory(input_dir: str, output_file: str): all_docs [] for file_path in Path(input_dir).rglob(*): if not file_path.is_file(): continue if file_path.suffix.lower() not in {.txt, .docx, .pdf, .pptx}: continue try: docs load_document(str(file_path)) all_docs.extend(docs) print(f加载成功: {file_path}共 {len(docs)} 个基础文档块) except Exception as e: print(f加载失败: {file_path}原因: {e}) chunks split_documents(all_docs, max_chunk_size500, overlap_size80) print(f共生成 {len(chunks)} 个 chunk) with open(output_file, w, encodingutf-8) as f: json.dump(chunks, f, ensure_asciiFalse, indent2) if __name__ __main__: process_directory(./docs, ./output_chunks.json)运行命令mkdir docs # 将你的测试文档放入 docs 目录后执行 python main.py预期输出类似加载成功: docs/产品手册.pdf共 12 个基础文档块 加载成功: docs/内部培训.pptx共 20 个基础文档块 加载成功: docs/需求文档.docx共 1 个基础文档块 加载成功: docs/说明.txt共 1 个基础文档块 共生成 45 个 chunk生成的output_chunks.json可以直接导入向量数据库也可以先做一轮人工抽检确认切片质量。7. 常见问题与排查清单文档预处理过程中最常见的几个问题我整理成一个表格问题现象常见原因解决思路PDF 提取出来的文字乱码PDF 使用非标准字体编码或本身就是扫描件尝试 PyMuPDF扫描件改用 OCRPDF 按页切分后回答不完整页面之间的语义依赖被切断页级重叠区或改用滑动窗口PPT 提取后文本顺序错乱文本框顺序不是视觉顺序按 shape.top 排序PPT 每页单独检索效果差单页信息量太少按 2-5 页合并为一个 chunk加了重叠区后召回结果重复overlap 过大导致相邻 chunk 相似度过高减小 overlap 比例到 10% 左右表格内容被切碎表格在 Word/PDF 中没有被完整提取先转成 Markdown 表格再切片txt 文件中文乱码文件编码不是 UTF-8尝试 GB18030 解码下面针对三个高频问题做展开说明。7.1 PDF 中文乱码可以先判断 PDF 的文字是否可复制。如果复制出来是乱码说明字体编码是自定义的pypdf无法正确映射。此时可以用 PyMuPDFfitz再试一次它对中文支持更好import fitz # pymupdf def extract_pdf_with_pymupdf(pdf_path: str): doc fitz.open(pdf_path) for page_num, page in enumerate(doc, start1): text page.get_text(text) if text.strip(): yield page_num, text.strip()如果 PyMuPDF 也提取不到有效文字那就是扫描件需要 OCR。7.2 按页切分导致答案不完整这属于典型的“切块粒度与业务问题不匹配”。解决办法不是立刻调整 overlap而是先检查召回结果如果召回的是“相邻两页但都不是完整答案”考虑页级重叠如果召回的是“某一页中的残缺句”考虑把 chunk size 扩大到 800-1000让每页单块覆盖更多内容如果答案是跨多页的考虑“页组合 滑动窗口”混合方案。7.3 重叠区导致检索重复有些同学把 overlap 设置到 200-300 字符结果检索返回的 4 个结果里有 3 个都在重复同一段内容。这通常是因为 overlap 占比过高。建议经验值是 chunk_size 的 10%-20%且优先保证“自然语义边界完整”而不是盲目追求大 overlap。8. 最佳实践与工程建议8.1 预处理必须与评估联动切片参数不能靠拍脑袋定。建议准备一个小规模评估集包含 20-30 个典型业务问题每个问题标注正确答案所在的文档和页码。每调整一次 chunk size、overlap、分割策略就跑一遍召回评估对比召回率和命中答案的完整度。如果你在面试中遇到“RAG 测评怎么做”这类问题也可以这样回答先构建评估集再对比不同预处理策略下的召回率和生成准确率最后选择一个在业务场景上表现最稳定的配置。8.2 保留结构化元数据元数据是 RAG 预处理中最容易被低估的部分。一个 chunk 如果只保留 text 而没有来源后期定位问题会非常痛苦。建议至少保留source文件路径chunk_index文档内序号page / slide页码或幻灯片编号文档类型pdf/pptx/docx/txt生成的目录或章节标题如果能提取这些元数据在后续可以做检索后过滤filtered searchHybrid Search 的加权依据回答时引用来源提升可信度。8.3 安全与权限管理企业知识库、政务 RAG 项目中文档预处理阶段就要考虑权限问题。未授权文档不能被加载OCR 后的文本可能包含敏感信息向量库本身也需要做访问控制。正确的流程是文档进入预处理前先经过权限校验处理完成后在元数据中保留文档密级或部门信息检索阶段根据用户权限过滤结果。8.4 依赖版本锁定Python 的文档处理库变化较快比如pypdf的 API 在不同版本间有差异pptx和python-docx的调用方式相对稳定但也建议锁版本。在项目初期就使用 requirements.txt 或 Poetry 锁定依赖避免“本地能跑部署跑不了”的问题。下面是一个参考依赖清单pypdf4.0 python-docx1.1 python-pptx0.6.23 pymupdf1.23版本需要根据你的项目实际情况调整以上只是一个通常可用的组合。到这里关于 RAG 文档预处理的核心内容已经完整过了一遍。从加载规则、切片策略、重叠区设计到 PDF 与 PPT 的按页分割场景判断再到一个可直接扩展的多格式预处理 Pipeline整个链路都是围绕“让检索召回更稳”这一目标展开的。如果你正在做 RAG 知识库建议把这份代码跑通之后先拿自己的业务文档做一个小规模评估再决定是走“按页分割”还是“滑动窗口重叠区”的路线。两种方案没有绝对优劣只有适不适合当前文档类型和业务场景的区别。
返回列表