面向RAG的PDF语义增强:比MinerU多做一步的开源实践
说实话第一次看到这个标题很多人第一反应是又蹭MinerU的热度开源圈这种事见多了拿个开源项目改改UI写个花里胡哨的README然后去各大群里刷屏求star。但你有没有想过一个做PDF解析的项目凭什么在MinerU已经封神的情况下还能3个月切走3000多个star我们团队的答案很简单MinerU做的事情是把PDF变成Markdown这是我们也都做的事但我们比MinerU多做的一件事是把解析结果变成了RAG和LLM能直接用起来的东西。MinerU确实解决了PDF转Markdown的很多痛点版面还原、公式、表格识别都做得相当扎实。但如果把它接入Dify这类RAG工作流不少人会发现召回质量并没有想象中那么美好。PDF转出来的Markdown是“能看了”但给LLM用还差点意思。这篇文章不聊虚的把我们立项时的思考、多做的那个模块、工程落地的细节、以及开源冷启动的增长复盘全部摊开来讲。想做个能拿得出手的开源项目或者正在折腾文档解析、RAG知识库的这篇应该能给你一些能直接抄作业的东西。1. 立项之前的冷静思考PDF解析到底缺什么1.1 MinerU做得很好但“用户完整体验”还差一环不否认MinerU的出现把PDF文档解析的体验拉高了一个档次。它把版面检测、公式识别、表格还原、OCR这些以前要拼装好几个模型才能跑通的流程打包成了一个很顺滑的pipeline。很多人本地一跑发现连双栏论文都能还原得整整齐齐公式也不再是乱码表格结构基本保真。这个体验放在两年前几乎是不可想象的。但我们在实际使用和调研过程中注意到一个现象解析结果漂亮不等于下游好用。PDF转Markdown这件事本质上解决的是“人能不能看懂”的问题它保留的是版面结构、视觉顺序、排版层次。可当你把这一堆Markdown切片、向量化、丢给LLM或者RAG系统时问题就来了这些系统需要的不是“像人看到的样子”而是“理解文档语义组织的能力”。举个例子一份技术白皮书MinerU能准确地把“3.2.1 模型架构”这一节从PDF里提取出来变成一个###标题。但如果你问RAG系统“3.2节的结论是什么”它得先搞清楚3.2.1、3.2.2和3.2之间的父子关系得知道表格里的数据是属于哪一小节的。扁平化的Markdown不会给你这些。这就是我们看到的那个“还差一环”也是整个项目的起点。1.2 调研里的三个高频痛点立项之前我们做了大概两周的定向调研方式很朴素去GitHub issue区、Dify的讨论区、知乎相关话题下面翻用户吐槽然后自己手动测了上百篇不同格式的PDF。最后归纳出三个高频痛点每个都直接指向RAG场景的体验劣化。第一个痛点是段落碎片化严重。物理版面的一行并不等于语义上的一句话。PDF转Markdown时双栏文本的切割经常把一句话从中间劈开RAG切片的时候又按固定窗口硬切结果就是召回出来的片段上下文缺胳膊少腿。第二个痛点是章节层级信息彻底丢失。Markdown里虽然保留了标题的视觉样式但标题之间的父子关系、编号的层级关系并没有显式建模RAG无法判断某个段落属于哪一章哪一节检索时容易张冠李戴。第三个痛点是表格和图文的语义上下文断裂。表格是识别出来了但它属于哪个章节、描述什么主题、与前文的关联是什么全都没有检索命中表格时LLM拿到一个孤零零的表格根本没法正确回答。这三个痛点用一句话概括就是解析结果缺少一层“语义骨架”。而“比MinerU多做的这件事”就是给解析结果补上这层骨架。我们在调研报告中写了这么一句MinerU负责把PDF变成人类可读的Markdown我们负责把它变成机器可理解的语义文档。这句话后来成了项目的核心定位。2. 我们比MinerU多做的“那件事”面向RAG的语义增强层2.1 “多做的这件事”到底是什么简单说我们做的不是一个全新的PDF解析器而是在版面解析完成之后又跑了一层面向语义的后处理管线。它的输入是版面解析的结果——文本框坐标、字体信息、类型标签、阅读顺序输出是一个带完整语义结构的文档对象同时落成两种格式带语义标记的Markdown和结构化的JSON双轨输出。这一层后处理我们内部叫语义增强层Semantic Enrichment Layer。和MinerU的全流程解析不同它关注的不再是“这一行文字是什么”而是“这段内容在文档里扮演什么角色、和周围内容是什么关系”。它把五个子任务串在一起版面坐标的语义化重组把物理位置排序重建成真正的阅读顺序和段落归属章节树重建从标题字号和编号模式中恢复文档的层级结构表格语义头识别定位表头行、理清表格的语义字段图文相关性关联把图片、表格与其上下文中的引用文字绑定元数据固化给每个语义块保留页码、坐标和父级路径。之所以要输出JSON双轨是因为纯Markdown在表达“父子关系”和“块元数据”时能力有限。Markdown适合给人看JSON适合给程序用。双轨输出让我们可以同时兼顾可视化调试和机器消费也方便接进Dify这类RAG工作流时不丢信息。2.2 关键技术拆解从物理版面到语义树这一层里最难的部分是把物理版面恢复成语义结构。直接看文字很难理解我拿一个具体的处理流程来拆。第一步是坐标排序。PDF解析出来的每个文本块都有bbox边界框和坐标。难点在于阅读顺序不是简单按y坐标排列就行的双栏文档里左边栏读完要跳到右边栏表格里的单元格有自己独立的阅读顺序。我们的做法是先按y轴聚类出行再在行内按x轴排序最后用简单的版面区域划分来判断栏位。这里最关键的判断逻辑是当两个文本块的垂直投影有重叠时它们属于同一行当一行左侧有大量空白且上一行的x中心点偏离当前行x中心点超过阈值时考虑是否发生了栏位切换。第二步是章节树重建。Markdown里的标题看起来有层级但实际是从字体大小和编号模式推断出来的并非文档自带的语义信息。我们的做法是对所有标题候选块做一次综合评分标题字号权重、加粗权重、编号正则匹配权重、上下文段落风格权重然后从得分最高的标题开始用栈结构逐层压入构建出一棵文档树。这一步的精度直接决定后续所有段落归属的准确性。第三步是表格语义头识别。我们先用规则方法找出最可能的表头行规则包括单元格数量最多、位于表格顶部、包含非数字内容比例高。然后用一个轻量LLM做二次校验把规则方法给出的候选表头行拼成问题问LLM“这一行是否是该表格的表头”输出置信度。规则方法速度快但容易误判LLM准确率高但太慢两者结合才能兼顾精度和速度。2.3 与MinerU的差异化对比项目上线半年后我们自己拉了一个评测集从学术论文、技术文档、产品手册、行业报告四个类型共100篇文档里对比了纯MinerU的解析结果和加了语义增强层的解析结果。先说结论也放个表格方便对照。对比维度MinerU原生输出MinerU 语义增强层输出格式扁平Markdown语义Markdown JSON双轨章节层级视觉保留无显式结构完整的语义树父子关系显式表格上下文表格独立存在绑定所属章节与引用文本图文关联图片/公式独立关联引用语句与上下文RAG切片友好度依赖固定窗口硬切按语义块预切分保留元数据块级召回率30篇消融测试基线测试约72%提升到约86%这个召回率差距在内部文档上更明显。有一份200页的产品规格书我们用固定窗口切片经常出现“参数A的描述切片里没有参数A的数值”这种问题用语义增强层预切分之后同一个问题从“找不到”变成“精准命中”。当然不同文档集上的数字差异很大但方向是一致的多做的这一层在RAG场景下的收益是明确的。3. 工程落地从原型到可复现的pipeline3.1 整体架构与模块划分整个项目的pipeline有四层。第一层是预处理负责PDF解析成页面图像、按页提取原始文本框、检测字体和颜色信息第二层是版面解析这也是MinerU的强项我们直接基于开源的版面检测模型和OCR组件来打底第三层就是前面说的语义增强层这是我们自己写的核心模块第四层是输出层同时落Markdown和JSON。模块划分上我们坚持“把核心做重把外围做薄”的原则。版面解析、OCR、公式识别这些都是成熟能力没必要从头造轮子能挂开源的就挂开源的能调现成模型的就调现成模型把有限的研发精力全部压在语义增强层上。这一点是我特别想跟做开源项目的朋友说的如果你的项目想快速拿到用户心智核心差异化一定只能有一个把它打磨到极致其他所有环节能复用就复用不要贪多。依赖选型上也有几个考量。OCR我们试过PaddleOCR和Tesseract最终选型主要看中文字体和公式场景的识别率PaddleOCR在中文文档上明显占优。版面检测模型用了MinerU开源出的版面分析模型它在大文档类型上的泛化表现比我们自训练的更稳。后处理层里的章节树重建和表格语义头校验我们用了一个不到1B参数的轻量模型跑本地推理这样用户部署时不依赖外部API数据也不出本机。3.2 核心模块实现细节章章节树重建是整个语义增强层里最核心的部分这里贴一段核心逻辑的伪代码完整版在项目仓库里能跑通。def build_section_tree(title_blocks): root SectionNode(titleNone, level0, parentNone) stack [root] for block in title_blocks: # block.level 由字体大小、加粗、编号模式综合打分得出 node SectionNode(titleblock.text, levelblock.level, parentNone) # 栈顶元素是当前最后一个打开的节点 # 新节点的层级如果小于等于栈顶就不断出栈直到找到父节点 while stack[-1].level node.level: stack.pop() node.parent stack[-1] stack[-1].children.append(node) stack.append(node) return root这套逻辑其实和解析HTML DOM是一个思路用栈维护层级关系。看着简单真正难的在于前面每个标题块的level打分准不准。我们试过只靠字号判断效果很差因为很多PDF嵌入字体时不会暴露真实的字号信息后来加了“加粗状态”“编号正则”“是否独占一行”“该行和上一行的间距”四个特征做加权打分准确率才稳定在95%以上。性能方面我们在一张RTX 3090上测过基准单页PDF从版面解析到语义增强输出平均耗时在4到6秒之间其中语义增强层这部分只占不到1秒。内存占用峰值大约2.5GB。这个性能虽然比纯MinerU慢一点但处于可接受的范围内毕竟多出来的是结构信息。实测下来比起动辄几万块的商业解析服务自建的性价比还是高的。3.3 Windows 11本地部署与测试项目开源后“Windows部署失败”是issue区里数量最多的一类这也侧面说明很多想在本地试新项目的用户主力环境就是Windows。我们自己也在Windows 11上做了完整的部署验证讲几个容易踩的坑。第一个坑是环境隔离。项目依赖PyTorch、PaddleOCR、若干图像处理库互相之间版本敏感直接往系统Python里pip install大概率会冲突。我们推荐用venv或者conda单独建环境Python版本锁定3.10。GPU版本的话PyTorch的CUDA版本要和显卡驱动匹配这里不展开讲驱动怎么装只提醒一句装完驱动先跑一下nvidia-smi确认能正常输出GPU状态再继续不然装了半天模型跑不起来还以为是代码问题。第二个坑是中文字体缺失。版面解析和OCR在还原文档时如果系统里没有文档用到的字体渲染出来就是一片豆腐块。Windows 11中文系统一般自带微软雅黑和宋体但如果解析的是PDF里嵌入了特殊字体的文档仍然可能出问题。一个稳妥的做法是把常用中文字体目录复制到项目运行目录下并在配置里显式指定字体路径。第三个坑是显存占用。Transformer类版面模型跑起来显存占用波动很大我们遇到过文档里某几页图片特别多时直接OOM的情况而OOM的表现不是报错而是进程卡死看起来像死循环。我们在文档里专门放了几条nvidia-smi的监控命令比如每隔几秒刷一次显存占用一旦发现占用异常飙升就及时干预能少浪费很多排查时间。# 每隔2秒刷一次显存占用可以挂一个终端窗口专门观察 watch -n 2 nvidia-smi4. 开源冷启动3个月3000star的增长复盘4.1 产品名、定位与第一眼印象说句掏心窝的话开源项目能不能起来代码写得好只占一部分剩下很大一部分取决于用户第一眼看到你的仓库时能不能在三秒内理解你解决什么问题、你有什么不同。我们项目名起得就很直接不装高深README第一屏不用长文章直接放一个对比动图左边是某个复杂排版PDF的解析结果右边是我们加了语义增强层之后的结果同一段文字块级切片之后被打上章节标签和上下文关联。这个动图几乎拿下了所有第一次点进来的人。产品的差异化定位也浓缩成一句话PDF解析完之后还要有一层面向RAG和LLM的语义增强。这句话不绕弯子不堆术语每读一遍都能让用户明白“你比MinerU多做的到底是什么”。很多项目死就死在README写得像技术报告用户点进来三分钟还搞不清这项目能帮他干嘛。我们宁可牺牲一部分看起来很专业的描述也要保证第一屏的通俗性。4.2 冷启动阶段的传播动作项目正式开源的第一周我们没有急着发全网而是先做了一场“种子用户邀请测试”。内部找了几十个正在折腾RAG知识库的朋友把预发布版本发给他们跑收集反馈的优先级是部署失败率 解析效果 功能请求。之所以把部署失败率放在第一位是因为早期用户的耐心非常有限部署失败一次大概率不会再回来。第一周我们几乎都在处理Windows环境下的兼容问题把所有能提前踩平的坑全部踩平。第二周同步发了几篇技术博客标题角度都是跟着场景走的而不是项目介绍。比如其中一篇叫《RAG召回率上不去可能是你的解析结果缺了语义骨架》里面用我们的项目做了案例拆解。这类文章不硬广但它把“PDF转Markdown之后还缺什么”这个问题讲透了读者看完自然会去找解决方案。在V2EX、知乎、掘金和公众号各自发布效果最意外的是V2EX上的技术讨论帖评论区吵了一波“MinerU已经够用了为什么还要自己做”正好给了我们一次正面解释差异化的机会。HuggingFace Space上也挂了一个线上demo用户可以传一份PDF直接看到解析语义增强的完整效果。线上demo对star增长的拉动非常明显很多人是点了demo之后觉得效果超出预期才回来点star的。这一点很重要如果你的项目能让用户在线直接体验就不要让他先clone代码再配置环境。4.3 让star自然增长的一些运营技巧star的增长不是线性的而是有节点效应的。每次项目发布新版本、每次有大V文章提到、每次有人把对比评测发到社区都会带来一波脉冲式增长。我们要做的不是强行制造脉冲而是在脉冲到来时能让用户点下那个star。几个小技巧比较有效第一个README里在演示图下面放一行字“如果这个项目帮到了你请点一个star这对我持续维护非常重要。”这种坦诚的请求看着朴素但转化率比想象中高很多尤其是用户刚看完一个效果惊艳的demo之后情绪到位了顺手点个star是成本极低的事。第二个issue的响应速度和态度直接决定社区口碑。新项目初期每一个issue都是活广告。哪怕是一个用户报了个bug我们也会当天回复给出排查步骤或临时方案问题解决后再同步到文档里。很多用户就是因为issue的处理体验好从路人变成了持续关注者。第三个保持固定的release节奏。我们基本两周一版每次发布都有明确的更新点要么是支持了某类特殊PDF格式要么是提升了某个模型的准确率。固定的更新频率会让观望者觉得“这个项目还活着团队在持续投入”这一点在开源圈里比任何宣传都重要。第四点比较微妙我们遇到过拿我们的项目去和MinerU做AB对比然后发到社交媒体的用户。这种第三方对比文章带来的流量非常可观但不可控。我们能做的就是保证项目在对比时不会因为细节问题翻车所以每次收到这种对比文章的预览稿我们都会特别认真地把里面的每一个测试案例都验证一遍再回复。5. 踩坑实录那些文档里不会写的教训5.1 模型层踩坑版面误检与跨页表格断裂版面模型再强也架不住现实文档的多样性。我们遇到最多的问题是表格被误检成图片尤其是那种只有边框线、内部没有任何文字内容的表格版面模型容易把它归成图片类。误检之后语义增强层就拿不到表格内部的文字了下游RAG自然就检索不到。我们的对策是加了一个校验层如果检测结果是图片但图片内部可以通过OCR提取出排版规整的多行文本就把这个区域重新标记为表格候选再做一次表格结构解析。这套策略把表格误检率降了约四成。跨页表格断裂是另一个高频问题。一个表格可能在第5页底部开始第6页顶部结束排版解析会把它当成两个独立表格。我们处理的方式是在语义增强层里加了一个跨页实体合并模块同一列结构、相邻页码、表头一致的两个表格区域会在后处理阶段被合并成逻辑上的一个完整实体同时记录它的起止页码。这一步在做技术文档解析时非常有用但会显著增加代码复杂度。5.2 部署层踩坑Windows环境与并发推理前面已经聊了Windows部署的几个坑这里再补一个并发推理的坑。项目允许用户启动一个本地服务批量处理PDF多线程并发推理时如果显存管理不当很容易在几小时的长任务中慢慢泄漏最终整个服务挂掉。排查来排查去发现是某些OCR组件在批处理模式下没有正确释放中间张量。解决方案是在主进程里定时清理推理缓存并且对长任务做分批重启。如果你在Windows下跑批量任务还要注意一个系统级问题Windows对单个进程的虚拟内存有Commit Limit限制批量跑大量PDF时进程可能突然崩溃日志里什么都不留。我们后来在文档里明确建议Windows用户开启系统自动管理分页文件并且把批处理拆成小批次执行问题概率会小很多。这类问题不踩一次真的不会想到排查成本极高。5.3 评测层踩坑别被字符级精确率骗了项目做了半年评测指标迭代了三版。第一版我们用的是字符级编辑距离准确率高得惊人几乎到了99%但拿去接RAG一测效果照样拉胯。后来才想明白字符级准确率其实衡量的是“文字有没有识别错”而不是“结构有没有搞对”。一个段落哪怕被错误地归到了上一章字符本身依然完全正确但这个结构错误对RAG的危害远大于几个字符的识别误差。第二版我们加上了块级F1也就是以语义块为单位计算召回和精确率这次数字就诚实多了。又跑了30篇文档做了RAG消融测试才算真正摸清楚语义增强层带来的实际收益。做这种偏底层能力评估的项目我强烈建议早一点引入“下游任务指标”不要只盯解析本身的指标。解析效果是手段下游能不能用好才是目的。6. 一些个人体验如果让我复盘这个项目最值得说的经验不是语义增强层的技术细节而是那个立项决策没有一头扎进“把PDF解析做得更好”的存量竞争中而是找到了“解析完之后的语义化重组”这个增量空间。它不需要我们推翻MinerU做得很好的部分只需要在上面多盖一层就能给用户带来可感知的差异。这种思路在开源项目里同样适用与其做一个全面但平庸的替代品不如找准一个别人没做透的环节打穿打透。最后再分享一个小技巧我们所有的调优和评测都是围绕三个真实文档集反复进行的一份顶会论文PDF、一份几十页的产品规格书、一份双栏扫描版行业报告。这三个文档集覆盖了大多数用户的典型场景效果过了这三关基本能应对80%的真实需求。不要拿一堆PDF混合测完看平均分就收工单独看每份文档的失败案例才是提升质量的真正路径。