1. 为什么 RAG 的瓶颈往往不在模型,而在文档解析这一层
做 RAG 项目做久了,你会发现一个很反直觉的现象:向量库选型、Embedding 模型、重排策略这些环节,大家讨论得最多,但真正把系统效果拖垮的,往往是上游那份 PDF 解析出来的文本。我见过太多团队花两周调检索参数,最后发现问题出在解析阶段——表格被拆成了乱序的单字,页眉页脚混进了正文,跨页段落被硬生生截断,章节层级全丢了。检索命中率上不去,不是模型不行,是喂进去的原料本身就是碎的。
MinerU 4.0 这个版本值得单独拿出来讲,核心原因是它把“解析”这件事从“能出文本”推进到了“能出结构化、可定位、可工程化接入”的层面。所谓四档解析,本质上是给了你一个按精度和成本分级的旋钮:不同文档、不同业务场景,你不需要都用最重的那一档。而定位器解决的是 RAG 里一个长期被忽视的问题——检索到的内容,到底对应原文的哪一页、哪一节、哪一段。没有定位,引用就是空谈,溯源就是摆设。
这篇文章面向的是正在做 RAG 知识库、需要处理大量 PDF/Word/PPT 的工程同学,也适合刚接触文档结构化解析、想搞清楚“为什么解析这么难”的开发者。我会把四档解析的取舍逻辑、定位器的实现思路、CLI 和 API 两种接入方式、以及本地部署时踩过的坑,全部摊开讲。代码部分给的是可直接改参数复现的片段,不是伪代码。
先说结论性的判断:文档解析的工程化,重点不在“解析得多准”,而在“解析结果能不能被下游稳定消费”。页码、章节、段落、表格结构这些元信息,如果解析阶段不保留,后面任何环节都补不回来。MinerU 4.0 的四档设计,恰恰是围绕“下游怎么用”来分层的,这一点比单纯堆模型参数更有价值。
2. MinerU 4.0 四档解析的整体设计与选型逻辑
2.1 四档解析到底分的是什么
很多人第一次看到“四档解析”会以为是四个模型,其实不是。它分的是解析管线的处理深度,从轻到重大致是这样一条光谱:
- 第一档:纯文本抽取。只拿文字流,不保留版面、不识别表格结构、不做阅读顺序重排。速度最快,适合内容简单、以连续段落为主的文档,比如纯文字报告、小说、政策条文。
- 第二档:版面感知抽取。加入版面分析,能区分标题、正文、页眉页脚、脚注,阅读顺序基本正确。适合大多数常规文档,是性价比最高的一档。
- 第三档:结构化解析。在版面基础上识别表格、公式、列表,输出带层级和类型的结构化块。适合技术文档、财报、招标文件这类有复杂结构的材料。
- 第四档:高精度全要素解析。启用更重的模型组合,处理扫描件、复杂表格、多栏混排、图文混排。精度最高,但耗时和算力消耗也最大。
这个分档的逻辑很务实:不是所有文档都值得用最重的管线。一份 200 页的纯文字合同,用第四档跑,纯属浪费。反过来,一份满是合并单元格的财务报表,用第一档跑,出来的东西根本没法用。选档的本质,是在“解析质量”和“处理成本”之间找平衡点。
2.2 为什么这样分层,而不是一个模型打天下
我早期做过一个项目,图省事,所有文档统一走最重的解析管线。结果跑一批 5000 份的文档,光解析就花了大半天,而且大部分文档根本不需要那么重的处理。后来改成按文档类型分流,整体耗时降了六成,效果反而没掉。
MinerU 4.0 的分档设计,背后是同一个工程判断:解析成本必须可控,否则 RAG 的增量更新根本跑不起来。RAG 知识库不是一次性建完就完事,文档会持续进来,如果每次增量更新都要全量重跑最重的解析,运维成本会失控。分档让你可以给不同来源、不同重要级别的文档配不同的解析策略。
另一个原因是错误传播。解析越重,中间环节越多,出错的面也越大。对于结构简单的文档,轻档解析反而更稳,因为它引入的变量少。这一点在实操中很关键:不是精度越高越好,而是匹配文档复杂度的精度才是最好的。
2.3 定位器在整体架构里的位置
定位器不是一个独立功能,它是贯穿解析全流程的一条“坐标线”。每解析出一个文本块,定位器就给它打上一组坐标:文档 ID、页码、章节路径、块序号、在页面内的位置。这组坐标在解析阶段生成,随结构化结果一起输出,下游检索命中后直接读取。
为什么定位器要放在解析阶段做,而不是检索阶段补?因为页码和章节信息只在解析时可得。文本一旦被拆成纯字符串存进向量库,原始位置信息就丢了。你不可能在检索时反推出“这段话在第 37 页第 2 节”。所以定位必须在解析时一次性打好,这是不可逆的。
提示:如果你的 RAG 系统现在还没有在解析阶段保留页码和章节,那引用溯源基本是做不到的。补这个能力,越早越好,因为已经入库的文档要重新解析才能补上坐标。
3. 核心细节解析:结构化输出与定位器的实操要点
3.1 结构化文本到底要保留哪些字段
“结构化”这个词被用得很泛,落到工程上,其实就是一组明确的字段。以一份招标文件为例,我实际会要求解析结果至少包含这些:
| 字段 | 含义 | 为什么需要 |
|---|---|---|
| doc_id | 文档唯一标识 | 多文档检索时区分来源 |
| page_no | 页码 | 引用溯源、定位跳转 |
| section_path | 章节路径,如“第三章 > 3.2 投标要求” | 保留层级,检索时可加权 |
| block_type | 块类型:标题/正文/表格/列表/公式 | 下游按类型做不同处理 |
| block_index | 块在文档内的顺序号 | 还原上下文、做邻近检索 |
| bbox | 块在页面内的坐标 | 高亮定位、可视化 |
| text | 块的文本内容 | 向量化、展示 |
这组字段里,section_path 和 block_index 是最容易被忽略、但价值最高的两个。section_path 让你在检索时可以做层级加权——比如用户问的是“投标保证金”,命中“第三章 投标要求”下的内容,权重就该比命中附录里的高。block_index 则让你能做“邻近块扩展”:检索命中一个块后,把前后各一到两个块一起取出来,给 LLM 的上下文会完整得多。
3.2 定位器的坐标是怎么生成的
定位器的实现,说穿了就是在解析管线的每个阶段,把位置信息一路传递下去。具体分三步:
第一步,页面级定位。解析器逐页处理时,当前页码是已知的,这一页解析出的所有块,先打上 page_no。
第二步,版面级定位。版面分析模型会输出每个文本区域的边界框(bbox),块的坐标从这里来。同时,版面分析会判断这个区域是标题还是正文,标题就更新当前的 section_path 栈。
第三步,块级定位。在页面内按阅读顺序给块编号,结合全局顺序号,生成 block_index。到这里,一个块的四维坐标(页、节、序、框)就齐了。
这里有个实操细节:section_path 的维护要用栈结构。遇到一级标题压栈,遇到同级标题替换栈顶,遇到正文不动栈。这样跨页的时候,章节路径能自然延续,不会因为翻页就丢掉当前章节。我见过一些实现是每页重置章节,结果跨页的段落全被归到了“无章节”,检索时层级信息就废了。
3.3 表格解析为什么是重灾区
表格是文档解析里最难啃的部分,没有之一。难点在于:表格的视觉结构和逻辑结构经常不一致。合并单元格、跨页表格、无边框表格、嵌套表格,每一种都能让解析器翻车。
MinerU 4.0 在第三、四档里对表格做了专门处理,输出的是结构化的表格对象,而不是把表格拍平成一行行文字。这一点对 RAG 极其重要。举个实际例子:一份财务报表,如果表格被拍平成“营业收入 1000 万 营业成本 600 万 净利润 400 万”这样一串,检索“净利润”时命中的文本里混着其他指标,LLM 很容易读错。而结构化表格输出的是行列对应的对象,检索和问答的准确率完全不是一个量级。
注意:表格解析出来后,建议单独存一份结构化版本(比如 JSON 或 Markdown 表格),不要只存拍平的文本。下游做数值问答时,结构化表格是刚需。
3.4 阅读顺序重排的坑
多栏排版、图文混排的文档,解析出来的块顺序经常是乱的。阅读顺序重排就是把这些块按人眼阅读的顺序重新排列。这件事听起来简单,做起来很容易出错,尤其是跨栏的标题和跨栏的表格。
我的经验是:重排后一定要做一次人工抽检,尤其是首页和含表格的页。自动化指标(比如块顺序的连贯性打分)只能筛出明显错误,细微的顺序问题还得靠眼睛。抽检不用全量,按文档类型各抽几份,覆盖到复杂版面就行。
4. CLI 与 API 两种接入方式的完整实操
4.1 本地部署 MinerU 的环境准备
本地部署是很多团队的首选,尤其是文档涉密、不能出内网的场景。环境准备这一步,坑主要集中在依赖和模型权重上。
基础环境我一般这样配:
# 建议 Python 3.10 及以上 python -m venv mineru_env source mineru_env/bin/activate # 安装核心依赖,具体包名以官方发布为准 pip install mineru模型权重是本地部署的关键。四档解析用到的模型不一样,重档解析需要的权重文件更大。第一次跑的时候,程序会自动下载权重,如果网络环境受限,建议提前把权重下好放到指定目录,用配置项指过去,避免每次跑都卡在下载上。
显存方面,我的实测经验是:轻档解析 CPU 也能跑,就是慢;重档解析建议至少 8G 显存起步,处理扫描件和复杂表格时显存占用会明显上升。如果显存不够,可以调小批处理大小,用时间换空间。
4.2 CLI 方式的调用与参数说明
CLI 适合批处理和脚本化。基本调用形态是这样:
mineru parse \ --input ./docs \ --output ./parsed \ --level 3 \ --locator true \ --format json几个关键参数值得展开说:
--level:指定解析档位,1 到 4。批处理时建议按文档类型分组,不同组用不同档位。--locator:是否启用定位器,输出页码、章节、块坐标。做 RAG 一定要开。--format:输出格式,json 适合下游程序消费,markdown 适合人工查看。--input:支持单文件也支持目录,目录会递归处理。
批处理的时候,我习惯写一个简单的分流脚本:先快速扫一遍文档,按页数和是否含扫描页做个粗分类,再决定每份文档走哪一档。这样比一刀切省很多时间。
# 伪代码示意:按文档大小分流 for f in ./docs/*.pdf; do pages=$(pdfinfo "$f" | grep Pages | awk '{print $2}') if [ "$pages" -lt 20 ]; then level=2 else level=3 fi mineru parse --input "$f" --output ./parsed --level $level --locator true done4.3 API 方式的接入与并发控制
如果要做成服务,API 方式更合适。核心是把解析封装成一个接口,接收文档、返回结构化结果。接入时最需要注意的是并发和超时。
解析是重计算任务,并发开太高会把机器打满,反而拖慢整体吞吐。我的经验值是:按 CPU 核数或显存容量来定并发数,一般不超过核数的 1.5 倍。超时设置要留足,重档解析一份大文档几分钟很正常,超时设太短会导致任务被误杀。
# 示意:带并发控制的解析调用 from concurrent.futures import ThreadPoolExecutor def parse_doc(path, level=3): # 调用解析接口,返回结构化结果 result = mineru_client.parse( input_path=path, level=level, locator=True, output_format="json" ) return result with ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(parse_doc, doc_paths))并发数 4 是我在 8 核机器上的保守值,你可以根据实际负载调。关键是别让解析任务把机器占满,要给下游的向量化和入库留资源。
4.4 解析结果接入 RAG 的完整链路
解析只是第一步,结果怎么进 RAG 才是重点。完整链路大致是:解析输出结构化块 → 按块类型和章节做切分 → 生成向量 → 连同定位坐标一起入库。
这里有个关键决策:切分粒度。不要简单按固定字数切,而是优先按解析出的块边界切。一个块本身就是一个语义单元,按块切比按字数切语义完整得多。块太长再考虑二次切分,切的时候把 section_path 和 page_no 带上,保证每个子块都有完整坐标。
入库时,向量库存文本和向量,元数据库存定位坐标。检索命中后,从元数据库取坐标,就能实现“检索到内容 → 定位到原文位置”的闭环。这一步做扎实了,引用溯源、原文高亮这些功能都是顺带的事。
5. 常见问题与排查技巧实录
5.1 解析结果乱序、章节丢失怎么排查
这是最高频的问题。排查顺序我一般这样走:
先看是不是档位选低了。纯文字文档用第一档没问题,但含多栏、表格的文档用第一档,乱序几乎是必然的。升一档再试,很多问题直接消失。
再看是不是阅读顺序重排没生效。有些配置里重排是单独开关,默认可能没开。确认配置项,打开后重新解析对比。
最后看章节栈的维护。如果章节路径在跨页处断了,多半是栈被重置了。检查解析器是不是每页重新初始化了章节状态。
5.2 表格解析错位、合并单元格丢失
表格问题的排查,先确认档位。表格识别在第三档才比较完整,第一、二档基本不做表格结构还原。
如果档位够高还是错位,看是不是扫描件。扫描件的表格识别依赖 OCR 和版面检测,质量受扫描清晰度影响很大。清晰度差的扫描件,建议先做图像预处理(去噪、纠偏、提高对比度)再解析。
合并单元格丢失是常见现象,因为很多解析器会把合并单元格拆成多个空格。这种情况建议在解析后做一次表格结构校验,把明显的空单元格合并回去。校验规则可以基于行列对齐关系来写。
5.3 定位坐标不准、页码对不上
页码对不上,最常见的原因是文档本身有封面、目录等前置页,页码编号和物理页序不一致。解析器输出的 page_no 通常是物理页序,而文档里印的页码可能是从正文开始编的。做引用展示时,要明确用的是哪种页码,别混用。
坐标不准,多半是 bbox 的坐标系没对齐。不同解析器用的坐标系原点可能不同(左上角或左下角),接入可视化时要做一次转换。这个坑很隐蔽,表现是“高亮框位置整体偏移”,一看坐标转换就明白了。
5.4 本地部署显存不足、解析中断
显存不足的表现是解析到一半进程被杀,或者报 OOM。解决办法按优先级排:
- 降档位。重档解析显存占用高,能降就降。
- 调小批处理大小。一次处理的页数或块数减少,显存峰值会降下来。
- 分页处理。把大文档拆成小段分别解析,最后合并结果。合并时注意章节栈要跨段延续。
提示:分页处理时,章节路径的延续是个易错点。建议把上一段的章节栈状态序列化传给下一段,而不是每段重新开始。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 文本乱序 | 档位过低 / 重排未开 | 升档、检查重排配置 |
| 章节丢失 | 章节栈跨页重置 | 检查栈维护逻辑 |
| 表格错位 | 档位低 / 扫描件质量差 | 升档、图像预处理 |
| 页码对不上 | 物理页序与印刷页码混用 | 明确页码口径 |
| 坐标偏移 | 坐标系原点不一致 | 做坐标转换 |
| 解析中断 | 显存不足 | 降档、调小批处理、分页 |
6. 工程化落地时我踩过的几个坑
第一个坑是盲目追求高精度。早期我所有文档都走最高档,结果解析慢、成本高,而且对简单文档并没有带来质量提升。后来按文档复杂度分流,整体效率提升非常明显。选档这件事,匹配比堆料重要。
第二个坑是忽略增量更新。RAG 知识库的文档是持续进来的,如果每次新增文档都要全量重跑解析,运维会崩。正确做法是解析结果按文档 ID 缓存,新增文档只解析新增部分,更新文档只重解析变更页。这要求解析阶段就记录好文档版本和页级指纹。
第三个坑是定位信息没入库。我见过一个项目,解析时明明生成了页码和章节,但入库时只存了文本,坐标全丢了。等要做引用溯源时,只能重新解析全量文档。定位信息一定要和文本一起入库,这是不可逆的。
第四个坑是表格拍平。把表格拍成文本存进去,检索时数值问答准确率惨不忍睹。后来改成表格单独结构化存储,问答时按行列取值,准确率立刻上来了。表格和正文,在存储层就该分开。
第五个坑是没有抽检机制。解析质量会随文档来源波动,没有抽检就发现不了退化。我现在的做法是每批文档按类型抽 5% 人工看一遍,重点看首页、表格页、跨页段落。抽检成本不高,但能提前发现大问题。
7. 关于解析档位选择的一点个人经验
跑过几十个项目之后,我对档位选择有个比较稳定的判断:先按文档类型定基线,再按单份文档的复杂度微调。纯文字类文档,第二档基本够用;含表格、公式的技术文档,第三档起步;扫描件、复杂版面,直接上第四档,别省那点时间,省出来的时间后面排查问题全还回去了。
还有一个容易被忽略的点:解析档位不是越高越好,而是越稳越好。RAG 系统要的是稳定的输入,不是偶尔惊艳、经常翻车的输出。一个档位如果对某类文档的解析质量波动很大,宁可降一档用更稳的,也不要为了那点精度提升引入不确定性。
最后分享一个实操小技巧:解析结果里,给每个块加一个parse_level字段,记录它是哪一档解析出来的。这样下游如果发现某类文档质量问题,能快速定位到是不是档位选错了,排查效率高很多。这个字段几乎不占空间,但排查时非常有用。