PDF解析与分块:决定RAG应用检索效果的核心环节
2026/9/5 4:12:01 网站建设 项目流程

先说结论:从PDF到可检索,中间那段路——解析和分块——才是决定整个RAG(检索增强生成)应用能不能用的核心环节。做知识库问答、企业文档检索、法律/学术资料库这类项目,方向基本绕不开三条线:PDF里有什么、想要的片段该切多大、用户问的时候系统能不能快速捞到。这篇文章我会按实际做项目的顺序展开,先拆PDF的结构特性,再讲解析工具怎么选,然后进入分块环节的策略对比,最后回到检索侧讲怎么把前面做的活儿真正用起来。全文基于我自己的项目复盘写,所有方案都是我实测过、验证过、也踩过坑的。

1. 为什么“PDF提取文本”不等于“文档可检索”

先说一个最容易被新入场的开发者忽略的问题:PDF这个东西,本质上不是给计算机读的,它是给打印机制造的。你用PDF阅读器看到的“某一页”,其实不是一页文字,而是一组坐标、字体、指令的集合。原生的文本型PDF虽然包含文本对象,但这些对象的排列完全是为了“视觉呈现”,不是为了“语义顺序”。

我见过太多团队卡在这件事上:辛辛苦苦把PDF里的字全抽出来了,结果一问系统就答非所问,以为是embedding(向量嵌入)模型选得不行,翻来覆去调参无效,最后才发现问题出在上游——PDF文本的读取顺序、表格结构、页眉页脚、多栏排版全乱套了。

1.1 PDF的“假电子文档”本质

拆开看,一份PDF文件内部其实有几个关键结构层:

  • 页面对象(Page):每个页面有明确的宽高、坐标系统;
  • 内容流(Content Stream):存的是绘图指令,比如“在(x,y)位置用14号字体画字符串‘你好’”,这个指令序列不代表阅读顺序;
  • 字体对象(Font):记录了字符编码与实际字形(Glyph)的映射关系;
  • 资源字典(Resources):存放图片、字体、颜色等被引用的资源。

这里的核心矛盾是:阅读器(比如Acrobat)靠坐标把字符渲染到页面上,你靠人眼阅读时自然按照视觉流扫描;但机器处理时如果直接按内容流的内部顺序取字符,拿到的很可能是乱序的碎片。特别是双栏排版、表格跨页、文字环绕图片三类场景,内容流的线性顺序和视觉阅读顺序几乎完全脱节。

1.2 检索系统要的是“语义定位”机器

做检索(不管是向量检索、BM25还是混合检索),最终目的都是:用户输入一个问题,系统从文档库里找出一段“与问题语义最相关、信息最完整的片段”。

这意味着系统需要的不只是“把字提出来”,而是:

  • 一个片段内有相对完整的语义;
  • 边界尽量贴合自然段落或逻辑小节;
  • 不丢失表格的行列关系;
  • 不被页眉页脚、水印、页码污染;
  • 能从片段回溯到原文的具体页码,方便定位。

如果只做“文本提取”而不做“结构解析”,就会面临:段落被截断、表格变成一串无意义的数字流、页眉“技术白皮书”反复出现在每个片段里。整条链路的效果就毁在第一步。

1.3 解析任务的三个层级

实战中我习惯把“从PDF到可检索”划分为三个层级,每个层级各有自己的评估标准:

层级做什么评估标准
文本层把PDF里的字符确实提出来,包括排版顺序字符无遗漏、顺序接近阅读顺序
结构层识别标题、段落、表格、页眉页脚、列表、图片块级划分准确率、表格行列还原度
语义层把结构块组合成适合检索的文档片段(chunk)片段语义完整度、检索命中率、问答准确率

很多人一上手就想直接搞定第三层,结果第一层都没过。这篇文章我先压低重心,把第一二层做扎实,第三层放进分块策略里重点讲。

2. 解析层工具选型:五类工具,各管一摊

PDF解析工具生态里没有“银弹”,选型的前提是知道每一类工具的擅长边界。我自己整理了一套“PDF五类难度划分法”,日常收到的PDF文件基本都能对号入座:

  1. 纯文本型PDF:如论文、说明书,文字可选可复制,字体是内嵌标准字体;
  2. 图文混排型:文字和图片穿插,有多个文本流;
  3. 表格密集型:财务报告、审批表、招标文件,信息encode在表格里;
  4. 扫描版/图片型:整页是扫描图片,没有文本对象;
  5. 表单型:政府表格、申请PDF,字段可交互或可填。

