ELAI S.r.l.

AI 读取发票:如何避免错误金额进入业务系统

通过可复现算例,了解发票检查能发现哪些错误、会漏过什么,以及自动化前如何计算复核和修复工作。

AI 读取发票:如何避免错误金额进入业务系统

发票看起来读对了,可以导入吗?

一家小企业的办公室会收到不同供应商的发票。把金额和项目录入业务系统需要仔细核对,AI 助手似乎可以直接提供可用的数据。但整齐的表格不代表任务完成:合理的总额可能来自另一份文档,某个项目可能丢失,供应商也可能被误认为收件方。本文帮助负责行政或文档自动化的读者决定,在自动读取和正式登记之间应设置哪些检查。核心问题很具体:数字核对能够证明什么,又无法证明什么?

结论先说在前面:算术一致性能发现一部分错误,但不等于忠实于原始文档。我们通过十二条合成记录、实际执行的校验程序和处理时间模拟来说明。这里没有运行 OCR、调用模型、处理真实发票或测量员工工作。结果仅属于所声明的示例,不能估计任何产品的可靠性,也不构成税务合规校验。本文提供的是一种可验证的方法,用来设计向业务系统传递数据的过程。

先做选择:真的需要读取页面吗?

如果已有可用的结构化文件,字段明确且格式受支持,就应该先比较确定性导入程序与 AI 路径。确定性指明确规则以相同方式转换相同输入,而不是让模型猜测总额在哪里。这不代表文件一定真实或无误:身份、授权和核对仍然重要。它只是避免在信息已结构化时再做视觉重建。购买图像识别能力之前,应先评估这一替代方案。

对于扫描件和非结构化 PDF,需要区分三项工作。OCR,即光学字符识别,尝试读取字符和词语。信息提取把这些内容分配到供应商、金额等字段。校验检查字段是否满足约束并符合来源。数字可能被正确抄录,却放入错误字段。分开各阶段,错误才容易定位:更换模型不会自动修复误解小数逗号的规则。

一个可以从头到尾重现的示例

理想文档包含两项未税金额 120 和 80 欧元,未税合计 200 欧元,列示税额 40 欧元,总额 240 欧元,供应商 SUP-A,币种 EUR。这些是实验虚构数据。我们不根据税率计算税额,也不推荐税务处理。示例排除了文档级折扣与附加费、预付款、特殊舍入、其他币种和贷项通知。这个范围是为说明逻辑,而非把实际业务简化成四个数字。超出范围的文档应被标记,不能强行套用模式。

代码以整数分存储金额:120 欧元写成 12000。这样求和是精确的,无需为二进制浮点数设置任意容差。但不能因此悄悄把所有价格舍入到两位小数:单价、数量和舍入可能适用不同规则。这里的项目金额已经是以分表示的最终未税金额。完整系统应保存原始值,并明确十进制处理策略。Python 的 Decimal 文档区分了数值表示与舍入。

数字对得上:必要但不充分

公式回答什么问题?如果提取的项目覆盖文档且归属正确,其和应等于未税合计;未税合计加税额应得到总额。l_i 表示第 i 项金额,N 为未税合计,T 为税额,G 为总额。金额单位都是分,m 是无量纲的项目数。我们不是让 AI 自我评价,而是施加明确的外部约束。

N = sum(l_i, i=1,...,m) G = N + T r_line = N - sum(l_i) r_total = G - N - T

初始情况下,12000 + 8000 = 20000,20000 + 4000 = 24000,残差都为零。如果仅把 G 提取成 25000,第二个残差为 1000 分,即十欧元。在声明假设下,字段不可能全部正确,但我们还不知道哪个错了。自动修改总额使其等于求和会抹去错误证据,而且可能修改本来正确的总额,真正错误的却是税额。残差要求检查来源,不是命令修复某一列。

Peppol BR-CO-10 与 BR-CO-15 记录了项目求和与合计的关系。本程序只在简化子集中采用其算术逻辑,没有实现标准、XML、全部规则或舍入规则。含税总额不总等于尚需支付的余额。真实校验器需要明确文档类别与适用规则;本示例不证明符合 Peppol 或意大利发票规范。

