真实代码 + 真实数据逐帧拆解 · 财报数据管线 · 2026-07 口径

从一张扫描件 PDF,到 OCR、抄数、算账、看图兜底、人工核验的完整闭环 一张财报图片,是怎么变成逐格可信
结构化数据的

它不是"把 PDF 喂给 AI 让它读出来"那么简单。财报扫描件经 OCR 转成文字时经常读错数字——多一位、少一位、串了列。我们的做法是:让 AI 只负责逐字照抄、把所有算账交给确定性代码,用看图兜底救最烂的表,最后把"机器找到的疑点"排成队列交给人裁决。每一个数字都查得到出处、由代码裁决对错,AI 经手的算术是 0 次——连我们给自己打分的评分卡,也是拿最严的口径算的。

一句话讲明白

把它想象成一条极其较真的财务审计流水线:先把财报 PDF 拍照认字(OCR)变成带坐标的文本;干净的表由确定性代码逐格抽取,难啃的版式派只会照抄、不会算账的"蚂蚁"小模型去救;然后交给一个不会出错的"算盘"(确定性代码)按 34 条勾稽规则逐条核对,对不上就标红;被 OCR 彻底搞烂的跨页表,直接看图把数字读回来,且必须再过闸门才采信;最后所有疑点排成队列,人来判、机器改、全程留痕,导出一份能直接算的干净报表。

结果:在 1000 份真实招股书 / 年报上,抽取并逐格勾稽了 36 万个数字,独立复核确认了 4100+ 处真实 OCR 误读——每一处都能精确指回犯错的那一格。

第一幕
拍照成字 · OCR
MinerU 把扫描件解析成带页码/坐标的文本——但会读错数字。
第二幕
蚂蚁抄数 · 抽取
确定性代码优先逐格抽取;1.3B 小模型只"逐字照抄"救难版式,绝不算账。
第三幕
代码算账 · 勾稽
确定性引擎按 8 大族 34 条规则逐条核对,对不上就标红,红黄绿分级。
第四幕
看图兜底 · 视觉
代码抽不出的跨页/宽表,直接看图读回、跨页拼装,必须过闸门才采信。
第五幕
人工核验 · 工作台
疑点排队交人裁决;修正不改原文、全程留痕;导出能直接算的干净报表。

全程主线案例 · 000048(一家上市公司的合并财报,含一张被 OCR 拆成 4 页、代码彻底抽不出的合并资产负债表)

I
MinerU OCR · PDF → content_list JSON第一幕 · 拍照成字

一切的起点,是一份扫描件 PDF——本质就是一沓图片,机器看不懂里面的数字。第一步用 MinerU 把它"拍照认字"(OCR),解析成带页码、带块坐标(bbox)的结构化文本(一个叫 content_list 的 JSON)。这一步是后面所有"逐字溯源"的地基——但要命的是,这块地基本身就带着裂缝

1
输入 → 输出 · 图片变文本

PDF 进去,带坐标的文本出来

OCR 把每一页的每一块(标题、段落、表格)都框出来,记下它在第几页、在页面的什么位置,再认出里面的文字。表格会被还原成 HTML 形式的行列。

  • 输入:原始 PDF(扫描件,纯图片,无文字层)
  • 输出content_list —— 每个块带 type(表格/文本/标题)、page_idx(第几页)、bbox(0–1000 归一化坐标)、识别出的文字内容
打个比方:像把一本纸质账本用手机一页页拍下来,再用文字识别 App 把照片转成可编辑文字。文字大体出来了,但偶尔会把"3"认成"8"、把一行挤成两行——而财报里一个数字错位,可能就是几百亿的偏差。
content_list 节选 · 一个表格块
{
  "type": "table",
  "page_idx": 69,        # 第 70 页(0 起算)
  "bbox": [88, 120, 915, 880],  # 在页面的位置
  "table_body": "<table>…合并资产负债表…</table>"
}
# ↑ 后面"看图兜底"靠的就是 page_idx + bbox:
#   知道这张烂表在第几页的哪个框,才能把它单独渲成图片重读。
2
坏消息 · OCR 会读错数字

地基带裂缝:OCR 在财报上会犯七类典型错