2.1 核心工具清单与对比

我实际项目中用到的工具,按用途分成五类:

工具擅长场景局限我的使用场景
PyMuPDF(fitz)文本提取、页面渲染为图片、文本坐标定位、链接提取复杂表格结构还原能力一般全流程主力,先跑它
pdfplumber文本、表格的高精度提取,按坐标切分性能较慢,超大文档吃力需要精细表格和坐标时用
Camelot规则型板式表格抽取为DataFrame对无边框表格、复杂嵌套表格效果差表格为主的PDF优先试它
Tesseract OCR扫描版文字识别对清晰印刷体效果好,手写和低分辨率拉胯扫描版兜底
PaddleOCR中文识别、版面分析、表格还原模型较重,部署起来有成本中文扫描版主力OCR

这只是一个大纲,实际选型不能光看表格。说几个我踩过的坑:

第一坑:PyMuPDF抽不出文字时不要直接上OCR。先查PDF是不是有“保护性加密”或者“字体子集化”。字体子集化是PDF压缩的常规操作,抽出的字符集是自定义编码,原生文本对象在技术上存在,但用fitz提取时会返回乱码或空。这种文件直接用OCR可以解决,但显卡显存和耗时成本远高于修复字体映射。能定位到字体映射的话,还是一条路子的。

第二坑:pdfplumber的表格识别不是万能的。它的表格识别逻辑基于“页面上画出来的线”,无边框表格它基本识别不了。我处理过一批企业年报,表格就没有任何竖线,pdfplumber直接把这些数据当普通文本流输出来,语义全乱了。这种场景要么用Camelot的lattice模式,要么在解析后加一层“基于Tab键值和坐标对齐”的启发式规则。

第三坑:OCR不是提取工具,它是最后的降级方案。文本型PDF几十毫秒能解析完一页,OCR一页要几百毫秒到几秒。全库扫描时这个成本差异是数量级的。所以最经济的路径永远是:先判定类型,再决定是否走OCR。

2.2 我做解析管线的默认组合

根据上面的工具对比,我默认的解析管线是这样的:

PDF文件 -> PyMuPDF 快速探测:页数、是否含文本对象、字体是否正常映射 -> 若含文本对象: -> PyMuPDF 按页面提取文本块(带坐标和字体信息) -> pdfplumber 补充表格结构(需要精细表格时启用) -> 后处理规则:去除页眉页脚/页码/水印,按坐标修复多栏顺序 -> 若无文本对象: -> PaddleOCR 整页OCR,输出带坐标的文字块 -> 坐标聚类,还原阅读顺序 -> 输出统一结构化的解析结果(JSON)

这套管线在多个文档库上跑了一年多,F1(块的分类准确率加表格还原率加权后的综合评分)能稳定在90%上下。当然不可能一套吃遍天下,PDF变化多,后面单开一节说动态调整策略。

3. 文本型PDF的解析编排:从“全文本”到“结构块”

大部分项目第一步收到的PDF,文本型占多数。这一节把文本型PDF的解析编排讲透,扫描版和OCR放后面。

3.1 页眉页脚和页码怎么剔除最省事

页眉页脚污染检索片段的问题,比很多人想象得严重。一个商业白皮书的页眉“XX公司机密文件”如果在每个页面都出现,向量检索可能会把这个词和正文内容混在一起,导致检索权重被带偏。

我的做法不是简单的“找页眉删掉”,而是用“重复内容检测+坐标定位”双保险:

  • 纵向看:同一个字符串出现在多页的相同/近似坐标区域,且内容完全相同,基本可以判定是页眉页脚;
  • 横向看:页面顶部10%和底部8%的文本块,单独拎出来做重复度检测;
  • 命中规则后,从正文块池子里剔除,但保留在元数据里(页面号、页眉是什么),方便后续需要时仍能追溯。

这一步做完,正文里不会再出现“第3页共9页”这种噪音,分块效果会立刻变好。

3.2 多栏PDF的文本顺序修复

