一份海运单据里,品名可能换行,数量在下一页,表头又重复了一次。如果只让模型“提取成数组”,输出常常十分整齐,行却已经错配。错误最难发现的地方,是每个字段单独看都像真的。
我会先建立行与页面证据,再做字段规范化。这里讨论文档工程,不判断报关、退税或税率是否适用。
文档搜索不等于结构重建
Cloudflare在2026年10月1日宣布AI Search正式可用,并说明扫描PDF的OCR能力。官方变更说明 搜到一段相关文字,与恢复一张跨页表格的行列关系,是不同任务。检索片段不能自动充当原始表格。
数据模型至少保留页码、行标识、原始单元格和提取状态。跨页延续关系要有证据:同一个明细编号、明确续页标记,或人工确认。不能仅凭相邻位置拼接。
内图的连线只合并同一行身份;重复表头被排除。若两个页面有相同编号却属于不同单据,应先按文档ID隔离。
不让模糊匹配静默覆盖
以下例子接收已经人工标记行ID的片段,只按明确ID合并。遇到冲突就停止,不猜哪一份更可信。
const assert = require('node:assert/strict'); function mergeRows(parts) { const rows = new Map(); for (const p of parts) { if (!p.id || !Number.isInteger(p.page) || p.page < 1) throw new Error('missing row evidence'); const row = rows.get(p.id) || { id: p.id, pages: [], fields: Object.create(null) }; for (const [key, value] of Object.entries(p.fields)) { if (Object.hasOwn(row.fields, key) && row.fields[key] !== value) throw new Error('conflicting field: ' + key); row.fields[key] = value; } if (!row.pages.includes(p.page)) row.pages.push(p.page); rows.set(p.id, row); } return [...rows.values()]; } const row = mergeRows([ { id: 'docA:7', page: 1, fields: { item: 'demo' } }, { id: 'docA:7', page: 2, fields: { quantity: '2' } } ])[0]; assert.deepEqual(row.pages, [1, 2]); assert.equal(row.fields.quantity, '2'); assert.throws(() => mergeRows([ { id: 'x', page: 1, fields: { n: '2' } }, { id: 'x', page: 2, fields: { n: '3' } } ])); console.log('row-merge: 3 assertions passed');保存为row-merge.cjs,运行node row-merge.cjs。这个示例验证合并策略,不验证OCR能否正确生成行ID;真实系统还应限制字段名、数据类型和单据大小,防止任意字段进入业务对象。
前端要让错配显眼
复核页面可以左右并排:一侧保留原页,一侧展示行结构。点击一行时同时高亮涉及的多个页面片段,并提示跨页合并原因。冲突字段用并列候选展示,不要悄悄选择最后一次出现的值。
本地样本没有真实海运提单、税务凭证或多语言OCR,不能据此宣称提取精度。它只保证一件小事:当上游证据冲突时,下游不会仍然得到一行看似正常的数据。