财报的数字又密又长、表又宽,OCR 出错是常态。这些错人眼几乎翻不出来(几百页里某一格多了一位),却能让一个数偏差几百上千亿。下面每一类都是真实抓到的例子

认清这些错,才明白后面几幕为什么要那么"较真":AI 只敢照抄、算账只信代码,正是为了"抓出这些 OCR 错,而不是自己又添新错"。

关键认知:OCR 错不是偶发噪声,而是有规律的系统性错——所以可以用"会计等式必须成立"这种确定性规则去抓它。我们甚至把这七类错做成了错误注入弹药库,专门拿来考自己的引擎(见文末"给自己打分")。
前导位插入最前面凭空多一位
流动负债 19.4 亿 → OCR 819.4 亿(多读个"8",差 800 亿)
小数点错位千位逗号读成小数点
应付账款 4,020,607,320(40.2 亿)→ OCR 40,206.073.20(一行两个小数点,缩水 4000 倍)
末位截断结尾少认一位
权益合计本期增减 …622.88 → OCR …622.8(差 8 分钱)
负号丢失括号/负号被吃掉
净流出 (1,200,000) → OCR 1,200,000(方向直接反了)
字符乱码混入字母/符号
OCR D,001,000,101.00(数字里混进字母 D)
串列宽表里数值放错列
权益变动表(15 列)库存股/少数股东权益等多列整列错位
跨页拆表一张表被切成几页
合并资产负债表被拆成 4 块跨 4 页,中间一页还被抽空(本案例 000048)
II
确定性优先 + MiniCPM-V-4.6(1.3B)密集抽取 · 只抄不算第二幕 · 蚂蚁抄数

有了 OCR 文本,第二步是把每张表的数字"抄"进结构化字段。这里有两个反直觉的核心决定:其一,能不用 AI 的地方绝不用——结构干净的表由确定性代码逐格抽取,根本不惊动模型;其二,需要 AI 时,用的是一个只有 1.3B 参数的"小模型"(MiniCPM-V-4.6),不是大模型。它的任务被压到极致简单——只逐字照抄数字,一个加减都不许做。所谓"大规模",指的不是模型大,而是把这个小蚂蚁高密度并发调用,一张表叫一次。

1
分工 · 抽取与算术彻底分离

蚂蚁只管搬,不管算

这是整套系统的第一铁律LLM 只做"文本 → 结构化"的逐字翻译,所有加减、勾稽、推算全部交给纯代码。

  • 找表:确定性代码定位 OCR 里的资产负债表 / 利润表 / 现金流量表在哪、是合并口径还是母公司口径;每一张被丢弃的候选表都记进"丢表账本"(第几页、疑似什么表、为什么丢),绝不静默吞掉
  • 抄数:干净表走确定性网格逐格抽取;难版式才叫小模型,它只做一件事——把表里每行的科目名和金额原样抄成一条记录绝不改写一个字符
  • 认单位:表头写"万元"还是"元"?单位来源分三档(确认 / 推断 / 默认)——只有"确认"的才敢参与强校验,猜出来的绝不硬算
  • 算账:交给第三幕的确定性引擎(金额统一转成"分"的整数比对,永无浮点误差)
为什么这么分?因为算术正是大模型最容易"脑补"的地方。把算账从 AI 手里拿走、只让它当"复印机",系统才可能抓出 OCR 的错,而不是自己又算错。这也是"蚂蚁能胜大象"的根:胜负不在模型大小,在分工。
抄数员的"职责说明书"(系统提示词节选)
# 输入:一张表渲成的紧凑文本
货币资金 | 704,405,482.04 | 612,330,118.77
应付账款 | 40,206.073.20  | 38,915,200.00
…

# 铁律提示词(固定、极短):
· 逐字复制 OCR 数字,一个字符都不许改
· 输出 NDJSON,每行一个科目,5 个字段
· 不准做任何加减,不准"纠正"看起来不对的数

# 输出:每行一条(注意它照抄了那个坏值!)
{"label_raw":"应付账款","period_end_raw":"40,206.073.20"}
2
并发 · 一张表叫一次蚂蚁

三大表并行,宽表(SCE)干脆不让 AI 碰

