基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-04 14:06

发票抽取如何用校验节点拦住金额错误

dev.to · AI#n8n#数据校验#本地模型
Editor brief · 编辑速览

作者使用n8n和本地Qwen小模型,对14份不同格式票据各运行3次,42次结果中有9次总额错误。文章比较两个校验节点处理同一模型输出的效果,标题称改进版本拦截了全部9次错误。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

发票提取最麻烦的错误,是金额看着合理、JSON 也能解析,最后却把错数写进了表格。作者把 14 份不同格式的单据各跑 3 次,42 次输出中有 9 次总额错误;改进后的校验节点拦住了全部 9 次。

这次实验值得看的不是模型选型,而是校验思路:让模型提出字段,再用代码核对原文证据。同时还要看误报,否则规则拦得再多,也可能因为频繁打扰人工而被关掉。

JSON 全部合法,为什么还有 23 次错误输出

作者使用自托管的 n8n 1.123.84,在共享 CPU 机器上通过 Ollama 运行 Qwen 1.8B,量化规格为 Q4_K_M。调用走 OpenAI-compatible /v1 endpoint,设置 temperature: 0.2 和 response_format: json_object。

工作流是 Webhook、提示词、LLM,再让 Validate v1 和 Validate v2 读取同一份模型输出。比较的是校验规则,避免两次生成结果不同干扰结论。

14 份单据涵盖 GST 发票、咖啡馆小票、AWS 账单、自由职业者发票、电费单、酒店账单等,也包含金额写成 1.234,55 € 的德国托管发票。

42 次响应全部可以解析,但其中 23 次存在字段错误,19 次正确。标题中的 9 次,专指总额出错的运行次数。

其中 6 次总额错误来自重复加税:单据上的总额已经含税,模型又加了一遍,而且结果恰好等于「总额+税额」。这种数字很难靠扫一眼表格发现。

其他错误更像字段理解偏差。班加罗尔咖啡馆小票上的 02/11/2026,连续 3 次被读成 2 月 11 日,实际应是 11 月 2 日。自由职业者发票写着「15 天内付款」,模型两次生成了原文没有直接列出的到期日,一次算对,一次编错。

促销码 TRYNEW 则连续 3 次进入 gstin;另一份发票里的 Advance received (8 Oct) -10,000 也被填进这个字段。电费单同时存在 RR number 和 bill number,模型有 2/3 次选错。

格式正确只能证明数据能被读取,不能证明字段含义正确。

为什么更严格的算术规则反而不好用

Validate v1 检查总额和 ID 是否出现在原文、日期是否符合 YYYY-MM-DD,以及 subtotal + tax = total。

问题出在最后一个等式。折扣、配送费、分行列出的 CGST 和 SGST、酒店计费项目,都可能让这个简化关系失效。原文表示,v1 拦住了多数错误运行,却也拦住约一半正确运行。

日期还有另一类误报:直接拿 2026-10-28 和原文的 28 Oct 2026 比字符串。两者代表同一天,文本却不相等。

v2 因此把重点转向原文证据。先用代码读取日期,统一表示,再检查模型返回的日期是否属于这个集合。

下面这段解决数字日期的顺序问题,并检查日期是否确实出现在文档中:

const DATE_ORDER = 'DMY'; // 'MDY' if your vendors are in the US
const dates = new Set();
for (const x of doc.matchAll(/\b(\d{1,2})[\/.\-](\d{1,2})[\/.\-](20\d\d)\b/g)) {
  const [a, b] = [+x[1], +x[2]];
  dates.add(DATE_ORDER === 'DMY' ? iso(x[3], b, a) : iso(x[3], a, b));
}
// ...plus patterns for "14 Oct 2026", "Oct 3, 2026" and 2026-10-14
for (const k of ['invoice_date', 'due_date'])
  if (out[k] && !dates.has(String(out[k])))
    reasons.push(`${k} ${out[k]} is not a date in the document`);

要看的是 DATE_ORDER = 'DMY':日期顺序由业务明确指定。这条规则拦住了全部 3 次月份误读和两次原文未列出的到期日。片段中的 iso() 和其他日期模式没有完整给出,直接复制还不能构成完整实现。

总额采用相同思路:寻找单据自己的总额行,再与模型结果比较。

下面这段用总额关键词筛选行,排除小计和余额行,取最后一条候选进行核对:

const KW = /(grand total|order total|total due|amount payable|amount paid|net amount payable|gesamtbetrag|\btotal\b|charged)/i;
const totalLine = doc.split('\n')
  .filter(l => KW.test(l) && !/(sub ?total|item total|balance)/i.test(l)).pop();
let docTotal = null;
if (totalLine) {
  const m = totalLine.slice(totalLine.search(KW)).match(/\d[\d.,]*\d|\d/);
  docTotal = m ? amt(m[0]) : null;
}
if (docTotal !== null && Math.abs(t - docTotal) > 0.01)
  reasons.push(`total ${t} != document total ${docTotal}`);

重点是 .pop() 和 0.01:它选择最后一条候选行,并允许金额差不超过 0.01。amt() 负责金额解析,但原文未提供实现,包括 1.234,55 的处理也需要自行补齐。只有完全找不到总额行时,v2 才回退到算术校验。

拦住全部错误总额,为什么仍有漏网字段

v2 拦住了全部 9 次错误总额,却比 v1 漏掉更多错误运行,漏掉的都是 invoice number 或 GSTIN。它的收益是没有拦住正确发票;作者另外输入 14 份正确答案,也得到 0 次标记。

原因很直接:「出现在文档里」不能证明「适合这个字段」。TRYNEW 就确实存在。

v2.1 因此为 gstin 加上格式检查,使用正则 ^\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]$。它检查结构,不足以证明税号真实有效。

作者没有重新调用模型,而是回放保存的 42 份输出。结果是拦住 18/23 次错误运行,对 19 次正确运行仍然保持 0 次误报。

剩下 5 次分别是电费单 RR number 被当成发票号的 2 次,以及乘车小票 trip ID 被当成发票号的 3 次。后者是否算错,作者也承认有讨论空间。两个编号都像真的,也都在原文里,通用规则就很难继续判断,需要供应商专用规则或人工处理。

我的判断

我认同作者把原文证据放在算术关系前面。账单有折扣、费用和预付款,简单等式很容易把合理差异当成错误。把误报率一起测量,也比只展示拦截数量更有参考价值。

不过,「9/9 拦住」是这批样本的结果,不能直接当成通用准确率。 实验只有 14 份不同格式的文档,重复运行增加了输出样本,却没有增加单据类型;原文也没有更大模型、不同提示词或其他提取方案的对照数据。

总额规则同样有模板前提:目标金额需要和关键词处在同一行,而且关键词后第一个数字、最后一条候选行都得选对。换成多栏排版或金额换行,效果如何,原文没说清。

日期检查也取决于产品定义。如果业务允许根据「15 天内付款」计算到期日,那么正确推导出的日期不该直接判错。invoice total 和 balance due 也是如此,必须先确定要的是发票总额还是剩余应付额。

共享 CPU 上平均延迟为 8.9s,中位数 6.6s,还有一次 71s 异常值;校验节点没有增加可测出的耗时。这支持在该工作流中加入代码校验,但不是跨环境的性能保证。发票提取器也尚未进入作者的免费仓库,现有内容不能当作完整成品直接使用。

能拿来做什么

  • 在 n8n 的 LLM 节点后增加 Code 节点,优先核对总额、日期和 ID 格式。使用原文片段时补齐 iso()、amt() 与日期模式,并验证自己的单据格式。
  • 为 Node 提取接口先写清字段定义,明确总额与余额的区别、日期采用 DMY 还是 MDY,以及是否允许计算原文未直接给出的日期。
  • 保存模型原始输出和人工答案,修改规则后直接回放,同时统计漏检和误报,避免每轮重新生成造成比较偏差。
  • 对同一供应商反复出现的编号混淆增加专用规则;仍无法区分时转人工,让前端展示候选编号及对应原文,帮助审核者判断。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
将聊天记忆存成固定大小文件
下一篇
多智能体共享记忆的架构选择与冲突处理

资讯速览

dev.to · AI · 2026-11-04 14:06

AI 评分 8.0 / 10

相关标签

#n8n#数据校验#本地模型

继续阅读

  • Agent工具执行前,授权应交给谁
  • 将聊天记忆存成固定大小文件
  • 多智能体共享记忆的架构选择与冲突处理
  • 用一页立项表约束AI试点范围
  • AI交付界面代码时应附验证证据