所有算式都对,记录却仍然错误

构造一个两条公式都发现不了的错误:提取结果为项目金额 132 和 88 欧元、未税合计 220、税额 44、总额 264,即每个金额都乘以 1.1。求和一致,却不符合总额为 240 欧元的来源。这个反例足以否定“只要合计相符,就可以导入”。证明逻辑推断错误不需要估计发生频率,但实际发生率必须通过代表性数据与可靠标注估计。

第二个案例保留金额,只把 SUP-A 改为 SUP-B,检查仍然通过。文档编号、日期和订单引用也一样:不进入约束的字段就不受约束保护。主数据检查能发现不存在的编号,却不能区分两个都存在的供应商。这需要把身份与文档核对,并保存字段的来源页码和图像区域。每项检查只覆盖特定错误,名称再让人放心,也不会使其万能。

用十二个案例检验导入策略

数据集有六份正确的基础记录 C1–C6 和六份错误记录 E1–E6。E1 只改总额,E2 只改未税合计,E3 只改税额,至少违反一项求和约束。E4 是上述一致变换,E5 与 E6 只改供应商。这些是专门挑战检查逻辑的案例,并非企业发票随机样本。一半记录错误是教学选择,不代表实际工作。数据完全确定且全部包含在代码中,所以没有随机种子。

为每条记录分配合成分数。C1–C6 依次为 0.99、0.97、0.94、0.85、0.98、0.92;E1–E6 依次为 0.99、0.98、0.96、0.99、0.97、0.90。分数不是模型输出,也不是校准概率。它用于检验“检查通过且系统看起来足够有信心就接受”的做法。阈值 0.95 是示例参数,不是生产建议。

把决策形式化以便计数。记录 i 通过检查时 V_i 为一,否则为零;s_i 是分数,tau 是阈值。A_i 表示模拟接受。只有所有字段都符合参考时,Y_i 才为一;实验知道该参考,实际使用中的系统却不知道。V 与 Y 不同:不知道答案也能观察 V,而 Y 需要独立真值。下式把自动化多少与漏过多少错误分开衡量。

A_i = 1 if (V_i = 1 and s_i >= tau), else 0 coverage = sum(A_i) / n error_accepted = sum(A_i * (1-Y_i)) / sum(A_i)

阈值 0.95 时,接受 C1、C2、C5、E4、E5,共五条,七条进入复核。覆盖率为 5/12,约 41.7%;接受的五条中有两条错误,即 40%。第二个比例不能用十二作分母,因为衡量的是绕过复核的子集。当什么也不接受时,条件错误率是未定义,而非零,因为该子集不存在,代码记为 None。什么都不导入,不等于正确导入。

阈值接受复核接受错误数接受错误率
093333.3%
0.984337.5%
0.9557240.0%
0.9839133.3%
1.010120—

表格比较相同数据集上的五个阈值。提高阈值不一定排除最严重的错误:E4 内部一致但偏离来源,分数仍为 0.99。较低分正确记录被排除时,覆盖率下降,条件错误率却可能上升。这不证明阈值无用,而是说明需要在目标业务中检验分数与错误的关系。一个数字不会仅因处于零和一之间就成为概率。

节省时间要计入修复工作

假设人工录入每份文档两分钟,启动每次提取需要十秒主动工作,复核每份被转交文档一分钟,修复每个已接受错误五分钟。这些不是测量值。一分钟复核包含转交案例的纠正与录入;五分钟只指漏过检查后的后续工作。假设复核能改正所有问题,且所有接受错误最终被发现。两项假设都可能不成立:时间模型不保证质量。

要判断是否减少工作,就要把隐藏活动加起来。n 为文档数量,a 为自动接受数,e 为其中错误数。t_0、t_r、t_e、t_m 分别是启动、复核、修复、人工录入的秒数。W 为总主动工作时间,并非客户等待或服务历时;并行可能缩短经过时间却不减少人工投入,反之亦然。最后的不等式说明,在失去优势前最多能承担多少修复工作。