系统不是把整本财报塞给模型,而是按表拆开、并发抄:资产负债表、利润表、现金流量表三类,每类还分"合并"和"母公司"两口径,每张表单独叫一次小模型。

  • 三大表并行抽取(BS / IS / CF),表级并发默认 5 路同时跑
  • 所有者权益变动表(SCE)不走 AI:它有 11–16 列、极宽,小模型一读就串列 → 改由确定性代码按网格直接展开
  • 可选多源投票:同一张表可抄 3 次,逐行取"众数"压平抖动(默认关,作为补救杠杆)
为什么不用大模型一把梭?实测过:让小模型像大模型那样"多轮思考+调工具",240 场成功率是 0。小模型扛不住复杂流程,但"只抄一张表"这种极简单步任务它能稳定做好——于是化整为零,用数量换质量。
抽取派工 · WORKER_SPECS
并行抽取:
  BS 资产负债表   → 合并 / 母公司
  IS 利润表       → 合并 / 母公司
  CF 现金流量表   → 合并 / 母公司
不走 AI(确定性):
  SCE 权益变动表  → 代码按网格展开(防串列)
模型
MiniCPM-V-4.6 · 1.3B 小模型
每张表
叫 1 次模型 · 单步 NDJSON · 无多轮
表级并发
默认 5 路
采样温度
0(可复现)
AI 经手算术
0 次
动手看 · 抽取阶段

"一张表叫一次蚂蚁" 的并发流水

系统把本案例 000048 里所有要抄的表排成队列,用 5 个并发工位同时抄;读完一张立刻从队列补下一张。注意:SCE(黄色)不进 AI 工位,由确定性代码单独处理。点"开始抽取"看它怎么转 👇

并发上限 5已完成 0/8队列剩 8
待抄表队列8
5 个并发工位(小模型)并发=5
已抄好的结构化表0
绿色 = 小模型抄好的一张表;黄色 = SCE 走确定性代码(不进 AI)。 全部抄完 → 交给第三幕"算账" ↓

放大看一个工位抄一张表到底吃进什么、吐出什么——本质就是一次极简单的"复印"调用

▸ 投入 · 一次小模型调用的输入
固定短提示词:逐字复制、不准算账、输出 NDJSON
一张表的紧凑文本科目 | 期末数 | 期初数 逐行
MiniCPM
1.3B
▸ 产出 · 逐字照抄的结构化记录
每行:label_raw / role / canonical_code / 期末 / 期初
原样保留 OCR:哪怕值看着不对(如那个坏值),也照抄不纠正

单张表失败被隔离:抄不出/残缺 → 记为该表失败(fail-loud),其余表照常完成,绝不拖垮整批,也绝不静默吞成"抽到了"。

动手看 · 年报最大的坑

一张表印过了页:续表拼接

年报里最常见的"丢表"原因不是认不出字,而是一张长表被印到了好几页上——OCR 把它切成几个碎块,每块单看都"缺头少尾"。归因分析显示,年报留出集没抽齐的文档里约 45% 卡在这一步。拼接不靠 AI 猜,靠三条确定性判据:列结构得对得上、科目序号得能衔接、中间不能夹别的表。点"拼接"看碎块怎么合成一张完整逻辑表 👇

continuation merge · 跨页续表拼接(确定性,零 AI)
p69 · 表头 + 前 12 行缺合计 ✗
p71 · 中段 18 行(无表头)缺头 ✗
p72 · 尾段 + 资产总计缺头 ✗
列结构一致
序号可衔接
中间无异表
拼接后 · 一张完整逻辑表等待拼接
拼上才算"抽到"。这一处确定性修复,把年报留出集的合并三大表抽取率从 86.0% 提到 89.7%、文档齐套率从 71.0% 提到 76.0%——不靠换模型,靠把碎块拼回去。(2026-07-03 经 1000 份独立 GT 逐份复核校准:剔除源文档真没有的报表后,确定性底座实为 91.9% / 78.0%。)
III
确定性勾稽引擎 · backend/reconcile · 8 大族 34 条规则第三幕 · 代码算账

数字抄进来了——但 OCR 抄错的那些怎么揪出来?答案是第二铁律:所有算账只信代码,绝不信 AI。一个纯代码的"算盘"拿着 8 大规则族、共 34 条勾稽规则(资产总计该等于负债加权益、营业利润链条得能推下来、现金期末该等于期初加净增……)逐条核对。对不上的,当场标红。这道关卡 AI 没有任何参数能绕过。