多栏排版是解析顺序错误的第一大来源。解决思路不复杂,但工程上要细心:

  1. fitz.Page.get_text("dict")拿每个文本块的bbox(边界框坐标);
  2. 单页内按x坐标做聚类:文本块的中心点x值相近的归为一栏;
  3. 栏内按y坐标从上往下排序;
  4. 最后按“左栏全部读完后读右栏”的顺序输出文本流。
import fitz def fix_multi_column_order(page): blocks = page.get_text("dict")["blocks"] lines = [] for block in blocks: for line in block.get("lines", []): bbox = line["bbox"] x_center = (bbox[0] + bbox[2]) / 2 y_center = (bbox[1] + bbox[3]) / 2 text = "".join(span["text"] for span in line["spans"]) lines.append({"x": x_center, "y": y_center, "text": text}) # 按x聚类再用y排序,代码略

这段话的理解要点:PDF阅读器显示文本时是按指令顺序画的,并不会自动做多栏分栏;我们靠坐标相似度把文本行聚成“栏”,再按阅读习惯重排。这个简单规则在双栏论文、杂志页面上的准确率很高,遇到三栏甚至四栏的报纸页面也能跑,只是聚类参数要调。

3.3 表格解析:Markdown化才是正道

表格处理我踩过的大坑是:把表格里的每个单元格当作独立文本块输出,结果检索时完全丢失了行和列的上下文。比如:

产品价格库存
iPhone5999200
Android3999150

如果拆成六个孤立的字符串“iPhone”“5999”“200”,检索“iPhone价格”的时候,传统的向量检索根本拼不回“iPhone=5999”的关系。

我的方案是:把表格整体提取出来后,强制转换成Markdown表格结构。步骤如下:

  1. 先用pdfplumber或Camelot把表格区域识别成二维矩阵(行、列、单元格);
  2. 再将二维矩阵序列化为Markdown表格;
  3. 然后将这个表格文本作为一个独立chunk(或一个chunk的独立部分)进入后续检索。

这样即使被embedding,表格的行列关系在文本里也是显式的,模型能够更好地把握语义。

表格还原时还有个小技巧:如果表格有表头(表格第一行是字段名),单独把表头提出来作为该表格chunk的metadata,后续做语义检索时可以让“问题”和“表头”先做一次匹配,超实用。

3.4 扫描版PDF降级方案

扫描版PDF没有文本层,必须OCR。我实际使用的流程是:

  • 探测阶段:fitz读出的文本块几乎没有有效字符,且页面有/XObject类型的图片对象;
  • 触发OCR:整页转成PNG图片,分辨率不低于200dpi,送入PaddleOCR;
  • 输出处理:PaddleOCR的ocr()接口会返回带坐标的文字框,再走一遍坐标分栏和表格还原;
  • 重要:OCR的识别文本保留“置信度”字段,低于阈值的字让模型做二次校对,不要直接入库。

这里必须提醒:OCR在大库场景的成本很高,一定要有缓存。同一个PDF解析一次后把结果存成JSON或Pickle,下次直接读取。我们系统里OCR结果缓存命中率超过70%,大量重复文档浪费的算力就是这样省下来的。

4. 分块策略实操:切片大小、重叠与metadata注入

解析做完,你手头是一堆“结构块”(标题、段落、表格、列表)。下一步是决定怎么把这些块切分成适合检索的单元。这一步做得好不好,直接影响用户搜索的质量。

4.1 分块的三个目标

分块不是简单按字数切,它要同时满足三个目标:

  1. 语义完整:一个chunk最好是一个完整的论点、一条流程、一段描述,而不是半截话;
  2. 长度可控:chunk太长,embedding的注意力会分散;太短,又缺少上下文。一般500-800词(中文约1200-2000字)是相对稳的范围;
  3. 可回溯:每个chunk要能反查到原文页码和所属章节,方便用户人工核对。

4.2 四种主流分块策略对比

策略原理优点缺点适合场景
固定窗口按token数硬切,加overlap简单稳定切碎语义,段落中途断裂快速原型、内容格式统一的库
段落分段按段落标记分块语义完整度较好段落太长或太散时不均衡论文、规范、说明类文本
语义切分用embedding相似度找句子间“断点”边界语义自适应计算成本高,边界仍不稳定语义跳跃大、主题分散的文档
父子块/多级分块大块(父)负责检索,小块(子)负责回答兼顾上下文和精准度结构较复杂,需维护映射长文档、多级标题结构明显的内容