W = n*t_0 + (n-a)*t_r + e*t_e W_manual = n*t_m W < W_manual iff e*t_e < n*(t_m-t_0) - (n-a)*t_r

阈值 0.95 时,初始工作为 12×10 + 7×60 = 540 秒,即九分钟,看起来少于人工的二十四分钟。但两个错误增加 2×300 = 600 秒,总量为 1140 秒,即十九分钟,只剩五分钟假设优势。每个错误修复超过 450 秒时,优势消失;等于 450 秒时持平。这是基于假设的计算,并非节省承诺。还未包含软件、部署、保存、错误后果和后续检查,完整经济决策应加入这些成本,避免重复计算同一活动。

十二条合成记录:接受和错误数量,以及修复前后主动工作时间。时长是假设值,未测量模型或人员。
十二条合成记录:接受和错误数量,以及修复前后主动工作时间。时长是假设值,未测量模型或人员。

左侧应同时观察接受数量与错误数量,不只看最高柱。右侧比较修复前工作、包含修复的工作和人工基线。阈值 1.01 排除所有记录,模型得到十四分钟:两分钟启动和十二分钟复核,假设复核比人工录入快。这个表面优势并不证明 AI 的功劳,而来自“一分钟检查已填数据”的假设。投资前必须测量这项差异。

研究衡量什么,办公室应衡量什么?

Šimsa 等人的 DocILE,arXiv v2 发布于 2023 年 5 月 3 日,区分定位与项目归组。主要指标不等于登记正确率,包含训练未见版式。这是历史方法参考,不是当前排名。我们没有复现模型或结果。

真实流程需要单独评估。先固定获准文档,建立可验证参考,并按供应商、版式和时间分开开发与评估,避免只测用于选择规则的表单。字段精确性、项目匹配、完整文档正确率、覆盖率和接受后错误数回答不同问题。十一个字段中十个正确,文档仍可能不可用,因为第十一个字段可能指明收款人。没有证明错误独立性,就不能把字段准确率相乘来推断整份文档正确率。

时间同样需要认真测量。比较等价任务,记录启动、来源检查、纠正、登记与返工。如果操作员只检查高亮字段,未高亮错误可能永远不被统计,因此应独立核查部分已接受记录。实验假设真值已知,真实项目必须建立并承担其成本。我们未测试人员,也不把假设时长说成用户观察。人工复核也可能出错:假设复核完美的比较必须明确声明,不能用它掩盖错误。

从校验程序到业务系统:可控制的路径

设计结论是把候选结果与正式登记分开。保留原始文档、候选字段、来源坐标、提取器版本、规范化与约束结果,才能重建决策。复核应能纠正数据而不覆盖原文档,并显示失败原因。准备导入时应保存操作标识和结果状态:超时不证明业务系统尚未登记。本例不实现集成,也不登记真实数据,只明确测试需要覆盖的边界。

初期限定文档类别并使用明确的异常队列,可使流程可检查。“需要复核”不够,还应显示哪些项目不一致、缺少哪个字段、检查哪一页。不能把不符合模式的文档变成零来通过检查。批准仍是可追踪的决策,不是模型自动认证。这不证明 EL-AI 已有记账产品:我们分析可能的应用架构,不宣布已实现的服务。

读代码时别忘记问题

make 创建记录并保留与参考的比较。coherent 检查整数、币种、等式,不检查身份或法规。policy 应用阈值,用已知真值统计接受和错误,再累加假设时长。断言检查被发现错误和一致但漏过的错误,固定 0.95 时的 5/7/2 数量与 540/1140 秒。求和检查随项目数线性增长,不代表视觉模型的复杂度或延迟,因为未运行模型。复现时先执行 experiment.py 再执行 plot.py,结果与版本在压缩包中。

可以决定什么,还有什么尚未证明?

