它不是"把 PDF 喂给 AI 让它读出来"那么简单。财报扫描件经 OCR 转成文字时经常读错数字——多一位、少一位、串了列。我们的做法是:让 AI 只负责逐字照抄、把所有算账交给确定性代码,用看图兜底救最烂的表,最后把"机器找到的疑点"排成队列交给人裁决。每一个数字都查得到出处、由代码裁决对错,AI 经手的算术是 0 次——连我们给自己打分的评分卡,也是拿最严的口径算的。
把它想象成一条极其较真的财务审计流水线:先把财报 PDF 拍照认字(OCR)变成带坐标的文本;干净的表由确定性代码逐格抽取,难啃的版式派只会照抄、不会算账的"蚂蚁"小模型去救;然后交给一个不会出错的"算盘"(确定性代码)按 34 条勾稽规则逐条核对,对不上就标红;被 OCR 彻底搞烂的跨页表,直接看图把数字读回来,且必须再过闸门才采信;最后所有疑点排成队列,人来判、机器改、全程留痕,导出一份能直接算的干净报表。
结果:在 1000 份真实招股书 / 年报上,抽取并逐格勾稽了 36 万个数字,独立复核确认了 4100+ 处真实 OCR 误读——每一处都能精确指回犯错的那一格。
全程主线案例 · 000048(一家上市公司的合并财报,含一张被 OCR 拆成 4 页、代码彻底抽不出的合并资产负债表)
一切的起点,是一份扫描件 PDF——本质就是一沓图片,机器看不懂里面的数字。第一步用 MinerU 把它"拍照认字"(OCR),解析成带页码、带块坐标(bbox)的结构化文本(一个叫 content_list 的 JSON)。这一步是后面所有"逐字溯源"的地基——但要命的是,这块地基本身就带着裂缝。
OCR 把每一页的每一块(标题、段落、表格)都框出来,记下它在第几页、在页面的什么位置,再认出里面的文字。表格会被还原成 HTML 形式的行列。
content_list —— 每个块带 type(表格/文本/标题)、page_idx(第几页)、bbox(0–1000 归一化坐标)、识别出的文字内容{
"type": "table",
"page_idx": 69, # 第 70 页(0 起算)
"bbox": [88, 120, 915, 880], # 在页面的位置
"table_body": "<table>…合并资产负债表…</table>"
}
# ↑ 后面"看图兜底"靠的就是 page_idx + bbox:
# 知道这张烂表在第几页的哪个框,才能把它单独渲成图片重读。
财报的数字又密又长、表又宽,OCR 出错是常态。这些错人眼几乎翻不出来(几百页里某一格多了一位),却能让一个数偏差几百上千亿。下面每一类都是真实抓到的例子。
认清这些错,才明白后面几幕为什么要那么"较真":AI 只敢照抄、算账只信代码,正是为了"抓出这些 OCR 错,而不是自己又添新错"。
有了 OCR 文本,第二步是把每张表的数字"抄"进结构化字段。这里有两个反直觉的核心决定:其一,能不用 AI 的地方绝不用——结构干净的表由确定性代码逐格抽取,根本不惊动模型;其二,需要 AI 时,用的是一个只有 1.3B 参数的"小模型"(MiniCPM-V-4.6),不是大模型。它的任务被压到极致简单——只逐字照抄数字,一个加减都不许做。所谓"大规模",指的不是模型大,而是把这个小蚂蚁高密度并发调用,一张表叫一次。
这是整套系统的第一铁律:LLM 只做"文本 → 结构化"的逐字翻译,所有加减、勾稽、推算全部交给纯代码。
# 输入:一张表渲成的紧凑文本 货币资金 | 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"}
系统不是把整本财报塞给模型,而是按表拆开、并发抄:资产负债表、利润表、现金流量表三类,每类还分"合并"和"母公司"两口径,每张表单独叫一次小模型。
并行抽取: BS 资产负债表 → 合并 / 母公司 IS 利润表 → 合并 / 母公司 CF 现金流量表 → 合并 / 母公司 不走 AI(确定性): SCE 权益变动表 → 代码按网格展开(防串列)
系统把本案例 000048 里所有要抄的表排成队列,用 5 个并发工位同时抄;读完一张立刻从队列补下一张。注意:SCE(黄色)不进 AI 工位,由确定性代码单独处理。点"开始抽取"看它怎么转 👇
放大看一个工位抄一张表到底吃进什么、吐出什么——本质就是一次极简单的"复印"调用:
科目 | 期末数 | 期初数 逐行label_raw / role / canonical_code / 期末 / 期初单张表失败被隔离:抄不出/残缺 → 记为该表失败(fail-loud),其余表照常完成,绝不拖垮整批,也绝不静默吞成"抽到了"。
年报里最常见的"丢表"原因不是认不出字,而是一张长表被印到了好几页上——OCR 把它切成几个碎块,每块单看都"缺头少尾"。归因分析显示,年报留出集没抽齐的文档里约 45% 卡在这一步。拼接不靠 AI 猜,靠三条确定性判据:列结构得对得上、科目序号得能衔接、中间不能夹别的表。点"拼接"看碎块怎么合成一张完整逻辑表 👇
数字抄进来了——但 OCR 抄错的那些怎么揪出来?答案是第二铁律:所有算账只信代码,绝不信 AI。一个纯代码的"算盘"拿着 8 大规则族、共 34 条勾稽规则(资产总计该等于负债加权益、营业利润链条得能推下来、现金期末该等于期初加净增……)逐条核对。对不上的,当场标红。这道关卡 AI 没有任何参数能绕过。
所有金额先统一换成"分"的整数再比(永无浮点误差),按会计等式核对。但这里有个极诚实的细节:标红 ≠ 真错。标红只是"这条等式没对上",背后可能是三种情况:
所以系统从不把"标红"直接当"真错"汇报,而是把每一处标红都送去独立第三方复核(一个跟抽取链路完全无关的裁判,两轮投票)归类——这才是这套系统价值的"成色"。
前导位多一位、小数点错位、断位少计——人眼翻不出,等式一核对就露馅。占可判定标红约 42%(首批独立复核口径)。
小模型漏抄一行、合并/母公司口径错配、单位没认对。约 35%——归因分析确认这是当前假阳的主战场,随抽取改进递减。
财报自己四舍五入造成的几分钱差。约 23%,差额多 ≤1 元——现已按呈报精度给了容差,多数不再打扰人。
这是"读算分离"最硬的一块证据。某招股书里应付账款被 OCR 把千位逗号读成小数点,真值约 40.2 亿,差出 4020 万量级的错。
balanced=true——根本没真加,漏掉了真错。放大到全集:63 份真实不平衡(差额≥100 元)里,大模型漏报了 40 份(63%);确定性引擎全部抓到。
LLM_VALUE_DRIFT)会被当场抓出。# 表内 · 资产负债表(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 坏格常常同时弄崩好几条等式——它所在的小计不平、总计不平、跨表也对不上。要是每条都单独报一遍,用户会被同一个错淹没三次。所以引擎在报错前做一步确定性聚类:把指向同一个源头格的发现折成一簇,队列里只出一条"根因",派生项折叠在里面。点"报错"看它怎么折 👇
还剩最后一类硬骨头:有些表被 OCR 搞得太烂——跨好几页、中间一页还被抽空,连确定性代码都拼不起来、直接抽不出。这时启动看图兜底:按第一幕记下的页码和坐标,把这张烂表单独渲成图片,让小模型直接看图把数字读回来,再跨页拼成完整表,必须再次通过确定性勾稽才采信。这正是主线案例 000048 的高潮。
看图兜底只在确定性/抽取链路明确报缺、报错(fail-loud)时才对那一张表启动——只兜底,不进主线全量,避免无谓成本和风险。
page_idx + bbox 精确框到这张表在第几页的哪个区域# 14 列宽的权益变动表,代码抽 0 行。 # 看图:垂直投影找出 17 条网格竖线,精确切出每一列 # 逐列密集读回后,确定性引擎自验: 归属母公司小计 9,175,030,210.24 + 少数股东权益 95,744,524.42 ───────────────────────────────── = 所有者权益合计 9,270,774,734.66 ✓ 分毫不差 # 同一张表里"盈余公积"一列疑似读错(622… vs 图上 513…), # 正好被"每行各列求和=小计"那条规则该抓 → 标红交人工。
这是第四幕的灵魂、也是第三铁律的延伸:看图读回的值天生可疑,绝不自动写回、绝不改原文。它必须把读回的数喂回同一套确定性勾稽引擎,逐格打三种标签:
双重闸门:整表必须过会计恒等式(如 BS 必过 BS_EQUATION)且装配必须完整(所有页都抽到、没被截断),任一不满足,这张表一个格都不采信。
# 合并资产负债表被 OCR 拆成 4 块跨 p69–72, # 其中 p70 续页被抽空(cols=0)→ 确定性代码拼不起来 → 报"缺合并BS"
2026-07 我们把看图兜底接进主线后,拿年报留出集里最难的 24 份(确定性修复都救不回的尾部)做端到端复测。结果如实记录:视觉管线真实有效——比如某年报的母公司现金流量表,看图读回了 18 行 24 个值、勾稽全对;但严格闸门最终一张整表都没采信(多因跨页装配不完整,闸门要求"整表拼齐 + 恒等式过"才放行)。
输入 年报留出集最难尾部 24 份(缺 BS/IS/CF) 管线 渲图 → 逐列读回 → 跨页装配 → 清洁闸 真实读回 有(例:母公司 CF 18 行 24 值,勾稽全对) 闸门采信 0 张整表(拒收主因:装配不完整 / 缺合计行) 结论 管线有效性确认;闸门按设计拒收残表 # 「0 finding ≠ 无错」的镜像是「读回了 ≠ 能采信」。 # 严格的闸门,是这套系统敢让人信任的原因。
机器把疑点找出来,最后一锤子必须留给人。第五幕是一个网页版对账核验工作台(就是本站的 /demo):左边是财报 PDF 原文,右边是抽出来的活报表和疑点队列,点任何一处,三边同时高亮到那一格。人在这里逐条裁决:"确实是 OCR 错,改"、"误报,放行"——而系统保证:不管人怎么改,OCR 原文一个字符都不动。
用户对一处标红提交修正值后,系统绝不覆盖 OCR 原值,而是记一笔可追溯的修正事务(谁、什么时候、把哪格从多少改成多少、为什么),然后把修正值喂回同一台确定性算盘重算——之前因这格不平的等式如果全部转绿,才算"闭环"。
工作台首屏用一张覆盖状态卡替代吓人的红色告警:主表抽到几张、多少数字参与了勾稽、几张表解析异常(只计真实解析失败,账本里的良性丢弃不算)——一句人话一项,异常才用警示色。
核验完点"导出核验成果",拿到的是三份各有用途的产物:
① 修正后干净报表.xlsx 真数值格 · 负数为负值 · 单位注明 · 可直接 SUM ② 原值对照核验底稿.xlsx raw / 修订值 / 勾稽状态 三列并排 · 疑错格标色 ③ 审计轨.json {时间, 规则, 位置, 裁决, 原值, 修订值, 重算结论} # 文件名:{公司简称}-核验成果-{日期}.xlsx # 每份都带说明 sheet:这是什么、每列什么意思、数据口径。
一套抓别人错的系统,先得经得起抓自己的错。我们给它建了两套考卷:一套错误注入(在 4 份真实财报上按 OCR 的真实出错方式埋进 1044 处已知错误,看能逮回多少)、一套提取覆盖(200 份从未见过的年报/招股书,看能抽全多少)。2026-07 我们重修了评分卡——发现旧算法给自己放水:只要表上冒出任何新告警就记"检出",哪怕根本没指对格。修严之后,同一份成绩单按三层口径重新亮出来:
年报 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 同事)改一行代码,三道门全绿才允许合入。这就是"确定性引擎"能一直保持确定的原因:
对内置演示样本重跑全管线,findings 与基线快照逐字节比对——多一条、少一条、变一个字都算失败。
格级检出率不得下降、干净对照假阳不得上升。想"顺手放松一条容差"?这道门当场变红。
三大表抽取率与齐套率不得倒退——修 A 表不许弄丢 B 表。
五幕走完,PDF 变成了一份结构化文档:每张表逐行逐期的数字、每一处标红、每一条勾稽结论、每一笔人工修正,都带着页码坐标和判定来源。下面是这套管线在 1000 份真实招股书 / 年报(300 份训练 + 700 份泛化)上的累计成绩单:
每一处标红都送独立第三方复核才下"真错"结论——累计确认了 4100+ 处真实 OCR 误读,每一处都能精确指回犯错的那一格,单处偏差最高达千亿量级。同样如实写上:标红里还混着抽取偏差与舍入(见第三幕红黄绿),假阳治理是当前的主战场——所以最后一锤子留给人,而不是留给分数。
{
"科目": "应付账款",
"值": "40,206.073.20", # 逐字保留 OCR 原文,未改
"出处": { page: 71, bbox: [..] }, # 指回原文那一格
"勾稽": "flagged", # BS_SUBTOTAL_TIE 没对上
"人工修正": "40,206,073.20", # 修正事务,与原值并存
"判定来源": "deterministic" # 永远是代码,不是 AI
}
LLM_VALUE_DRIFT)当场抓出。人工修正也只记事务、不碰原文。