1
分级 · 标红不等于真错

红黄绿:把"标红"诚实地分成三种

所有金额先统一换成"分"的整数再比(永无浮点误差),按会计等式核对。但这里有个极诚实的细节标红 ≠ 真错。标红只是"这条等式没对上",背后可能是三种情况:

所以系统从不把"标红"直接当"真错"汇报,而是把每一处标红都送去独立第三方复核(一个跟抽取链路完全无关的裁判,两轮投票)归类——这才是这套系统价值的"成色"。

打个比方:像验钞机响了不代表一定是假钞——也可能是钞票折了角。响归响,但最终是真是假,要送去专门的鉴定,不能验钞机自己说了算。

真实 OCR 误读 · 系统真正的价值

前导位多一位、小数点错位、断位少计——人眼翻不出,等式一核对就露馅。占可判定标红约 42%(首批独立复核口径)。

抽取 / 口径偏差 · 会继续收敛的假阳

小模型漏抄一行、合并/母公司口径错配、单位没认对。约 35%——归因分析确认这是当前假阳的主战场,随抽取改进递减。

财报本身舍入 · 无害

财报自己四舍五入造成的几分钱差。约 23%,差额多 ≤1 元——现已按呈报精度给了容差,多数不再打扰人。

2
铁证 · 大模型会"假装算过",代码不会

同一道题:大模型说"平了",代码说"差 4020 万"

这是"读算分离"最硬的一块证据。某招股书里应付账款被 OCR 把千位逗号读成小数点,真值约 40.2 亿,差出 4020 万量级的错。

  • 大模型一把梭:它把这个坏值照抄对了(甚至标了"可疑"),但让它自查"流动负债合计 = 各分项相加"时,它直接把打印的合计当成了相加结果、报 balanced=true——根本没真加,漏掉了真错
  • 确定性引擎:逐项真加,精确指回应付账款那一格,每次结果都一样、可复现。

放大到全集:63 份真实不平衡(差额≥100 元)里,大模型漏报了 40 份(63%);确定性引擎全部抓到。

关键认知:大模型不是"不会读",是"不会真去算"——它会给你一个看起来对的答案。把算术交给永不走神的代码,才是抓错的底气。这也是为什么忠实性守卫会盯着:抽取值必须真出现在 OCR 原文里,AI 偷偷"顺手纠正"过的值(LLM_VALUE_DRIFT)会被当场抓出。
8 大规则族 · 34 条(节选)
# 表内 · 资产负债表(3 条)
BS_EQUATION        资产总计 = 负债合计 + 所有者权益合计
BS_SUBTOTAL_TIE    各分类合计 = 明细项求和
# 表内 · 利润表主链(7 条 · 2026-07 补齐,消灭大面积"未参与勾稽")
IS_OPERATING_PROFIT_TIE  营业利润 = 收入 − 成本费用 ± 其他
IS_PRETAX          净利润 = 利润总额 − 所得税
IS_COMPREHENSIVE_TIE     综合收益 = 净利润 + 其他综合收益
# 表内 · 现金流量表(5 条)/ 权益变动表(4 条)
CF_END_BALANCE     期末现金 = 期初 + 本期净增加
SCE_COL_ROWSUM     每行各列求和 = 小计/合计
# 跨表(6 条)/ 附注(6 条)/ 文表一致(1 条)
CROSS_CF_BS_CASH   现金流期末现金 ≤ 资产负债表货币资金
NOTE_TABLE_SUBTOTAL_TIE  附注表合计 = 明细求和
# 守卫(2 条)——盯 AI、盯坏格
LLM_VALUE_DRIFT    抽取值必须真出现在 OCR 源文本里
VALUE_UNPARSEABLE  读不成数的格先指名道姓,不牵连等式
动手看 · 确定性核对

点一道等式,看代码当场算出"红还是绿"

下面三道都是真实数据。点任意一道,看那台"算盘"怎么把数字算成判决——同样的输入永远得同样的结果,没有 AI 的临场发挥 👇

● 确定性勾稽 · 互动演示(真实数据)
待核对的等式 · 点一条
↑ 点任意一道等式
🧮点左边一道等式,
这里显示确定性算盘的核对过程