在声明范围内,算术检查能转交不一致记录,但不能单独授权导入。264 欧元反例不依赖模型即可证明这一点。修复成本也会改变决策:在假设下,九分钟变成十九分钟,每个错误修复超过七分半就失去相对人工录入的优势。十二个构造案例不能估计多少真实发票可自动处理,不能选择通用阈值,也不能承诺回报。

有用的决策应从已有格式出发,分开提取与授权,衡量接受记录正确性,并计算使数据可用的全部工作。如果结构化导入已满足需求,可能不需要 AI。如果要解释多变页面,模型应加入可验证流程,不能用让人放心的分数替代检查。下一步应在办公室实际文档和异常上独立评估,测量人工对照并明确停止条件,而非做更炫的演示。

来源与可复现材料

Peppol BIS Billing 3.0 — BR-CO-10, May 2026 release.

Peppol BIS Billing 3.0 — BR-CO-15, May 2026 release.

Šimsa et al. — DocILE, arXiv:2302.05658v2, 3 May 2023; sections 3–5.

Python — decimal; documentation and runtime versions may differ.

import json,sys,copy
from pathlib import Path
BASE=dict(lines=[12000,8000],net=20000,tax=4000,total=24000,supplier="SUP-A",currency="EUR")
def make(name,score,change=None):
 x=copy.deepcopy(BASE)
 if change:x.update(change)
 return dict(id=name,score=score,data=x,correct=x==BASE)
rows=[make("C1",.99),make("C2",.97),make("C3",.94),make("C4",.85),make("C5",.98),make("C6",.92),
 make("E1",.99,dict(total=25000)),make("E2",.98,dict(net=21000)),make("E3",.96,dict(tax=4100)),
 make("E4",.99,dict(lines=[13200,8800],net=22000,tax=4400,total=26400)),
 make("E5",.97,dict(supplier="SUP-B")),make("E6",.90,dict(supplier="SUP-C"))]
def coherent(x):
 return (all(type(v) is int for v in x['lines']+[x['net'],x['tax'],x['total']])
  and sum(x['lines'])==x['net'] and x['net']+x['tax']==x['total'] and x['currency']=="EUR")
def policy(threshold):
 auto=[r for r in rows if coherent(r['data']) and r['score']>=threshold]
 errors=sum(not r['correct'] for r in auto);n=len(rows);accepted=len(auto)
 return dict(threshold=threshold,accepted=accepted,review=n-accepted,errors=errors,
  coverage=accepted/n,conditionalError=errors/accepted if accepted else None,
  activeSeconds=n*10+(n-accepted)*60,withRepairSeconds=n*10+(n-accepted)*60+errors*300)
for r in rows:r['coherent']=coherent(r['data'])
assert sum(r['correct'] for r in rows)==6
assert sum(not r['coherent'] for r in rows)==3
assert coherent(rows[9]['data']) and not rows[9]['correct']
assert coherent(rows[10]['data']) and not rows[10]['correct']
policies=[policy(t) for t in [0,.90,.95,.98,1.01]]
r=policy(.95)
assert (r['accepted'],r['review'],r['errors'],r['activeSeconds'],r['withRepairSeconds'])==(5,7,2,540,1140)
result=dict(python=sys.version.split()[0],seed=None,scope='Deterministic synthetic extracted records; no OCR, model, invoices, people or accounting system tested',currency='EUR',amountUnit='integer cents',baselineSeconds=12*120,assumptions=dict(intakeSeconds=10,reviewSeconds=60,repairSeconds=300,manualSeconds=120),records=rows,policies=policies)
Path(__file__).with_name('results.json').write_text(json.dumps(result,indent=2),encoding='utf8')
print(json.dumps(r))

代码、数据与说明 · JSON. 教学计算使用 Python 3.14.0,图使用 Matplotlib 3.11.2。分析由 AI 辅助,不声称经过同行评审或人工审核。原创 ImageGen 封面仅作示意,不记录 EL-AI 人员、场所或实际安装。来源查阅于 2026 年 10 月 26 日。