我日常做知识库用得最多的是段落分段+父子块混合:按段落边界基本定位,标题结构再生成父级块作为索引,子级块作为回答引用。如果文档本身没有清晰的段落标题,就退化成固定窗口策略,窗口大小按embedding模型的最优输入长度设置。

4.3 工程参数:chunk_size、overlap、metadata注入

分享一组我反复调之后觉得比较稳的默认参数(以中文文本、OpenAI/ChatGLM等常用embedding模型为例):

参数推荐值说明
chunk_size800-1200 tokentoken数比字符数更准确,中文一个token约为0.5-0.7个汉字
overlap150-250 token保留前后文,避免关键句被硬生生切断
段落保持尽量整段如果段落超过chunk_size,按句子边界切
表格块单独成块不混合到文字段落里,避免噪声
标题块作为父块父块包含整章节,子块指向父块

metadata注入是很多人忽略的环节。一个高质量chunk的metadata至少包含:

  • 来源文件名和版本号;
  • 该片段在原文中的页码(或多个页面的起止区间);
  • 所属章节路径(如“第3章/3.2节/表格3-1”);
  • 创建时间、文档类型;
  • 如果是表格,保留表头。

这里有个小技巧:不要把metadata加入到正文的embedding里,但一定要在向量检索后的后处理阶段可用。因为metadata里的“章节号”信息如果塞进正文,容易干扰语义;但检索结果做rerank时,metadata的路径信息对修复排序帮助极大。

常见的错误做法是直接把PDF解析结果怼进向量库,不带任何metadata,导致检索结果没办法定位到原文,用户看一眼就退出,体验极差。

5. 检索质量调优:让“前面的解析活”在检索侧发挥价值

解析和分块的最终目的,是让检索命中更准。这一节讲讲我在检索侧工作流里怎么使用前面解析出来的结构信息,可复现性很强。

5.1 先建评测集,再谈调优

不建评测集就调检索参数,等于闭眼开车。我的做法是:

  1. 从库里随机抽50-100个真实用户问题,加上10-20个我人为构造的“边角问题”(多栏、表格、专业术语简称);
  2. 人工标注每个问题对应的“正样本”原文片段(哪怕是一个段落级别的大块);
  3. 检索接口跑完,计算Hit Rate@5(前5个结果里是否有正样本)和MRR(倒数排名均值)。

注意:这个评测集不用大,但要持续维护,每次调整参数后重跑一遍。没有数据支撑的“我觉得变好了”,都是幻觉。

我做过一次印象很深的实验:评测集上线后,我把分块的overlap从128调到256,Hit Rate@10直接提升8.6%。原因很朴素——一个250字关键段落之前如果正好被切掉了一半,128 token的overlap补不回来,256就刚好能兜住。

5.2 混合检索:向量+关键词,两条腿走路

做RAG应用久了就会明白:向量检索擅长“语义相关”,但面对“专有名词精确匹配”时容易拉胯。比如专利号“CN202310123456.7”,embedding之后和普通文本混在一起,语义检索找不全。这时候BM25就能精准命中关键词。

实际工程中最稳的检索组合:

  1. 向量检索Top 30;
  2. BM25关键词检索Top 30;
  3. 两路结果合并去重,按Reciprocal Rank Fusion(RRF)排序;
  4. 取Top 10进入重排序(rerank)阶段;
  5. 最后把Top 3-5个chunk拼起来喂给LLM生成回答。

RRF的公式不复杂:score = sum(1/(k + rank))k通常取60。它不依赖得分归一化,直接把两条路的结果排名融合,工程上最省心。

5.3 查询改写:用户问的,不一定是他真正想问的

用户输入“多栏PDF怎么处理”这种问题,如果用原文“PDF双栏排版解析”去检索,词面上就差挺远,向量倒还能兜住,但BM25会直接拉胯。所以我的检索环节前加了一个轻量级的查询改写模块,用LLM把用户问题做三个处理:

  • 去掉问句中的废话(“请问”“有没有”“呢”之类);
  • 补全缺失的限定词(把“它”替换成前一句的实体名);
  • 扩展同义词(“分栏”->“多栏/双栏/column layout”)