所有金额先转成"分"的整数再比,永无浮点误差;核对结果 100% 可复现、可单元测试。

动手看 · 少报错,报对错

一个坏格,只打扰你一次:根因聚类

一个 OCR 坏格常常同时弄崩好几条等式——它所在的小计不平、总计不平、跨表也对不上。要是每条都单独报一遍,用户会被同一个错淹没三次。所以引擎在报错前做一步确定性聚类:把指向同一个源头格的发现折成一簇,队列里只出一条"根因",派生项折叠在里面。点"报错"看它怎么折 👇

finding_cluster · 根因聚类(确定性纯函数,零 AI)
被 OCR 读坏的一格 ↓
科目期末数
应付票据82,504,331.00
应付账款40,206.073.20
合同负债12,118,205.71
流动负债合计230,148,626.02
负债合计301,522,884.10
它弄崩的等式 → 折成一簇
BS_SUBTOTAL_TIE 流动负债合计不平 · Δ 40,206,073.20
BS_SUBTOTAL_TIE 负债合计不平 · Δ 40,206,073.20
BS_EQUATION 资产 ≠ 负债+权益 · Δ 40,206,073.20
⚑ 1 簇 · 根因:应付账款(期末数)疑似 OCR 误读
同一源头格牵连 3 条等式,折叠为一条待核项;差额一致(40,206,073.20),指回同一格。核这一格,三条等式一起复活。
同理还有两道"不冤枉人"的闸:VALUE_UNPARSEABLE(格子里根本不是数 → 先报"这格坏了",不牵连整段等式);单位置信度(单位是猜的 → 强规则直接不跑)。少报错,报对错。
IV
vision_rescue · 渲图 → 密集读 → 跨页装配 → 勾稽采信第四幕 · 看图兜底

还剩最后一类硬骨头:有些表被 OCR 搞得太烂——跨好几页、中间一页还被抽空,连确定性代码都拼不起来、直接抽不出。这时启动看图兜底:按第一幕记下的页码和坐标,把这张烂表单独渲成图片,让小模型直接看图把数字读回来,再跨页拼成完整表,必须再次通过确定性勾稽才采信。这正是主线案例 000048 的高潮。

1
触发 · 只在代码"抓瞎"时兜底

不到万不得已,绝不看图

看图兜底只在确定性/抽取链路明确报缺、报错(fail-loud)时才对那一张表启动——只兜底,不进主线全量,避免无谓成本和风险。

  • 触发信号:整张表漏抽(如缺合并 BS)、跨页 BS 拼不起来、宽矩阵 SCE 抽 0 行——如今这些信号全部来自丢表账本,一笔不漏
  • 定位:用第一幕的 page_idx + bbox 精确框到这张表在第几页的哪个区域
  • 读法:把表区渲成图片,逐列裁成窄条让 1.3B 小模型"照抄",3 遍投票取众数压掉抖动
打个比方:OCR 是"自动扫描全书",看图兜底是"对那几页实在认不清的,拿放大镜一列一列重新誊一遍"——慢,但只对最难的那几张表用。
SCE 宽矩阵看图自验 · 案例 002065 p86
# 14 列宽的权益变动表,代码抽 0 行。
# 看图:垂直投影找出 17 条网格竖线,精确切出每一列
# 逐列密集读回后,确定性引擎自验:

  归属母公司小计   9,175,030,210.24
+ 少数股东权益         95,744,524.42
─────────────────────────────────
= 所有者权益合计   9,270,774,734.66   ✓ 分毫不差

# 同一张表里"盈余公积"一列疑似读错(622… vs 图上 513…),
# 正好被"每行各列求和=小计"那条规则该抓 → 标红交人工。
2
采信 · 看图读回的值,恒"标红"不自动改

看图读回 ≠ 直接采信,必须再过勾稽

这是第四幕的灵魂、也是第三铁律的延伸:看图读回的值天生可疑,绝不自动写回、绝不改原文。它必须把读回的数喂回同一套确定性勾稽引擎,逐格打三种标签:

verified 参与了某条通过的会计等式(如整表 BS_EQUATION 对平)→ 复核优先级低
flagged 参与了没对上的等式 → 重点复核
uncovered 没有任何规则覆盖(如 SCE 无列名)→ 一律交人工

