ASL Tool 3 Development Plan: - Architecture blueprint v1.5 (6 rounds of architecture review, 13 red lines) - M1/M2/M3 sprint checklists (Skeleton Pipeline / HITL Workbench / Dynamic Template Engine) - Code patterns cookbook (9 chapters: Fan-out, Prompt engineering, ACL, SSE dual-track, etc.) - Key patterns: Fan-out with Last Child Wins, Optimistic Locking, teamConcurrency throttling - PKB ACL integration (anti-corruption layer), MinerU Cache-Aside, NOTIFY/LISTEN cross-pod SSE - Data consistency snapshot for long-running extraction tasks Platform capability: - Add distributed Fan-out task pattern development guide (7 patterns + 10 anti-patterns) - Add system-level async architecture risk analysis blueprint - Add PDF table extraction engine design and usage guide (MinerU integration) - Add table extraction source code (TableExtractionManager + MinerU engine) Documentation updates: - Update ASL module status with Tool 3 V2.0 plan readiness - Update system status document (v6.2) with latest milestones - Add V2.0 product requirements, prototypes, and data dictionary specs - Add architecture review documents (4 rounds of review feedback) - Add test PDF files for extraction validation Co-authored-by: Cursor <cursoragent@cursor.com>
21 KiB
21 KiB
PDF 表格提取引擎设计方案
文档版本: v1.0
创建日期: 2026-02-23
最后更新: 2026-02-23
文档目的: 定义 PDF 表格提取引擎的统一架构,为系统综述/Meta 分析等场景提供精确的结构化表格数据
核心原则: 引擎对使用者透明 — 提交 PDF,返回结构化表格,无需关心底层实现
当前状态: MinerU Cloud API (VLM) 已接入并完成测试,其他引擎待逐步评测
1. 业务背景
1.1 核心需求
ASL 智能文献模块的全文复筛环节,需要从医学 PDF 文献中精确提取数据表格:
- 系统综述 (Systematic Review): 基线特征表、结局指标表、不良事件表
- Meta 分析: 效应值、置信区间、样本量等关键数值
- 数据核验: 数值必须与原文 100% 一致,不容许任何精度损失
1.2 为什么独立建设
当前文档处理引擎基于 pymupdf4llm,定位是 PDF → Markdown 全文文本转换,在表格提取场景中存在严重缺陷:
| 问题 | 实测数据 |
|---|---|
| 8 篇 PDF 仅 1 篇输出结构化表格 | 表格检出率 12.5% |
| 其余 7 篇表格退化为纯文本 | 行列结构完全丢失 |
| 不支持合并单元格 | 医学表格大量使用 rowspan/colspan |
结论:全文文本提取和结构化表格提取是两个不同的能力,需要分别建设。
2. 引擎架构设计
2.1 核心理念
使用者不需要关心底层用了什么技术,只需要:提交 PDF → 获取结构化表格。
底层引擎可以是 MinerU、Qwen-VL、PaddleOCR、Docling 或任意其他方案,通过统一接口抽象,实现热切换和渐进升级。
2.2 统一架构
┌─────────────────────────────────────────────────────────────┐
│ 业务层 (使用者) │
│ ASL 全文复筛 / 系统综述数据提取 / Meta 分析 │
│ │
│ const tables = await tableEngine.extract(pdfBuffer); │
│ // 只关心输入 PDF 和输出 tables,不关心底层引擎 │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ PDF 表格提取引擎 (统一抽象层) │
│ │
│ interface TableExtractionEngine { │
│ extract(pdf: Buffer): Promise<ExtractedTable[]> │
│ extractFromUrl(url: string): Promise<ExtractedTable[]> │
│ } │
│ │
│ 统一输出:ExtractedTable[] │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ { title, headers, rows, mergedCells, footnotes, │ │
│ │ pageNumber, confidence, rawHtml } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ 引擎适配器 (可插拔) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MinerU │ │ Qwen3-VL │ │ PaddleOCR-VL │ │
│ │ Cloud API │ │ 多模态 LLM │ │ 百度 OCR │ │
│ │ (VLM) │ │ │ │ │ │
│ │ ✅ 已接入 │ │ 📋 待评测 │ │ 📋 待评测 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Qwen-OCR + │ │ Docling │ │ DeepSeek │ │
│ │ Qwen-Long │ │ (IBM) │ │ LLM │ │
│ │ │ │ │ │ │ │
│ │ 📋 待评测 │ │ 📋 待评测 │ │ ✅ 已测试 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.3 统一输出格式
无论底层使用哪个引擎,输出都遵循统一的 ExtractedTable 结构:
interface ExtractedTable {
/** 表格标题 (如 "Table 1 Baseline characteristics") */
title: string;
/** 表头行 */
headers: string[];
/** 数据行 (二维数组) */
rows: string[][];
/** 合并单元格信息 */
mergedCells?: MergedCell[];
/** 脚注 */
footnotes?: string[];
/** 所在 PDF 页码 */
pageNumber?: number;
/** 引擎自信度 (0-1) */
confidence?: number;
/** 原始 HTML (供前端渲染或调试) */
rawHtml?: string;
/** 原始 Markdown (备选格式) */
rawMarkdown?: string;
}
interface MergedCell {
row: number;
col: number;
rowSpan: number;
colSpan: number;
}
3. 候选引擎全景
3.1 引擎候选清单
| 引擎 | 类型 | 特点 | 成本 | 状态 |
|---|---|---|---|---|
| MinerU Cloud API | VLM 云端 | 表格结构最完整,rowspan/colspan 支持 | 2000 页/天免费 | ✅ 已接入 |
| Qwen3-VL | 多模态 LLM | 多模态理解最强,复杂表格语义识别好 | 按 token 计费 | 📋 待评测 |
| Qwen-OCR + Qwen-Long | OCR + LLM 组合 | 成本最低、功能最全的组合方案 | 极低 | 📋 待评测 |
| 百度 PaddleOCR-VL 1.5 | VL OCR | 医学场景案例多,准确率高,免费额度最多 | 官方免费额度多 | 📋 待评测 |
| Docling (IBM) | 本地部署 | MIT 开源,TableFormer 模型,可完全离线 | 免费 (本地部署) | 📋 待评测 |
| DeepSeek LLM | 文本 LLM | 从原始文本重构表格,Markdown 输出 | ~0.14 元/万 token | ✅ 已测试 |
3.2 推荐分类
最佳性价比组合:
- Qwen-OCR + Qwen-Long — 成本最低,功能最全
- 百度 PaddleOCR-VL — 官方免费额度最多,技术最成熟
医学文献表格提取最佳选择:
- Qwen3-VL — 多模态理解最强,支持复杂表格
- 百度 PaddleOCR-VL 1.5 — 医学场景案例多,准确率高
数据合规 / 离线场景:
- Docling (IBM) — MIT 开源,完全本地部署
3.3 评测计划
按优先级逐步评测,使用同一组 8 篇医学 PDF 文献作为基准:
| 阶段 | 引擎 | 优先级 | 评测重点 |
|---|---|---|---|
| ✅ 已完成 | MinerU Cloud API | — | 作为 baseline |
| ✅ 已完成 | DeepSeek LLM | — | 文本 LLM 方案的上限 |
| P1 待测 | Qwen3-VL | 高 | 多模态 vs MinerU VLM 的表格精度 |
| P1 待测 | PaddleOCR-VL 1.5 | 高 | 免费额度 + 医学场景准确率 |
| P2 待测 | Qwen-OCR + Qwen-Long | 中 | 验证最低成本方案的可行性 |
| P2 待测 | Docling | 中 | 离线方案,评估部署成本 |
4. 已完成测试:MinerU vs pymupdf4llm vs DeepSeek
4.1 测试概要
- 测试对象: 8 篇真实医学 PDF 文献(含 1 篇中文),涵盖 RCT、队列研究
- 测试方法: pymupdf4llm (本地) / MinerU Cloud API (VLM) / DeepSeek LLM (deepseek-chat)
4.2 核心结果
| 指标 | pymupdf4llm | MinerU API (VLM) | DeepSeek LLM |
|---|---|---|---|
| 结构化表格检出 | 3 个 (12.5%) | 28 个 (100%) | 24 个 (85%) |
| 输出格式 | 纯文本 | HTML <table> |
Markdown |..| |
| 合并单元格 | ❌ | ✅ rowspan/colspan | ⚠️ 文字描述 |
| 数值精度 | ✅ 原始 | ✅ 100% 保真 | ⚠️ 可能翻译 |
| 总耗时 (8 篇) | 16.1s | ~50s | 234.6s |
| 综合评分 | 2.7/5 | 4.6/5 | 3.4/5 |
4.3 逐文件对比
| # | 文件 | pymupdf4llm | MinerU API | DeepSeek LLM |
|---|---|---|---|---|
| 1 | S2589537025 (EClinMed) | 0 表格 | 1 HTML | 1 MD |
| 2 | Dongen 2003 | 0 结构化 | 4 HTML | 3 MD |
| 3 | Ginkgo+Donepezil | 0 结构化 | 3 HTML | 3 MD |
| 4 | Ginkgo Community | 0 结构化 | 6 HTML | 6 MD |
| 5 | Ginkgo NPS | 3 MD | 3 HTML | 3 MD |
| 6 | Herrschaft 2012 | 0 结构化 | 3 HTML | 3 MD |
| 7 | Ihl 2011 | 0 结构化 | 3 HTML | 3 MD |
| 8 | NIRS 队列研究 (中文) | 0 结构化 | 5 HTML | 2 MD |
4.4 质量深度分析 (Herrschaft 2012 — Table 1)
原始表格: 5 列、18 行,"Type of dementia" 合并 3 行。
| 特征 | pymupdf4llm | MinerU API | DeepSeek LLM |
|---|---|---|---|
| 列数正确 | ❌ 无结构 | ✅ 5 列 | ✅ 4 列 |
| 行数完整 | ✅ 数据在 | ✅ 18 行 | ✅ 18 行 |
| 合并单元格 | ❌ | ✅ rowspan=3 | ⚠️ 加粗标注 |
| 数值保真 | ✅ | ✅ 含 ± | ⚠️ 翻译行名 |
4.5 综合评分
| 维度 | pymupdf4llm | MinerU API | DeepSeek LLM |
|---|---|---|---|
| 表格检测率 | 1/5 | 5/5 | 4/5 |
| 结构保真度 | 1/5 | 5/5 | 4/5 |
| 数值精度 | 5/5 | 5/5 | 4/5 |
| 速度 | 5/5 | 3/5 | 2/5 |
| 合并单元格 | 1/5 | 5/5 | 3/5 |
| 中文支持 | 3/5 | 5/5 | 4/5 |
| 成本 | 5/5 | 4/5 | 3/5 |
| 综合 | 2.7 | 4.6 | 3.4 |
5. 技术实现设计
5.1 接口抽象
// common/document/tableExtraction/types.ts
/** 统一引擎接口 — 所有适配器必须实现 */
interface ITableExtractionEngine {
readonly name: string;
extract(pdf: Buffer, options?: ExtractionOptions): Promise<ExtractionResult>;
}
interface ExtractionOptions {
language?: 'ch' | 'en' | 'auto';
/** 指定页码范围,如 "1-5,8" */
pageRanges?: string;
/** 是否启用公式识别 */
enableFormula?: boolean;
}
interface ExtractionResult {
tables: ExtractedTable[];
/** 引擎名称 */
engine: string;
/** 处理耗时 (ms) */
duration: number;
/** PDF 总页数 */
pageCount: number;
/** 原始 Markdown 全文 (可选) */
fullMarkdown?: string;
}
5.2 引擎管理器
// common/document/tableExtraction/engineManager.ts
class TableExtractionEngineManager {
private engines: Map<string, ITableExtractionEngine> = new Map();
private defaultEngine: string = 'mineru';
/** 注册引擎适配器 */
register(engine: ITableExtractionEngine): void {
this.engines.set(engine.name, engine);
}
/** 设置默认引擎 */
setDefault(name: string): void {
this.defaultEngine = name;
}
/** 提取表格 — 使用者唯一入口 */
async extract(
pdf: Buffer,
options?: ExtractionOptions & { engine?: string }
): Promise<ExtractionResult> {
const engineName = options?.engine || this.defaultEngine;
const engine = this.engines.get(engineName);
if (!engine) throw new Error(`Engine not found: ${engineName}`);
return engine.extract(pdf, options);
}
}
5.3 MinerU 适配器 (第一个实现)
// common/document/tableExtraction/engines/mineruEngine.ts
class MinerUEngine implements ITableExtractionEngine {
readonly name = 'mineru';
async extract(pdf: Buffer, options?: ExtractionOptions): Promise<ExtractionResult> {
// 1. 请求上传 URL
// 2. 上传 PDF
// 3. 轮询等待解析完成
// 4. 下载结果 ZIP
// 5. 解析 HTML 表格 → ExtractedTable[]
// ...
}
}
5.4 未来适配器 (预留接口)
// 后续逐步实现
class Qwen3VLEngine implements ITableExtractionEngine { ... }
class PaddleOCRVLEngine implements ITableExtractionEngine { ... }
class QwenOCRLongEngine implements ITableExtractionEngine { ... }
class DoclingEngine implements ITableExtractionEngine { ... }
5.5 文件规划
backend/src/common/document/tableExtraction/
├── types.ts # 统一类型定义
├── engineManager.ts # 引擎管理器 (统一入口)
├── htmlTableParser.ts # HTML <table> → ExtractedTable 转换
├── engines/
│ ├── mineruEngine.ts # MinerU Cloud API 适配器 ✅ 首个实现
│ ├── qwen3vlEngine.ts # Qwen3-VL 适配器 (待实现)
│ ├── paddleOcrEngine.ts # PaddleOCR-VL 适配器 (待实现)
│ ├── qwenOcrLongEngine.ts # Qwen-OCR + Qwen-Long 适配器 (待实现)
│ ├── doclingEngine.ts # Docling 适配器 (待实现)
│ └── deepseekEngine.ts # DeepSeek LLM 适配器 (已测试,可选)
└── index.ts # 导出统一入口
6. 使用方式
6.1 业务层调用 (使用者视角)
import { getTableExtractionEngine } from '@/common/document/tableExtraction';
// 使用者不需要知道底层是 MinerU 还是 Qwen-VL
const engine = getTableExtractionEngine();
const result = await engine.extract(pdfBuffer, { language: 'auto' });
for (const table of result.tables) {
console.log(`${table.title}: ${table.rows.length} 行 × ${table.headers.length} 列`);
// 直接使用结构化数据
}
6.2 管理员切换引擎
# backend/.env — 切换默认引擎
TABLE_EXTRACTION_ENGINE=mineru # 当前默认
# TABLE_EXTRACTION_ENGINE=qwen3vl # 未来切换
# TABLE_EXTRACTION_ENGINE=paddle # 未来切换
# MinerU 配置
MINERU_API_TOKEN=your_token
MINERU_API_BASE=https://mineru.net/api/v4
MINERU_MODEL_VERSION=vlm
6.3 场景决策矩阵
| 场景 | 推荐引擎 | 说明 |
|---|---|---|
| ASL 标题摘要初筛 | pymupdf4llm (文本引擎) | 不需要表格,只需全文文本 |
| ASL 全文复筛 — 表格提取 | PDF 表格提取引擎 | 自动选择最优引擎 |
| 系统综述数据提取 | PDF 表格提取引擎 | 需要精确数值表格 |
| Meta 分析效应值识别 | 表格引擎 + LLM 语义理解 | 提取 → 理解两步走 |
| PKB 知识库入库 | pymupdf4llm (文本引擎) | 只需 Markdown 文本 |
7. MinerU Cloud API 接入指南 (当前默认引擎)
7.1 API 概览
| 项目 | 说明 |
|---|---|
| 服务商 | OpenDataLab (上海人工智能实验室) |
| API 地址 | https://mineru.net/api/v4 |
| 认证方式 | Bearer Token |
| 模型版本 | vlm (视觉语言模型,推荐) |
| 免费额度 | 2000 页/天 |
| 文件限制 | 单文件 ≤ 200MB,≤ 600 页 |
7.2 核心流程
PDF 文件
│
▼
Step 1: POST /file-urls/batch → 获取预签名上传 URL + batch_id
│
▼
Step 2: PUT {pre-signed URL} → 上传 PDF 文件
│
▼
Step 3: 云端 VLM 模型自动解析 → 识别表格/文本/图片
│
▼
Step 4: GET /extract-results/batch/{batch_id} → 轮询状态
│
▼
Step 5: 下载结果 ZIP → 含 .md (内嵌 HTML 表格) + .json + images
7.3 代码示例
import requests, time, zipfile, io
TOKEN = "your_token"
API = "https://mineru.net/api/v4"
headers = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
# Step 1: 请求上传 URL
resp = requests.post(f"{API}/file-urls/batch", headers=headers, json={
"files": [{"name": "paper.pdf", "data_id": "paper1"}],
"enable_table": True,
"model_version": "vlm",
})
batch_id = resp.json()["data"]["batch_id"]
upload_url = resp.json()["data"]["file_urls"][0]
# Step 2: 上传文件
with open("paper.pdf", "rb") as f:
requests.put(upload_url, data=f)
# Step 3-4: 轮询等待
while True:
time.sleep(10)
r = requests.get(f"{API}/extract-results/batch/{batch_id}", headers=headers)
results = r.json()["data"]["extract_result"]
if all(x["state"] in ("done", "failed") for x in results):
break
# Step 5: 下载解析
for result in results:
if result["state"] == "done":
zr = requests.get(result["full_zip_url"])
with zipfile.ZipFile(io.BytesIO(zr.content)) as zf:
for name in zf.namelist():
if name.endswith('.md'):
md = zf.read(name).decode('utf-8')
# md 中包含 HTML <table> 格式的表格
7.4 输出格式
MinerU 的表格以 HTML <table> 嵌入 Markdown 中,完整保留合并单元格:
<table>
<tr><td rowspan="3">Type of dementia</td><td>Probable AD</td><td>107 (54)</td></tr>
<tr><td>Possible AD with CVD</td><td>73 (36)</td></tr>
<tr><td>Probable VaD</td><td>20 (10)</td></tr>
</table>
8. 成本估算
8.1 MinerU (当前)
| 场景 | 文献数 | 平均页数 | 总页数 | 天数 | 费用 |
|---|---|---|---|---|---|
| 小型综述 | 20 篇 | 10 页 | 200 页 | 1 天 | 免费 |
| 中型综述 | 100 篇 | 10 页 | 1000 页 | 1 天 | 免费 |
| 大型综述 | 500 篇 | 10 页 | 5000 页 | 3 天 | 免费 |
8.2 各引擎预估成本对比
| 引擎 | 免费额度 | 超出后单价 | 500 篇 (5000 页) 预估 |
|---|---|---|---|
| MinerU | 2000 页/天 | 待确认 | 免费 (分 3 天) |
| Qwen-OCR + Qwen-Long | 按 token | ~0.004 元/千 token | 约 10-20 元 |
| PaddleOCR-VL | 官方免费额度多 | 极低 | 接近免费 |
| Qwen3-VL | 按 token | ~0.02 元/千 token | 约 50-100 元 |
| Docling | 本地部署 | 仅算力成本 | 免费 |
| DeepSeek LLM | 按 token | ~0.14 元/万 token | 约 30-50 元 |
9. 测试脚本
9.1 已有脚本
| 脚本 | 路径 | 功能 |
|---|---|---|
| 三方对比测试 | extraction_service/test_pdf_table_extraction.py |
pymupdf4llm / MinerU / DeepSeek 完整对比 |
| 结果分析 | extraction_service/analyze_table_results.py |
从提取结果生成对比报告 |
9.2 运行方法
cd AIclinicalresearch
# 运行全部三个方法
python extraction_service/test_pdf_table_extraction.py
# 单独运行某个方法
python extraction_service/test_pdf_table_extraction.py pymupdf
python extraction_service/test_pdf_table_extraction.py mineru
python extraction_service/test_pdf_table_extraction.py deepseek
# 生成对比报告
python extraction_service/analyze_table_results.py
9.3 测试输出
extraction_service/test_output/pdf_table_extraction/
├── pymupdf4llm/ # pymupdf4llm 提取结果
├── mineru/ # MinerU 提取结果
├── deepseek/ # DeepSeek 提取结果
├── raw_results.json # 原始测试数据
└── comparison_report.md # 综合对比报告
9.4 后续评测扩展
新引擎的评测脚本将遵循同样的结构,添加到 test_pdf_table_extraction.py 中:
python extraction_service/test_pdf_table_extraction.py qwen3vl
python extraction_service/test_pdf_table_extraction.py paddle
python extraction_service/test_pdf_table_extraction.py qwenocr
10. 路线图
Phase 1: 基础框架 + MinerU (当前)
- MinerU Cloud API 对比测试
- DeepSeek LLM 对比测试
- 实现统一接口
ITableExtractionEngine - 实现
MinerUEngine适配器 - 实现
engineManager引擎管理器 - ASL 全文复筛集成
Phase 2: 多引擎评测
- Qwen3-VL 评测 + 适配器
- PaddleOCR-VL 1.5 评测 + 适配器
- 同一基准集横向对比报告
- 确定最优引擎组合策略
Phase 3: 性价比优化
- Qwen-OCR + Qwen-Long 评测 (最低成本方案)
- Docling 本地部署评测 (离线方案)
- 引擎路由策略 (按文档复杂度自动选择引擎)
Phase 4: 生产加固
- 提取结果缓存 (避免重复解析)
- 批量提取队列 (pg-boss 异步任务)
- 质量监控 (空表格/异常值检测)
- 引擎降级策略 (主引擎不可用时自动切换)
11. 相关文档
- 文档处理引擎 README — 引擎总览 (含全文文本提取)
- 文档处理引擎设计方案 V1 — pymupdf4llm 全文文本架构
- 文档处理引擎使用指南 — 现有 API 调用指南
- MinerU 官方文档 — MinerU Cloud API 在线文档
- 对比测试报告 — 完整测试数据
维护人: 技术架构师
设计原则: 引擎对使用者透明,底层可热切换,以测试数据驱动选型