改写后的查询文本同时喂给向量检索和BM25,实测命中率提升非常明显,尤其对口语化提问、专业术语变体多的情况。

5.4 rerank的必要性

向量检索出来Top 30里,真正能被LLM使用的可能只有前5。中间夹了一堆“看起来相关但其实是背景介绍”的文本。

所以我的流程里必须有rerank:用一个交叉编码器(cross-encoder)替代双塔结构的回归打分,对候选chunk和查询做完整编码。这个模型虽然推理速度慢,但精度高,只对Top 30跑一遍完全没问题。对应工具库推荐FlagEmbeddingBGE-Reranker系列,或者MiniLM变体,都是社区验证过的开箱即用选项。

6. 拿过去就能用的落地细节:输出结构、参数与踩坑清单

到这节,全部核心思路已经讲完,接下来是最容易被直接“抄作业”的部分:解析结果的标准输出结构、分块参数速查表、以及实战中反复踩到的坑。

6.1 标准输出Schema参考

建议所有PDF解析结果统一用JSON结构存储,便于后续接向量库、做缓存、做审计:

{ "doc_id": "uuid", "source_file": "xxx.pdf", "page_count": 12, "parsed_pages": [ { "page_no": 1, "blocks": [ { "block_type": "heading|paragraph|table|image|list", "bbox": [0, 0, 595, 842], "text": "解析后的文本/表格Markdown", "table_data": "二维数组,非表格时为空", "confidence": 0.98 } ] } ], "chunks": [ { "chunk_id": "xxx", "page_start": 1, "page_end": 2, "parent_chunk_id": null, "text": "分块后的完整文本", "metadata": { "section": "第3章/3.2节", "table_header": "产品价格库存" } } ] }

这个schema里,blocks是解析层的输出,chunks是分块层的输出,两者分开存。后续如果分块策略要改,不需要重新解析PDF,只要重新跑分块就行。

6.2 分块参数速查表

不同场景下的分块参数我整理如下,可直接套用:

文档类型策略chunk_sizeoverlap备注
论文段落分段+父子块800-1000150父块用章节标题,子块按段落
合同固定窗口+句子边界600-800128关键词命中优先提升权重
产品手册按“功能点”拆分1000-1200200表格单独成块
年报段落+表格混合800256表格用Markdown化
扫描版历史档案OCR后固定窗口500100OCR置信度低于0.9的片段降权

6.3 容易反复踩的坑清单(浓缩版)

下面这些坑,每一个都是我或我的项目组在真实生产环境踩过、并且花了不少时间填的:

现象解决方案
字体子集化文本型PDF提取出乱码先检测字体映射,再决定是否走OCR
多栏顺序错乱提取文本语义断成碎片用坐标聚类重排栏顺序
扫描版误判有少量文本却局部是图页面级文本量阈值+图片占比综合判断
页眉页脚污染chunk里全是“公司名称”重复性检测+坐标区域排除
表格拆碎数字和表头对不上表格强制Markdown化、独立成块
chunk过长检索结果包含无关信息chunk_size上限+句子边界
metadata丢失检索结果无法回溯页码分块阶段强制写入页码和章节路径
评测缺失调参像掷骰子建小规模评测集,固定指标测算

6.4 最后分享一个我自己项目里的小技巧

这条技巧不是技术,是工程经验:在解析与分块阶段,永远优先保证“可回滚”和“可审计”

也就是说,最初跑通一个最小可用的解析-分块-检索闭环后,先别急着堆大库,而是抽20份文档,人工逐页核对解析结果。很多PDF本身的“脏”程度远超过你初步抽样时看到的。多数情况你会发现:不是工具不行,而是PDF文件的排版本身太混乱,机器不可能百分百复现人眼阅读顺序。这一步人工核对后,再决定哪些规则需要正则、哪些需要坐标逻辑、哪些需要OCR。

我始终认为,PDF解析和分块的终点不是“把文本提出来”,而是“把文本变成计算机能理解、检索系统能用、用户能找到的东西”。

这个项目最大的收获,是我终于明白了一个简单的道理:文档解析做得好,后面的检索和生成都会变得异常轻松;解析做不好,再贵的模型也白搭。这套“解析+分块+检索”的闭环思路,我在很多项目和团队里复制过,核心就是先把上游做扎实,下游的效果就能水到渠成。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询