双重闸门:整表必须过会计恒等式(如 BS 必过 BS_EQUATION)且装配必须完整(所有页都抽到、没被截断),任一不满足,这张表一个格都不采信

主线案例 000048 · 五步把"抓瞎的表"救回来
# 合并资产负债表被 OCR 拆成 4 块跨 p69–72,
# 其中 p70 续页被抽空(cols=0)→ 确定性代码拼不起来 → 报"缺合并BS"
vision_rescue · 五步管线
1
代码抓瞎
合并 BS 拆 4 块跨 p69–72,p70 被抽空 → fail-loud:缺合并BS
2
渲图重读
按 page_idx+bbox 把 4 页表区渲成图,1.3B 小模型逐列看图照抄,救回 p70 的资产总计等
3
跨页装配
按报表标题边界把 4 块拼成一张 97 行的完整逻辑表
4
确定性对平
喂回同一引擎:资产总计 = 负债合计 + 所有者权益合计 ✓ 自动对平
5
采信裁决
三项总额标 verified;其余无规则覆盖 → uncovered 交人工;全程恒 flagged,绝不自动改原文
3
诚实结论 · 闸门宁可拒收

如实说:最难的 24 份,闸门一张都没放行

2026-07 我们把看图兜底接进主线后,拿年报留出集里最难的 24 份(确定性修复都救不回的尾部)做端到端复测。结果如实记录:视觉管线真实有效——比如某年报的母公司现金流量表,看图读回了 18 行 24 个值、勾稽全对;但严格闸门最终一张整表都没采信(多因跨页装配不完整,闸门要求"整表拼齐 + 恒等式过"才放行)。

  • 为什么不放松闸门?因为放进一张残表,比少收一张表危险得多——那是"看起来抽到了"的假数据
  • 好啃的部分去哪了?已经被第二幕的续表拼接等确定性修复先拿走了——留给视觉的本来就是最硬的骨头
  • 下一步:残表按"部分可信"分级放行的安全设计、超大页图分块渲染、把块级证据直接供人工核验(不合入报表)
这就是 fail-loud 的样子:兜底管线跑通了、也真读回了数,但只要闸门没点头,成绩单上就写 0。0 就是 0,不粉饰。
vision_rescue 复测 · 2026-07 如实记录
输入       年报留出集最难尾部 24 份(缺 BS/IS/CF)
管线       渲图 → 逐列读回 → 跨页装配 → 清洁闸
真实读回   有(例:母公司 CF 18 行 24 值,勾稽全对)
闸门采信   0 张整表(拒收主因:装配不完整 / 缺合计行)
结论       管线有效性确认;闸门按设计拒收残表
# 「0 finding ≠ 无错」的镜像是「读回了 ≠ 能采信」。
# 严格的闸门,是这套系统敢让人信任的原因。
V
对账核验工作台 · PDF ↔ 活报表 ↔ 疑点队列 三联动第五幕 · 人工核验

机器把疑点找出来,最后一锤子必须留给人。第五幕是一个网页版对账核验工作台(就是本站的 /demo):左边是财报 PDF 原文,右边是抽出来的活报表和疑点队列,点任何一处,三边同时高亮到那一格。人在这里逐条裁决:"确实是 OCR 错,改"、"误报,放行"——而系统保证:不管人怎么改,OCR 原文一个字符都不动。

1
修正闭环 · 改的是账,不是原文

人工修正的五步闭环:原文永不可变

用户对一处标红提交修正值后,系统绝不覆盖 OCR 原值,而是记一笔可追溯的修正事务(谁、什么时候、把哪格从多少改成多少、为什么),然后把修正值喂回同一台确定性算盘重算——之前因这格不平的等式如果全部转绿,才算"闭环"。

  • raw 不可变:原值和修正值并排保存,永远查得到"OCR 当初读的是什么"
  • 确定性重算:修正后等式通过与否,仍由代码裁决,不是"改了就算对"
  • 守卫豁免有据:忠实性守卫知道这个值是人改的(有事务记录),不会误报"AI 改数"
  • 状态全链同步:队列该条转"已核"、首屏计数减一、报表格盖"已修正"徽标、导出物同步——一处裁决,处处一致
