做过几个真实 RAG 项目后,我越来越确认一个反直觉的结论:决定知识库效果上限的,往往不是模型选得多好、Embedding 多强,而是最不起眼的“数据入口”——尤其当语料里塞满 PDF 和图片时。文本类数据还好,Markdown 和纯文本基本白送;一旦碰上扫描件、论文双栏、复杂表格、图文混排,PDF 解析直接崩坏,向量库里全是残次品。这篇是“RAG 数据导入与解析全攻略”第二篇,专门聊图文与 PDF 解析:OCR 方案怎么选、多模态大模型何时出手、九种 PDF 工具各自适合什么场景。后面所有结论都来自我实际跑过的测试和项目,可以直接照着用。
1. 为什么 PDF 和图文是 RAG 知识库最普遍的瓶颈
1.1 “垃圾进、垃圾出”在知识库场景的放大效应
RAG 项目的完整链路里,数据解析是离模型最远、却最能决定成败的一环。很多团队把精力花在调 Embedding、换向量库、加 rerank 上,结果上线后检索质量依然拉胯,最后排查到源头才发现:PDF 解析出来的文本本身就是残的。文本型 PDF 抽取后缺行缺列,扫描件压根没有文字层,表格变成一行行无意义的字符串,这些垃圾向量被检索到以后,大模型只能基于垃圾内容做生成,答非所问是必然的。
这里要顺带区分一下 RAG 知识库和结构化知识库(KG)的差异。知识图谱类项目的主战场在实体抽取、关系构建和逻辑推理,文档解析只是上游;而面向非结构化文档的 RAG 知识库,解析环节就是核心容器本身。你可以在 KG 项目里容忍粗糙的全文提取,但在 RAG 项目里不能——因为后续没有图结构帮你弥补丢失的语义,向量检索能依赖的就是解析后文本切出来的块。这也是为什么行业里常有人感慨“RAG 瓶颈根本不在模型,在数据入口”。我自己的经验是,至少 60% 的检索翻车案例,回溯到最后都能归因到解析阶段。
1.2 五种 PDF“体质”与各自的陷阱
不是所有 PDF 都能用同一套工具无脑处理。先给 PDF 分个类,后面选型才不会乱。
| PDF 形态 | 典型来源 | 主要陷阱 | 推荐主线 |
|---|---|---|---|
| 原生文本型 | Word/WPS 导出、电子书、论文 | 双栏顺序错乱、特殊字体导致字符错位 | PyMuPDF、pdfplumber |
| 扫描图片型 | 扫描仪、老书、复印件 | 没有文字层,直接抽文本几乎为空 | OCR 全流程 |
| 表格密集型 | 财务报表、报价单、审批表 | 表格被线性化,行列语义全部丢失 | Camelot、Tabula、VLM |
| 复杂版式型 | 期刊论文、白皮书、说明书 | 双栏、页眉页脚、图注穿插打乱阅读顺序 | 版面分析、Marker、MinerU |
| 图文混排/表单型 | 合同、票据、宣传册 | 图片信息丢失、填写内容与表头错位 | VLM 兜底 |
别小看这个分类。实战中我见过太多项目组拿一个通用 PDF 抽取库跑所有文档,结果对原生单栏文本型 PDF 表现还行,一旦换成扫描版就直接翻车。本质上,不同“体质”的 PDF 对应的是完全不同的解析路线,先体检再路由,比什么工具都重要。
1.3 一个让我印象深刻的翻车案例
有次帮朋友看一个内部 SOP 知识库,文档源是扫描版操作手册。他们当时用 PyMuPDF 直接抽文本入库,检索查询“设备故障上报流程”,结果返回的全是页眉里的公司名和页码,因为整个 PDF 扫描件没有文字层,唯一抽出来能进向量的就是页面底部那一行页脚。用户问啥都答非所问,系统一度被判定为“大模型智障”。后来我在原始 PDF 上跑了一遍 OCR,单页文本量从不到 30 个字符变成 800 多字符,检索命中率立刻就不一样了。
这件事给我最大的教训是:任何 RAG 项目的第一个步骤,都应该是对 PDF 做“体检”——确认它到底是文本型还是扫描型、有没有隐藏文字层、平均每页能抽出多少字符。连这一步都跳过,后面所有优化都是缘木求鱼。
2. OCR 选型实录:Tesseract、PaddleOCR 到 PP-Structure,识别只是第一步
2.1 OCR 的白话原理:先圈人、再认脸、最后排座位
OCR 不是一个单一动作,而是四个子能力的组合。第一步是文本检测,找到图像里哪些位置有文字;第二步是方向分类,判断页面是正的还是旋转了 90 度或 180 度;第三步才是文字识别,把图像块转成字符序列;最后还有结构重建,理解标题在哪、正文从哪开始、哪些区域是表格。很多人只把 OCR 理解成“识别”,忽略了前面的检测和后面的结构化,导致输出的文本顺序东一块西一块。
一个比较好懂的生活类比:OCR 像是让机器“先圈出人群里的每个人,再逐个认脸,认完之后还得让他们按原来的位置坐好”。圈错了人,后面认脸再准也没用;座位排错,整段故事就讲乱了。大多数 OCR 效果不好,问题不是出在“认字”环节,而是“圈人”和“排座位”环节。
2.2 主流 OCR 引擎怎么选
先把市面上用得最多的几类 OCR 方案拉出来对比,大家心里有个底:
| 引擎 | 优势 | 短板 | 适合场景 |
|---|---|---|---|
| Tesseract | 免费、轻量、支持 100+ 语言、可离线、命令行友好 | 中文长文效果一般,复杂版面弱,表格基本不支持 | 英文单栏、简单扫描件、Linux 服务端批量 |
| PaddleOCR | 中英混合效果好,模型模块化,带版面分析和表格识别 | 依赖 Paddle 框架,部署体积偏大 | 中文扫描件、票据、表格密集场景 |
| EasyOCR | 上手快,API 简单,中文可用 | 速度慢,工程化能力弱,不适合生产批量 | 少量图片测试、原型验证 |
| 云端 OCR | 稳定性好,识别精度高,自带表格/票据模板 | 按量计费,数据需要出内网,有并发限制 | 数据合规允许、对精度要求高的批量业务 |
| 开源 VLM OCR | 能“理解”版面语义,公式和复杂版式效果好 | 需要 GPU,输出需要校验,部署成本高 | 复杂文档、公式、图文混排的深度解析 |
如果你只给一个建议,我会说:中文 RAG 场景默认从 PaddleOCR 起步,Tesseract 作为轻量备选。PaddleOCR 的文本检测在倾斜、模糊、中英混排上的鲁棒性明显更好,而且它自带的版面分析能力直接关系到后续分块质量,这一点是 Tesseract 给不了的。
2.3 PaddleOCR 落地:别只调用默认接口
PaddleOCR 的安装很简单,但真正的生产配置要看业务场景。基础安装:
pip install paddlepaddle paddleocr一个比较完整的调用姿势是这样:
from paddleocr import PaddleOCR ocr = PaddleOCR( lang="ch", # 中英混合模型 use_doc_orientation_classify=True, # 自动判断页面方向 use_doc_unwarping=True, # 针对弯曲扫描件做矫正 use_doc_parse=True, # 做版面解析,识别标题/正文/表格 ) result = ocr.predict("scan_page.png") for page in result: for line in page["res"]: print(line["text"])这里use_doc_parse=True是关键。它背后的 PP-Structure 系列模型会把页面拆成标题、正文、表格等区域,而不是简单地按坐标从上往下吐文本。这在 RAG 场景里价值很大,因为解析结果直接可以作为结构化切块的依据。
真正的生产流水线里,我通常用 PyMuPDF 把 PDF 每页渲染成 PNG,再交给 PaddleOCR 逐页处理。渲染时 DPI 是一个需要权衡的参数:DPI 太低,小字号识别不清;DPI 太高,单张图太大,识别速度和显存都会飙升。我常用的范围是 200 到 300,A4 页面在 300 DPI 下大约 2500×3500 像素,对大多数 OCR 模型来说信息量已经够用。
2.4 几个实操后才知道的 OCR 细节
先说预处理。扫描件如果对比度差,直接丢给 OCR 往往效果很差。经验性的做法是先转灰度图,再做二值化和去噪,必要时做个轻度锐化。PaddleOCR 内置了一些矫正能力,但它不是万能的,图像质量本身就决定了大半个天。
再说语言设置。中英混排的合同、说明书,直接用lang="ch"往往比强制分开中英文模型更稳,因为模型内部已经见过大量混合排版样本。如果你想提高特定版面的精度,比如发票识别,优先找云厂商的专用票据 OCR 接口,而不是自己从零调通用模型。
最后一定要强调:OCR 的输出不是终点,而是清洗的起点。页眉页脚、页码、识别出来的乱码符号都需要用正则或规则组件清理。RAG 构造的文本里有大量“第 1 页 共 20 页”这种噪声,切块时会把小块撑坏,检索时也会干扰相关性判断。
3. 多模态大模型做文档解析:适合什么文档、怎么调、成本怎么控
3.1 传统 OCR 与 VLM 的定位差异:相机与实习生的区别
传统 OCR 是精确的字符识别器,它不认识“语义”。它能告诉你这一行是“总金额¥1,234.00”,但不知道这个金额和上方税率条目之间的关系;它能识别出双栏论文左右两段文字,但按坐标吐出来之后,阅读顺序是左半栏+右半栏还是交错输出,它完全不关心。
多模态大模型(VLM)则完全不同。它把整页图像当作一个“视觉文本”来理解,可以同时看到标题层级、段落位置、表格结构、图片和说明文字之间的关系。它甚至能理解“这张合同里甲方是哪个公司”“这张票据的价税合计是不是用公式算出来的”。我常和团队说:OCR 是一台高精度照相机,VLM 是看过很多文档版式的实习生。照相机不会漏字,但实习生能看懂版式逻辑。
3.2 什么时候应该上 VLM,什么时候不该上
不是所有 PDF 都值得用 VLM。我的判断标准是:
- 版面复杂到传统 OCR 输出“顺序错乱、结构丢失”时,值得上 VLM
- 文档里有很多图片、图表、盖章、手写批注这类纯 OCR 搞不定的内容时,值得上 VLM
- 需要输出 Markdown 或 JSON 结构化结果,而不是纯文本时,值得上 VLM
- 数据量很小(几百页以内)且对准确性要求很高时,值得上 VLM
- 如果只是几千页扫描版纯文字书,且版面是横平竖直的单栏,传统 OCR 更划算
一个很实用的策略是“混合路由”:先用代码判断 PDF 类型,常规扫描件走 PaddleOCR,复杂疑难页才送 VLM。这样既保证了成本可控,又解决了传统 OCR 的结构天花板问题。
3.3 核心调用姿势:渲染页面 → 编码图片 → 让 VLM 输出 Markdown
用多模态模型解析 PDF 最重要的前置步骤,是把 PDF 页面渲染成图片。这一点很多人会忽略,结果拿着二进制 PDF 直接请求模型,被拒了才知道要先转图。用 PyMuPDF 渲染非常快:
import base64 import fitz # PyMuPDF def render_page_to_base64(pdf_path, page_index, dpi=200): doc = fitz.open(pdf_path) pix = doc[page_index].get_pixmap(dpi=dpi) doc.close() return base64.b64encode(pix.tobytes("png")).decode() image_b64 = render_page_to_base64("年报.pdf", 3)拿到图片编码后,构造一个结构化的 Prompt。我的常用模板大概长这样:
你是一个文档结构化助手。把输入页转成 Markdown: - 双栏内容按阅读顺序重排,不要按视觉列输出; - 表格转成 Markdown 表格,保留全部单元格内容,不要漏列; - 图片用  占位,并描述它在文中的作用; - 去掉页眉、页脚和页码; - 公式用 LaTeX 语法输出。 不要解释,直接给出 Markdown。接口调用部分各家有差异,但整体逻辑一致:
client = get_vlm_client() resp = client.chat.completions.create( model="vision-model", messages=[ { "role": "user", "content": [ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_b64}"}}, ], } ], ) md_text = resp.choices[0].message.content实际项目里我不会只调一次就完事。像表格列数对不上、双栏顺序依然颠倒这类问题,我会把输出结果返回给模型做一轮“校验修正”,专门让模型对照原图检查表格结构和阅读顺序是否符合要求。
3.4 模型选型与成本/吞吐的真实账本
模型选择上有两条路。一条是本地部署开源 VLM,比如带视觉能力的 Qwen2-VL、InternVL2 系列、GOT-OCR2.0 这类专门为 OCR 设计的模型。优点是数据不出内网,适合敏感文档,也适合批次量大的离线解析;缺点是部署需要 GPU,显存和推理时间都是成本。另一条是直接调用云上多模态接口,效果好、上手快、不用运维资源,但要考虑费用和数据合规,适合做“疑难页兜底”。
| 路线 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 本地开源 VLM | 数据不出内网、可批量、长线成本低 | 需要 GPU、部署调参成本高 | 成百上千页批量解析、数据敏感 |
| 云端多模态接口 | 效果好、免运维、可弹性扩容 | 按量计费、数据要出内网 | 少量疑难页、票据、复杂版式 |
成本上可以大致估算:一页 A4 渲染成图后约 1-2 MB,折算成多模态模型的输入 token 大约 1500-3000 个。一千页文档全量跑云端,费用可能到几百元量级。但因为通常只有 20% 左右的复杂页需要 VLM 兜底,实际成本会小很多。“本地批量 + 云端兜底”是我目前最推荐的组合。
3.5 VLM 的幻觉问题:结构化输出也要验收
量化宽松的代价是幻觉。VLM 在模糊扫描件上偶尔会“脑补”数字和内容,尤其是表格密集页面,它可能把模糊单元格猜出一个非常合理但错误的值。所以凡是对数据准确率有要求的场景,我都不建议直接信任 VLM 输出。
实际操作中,我会做三件事:第一,让模型输出尽量保守,Prompt 里明确写“看不清的区域用占位符代替,不要猜测”;第二,关键字段(金额、日期、编号)与 PaddleOCR 结果交叉核对,两边不一致时人工介入;第三,建立抽检机制,每批次随机抽 5% 页面人工比对原文。这些环节看起来笨,但在生产环境里能拦住大量“看似成功、实则错误”的数据入库。
4. 九种 PDF 工具逐一评点:从快读文本到端到端文档管线
4.1 先给一张“对号入座”速选表
市面上处理 PDF 的工具非常多,但真正适合 RAG 数据导入的,我认为这九种最值得掌握。先看速选表,后面再说细节。
| 工具 | 定位 | 适合场景 | 不适合场景 |
|---|---|---|---|
| PyMuPDF (fitz) | 高速文本抽取 + PDF 渲染 | 原生文本型 PDF、批量提文本、PDF 转图片 | 复杂表格、双栏阅读顺序还原 |
| pypdf | 纯 Python PDF 基础操作 | 合并、拆分、加密、旋转、加水印 | 高质量文本抽取和表格解析 |
| pdfminer.six | 底层 PDF 布局解析 | 研究 PDF 内部结构、自定义布局分析 | 快速上手、性能敏感场景 |
| pdfplumber | 精确坐标 + 表格提取 | 固定版式表单、票据、按区域截取文本 | 扫描 PDF、超大文件 |
| Camelot | 专门提取表格 | 有线表格、无线表格还原为 DataFrame | 表格线混乱、跨页表格 |
| Tabula-py | Java/PDFBox 表格提取 | 原生文本类 PDF 的干净表格 | 中文表格、扫描件、无 Java 环境 |
| unstructured | 文档分区 + 清洗 + 向量友好 | RAG 项目快速建库、多格式统一接入 | 需要精细控制版式和表格 |
| marker | PDF 转 Markdown | 论文、书籍、双栏版式转结构化文本 | 对中文复杂版式的支持仍在完善 |
| MinerU | 端到端文档解析(版面/公式/表格) | 扫描论文、公式、中文文档、Markdown/JSON 输出 | 部署较重,首次下载模型体积大 |
4.2 第一梯队:轻量快取的三个工具
PyMuPDF 基本是我所有 PDF 解析流程的第一站。它是 C 底层实现,抽取文本的速度比纯 Python 库快一个量级,而且能拿到每个文本块的坐标、字体信息和页面渲染图像。原生文本型 PDF 用它直接抽就行,几行代码就能出结果:
import fitz doc = fitz.open("manual.pdf") for page in doc: text = page.get_text("text") print(text) doc.close()它的坑在于get_text("text")是按 PDF 内部对象顺序输出的,双栏论文可能出现左侧第一段、右侧第一段、左侧第二段这种交错顺序。要解决双栏问题,得读取每个块的坐标,自己做列聚类,或者用 VLM 重建阅读顺序。另外它的表格抽取能力几乎没有,表格相关需求它帮不上忙。
pypdf 是 PyPDF2 的后续维护版本。它擅长合并、拆分、加密、旋转等 PDF 管理操作。如果你只想在 PDF 里找关键字然后提取某几页,pypdf 够用;但如果想靠它抽完整文本喂给向量库,我劝你放弃,它抽出来的文本经常带奇怪的换行和空格。它的价值更多体现在“预清洗”环节,比如把一个几百页的大 PDF 按目录拆成章节文件。
pdfminer.six 是底层解析工具,它提供更接近 PDF 原始布局的对象模型,可以拿到页面上的直线、矩形、文本字符的精确位置。好处是控制力强,坏处是 API 学习成本高,新版里一个extract_text()有时候还会得到让人摸不到头脑的结果。我在项目里把 pdfminer 当“军火库”用,只有在其他工具搞不定、需要从底层定制布局解析时才搬出来。
4.3 第二梯队:精读和表格的专项工具
表格是 PDF 解析里的重灾区,这一梯队专治表格。
pdfplumber 是对 pdfminer 的高层封装,使用体验好很多。它能按坐标区域提取文本,也能直接抽取表格。碰到固定版式的表单,比如每页排版一致的入库单,pdfplumber 可以精确到“只取某个格子里的内容”:
import pdfplumber with pdfplumber.open("form.pdf") as pdf: page = pdf.pages[0] table = page.extract_table() for row in table: print(row)pdfplumber 的短板是速度和大型文件。一个二三百页的报告,逐页解析可能要等好几分钟。所以我的建议是,大文件先用 PyMuPDF 抽文本,遇到表格区域再用 pdfplumber 精读,不要全文档无脑跑。
Camelot 是表格提取的专职选手,支持lattice和stream两种模式。lattice适合有清晰线框的表格,stream适合无线框的表格。它能把表格直接转成 Pandas DataFrame,对后续入库非常友好:
import camelot tables = camelot.read_pdf("tables.pdf", flavor="lattice") for table in tables: df = table.df print(df.to_csv())Camelot 的坑在于对表格线残缺、跨页合并的情况很敏感,经常会把一个跨页长表拆成两段,需要自己合并。另外它依赖 OpenCV、Ghostscript 等组件,部署时容易踩环境依赖的坑。
Tabula-py 背后是 Java 的 Tabula 库,抽表格能力稳定,尤其适合原生文本型 PDF 里结构规整的表格。但它的动态性较差,中文表格有时需要反复调参数,而且没有 Java 运行环境就跑不起来。如果你的环境禁止装 Java,直接放弃它选 Camelot 就行。
4.4 第三梯队:面向 RAG 的端到端管线工具
前面几个工具更多是“组件”,这一梯队的工具直接为文档解析和知识库服务而生。
unstructured 是 RAG 场景绕不开的名字。它把 PDF、DOCX、HTML、图片统一解析成分区块(Element),支持标题、正文、表格、图片说明的分类,并自带清洗逻辑。它跟向量库生态的适配度极高,基本是“接上就是 chunk”的路线:
from unstructured.partition.pdf import partition_pdf elements = partition_pdf("report.pdf", strategy="hi_res") for el in elements: print(el.category, el.text)strategy="hi_res"会自动走图像检测模型,对扫描件和复杂版式的处理更精细,代价是速度慢、依赖重。全量处理时我先用默认的auto策略,遇到复杂页再提升到hi_res。
marker 的核心能力是把 PDF 转 Markdown,内部用深度学习模型做版面检测,输出结构比较干净。它对英文论文、书籍的效果我是比较认可的,跑起来命令也很简单:
marker_single input.pdf --output_dir output/中文版式方面,早期版本表现一般,新版有所改善但仍需实测验证。我的习惯是拿它处理英文技术文档和海外论文,中文语料则以 MinerU 为主。
MinerU 是这一类里我最推荐的中文文档解析工具。它能做版面分析、公式识别(输出 LaTeX)、表格转 HTML,最终统一输出 Markdown 和 JSON,甚至能保留每块内容的坐标与层级信息。对扫描版中文论文、书籍版式复杂的 PDF 效果很能打:
magic-pdf -p input.pdf -o output_dir -m autoMinerU 的缺点是部署门槛比较高,模型文件大,首次安装或下载模型会比较久,CPU 环境下处理速度也慢。但一旦部署好,它在中文复杂文档上的表现,完全可以替代“OCR + 版面分析 + 表格结构化”三段式自建流水线。
4.5 我实际选型的决策思路
我不会迷信任何一个工具,而是按 PDF 类型做路由。第一步永远是用 PyMuPDF 快速抽几页文本,看字符量判断是否扫描件。如果字符量正常,就进入文本型路线,普通页面用 PyMuPDF 抽,表格密集页面用 pdfplumber 或 Camelot。如果字符量很低,判断为扫描件,先走 PaddleOCR,复杂版面再上 VLM 或 MinerU。批量处理且目标是快速搭建 RAG 原型时,直接用 unstructured 或 marker 一把梭,先跑通再谈精细化。
这九个工具不是竞争关系,而是配合关系。真实项目里我往往同时用四个以上,各干各的活。
5. 从拿到 PDF 到向量入库:一条可以照抄的解析流水线
5.1 总体流程:体检、路由、抽取、清洗、切块
把前面所有知识点串成流水线,大概是这几步。第一步给 PDF 体检:有没有文字层、页数、每页平均字符数、是否包含图片。第二步路由:根据体检结果决定走文本型、扫描型还是复杂版式型。第三步抽取:使用对应工具拿到文本或结构化数据。第四步清洗:去掉页眉页脚页码,修复阅读顺序,规范换行。第五步切块:基于标题和段落结构切分,而不是无脑按 token 数切。第六步做向量化和入库,顺带在向量库记录来源文件、页码、标题路径等元数据。
这套流水线听起来简单,但每一步都有专门工具和坑。我把它拆成三类典型场景给代码。
5.2 场景一:原生文本型 PDF 的最小实现
一个能处理大部分单栏页面和简单双栏页面的抽取脚本,长这样:
import fitz import re def extract_text_blocks(file_path): doc = fitz.open(file_path) blocks = [] for page in doc: page_blocks = page.get_text("blocks") # 按垂直坐标排序,再按水平坐标排序,尽量还原阅读顺序 page_blocks.sort(key=lambda b: (round(b[1], 1), b[0])) for x0, y0, x1, y1, text, *_ in page_blocks: text = re.sub(r"\n{3,}", "\n\n", text.strip()) if text: blocks.append({"page": page.number, "y": y0, "text": text}) doc.close() return blocks这不是万能方案。真正的双栏论文需要先根据文本块的水平坐标估计左右两个区域的中心点,然后分别排序。更省事的办法是直接用 marker 或 MinerU 做版面重建,把顺序问题甩给训练好的模型。
5.3 场景二:扫描式 PDF 走 OCR 的实现
扫描版 PDF 的完整处理路径是:渲染页面 → OCR 识别 → 输出文本。用 PyMuPDF 渲染,用 PaddleOCR 识别:
import fitz from paddleocr import PaddleOCR ocr = PaddleOCR(lang="ch") doc = fitz.open("scan.pdf") all_text = [] for page_no, page in enumerate(doc): pix = page.get_pixmap(dpi=200) img_path = f"/tmp/page_{page_no}.png" pix.save(img_path) result = ocr.predict(img_path) page_text = "\n".join([line["text"] for line in result[0]["res"]]) all_text.append({"page": page_no, "text": page_text}) doc.close()如果页面复杂度高,比如图文混排、表格、公式都有,我会把渲染出来的图片直接交给 VLM,让模型按前面给的 Prompt 输出 Markdown。这样能省掉自己拼 PaddleOCR 表格结构模型的功夫,但成本和幻觉风险要自行评估。
5.4 场景三:复杂版式 PDF 走端到端工具
遇到期刊论文、学位论文、扫描版书籍这类难啃的文档,直接上 MinerU 是省心路线。处理完成后生成 Markdown 和一个 JSON,JSON 里有每个版面块的类型、坐标和内容。我会先看它的 Markdown 输出,确认标题层级是否完整、表格是否保留、公式是否被转成 LaTeX,然后再写一个后处理脚本,把 Markdown 里的标题行转成文档节点,用作切块依据。
marker 在处理英文文档时更轻量。它的命令行输出就是 Markdown 文件,如果英文论文占多数,marker 的性价比很高。中文文档我一般只相信 MinerU,这个判断目前没有变过。
5.5 切块策略:解析得好也要切得对
解析做完了,切块不对照样白搭。无脑按 512 token 切块,最容易发生的悲剧是:一个“操作流程注意事项”刚好被从中间切断,用户问“这个流程需要注意什么”,两块里哪块都不包含完整答案,召回自然失败。
结构感知切块的基本思路是:优先按标题层级切,把#、##、###作为块边界;没有明显标题的文档,根据段落间距和主题变化切分;表格单独成块,不要硬塞进上下文字块里;跨页的段落要尝试拼接,不要让句子碎在页边界。切块之后,每一块都要记住来源元数据,包括文件名、页码、标题路径。这些元数据在后续做引用溯源时非常重要,RAG 回答如果没有可靠的来源说明,等于白做。
6. 验收与踩坑:那些“看起来成功”、检索时却翻车的细节
6.1 解析效果怎么验收:别只看输出有没有字
很多团队验收 PDF 解析,只看“有没有解析出文本”,这个标准太低。我通常建立一套轻量但有效的验收指标。
第一项是文本覆盖率。抽样人工检查 10 页源 PDF,估算真实字符数,再对比解析后的可读文本字符量,覆盖率低于 90% 就要查问题。第二项是结构保真度。抽查标题层级是否保留、表格行列数是否一致、公式是否被转成了可读格式。第三项是检索命中率。构造 20-30 个来自真实业务的问答题,跑一遍 RAG 检索,看 top 5 内是否出现正确答案。最后一个也是最实用的办法:让最终用户做一轮回归问答,看生成答案能否定位到正确段落和页码。
只有完成这些验收,解析流水线才算“可用”,而不是“跑通”。
6.2 七个高频踩坑复盘
把这些年常见的坑整理成表,遇到问题可以对照排查。
| 坑 | 现象 | 根因 | 解法 |
|---|---|---|---|
| 扫描件空白入库 | 检索返回全无实质内容 | 没判断 PDF 是否有文字层直接抽取 | 先体检,扫描件走 OCR |
| 双栏顺序错乱 | 论文内容左右穿插 | 默认文本抽取按对象顺序输出 | 版面分析或 VLM 重排 |
| 表格线性化 | 表格变成无结构的长串 | 普通文本抽取不认识表格 | 用 Camelot/Tabula 或 VLM |
| 页眉页脚污染 | 检索命中公司名、页码 | 清洗不彻底 | 正则过滤 + 版面区域裁剪 |
| 公式丢失或乱码 | 数学公式变成空白符号 | 公式识别能力不足 | MinerU 或带公式能力的 VLM |
| 中文扫描乱码 | 中文识别成符号 | 用了英文 OCR 模型 | 指定中文模型 |
| 图片信息缺失 | 文档含图但检索不到图内容 | 只抽取文本不处理图片 | VLM 生成图片描述并入库 |
每条后面都是一段血泪史。特别是“双栏顺序错乱”和“表格线性化”,这两个坑在金融研报、学术论文类语料里几乎必然出现,初期项目组通常会以为是检索问题,绕了很大弯才发现是解析问题。
6.3 我的项目管理经验:预留时间、建回归集、留好失败样本
给正在搭 RAG 采购或自建解析流水线的团队一个预算建议:解析与数据清洗模块的投入,至少要占整个项目四分之一到三分之一。这个比例很多人会嫌高,但它换回来的是可靠的数据底座。数据错了,后面模型调参、向量调参全都无效。
我自己的固定做法是,在项目一开始就建一个“回归集”:每个文档类型抽 3-5 页作为代表样本,覆盖扫描件、论文双栏、表格页、公式页、支票票据页。以后每次换工具、升级模型、调参数,都拿这个回归集跑一遍,对比输出差异。别凭感觉判断效果好了一点点,要看到解析前后对照表才下结论。
另外,我会留一个“失败样本夹”。每次发现某个文档类型解析翻车,就把这个样本单独收进来,下一轮迭代专门针对它优化。这个夹子积累一段时间后,基本就是这个知识库最容易踩坑的完整地图。与其反复踩同一个坑,不如把坑记录成资产。
最后分享一个小习惯:每一批次 PDF 入库前,我都会先随机抽一页把解析文本和原 PDF 并列打开,肉眼扫一遍。这个动作可能只需要一分钟,但总能发现一些自动化指标发现不了的细节问题,比如抬头被切成了两行、表格里金额符号丢失、封面信息被误当成正文。做数据解析这件事,永远要留一双人眼在现场。