一篇技术处理分享:问题拆解、方法选择与验证路径
在知识库(RAG)建设中,复杂表格进向量库会面临切块截断与召回失败;把这类表格统一结构化,就是本分享要解决的问题。一个看似单一的"PDF 转 JSON"任务,真正的难度不在转换工具本身,而在三个决策:
- 把问题拆成两个子问题:结构还原(PDF→Markdown)与语义提取(Markdown→JSON)分离,各自独立选型、独立替换;
- 用数据建模定义"正确的输出":将最小数据单元定义为"定额编号 × 规格",而不是"一张表"——输出粒度决定了数据可用性;
- 用评估矩阵做工具选型,用平行对比做质量验证:贯穿全程的三个原则——先定评估维度再选工具、先归纳模式再写算法、先跑小样本再规模化。
最终产出:12 个PDF复杂表格文件全部结构化,12,714 条数据,下游问答对 CSV 8,527 条,端到端管线可重复运行。
本文的另一个目的是说明这套方法的边界:它解决的是"印刷语义与数据语义存在系统性偏差的复杂矩阵表"这一类问题。1.4 节用真实结构对照的方式,展示供应链运价表、上市公司财报中的同构输出——同样的结构难题(分档拆分、合并单元格、括号负数、跨页断表)在不同行业反复出现,方法也因此可迁移。
表象:"把 PDF 里的预算定额表转成 JSON"。 本质是两个性质完全不同的问题:
| 子问题 | 内容 | 性质 |
|---|---|---|
| 结构还原 | 把 PDF 中的表格在 Markdown 里还原出来(行、列、合并单元格) | 工具能力问题,有上限 |
| 语义提取 | 把还原出的表格按业务语义映射为数据模型 | 设计问题,可无限改进 |
这个拆分决定了整个架构:转换环节只负责"看得清",提取环节只负责"解得对"。环节间解耦换来的好处是任意一环可单独替换——后来云端解析工具出现时,只替换了转换环节,其余全部保留,验证了架构的合理性。
如果以"一张表"为输出单位,下游没法查"DN400 的基价是多少"。所以必须定义最小数据单元:
一条数据 = 主键 × 分档维度(在定额语境即"定额编号 × 规格",跨行业映射见 1.4)
横向展开的多规格表(DN300~DN800 并列多列),每列拆成一个独立对象。这个决策带来三个连锁要求,后来全部成为提取规则:
- 拆分后每条都需独立完整(项目名称、工作内容、单位随行复制)——所以拆页表必须合并回完整表再拆;
- 明细(人工/材料/机械)必须嵌套在父条内,不可独立浮出;
- 数量为 0 的消耗项也保留——"有这行但数量为 0"本身就是信息(该定额不含此消耗)。
印刷品转数据存在四类系统性偏差——注意,它们不是工程造价的特产,而是跨行业的通病:
| 系统性偏差 | 工程造价定额表 | 供应链运价表 | 上市公司财报 |
|---|---|---|---|
| 分档多列拆分 | DN300~DN800 多规格并列 | 重量档 0-45kg / 45-100kg / 100-300kg 并列 | 期间列 2024 / 2023 / 同比% 并列 |
| 合并单元格 | 项目名称跨多行 | 货物大类跨多行 | 科目大类("其中:主营业务")跨行缩进 |
| 跨页拆表 | 一张逻辑表断成两截 | 航线费率长表跨页断截 | 年报附注长表(如账龄表)跨页断截 |
| 括号/特殊符号语义 | (0.200) = 冲减量 -0.200 |
(15%) = 附加费冲减、— = 不承运 |
(16,930) = 负数——财报世界通行的"括号即负数"惯例 |
- 分档多列拆分:横向 DN300~DN800 多规格并列,每列拆成一个独立对象(即 1.2 的"主键 × 规格");
- 合并单元格:项目名称跨多行;转换工具还会把同格多个条目黏成一个文本块——多个"名称+单位+单价"挤一格,靠
<br>、空格、中文括号分隔,拆不出独立条目; - 跨页拆表:一张表跨页断成两截,下半截没有主键列,仅以明细表头开头;有时标题/表头行还被完整复制一次,产生重复段;
- 括号负数:全角括号表负数或冲减,
(0.200)实义-0.200,与财报(16,930)是同一类编码惯例。
方法论启示:处理任何"印刷品 → 数据"的转换,要先盘点源格式的系统性偏差,把它们当成必须显式处理的清单,而不是遇到一个修一个。本项目把这四类全部写进了算法与提示词的硬规则。
下面的三份 JSON 中,第一份取自真实交付数据的结构(项目/材料名称已脱敏,字段名与数值精度保留原样);后两份为供应链运价表、上市公司利润表按同一模板映射出的对应结构。先看字段级映射:
| 定额字段 | 供应链运价表 | 财报利润表 |
|---|---|---|
| 定额编号 | 运价编号 | 行次 |
| 项目名称 | 货物类别 | 报表项目 |
| 规格 | 重量档(0-45kg…) | 会计期间(2024 / 2023) |
| 计量单位 | 票 / kg | 百万元 |
| 基价 | 基础运价 | 金额 |
| 费用构成(人工费/材料费/机械费) | 附加费构成(燃油/旺季/偏远) | 明细行("其中"嵌套) |
| 人工 / 材料 / 机械[](名称/单位/单价/数量四字段) | 附加费明细[] | 明细[](名称 + 金额) |
| 括号负数 → 数字负号 | 费率冲减 → 数字负号 | 财报括号惯例 → 数字负号 |
| _source 溯源字段 | _source 溯源字段 | _source 溯源字段 |
① 工程造价定额表:
{
"定额编号": "9-78",
"项目名称": "某某工艺 · 某某工序",
"工作内容": "施工准备、拆除、清洗、回装、检验、清理现场。",
"规格": "DN400",
"计量单位": "处",
"基价": 1496.35,
"费用构成": { "人工费": 261.22, "材料费": 663.05, "机械费": 572.08 },
"人工": [
{ "名称": "综合工日", "单位": "工日", "单价": 183.86, "数量": 1.419 }
],
"材料": [
{ "名称": "材料A", "单位": "套", "单价": 2212.39, "数量": 0.025 },
{ "名称": "材料B", "单位": "片", "单价": 10.96, "数量": 0.100 },
{ "名称": "材料C", "单位": "m", "单价": 118.60, "数量": -0.200 }
],
"机械": [
{ "名称": "机械甲", "单位": "台班", "单价": 287.42, "数量": 1.811 },
{ "名称": "机械乙", "单位": "台班", "单价": 68.85, "数量": 1.773 }
],
"_source": { "chapter": "第一章 某某工程", "h3": "十、某某项目" }
}注:
材料C的-0.200来自原表(0.200)——括号即冲减量;数量为 0 的消耗项同样保留对象。
② 供应链跨境物流分区运费费率表(同一模板推导的一般结构):
{
"运价编号": "7-1",
"货物类别": "普货",
"规格": "0-45kg",
"计量单位": "票",
"基础运价": 85.0,
"费用构成": { "燃油附加费": 12.75, "旺季附加费": 8.5, "偏远附加费": 0.0 },
"附加费明细": [
{ "名称": "燃油附加费率", "单位": "%", "单价": 15.0, "数量": 1.0 },
{ "名称": "旺季附加费率", "单位": "%", "单价": 10.0, "数量": 1.0 }
],
"_source": { "version": "分区费率表 Zone A~K", "segment": "普货 0-45kg 档" }
}注:
0-45kg / 45-100kg / 100-300kg / 300kg+每个重量档 = 一个独立对象(同"编号 × 规格");原表—(该档不承运)= 定额的"数量为 0 也保留";原表(15%)冲减 = 括号负数问题;分区版本 = 多区域定价。
③ 上市公司年度利润表(同一模板推导的一般结构):
[
{
"行次": "1-1",
"报表项目": "营业总收入",
"规格": "2024年度",
"计量单位": "百万元",
"金额": 18420.0,
"明细": [
{ "名称": "其中:主营业务收入", "金额": 17900.0 },
{ "名称": "其中:其他业务收入", "金额": 520.0 }
],
"_source": { "report": "年度报告", "chapter": "财务报告" }
},
{
"行次": "2-1",
"报表项目": "营业总成本",
"规格": "2024年度",
"计量单位": "百万元",
"金额": -16930.0,
"_source": { "report": "年度报告", "chapter": "财务报告" }
}
]注:每个会计期间(2024 / 2023)= 一个独立对象(同规格拆分);
-16930.0来自原表(16,930)——财报世界通行的"括号即负数";"其中"缩进行 = 嵌套明细。
结论:同一套数据模型(主键 × 分档维度 → 主数值 + 三分类费用 + 四字段明细 + 溯源),加同一套边界规则(分档拆列、括号转负、空档保留),可以不做任何结构性修改地覆盖三个行业。工具的评估维度、切分的模式归纳、提取的边界规则,全部建立在这类"印刷语义与数据语义偏差"上,与行业无关。这也是本文以"复杂表格"而非"工程定额表"为题的原因。
表格数据的最终去处是问答知识库。知识库建设的常规路线是:PDF → Markdown → 切块 → 向量化入库,问答时靠向量检索召回相关片段。这套流程对密集文本是成熟的,但对复杂表格有两处失效:
- 切块截断表格:切分按长度进行,跨页长表被拦腰截断,进库的是一堆残表;
- 向量召回失败:Markdown 表格结构复杂,向量化后语义丢失——问"DN400 基价是多少",召回的可能是相邻章节的文字,而不是这张表。
先说适用边界:表格量少时,普通 RAG 加人工核对完全够用,不需要任何结构化处理。本方案瞄准的是"大批量、复杂、同类型表格数据"。在此边界下,三条路线:
| 路线 | 做法 | 问题 |
|---|---|---|
| A 普通 RAG 直接切块入库 | 表格与正文一起向量化 | 截断 + 召回失败;量少尚可,批量场景不可用 |
| B 大模型直接读 PDF 逐份提取 | 跳过转换环节 | 单份文档超出上下文容量;批量调用成本过高 |
| C(采用)先转、再分、再提取 | PDF 低成本转 Markdown → 算法区分表格与正文 → 表格统一结构化入库 | 需要一条专用管线——本文的全部内容 |
选 C 的理由:转换走批量异步、按量计费,成本可控;提取只处理表格,把钱花在刀刃上。更重要的是治理思路的差别——表格结构化入库后,问答走精确查询(按编号/规格查字段),不再依赖向量召回。这是对 RAG 失效点的根治,而不是修补。
PDF ──①格式转换──→ Markdown ──②切分/分区──→ 表格片段 ──③提取──→ JSON ──④验证与批处理──→ 结构化入库
| 环节 | 做什么 | 解决什么问题 |
|---|---|---|
| ① 格式转换 | 把排版还原成表格 | 后续一切的地基,还原质量决定上限 |
| ② 切分/分区 | 表格与正文分离;长文档切成可处理的小片段且不切断任何表 | 截断问题;单次处理的容量问题 |
| ③ 提取 | 表格 → 第一章定义的统一数据模型 | 召回失败的根因(把"模糊召回"替换为"精确结构") |
| ④ 验证与批处理 | 多路对比、重试、监控 | 批量场景下单条正确 ≠ 整体可靠 |
后续第三~六章沿这四步展开——每步先讲做法逻辑,再讲选型与做法考虑。
这一步把 PDF 还原成 Markdown 表格。工具用错了,后面环节全错——所以先看失败长什么样,再谈评估维度与选型。如果数据比较敏感,建议用本地 OCR 模型——它同样把表格输出成 HTML,方便后续用算法识别定位。
同一张定额表,两种工具的两种典型失败(早于一切评估):
- OCR 视觉解析类:合并单元格的结构还原不错,但一个单元格里的多个材料条目被黏成一个文本块——提取环节无法拆出独立条目,5 条材料变 1 条;
- PDF 底层对象解析类:每条数据保持独立,但跨行跨列的合并单元格层级还原错位,表头对不上数据列——条目全在,位置全错。
两种工具各对一半、各错一半。把"对在哪、错在哪"抽象成两个正交的评估维度:
| 维度 | 含义 | 为什么关键 |
|---|---|---|
| 复杂结构还原 | rowspan/colspan、合并单元格的层级能否还原 | 定额表合并单元格极多,结构错了语义必错 |
| 独立条目分辨 | 同一单元格内的多个数据条目能否保留边界 | 边界丢了,拆不出"一码一规格",后患无穷 |
| 转换器 | 结构还原 | 条目分辨 | 结论 |
|---|---|---|---|
| OCR 视觉模型解析类 | ✅ | ❌ 同格多条目被合并 | 单独用必错 |
| PDF 底层对象解析类(opendataloader) | ❌ | ✅ 天然保留条目独立 | 单独用必错 |
| 云解析类(MinerU) | ✅ | ✅ | 两端同时满足 |
两类工具的差异根源:OCR 类是从页面渲染图里"看"出结构,容易把同一单元格的多条文本黏成一个块;底层对象解析类直接读 PDF 的文本对象流,每条数据天然独立,但不理解视觉布局,表头层级易错。这正是选型困难的根源——常见工具没有一个两个维度都强。
- 第一阶段(无云解析类时):前两类能力互补 → 用底层对象类的条目边界去修复 OCR 视觉类的结构。这是"没有满分工具时,用组合逼近"的常规思路;
- 第二阶段(云解析类可用后):引入实测对比,发现其两个维度都满分,且自带完整上下文。此时果断删除融合后处理——管线少一环,代码更少、失败面更小。
方法论启示:工具选型要建立评估维度矩阵,用实测数据说话;候选集合变化时敢于推翻既有方案做减法。"删掉的复杂度"和"新增的能力"同样有价值。
目的很单纯:把 Markdown 里的表格数据完整地划分成一块块,方便逐个传给模型,保证任何一张表都不被切断;同时把表格和正文区分开——正文不进提取环节,只处理表格。OCR 模型或云解析工具输出的表格基本都是 HTML,天然便于按标签切分。
切分规则不复杂。一开始试了按固定长度切,发现会切断表格、数据错位;于是把全部转换产物过了一遍,归纳出实际存在的几种表格排布,逐一定下切法:
| 排布 | 切法 |
|---|---|
| 一节就一张表 | 直接切出 |
| 同一张表跨页拆成两截 | 按定额编号一致先合并 |
| 按规格/条件分档的一组表 | 表与表之间切 |
| 一章里几十上百张同类表 | 按表边界切 |
| 纯文字说明 | 跳过,不进提取 |
"一张表是否完整"的判定同样简单:表里含"基价"行就是完整表;表头是"名称/单位"又没有基价行,就是拆页表的下半截。识别代码直接交给 AI 写——把需求和判定规则讲清楚,AI 生成算法,再拿真实样本验证一遍就行,这部分没有多少需要费脑子的地方。
这一步负责把摆好位置的表格,映射成第一章定义的数据模型(主键 × 分档维度)。格式层面的麻烦在前两个环节已经解决,这里剩下的全是语义边界:一条数据从哪里开始、到哪里结束、哪些算一条。
多轮提示词迭代揭示了一条经验:大量提取错误来自"边界情况没说清",而不是模型能力不足:
| 决策 | 内容 | 失败教训来源 |
|---|---|---|
| 一码一规格拆对象 | 横向多列规格 → 每列独立对象 | 不拆则无法按规格查询 |
| 明细必须嵌套 | 名称/单位/单价/数量必须入父条 人工/材料/机械 数组 | 不嵌套则明细浮出成垃圾顶层条目 |
| 边界规则显式化 | 括号→负数、0 值保留、补零规则 | 长尾错误几乎全是这类边界 |
规则引擎(可调试、零 API 成本、确定性输出)先把通用逻辑跑通。其有价值的三处设计:
- 多值单元格拆分策略 A/B:中文括号后切分为主,空格拆分+型号后缀合并为回退——先精准后兜底;
- 规格配套材料补零:有的材料只在匹配当前规格时才有消耗量(如管径配套件只出现在对应管径的行里)——按规格判断,无消耗的记 0,而不是丢掉这条材料;
- 适用条件提取:比较符 + 条件关键字匹配(如"蜡厚≤20mm"),输出到独立"条件"字段。
规则引擎每遇到一种新表格式就要写一套提取器。LLM 引擎改变了范式:
"字段名使用表格中实际出现的标签文字——不要自己发明字段名。"
规则引擎的思路是"我定义 Schema,你去匹配";LLM 引擎的思路是"表格自己定义了 Schema,你把它读出来"。一套提示词因此适配了定额表 / 全费用清单 / 多区域定价三种差异极大的表型。这是从"规则匹配"到"结构模式识别"的转变。
配套的兜底机制(LLM 输出不可控,必须工程化兜底):
- 续表识别:无编号行的表格 → 明细归入上文最近编号;
- 孤立明细归附:输出解析后,把游离的明细项归附到父条;
- 异常自动重试:解析失败的输出自动重试;
- 健康监控:按节统计解析失败率、孤立计数、编号断档——批处理必须有可见性。
不依赖人工逐条标注(成本不可行),而是让三套后端(规则引擎 / LLM / 修复版——即规则引擎按第一轮差异暴露的问题修完 bug 后的版本)处理同一批输入,平行产出三套结果,用对比找差异:
- 差异点 = 两类问题:规则引擎的漏网格式、LLM 的幻觉/遗漏;
- 对比工具从条目数、类型分布、行/列数、页面覆盖等维度做差异定位;
- 最终结果以对比结论为准整合,两条路径的结果收敛到完整覆盖。
方法论启示:质量评估不一定需要黄金标注集,"多路独立实现 + 差异分析"是低成本有效的替代,尤其适合人力标注昂贵的垂直领域数据。
大批量场景下,单条正确 ≠ 整体可靠:这一步把 5.4 的验证能力固化进批处理流程,保障过程可见、出错可修、结果可验。规模化时真正耗时的不是提取本身,而是批处理状态的可见性与故障恢复:
- 任务拆解与并发:转换批量并发(5 路 / 3 路),并带任务完成通知,长任务不干等;
- 断点进度:进度持久化,中断后能续跑,不必重来;
- 单一 CLI 入口:convert / batch / validate 三子命令,干跑模式先验证解析再花 API 钱;
- 配置外置:转换器、模型、密钥全部走环境变量,无硬编码。
| # | 方法 | 本项目中的体现 |
|---|---|---|
| 1 | 先小样本试点,再归纳共性,最后规模化 | 单册试点 → 全量文件模式归纳 → 全套批处理 |
| 2 | 先定评估维度,再选工具;候选变化时重估并敢做减法 | 转换器能力矩阵;云解析工具引入后删除融合环节 |
| 3 | 数据驱动设计:先归纳实际排布再定切法 | 按真实表格排布分 5 类逐一定策略 |
| 4 | 边界规则显式化是提示词质量的主要来源 | 括号负数、0 值保留、补零规则 |
| 5 | LLM 输出不可控,兜底必须工程化 | 续表识别、孤立归附、自动重试、健康监控 |
| 6 | 用多路独立实现 + 差异分析替代黄金标注 | 三后端平行对比 + 差异定位工具 |
| 7 | 抽象层次循序渐进,消除重复 | 单脚本 → 多套提取器 → 适配器 + 单一提取引擎 |
| 8 | 语义完整性优先于形式约束 | 超限单表不切断;逻辑表合并优先于长度 |