打个比方:会计改账从不用橡皮擦,而是划线更正、旁边签名——错的留着看得见,改的有人负责。这套闭环就是数字版的划线更正。
override · 人工修正五步闭环
1
人来裁决
复核队列里那条"应付账款疑似误读",对照左侧 PDF 原文,确认真错
2
提交修正
录入正确值 40,206,073.20 → 系统记一笔修正事务(原值不动)
3
确定性重算
同一台算盘重跑所有关联等式:流动负债合计、负债合计、BS 恒等式 ✓ 全部转绿
4
守卫放行
忠实性守卫查到这笔事务 → 修正值豁免 DRIFT,但豁免本身也留痕
5
全链同步
队列转"已核"、首屏计数 −1、报表格盖"已修正"徽标;审计轨落一条完整记录
2
出口 · 看得懂,算得动

首屏不吓人,导出能直接算

工作台首屏用一张覆盖状态卡替代吓人的红色告警:主表抽到几张、多少数字参与了勾稽、几张表解析异常(只计真实解析失败,账本里的良性丢弃不算)——一句人话一项,异常才用警示色。

核验完点"导出核验成果",拿到的是三份各有用途的产物

  • 修正后干净报表(Excel):数字是真正的数值单元格,拉个 SUM 就能算;没修的疑错格标色注明
  • 原值对照核验底稿(Excel):OCR 原值 / 修订值 / 勾稽状态三列并排,给审计复核用
  • 审计轨(JSON):谁、何时、因为什么、把哪格从多少改成多少——给系统对接与追责
一个曾经真实存在的反面教材:早期版本导出的"修订值"里混着全角逗号和空格,Excel 一个 SUM 都拉不动——导出物用户用不了,前面四幕做得再好也白搭。现在"能 SUM"是导出的验收标准。
导出核验成果 · 三产物
① 修正后干净报表.xlsx
   真数值格 · 负数为负值 · 单位注明 · 可直接 SUM
② 原值对照核验底稿.xlsx
   raw / 修订值 / 勾稽状态 三列并排 · 疑错格标色
③ 审计轨.json
   {时间, 规则, 位置, 裁决, 原值, 修订值, 重算结论}
# 文件名:{公司简称}-核验成果-{日期}.xlsx
# 每份都带说明 sheet:这是什么、每列什么意思、数据口径。
给自己打分 · 双 benchmark + 三道门

我们主动把自己的分数改低了

一套抓别人错的系统,先得经得起抓自己的错。我们给它建了两套考卷:一套错误注入(在 4 份真实财报上按 OCR 的真实出错方式埋进 1044 处已知错误,看能逮回多少)、一套提取覆盖(200 份从未见过的年报/招股书,看能抽全多少)。2026-07 我们重修了评分卡——发现旧算法给自己放水:只要表上冒出任何新告警就记"检出",哪怕根本没指对格。修严之后,同一份成绩单按三层口径重新亮出来:

错误注入考卷 · 1044 处已知错 · 可检出子集的检出率(三层口径)run-to-run 波动 std 0.009
格级检出必须精确指回被注入的那一格 · 最严=主指标
78.7%
表级检出指对了出错的那张表
86.2%
弱归因检出表上冒出了新告警(≈旧口径,仅作参照)
94.2%

最扎心的一行:字符乱码类在旧口径下接近全抓,按"精确指格"只剩 45.5%——很多乱码格其实是被别的告警"顺带"盖住的,没被指名道姓。这行丢分我们照实写上。同时如实记录另一面:干净无错的对照文档,平均每份仍会冒出 ~20.8 条待核提示(假阳)。2026-07-03 我们把这些假阳逐条全量裁决完毕(去重后 168 个告警键,含 2 份从未参与调参的 heldout 文档):148 条错在抽取层错位、20 条是源 OCR 本身乱码(告警反而是真信号)、归咎于勾稽规则或容差的为 0 条——治理方向唯一且明确。另抽 60 处注入事件反向核对检出归因:仅 1 例属"碰巧命中",其余货真价实。

提取覆盖考卷 · 200 份公司互斥的留出文档(训练时从未见过)
年报 100 份     合并三大表 91.9%(确定性)/ 95.2%(+小模型) · 齐套 78.0% / 85.7%
                 (年报是主攻盲区:小模型增益在这里最大,齐套 +7.7pp)
