MinerU 4.0四档解析与定位器:构建可溯源的RAG文档解析管线
发布时间:2026/9/30 16:14:33来源:尧图网络
做 RAG 项目这么多年我越来越觉得一个残酷的事实检索链路再花哨都救不了一坨稀烂的文档解析结果。很多团队把大量精力花在调 embedding 模型、折腾 rerank却忽视了最上游的 PDF 解析。直到某一天你发现合同里的“质保期”明明就在第 24 页可知识库怎么检索都召回不到才意识到问题出在解析那一层文字乱序、表格串页、页眉页脚混进正文、扫描件直接成空文档……这时候再回头补解析成本和返工量都相当痛苦。我最近把一套基于MinerU 4.0的文档解析管线跑通并沉淀成了工程模板核心就两件事四档解析选型 定位器还原原文位置。用它处理实施方案、合同、招标文件这类结构化要求极高的文档不仅能把内容干净地抽出来还能让 RAG 在回答时带上精确的页码、章节和段落坐标实现“回答即溯源”。这篇文章把思路、选型逻辑、部署方式和完整代码都摊开讲适合做 RAG 落地、知识库问答、文档结构化抽取的工程师和团队参考。文章偏工程实战会尽量少讲空话多给能直接抄走的方案。1. 先聊清楚RAG 的文档解析到底难在哪1.1 PDF 表面上是文本实际上是一堆排版指令很多开发者的直觉是“PDF 不就是文本文件吗直接抽字符串不就完了”。这个直觉在简单文档上成立一旦遇到真实世界的合同、标书、技术方案立刻破功。PDF 本质上是一套“打印指令集”而不是“文档语义格式”。同一个段落可能被拆成几十个 text run每个 run 有自己的坐标、字体、字号多栏文档的文字流顺序和人类阅读顺序完全是两回事表格在内容流里可能被打散成碎片页眉页脚、水印、页码混进正文里如果直接按内容流顺序拼出来的是一段逻辑断裂的“拼接怪”。更麻烦的是扫描件。工程领域的很多历史合同、传真件、纸质扫描件根本没有文本层整页就是一张图片。这时候如果没有 OCR 能力解析结果就是一个空壳。即便有 OCR表格线、手写批注、印章遮挡也能让结果千疮百孔。所以第一件事是建立正确认知文档解析不是“读取文本”而是“理解版面”。版面不理解后续的 embedding 切块、检索、溯源都是在垃圾数据上做精加工。1.2 解析质量直接决定了 RAG 的“命中率天花板”我在多个 RAG 项目里观察到一个规律检索率的上限不是模型决定的是解析质量决定的。你把 10 份 PDF 喂进去如果解析出来的文本乱序、残缺、把表格和正文糊成一团那再好的 embedding 模型也救不回来。因为 embedding 模型是在“解析后的文本”上计算相似度如果原文本来就是乱的语义向量自然也是乱的。另外还有一个容易被忽视的问题知识库可信度。企业内部用 RAG 做合同问答、标书审查时用户不仅要知道答案还要能核验“答案来自哪里”。一个只能给出回答、却无法定位到具体页面的 RAG 系统在正式业务里基本是不可用的——没人敢拿一个无法溯源的回答去签合同、做招投标决策。所以“定位器”不是锦上添花而是工程化 RAG 的刚需。MinerU 4.0 输出里天然携带页码、块级坐标、版面结构这正好补上了 RAG 溯源最缺的一环。1.3 实施方案、合同、招标文件的特殊需求这批文档有非常典型的共性需求需要输出包含页码、章节、段落的结构化文本而且对结构完整性要求极高。比如招标文件里“第三章 评标办法 3.2 技术评分标准”这种层级信息如果解析器把它拍平成一段普通文字后续做章节级过滤、条款级问答就会非常吃力。合同里的“第 5 条 违约责任”和“5.1 违约情形”如果丢失层级检索“甲方违约”时返回的上下文就缺少约束边界。这时候四档解析里高还原度的档位 定位器的价值就体现出来了它能在保留文本的同时把层级关系、版面位置一并带出来为 RAG 提供真正可用的结构化语料。2. MinerU 4.0 四档解析档位含义与选型逻辑2.1 四档解析分别是什么MinerU 4.0 的解析管线并不是“一把梭”式的全量 OCR而是把解析路径拆成了多个档位每个档位对应不同的计算开销、处理速度和还原度。我把实测中常用的四个档位整理如下不同小版本的档位命名可能有差异实际部署后先跑一下mineru-cli --help确认本地参数即可。档位典型 pipeline 参数触发逻辑适用场景速度体感精度特征L0 文本直取text直接从 PDF 内容流抽取文本不做 OCR原生数字 PDF、版式简单、文本层完整最快接近实时文本准但版面还原弱多栏可能乱序L1 全量 OCRocr每一页都渲染成图像再识别扫描件、图片型 PDF、无文本层较慢依赖 GPU版面还原好但受 OCR 识别率影响L2 自动档auto按页判断是否有可靠文本层混合走 L0/L1原生 PDF 和扫描页混排的文档视扫描页占比而定兼顾速度与召回适合通用场景L3 表格公式增强enhance/core在 OCR 基础上开启表格结构识别、公式识别、版面精细还原招标文件、财务报表、论文、产品手册最慢表格与公式还原度最高适合强结构化场景以我处理过的真实文档为例一份 80 页的电子招标文件用 L0 档大约 10 秒内就能出结果但多栏部分的阅读顺序偶尔会乱切成扫描页后改用 L2 自动档速度慢了但顺序稳定而一份 30 页的财务审计报告表格密集、还有跨页表我直接上 L3 档虽然耗时多了几倍但表格结构和数字列对应关系基本没出错。档位选择的核心逻辑是用成本换确定性文档越关键越值得开高档位。2.2 档位怎么选给 PDF 做一次“分诊”选档位不是拍脑袋而是先给 PDF 做一个快速体检。我在工程里通常是三步判断第一步探测文本层覆盖率。用 PyMuPDF 打开 PDF统计每页可提取的字符数。如果整篇字符数几乎为 0直接判定为扫描件L1 或者 L3如果字符数正常但版式复杂多栏、大量表格继续第二步。import fitz def probe_pdf(path: str): doc fitz.open(path) total_chars 0 empty_pages [] for i, page in enumerate(doc): n len(page.get_text()) total_chars n if n 20: empty_pages.append(i) print(fpages{len(doc)} total_chars{total_chars} empty_pages{empty_pages}) return len(doc), total_chars, empty_pages第二步看版式复杂度。如果一个页面里同时存在多个文字块、且坐标水平方向落差明显大概率是多栏文档如果页面里检测到大量横竖线可以用 PyMuPDF 的page.get_drawings()粗略判断大概率是表格密集页。这类页面直接建议 L2 起步而不是 L0。第三步按文档用途决定上限。合同、标书、审计报告这类结构化要求极高的文档建议直接 L3 增强档普通产品手册、公告、内部制度文件L2 自动档性价比最高。我在项目中给用户的默认配置就是 L2但会留一个“高精度解析”开关遇到关键文档时手动切换 L3。2.3 解析输出到底长什么样MinerU 4.0 解析完成后输出目录里通常有几类关键产物Markdown 文件、JSON 文件、图片目录以及带版面和坐标信息的中间 JSON。工程上最值得关注的是 JSON因为它保留了结构化信息和坐标。{ pdf_info: { pdf: 招标文件.pdf, pages: [ { page_idx: 0, width: 842, height: 595, rotation: 0 } ] }, content_list: [ { type: heading, page_idx: 2, bbox: [57.2, 120.8, 300.1, 150.6], level: 2, text: 2.1 质量保证期 }, { type: text, page_idx: 2, bbox: [57.2, 156.4, 500.6, 178.2], text: 投标人应承诺提供不少于 24 个月的质量保证期... }, { type: table, page_idx: 5, bbox: [57.2, 200.0, 520.0, 260.0], table_body: table...表格 HTML.../table } ] }看到这个结构就明白了每个内容块都有 type、page_idx、bbox、text。这三样东西组合起来就是定位器的原始素材。章节标题有独立 type 和 level正文块可以挂到最近的标题下表格有 body甚至可以还原成 HTML 表格。RAG 工程里最需要的“页码 章节 段落”三维定位在这个输出里已经天然分好了。3. 定位器让 RAG 既能回答又能指出出处3.1 定位器定位的到底是什么很多讲解 RAG 的文章会把“定位”理解成“给 chunk 加个页码标签”这其实太粗了。页码只能说明内容在哪一页但无法说明它在哪一章、哪一节、哪个段落。招标文件第 30 页可能同时包含三个章节的内容如果只标页码检索回来还是一整页的噪声。MinerU 的 JSON 输出给了我们更细的维度page_idx 负责页面定位章节链条负责语义定位bbox 负责版面坐标定位。三者结合才是真正完整的“定位器”。用生活类比来说这就好比你要在图书馆找一本书里的某句话。只看页码page_idx等于告诉你这本书在第几个书架的哪一层章节链等于告诉你这句在“第 3 章 第 2 节”里bbox 坐标等于精确到这句话在第几页的第几行、从哪个文字坐标开始到哪个坐标结束。页码解决了“在哪一页”章节链解决了“属于哪部分”bbox 解决了“精确到哪一行”。RAG 拿到这三层信息后回答时的引用才能真正落到人话层面。3.2 章节链与段落坐标构建定位元数据要把定位器接入 RAG得先把 MineR 的 JSON 转换成带章节链的块数据。核心操作是遍历content_list遇到type heading的 block就把它作为当前章节标题按 level 压栈遇到type text的 block就把它挂到当前标题栈下作为该章节的一个段落表格 block 同理挂到当前章节下并保留 bbox 坐标。这样每个文本块都能拿到一个完整的章节链比如[第二章 合同主要条款, 2.1 质量保证期, 2.1.1 质保期限]。在实际项目里这个章节链会直接作为 chunk 的 metadata 之一。后面无论做向量检索还是关键字检索都可以利用这个链条做“章节级过滤”比如“只在第三章节中检索评分标准”。3.3 在 RAG 检索结果中还原“原文位置”带定位器的 RAG 在答案呈现上和不带定位器是天壤之别。不带定位器的回答是“合同里约定了质保期 24 个月”用户没法核验带定位器的回答是“合同里约定了质保期 24 个月出处合同.pdf 第 12 页第二章 2.1 质量保证期”。用户直接翻到对应位置对照原文信任度完全不一样。工程实现上我会把定位信息拼进 prompt 的引文区让模型在回答时带上引用同时在返回结果里单列一个citations字段方便前端渲染来源卡片。这样即使模型在回复正文里忘了加引用前端也可以拿着 citations 里的页码、章节、bbox 做“去原文定位”交互。这正是 RAG 在合同审查、标书问答场景里能被业务方接受的关键。4. 工程化接入API 调用与本地部署两种姿势4.1 本地部署 MinerU 4.0MinerU 4.0 的本地部署比我预想中省事。官方推荐用 pip 安装然后通过命令行或者 Python SDK 调用。我这边 GPU 环境是单张消费级显卡实测解析一份 50 页的混合文档L2 档大约 40 秒L3 档大约两分钟作为异步任务完全可接受。# 安装核心依赖 pip install -U mineru[core] # 查看 CLI 参数确认本地档位名称 mineru-cli --help # 单文件解析自动档 mineru-cli -p ./data/合同.pdf -o ./output --pipeline auto # 多文件批量 GPU batch mineru-cli -p ./data/合同.pdf -o ./output --pipeline ocr --device cuda --batch 4有一点要注意本地部署的模型权重首次运行时会下载网络不好的环境建议先跑一个小文件把权重拉下来免得正式任务卡住。还有CLI 是本进程一次性任务工程接入时建议封装成子进程调用不要把它当成在线服务直接打流量。后面我会给出 Python 里调用 CLI 并解析输出的封装方式。4.2 API 方式接入如果只是企业内部用也可以选择把 MinerU 部署成常驻服务走 HTTP API 提交任务。流程一般是上传 PDF 文件 - 拿到任务 ID - 轮询任务状态 - 下载/读取解析结果。不同版本的 API 路径可能不一样部署后先看/docs看到 Swagger 接口文档再接入。import requests, time def parse_pdf_by_api(file_path: str, api_base: str http://127.0.0.1:8000, poll_interval: int 5): # 1. 上传文件提交解析任务 with open(file_path, rb) as f: r requests.post( f{api_base}/api/v1/file/parse, files{file: f}, timeout120, ) r.raise_for_status() task_id r.json()[task_id] # 2. 轮询任务状态 while True: status requests.get(f{api_base}/api/v1/task/{task_id}, timeout30).json() if status.get(state) in (done, failed, error): return status time.sleep(poll_interval)本地部署和 API 两种方式选哪个我建议看团队规模和使用频率。只有你会偶尔跑几份测试文档本地 CLI 足够如果有多业务线都要调用、量还不小API 方式能统一管控并发、加鉴权、做资源隔离。两种方案我目前都实际用过后面实战代码里会以本地解析 JSON 读取为主API 方式的核心逻辑是等价的只要把上面这个函数返回的 JSON 路径对接好即可。4.3 解析管线的工程细节工程化接入的坑往往不在 MinerU 本身而在外围。我把踩过的教训列一下一是超时控制。L3 档解析大文档可能跑几分钟到十几分钟接口调用方容易超时所以必须做异步任务化不能同步等待结果。我这里统一封装成“提交任务 - 轮询 - 结果落盘”的模式。二是缓存复用。同样的 PDF 反复解析是巨大的浪费。我在管线入口加了一层 MD5 文件指纹缓存如果目标 JSON 已存在且时间戳在源文件之后直接复用不再触发解析。三是并发控制。OCR 和版面分析非常吃显存同时跑多个任务很容易 OOM。我用信号量把并发数限制在 1~2 个宁可慢一点也不要挂。四是中间产物落盘。MinerU 解析出的中间 JSON 是后续分块策略调整的重要原料我会统一存到parse_cache/{file_hash}/目录后续调试分块参数时可以反复读取不用重新解析。5. 实战代码从 PDF 到带定位的 RAG 分块5.1 整体链路设计这一节直接上可跑的工程代码。链路分为四步解析、结构化读取、分块、检索。目录结构大致如下rag_doc_parser/ ├── parser.py # 调用 MinerU 并读取 JSON 产物 ├── blocks.py # Block 数据类与章节链构建 ├── chunker.py # 带定位信息的分块器 ├── vector_store.py # FAISS 封装 ├── embedder.py # embedding 模型封装 └── main.py # 示例入口为了控制篇幅我会把最关键几个文件讲透其余合并同类项。5.2 解析与结构化读取parser.py里做两件事调起 MinerU 解析读取输出的 JSON 文件。由于 MineU 的 CLI 是子进程任务我封装成run_mineru函数解析完成后直接从输出目录中找到 JSON 并载入。import os, json, subprocess, hashlib from typing import List, Dict, Any, Optional def file_hash(path: str) - str: h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def run_mineru(pdf_path: str, output_dir: str parse_cache, pipeline: str auto) - str: 调用 MinerU CLI 解析 PDF返回 JSON 路径命中缓存时直接复用。 os.makedirs(output_dir, exist_okTrue) fh file_hash(pdf_path) cache_root os.path.join(output_dir, fh) os.makedirs(cache_root, exist_okTrue) # 找候选 JSONMinerU 主要产物是 .json 结尾的结构化结果 existing [f for f in os.listdir(cache_root) if f.endswith(.json)] if existing: # 简单判断存在非中间 JSON 就直接复用 return os.path.join(cache_root, existing[0]) # 调用 CLI 子进程按 pipeline 参数选择档位 cmd [ mineru-cli, -p, pdf_path, -o, cache_root, --pipeline, pipeline, ] proc subprocess.run(cmd, capture_outputTrue, textTrue, timeout1800) if proc.returncode ! 0: raise RuntimeError(fMinerU failed: {proc.stderr[-500:]}) json_files [f for f in os.listdir(cache_root) if f.endswith(.json)] if not json_files: raise FileNotFoundError(fno json output in {cache_root}) return os.path.join(cache_root, json_files[0])5.3 构建章节链与结构化块blocks.py里定义一个Block数据类把 JSON 里的基础字段映射到类上。然后通过扫描content_list构建章节链。标题块用type heading或者category里带 title/heading 的字段判断不同版本字段名略有差异我这里做了兼容。from dataclasses import dataclass, field from typing import List, Optional, Tuple dataclass class Block: text: str block_type: str # text / table / heading / image page_idx: int bbox: Tuple[float, float, float, float] # x0, y0, x1, y1 section: List[str] field(default_factorylist) # 章节链 level: int 0 def load_blocks(json_path: str) - List[Block]: with open(json_path, r, encodingutf-8) as f: data json.load(f) blocks: List[Block] [] for item in data.get(content_list, []): bt item.get(type, ) page_idx item.get(page_idx, 0) bbox item.get(bbox, [0, 0, 0, 0]) text if bt in (text, heading): text item.get(text, ) elif bt table: body item.get(table_body, ) text table_html_to_markdown(body) if not text.strip(): continue blocks.append(Block( texttext.strip(), block_typebt, page_idxpage_idx, bboxtuple(float(v) for v in bbox), levelitem.get(level, 0), )) return blocks def assign_sections(blocks: List[Block]) - List[Block]: 遍历块列表遇到标题就压栈正文块挂到最近的标题链下。 section_stack: List[str] [] for b in blocks: if b.block_type heading: # level 小于等于栈顶层级时先弹出再压入 while len(section_stack) b.level and section_stack: section_stack.pop() section_stack.append(b.text) b.section list(section_stack) else: b.section list(section_stack) return blocks def table_html_to_markdown(html: str) - str: 把 MinerU 输出的表格 HTML 简转成 Markdown 表格文本方便切块和检索。 try: import pandas as pd dfs pd.read_html(html) if dfs: return dfs[0].to_markdown(indexFalse) except Exception: pass # 退路去掉标签保留文字用空格拼接 return .join(html.replace(tr, \n).replace(/tr, ) .replace(td, | ).replace(/td, ) .replace(th, | ).replace(/th, ).split())这里assign_sections是最核心的定位逻辑。它在不修改文本内容的前提下给每个块挂上section链条。后面做 RAG 检索时只要把section里最后两个元素拼到 metadata 里就能实现“章节级定位”。5.4 带定位的 RAG 分块与检索chunker.py里实现分块。我的分块策略不是按固定字符数硬切而是按章节链聚合 长度上限二次切分。具体逻辑是先按标题把同一个章节下的所有块聚合在一起如果总字符数超过阈值再在这个章节内按段落边界切分成多个 chunk每个 chunk 都继承同一个章节链。from typing import List, Dict, Any def chunk_blocks(blocks: List[Block], max_chars: int 800) - List[Dict[str, Any]]: 按章节链聚合超长章节再按块边界切分返回带定位信息的 chunk。 chunks: List[Dict[str, Any]] [] current_section: tuple () buffer: List[Block] [] buffer_chars 0 def flush(): nonlocal buffer, buffer_chars if not buffer: return text \n.join(b.text for b in buffer) first buffer[0] last buffer[-1] chunks.append({ text: text, metadata: { source: 原文档.pdf, # 实际可以从 pdf_info 或上层传入 section: first.section, # 章节链 page_start: first.page_idx 1, # 1-based 页码 page_end: last.page_idx 1, bbox_start: first.bbox, # 首块坐标 bbox_end: last.bbox, # 尾块坐标 block_cnt: len(buffer), }, }) buffer [] buffer_chars 0 for b in blocks: key tuple(b.section) if key ! current_section: flush() current_section key buffer [b] buffer_chars len(b.text) continue # 当前块加入后超上限则先 flush再开新块 if buffer_chars len(b.text) max_chars and buffer: flush() buffer.append(b) buffer_chars len(b.text) flush() return chunksvector_store.py里封装一个轻量 FAISS 索引。我默认用IndexFlatIP也就是内积相似度使用前先对向量做 L2 归一化等价于余弦相似度。import os import numpy as np import faiss from typing import List, Tuple, Dict, Any class VectorStore: def __init__(self, dim: int 1024): self.index faiss.IndexFlatIP(dim) self.metadata: List[Dict[str, Any]] [] def add(self, embedding: List[float], metadata: Dict[str, Any]): vec np.array([embedding], dtypenp.float32) faiss.normalize_L2(vec) self.index.add(vec) self.metadata.append(metadata) def search(self, embedding: List[float], k: int 5) - List[Tuple[Dict[str, Any], float]]: vec np.array([embedding], dtypenp.float32) faiss.normalize_L2(vec) scores, idxs self.index.search(vec, k) results [] for idx, score in zip(idxs[0], scores[0]): if idx -1: continue results.append((self.metadata[idx], float(score))) return results5.5 检索验证用一份真实合同跑通把上面的模块串起来我拿一份脱敏的采购合同 PDF 做了一次完整验证。先解析再分块再建索引然后问一个问题“质保期是多久”最后返回的不仅包括答案文本还包括定位元数据。from parser import run_mineru from blocks import load_blocks, assign_sections from chunker import chunk_blocks from vector_store import VectorStore # 1. 解析 PDF拿到 JSON 路径 json_path run_mineru(合同_采购.pdf, pipelineauto) # 2. 读取块并构建章节链 blocks assign_sections(load_blocks(json_path)) # 3. 生成带定位的 chunk chunks chunk_blocks(blocks, max_chars800) # 4. 用本地 embedding 模型向量化并建索引 from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) store VectorStore(dimmodel.get_sentence_embedding_dimension()) for c in chunks: store.add(model.encode(c[text]).tolist(), c[metadata]) # 5. 检索并返回引用 query 质保期是多久 qvec model.encode(query).tolist() for meta, score in store.search(qvec, k3): print(f[score{score:.4f}] {meta[section]} 第{meta[page_start]}-{meta[page_end]}页) print(meta[source])执行后的结果大致是[score0.78] [合同主要条款, 2.1 质量保证期] 第12-13页 合同_采购.pdf模型返回答案时我会把这两行定位信息拼进 prompt 的引用区回答时如果引用了文档内容请在答案末尾标注 [来源: 合同_采购.pdf 第12页 2.1 质量保证期]这样上下文是干净的回答是带出处的用户核验路径也是明确的。整个 RAG 流程的“脏活”就算干完了。6. 常见问题与排查技巧实录6.1 问题速查表工程上线至今我把遇到最多的问题和对应解法整理成一个速查表遇到类似症状可以直接抄。问题现象根因分析处理方案扫描 PDF 解析后全是空白块文档没有文本层却用了 L0 文本直取档改用 L1 或 L2并在管线上做文本层探测自动提档跨页表格被拆成多个表格块MinerU 按页返回 table block跨页表天然拆开解析后按表格标题、表头行匹配把连续页的同表 block 合并成一个bbox 坐标和页面尺寸对不上页面存在旋转坐标基于旋转前坐标系读取 pdf_info 中的 rotation 字段按旋转角度换算坐标OCR 错别字导致关键词检索不到识别噪声太高对 OCR 结果做阈值过滤正文少于 20 字符的块丢弃必要时配合 BM25 混合检索兜底章节链为空标题 type 未被识别用字号、加粗、位置启发式回填标题或读取 item 里 heading 相关字段长文档解析内存暴涨一次性把大 PDF 读入内存按页流式解析或把解析文件拆成多个任务结果落盘中转chunk 太大导致 embedding 效果差章节内文本过长超了 embedding 模型上限按段落边界二次切分超长章节内部按 max_chars 切成多块同一文档反复解析浪费 GPU没有缓存机制按源文件 MD5 做解析缓存命中时直接复用 JSON6.2 几个值得长期沉淀的工程经验第一个经验是解析缓存要尽早加。我最初为了省事每次测试都重新解析结果后面对齐分块参数时一天跑了二十多次 MinerUGPU 排队排到怀疑人生。后来加了 MD5 缓存跑分块实验就变成了纯 CPU 活秒级完成。第二个经验是不要把 embedding 和解析耦合得太死。我项目里的 embedding 层是抽象接口本地部署用 BGE线上也能切 OpenAI embedding 或者别的商用模型。解析层只管输出干净的文本 定位 metadata向量化和检索细节不往里掺这样后续换模型、换向量库都不会推翻重来。第三个经验是分块粒度不要迷信固定 token 数。固定 500 token 硬切看似优雅实际会把语义割得稀碎。我用的是“章节优先 上限切分”的思路先保证语义完整性再控制长度天花板。对 RAG 来说块内容完整比块大小均匀重要得多。第四个经验是定位器信息一定要进 prompt 的引用区。很多团队把定位信息只存在 chunk metadata 里前端渲染时才发现模型根本没使用。更好的做法是分两步走检索时拿到强候选 chunk 的定位信息构造 prompt 时显式注入引用提示模型生成时自然就会带出处。6.3 小技巧标题识别失败时的兜底方案最后分享一个非常实用的小技巧。MinerU 对标准排版的标题识别率很高但遇到设计风格花哨的文档比如美术排版、装饰性线条多、无衬线艺术字体偶尔会把二级标题识别成正文。这时候不要急着换解析器我在assign_sections里加了一个“启发式回填”的兜底如果某个正文块的文本很短比如少于 30 字、且它前后两个块的行文逻辑明显是“标题 正文”就把它强制当作标题处理。def heuristic_promote(blocks: List[Block]) - List[Block]: 把疑似标题的短文本块提升为章节标题。 for i, b in enumerate(blocks): if b.block_type ! text: continue # 短文本 位于页面上部 1/3 前一块是标题或页首 if len(b.text) 30 and b.bbox[1] 150 and i 0: b.block_type heading b.level 1 return blocks这段代码不是万能钥匙但确确实实帮我救回了两个原本章节链为空的文档。工程里不要怕“规则丑陋”能解决问题的规则就是好规则。做 RAG 越久越觉得技术选型里最该花心思的地方恰恰是那些看似不性感的环节。MinerU 的四档解析让我在“速度”和“精度”之间有了可动态调节的空间而定位器让知识库从“答非所问的玩具”变成了“可核验、可溯源的工程工具”。我个人在实际项目里的体会是解析层做得越扎实后续每个环节都会顺——检索准了prompt 好写了业务方信任度也高了。如果你正准备搭一套文档型 RAG别急着选 vector DB先把 PDF 解析这一关打透你会回来感谢我的建议。
网站建设高端定制企业官网