招股书 100 份   合并三大表 93.6%(确定性)/ 93.9%(+小模型) · 齐套 88.9%
训练集 300 份   合并三大表 99.0%(确定性与 +小模型持平)
值忠实性抽查   7,397 个锚点格逐一对回 OCR 原文 · 错 44 格(0.595%)· 招股训练集零错
# 分母 = 源文档里实际存在的报表——2026-07-03 依 1000 份独立 GT 逐份裁决校准,
# 136 份非财务分册不计入分母:不凑好看分母,也不把"本来就没有"报成"漏抽"。

考卷之外还有三道随时会拦路的回归门——任何人(包括 AI 同事)改一行代码,三道门全绿才允许合入。这就是"确定性引擎"能一直保持确定的原因:

PASS

Demo 门

0.3 秒 · 每次改动必跑

对内置演示样本重跑全管线,findings 与基线快照逐字节比对——多一条、少一条、变一个字都算失败。

PASS

注入门

重评 1044 处注入错

格级检出率不得下降、干净对照假阳不得上升。想"顺手放松一条容差"?这道门当场变红。

PASS

覆盖门

5 分钟 · 重跑 200 份留出集

三大表抽取率与齐套率不得倒退——修 A 表不许弄丢 B 表。

交付 · 最终结构化数据

出口:一份逐格可信、查得到出处的结构化文档

五幕走完,PDF 变成了一份结构化文档:每张表逐行逐期的数字、每一处标红、每一条勾稽结论、每一笔人工修正,都带着页码坐标和判定来源。下面是这套管线在 1000 份真实招股书 / 年报(300 份训练 + 700 份泛化)上的累计成绩单:

0
真实文档
0
逐格勾稽数字
0
标红不平衡处
0
确认真OCR误读
0
AI经手的算术

每一处标红都送独立第三方复核才下"真错"结论——累计确认了 4100+ 处真实 OCR 误读,每一处都能精确指回犯错的那一格,单处偏差最高达千亿量级。同样如实写上:标红里还混着抽取偏差与舍入(见第三幕红黄绿),假阳治理是当前的主战场——所以最后一锤子留给人,而不是留给分数。

最终结构化输出节选 · 一行带证据
{
  "科目": "应付账款",
  "值": "40,206.073.20",   # 逐字保留 OCR 原文,未改
  "出处": { page: 71, bbox: [..] }, # 指回原文那一格
  "勾稽": "flagged",           # BS_SUBTOTAL_TIE 没对上
  "人工修正": "40,206,073.20", # 修正事务,与原值并存
  "判定来源": "deterministic" # 永远是代码,不是 AI
}
为什么整条链都不会胡编 · THE IRON LAWS

支撑这一切的,是五条贯穿始终的"铁律"

把"会不会编造数字"从概率问题,变成结构问题

  1. 抽取与算术彻底分离。抄数的是 AI(1.3B 小模型逐字照抄);但所有加减、勾稽、对账由确定性代码完成,AI 经手的算术是 0 次。算术是 AI 最易出错处,把它拿走,系统才能抓 OCR 错而不引入新错。
  2. 逐字保留 OCR,带源坐标。抽取值原样照抄、绝不顺手纠正,且每个值都绑页码 + 坐标;偷偷改写过的值会被忠实性守卫(LLM_VALUE_DRIFT)当场抓出。人工修正也只记事务、不碰原文。
  3. 只信闸门,不信"看起来读回来了"。系统只采信过了确定性闸门的结构化产出;看图兜底读回的值恒标红、必须整表拼齐且恒等式通过才采信——2026-07 复测最难 24 份,闸门一张没放行,成绩单就写 0。
  4. 出错就大声喊(fail-loud),绝不假装成功。丢掉的表记进账本、抄不出记状态位、"0 处标红"绝不等于"无错"。标红也只是"等式没对上",要不要算真错,交独立复核和人来裁决。
  5. 评分卡对自己最严。检出率按"精确指回那一格"的最严口径当主指标;发现旧评分放水就重算并公布更低的新分;干净文档冒出的每条假阳都记条数、做归因。宁可分数难看,不让分数说谎。