打开银行账单可不是什么乐事。它们以扫描版 PDF、CSV 导出或带有 OFX 等晦涩缩写的 XML 文件形式呈现。对于会计师、簿记员和金融科技开发者来说,将这些文档转换为干净、结构化的数据是一个持续的头痛问题。当大语言模型(LLM)横空出世时,它们似乎提供了一个逃生出口。只需把 PDF 喂给机器,然后要求输出 JSON。能出什么问题呢?
在构建 StatementDecoder(一个旨在将银行账单转换为可用数据的工具)时,我准确地领教了可能会出什么问题。像许多开发者一样,我原以为难点在于教系统识别各种不同的文档布局。但我错了。阅读文档几乎是微不足道的。真正的噩梦在于,当机器悄悄地编造了一个数字,或者调换了交易金额中的两个数字时,你该如何察觉。
效果过于完美的演示
我的第一次尝试简单得诱人。我直接将银行账单输入 LLM,并要求返回结构化的 JSON。结果感觉像魔法一样。模型轻松处理了不同的布局。它能读取标准解析器无法处理的扫描版 PDF。它似乎无需明确指令就能理解表格、标题和多页账单。在几个辉煌的小时里,我以为问题已经解决了。
接着,我用真实的客户数据进行了测试,魔法消失了。英国的银行各自使用不同的账单设计,这些差异不仅仅是外观上的。Wise 的账单有其独特的格式特征。Revolut 的 CSV 导出看起来很直观,直到你注意到它们如何处理多币种交易和元数据字段。旧的 OFX 文件——一种看起来真的像是属于 20 世纪 90 年代的格式——会向任何期望现代标记语言的解析器抛出陈旧的标签结构和编码问题。
模型的提取效果仍然远好于任何现成的模板系统。但当涉及金钱时,“好得多”是不够的。
当 99% 的准确率意味着失败时
这就是使用 AI 进行金融数据提取的根本问题。如果一个模型处理了两百行交易记录,其中一百九十九行都是正确的,输出结果看起来会非常完美。JSON 格式正确,键值对也对齐。随意的检查可能看不出任何异常。然而,如果那唯一的一个错误调换了金额中的两个数字、将存款变成了取款,或者移动了小数点,你的账目就会被破坏。你无法通过肉眼观察一堆结构化数据来发现它。
人类在审查原始 JSON 时,很难发现交易金额中被调换的数字。格式非常完美,这反而让错误变得更加危险。你不能发布一个“大部分时间是对的”金融工具。它必须是正确的,或者必须大声宣布它不确定。
我的初步反应是可以预见的。我设计了更好的提示词(prompts),升级到了能力更强的模型,并尝试使用思维链(chain-of-thought)推理让模型展示其推导过程。但这些都无法解决核心问题。我是在要求同一个概率系统生成答案,然后又要求同一个系统去证明答案是正确的。这不叫验证,这叫“自洽性表演”(self-consistency theater)。
让数学来做决定
银行账单有一个大多数文档都不具备的特性:内置的算术约束。期初余额加上所有交易的总和必须等于期末余额。如果存在滚动余额,则必须逐行对应。这些不是风格偏好,而是硬性规则。
我围绕这一洞察重构了架构。现在,每一次提取,无论来源如何,在用户看到之前都会流经一个验证层。数据是来自解释模糊 PDF 的 LLM,还是来自读取扫描页面的 OCR 引擎,亦或是直接的 CSV 解析,都不重要。验证器将所有来源都视为同样可疑。
检查过程极其简单粗暴:将每笔交易累加到期初余额中,将结果与声明的期末余额进行比较。如果数字不匹配,说明出了问题。标记该账单待审,拒绝该次提取,不要让它到达用户手中。
这单一的改变改变了整个产品的特性。语言模型不再需要追求完美,它只需要足够好,能够通过数学检验即可。压力从“在无约束领域实现不可能的准确率”,转向了“在生成与验证之间建立紧密的反馈闭环”。
验证器还揭示了错误中的规律。某些文档类型在数学校验中始终失败,这准确地告诉了我应该在哪里投入精力。我不再盲目地进行全面的提示工程优化,而是能看出特定的银行布局导致了系统性的错误。
该用代码的地方用代码,该用 AI 的地方用 AI
或许最令我感到谦卑的教训是,我意识到流水线中很大一部分根本不需要 AI。当我遇到混乱的澳大利亚 OFX 文件时,我的直觉是向这个问题“投喂” token。我曾一度考虑将损坏的 XML 直接喂给模型,让它在解析前修复结构。相反,我写了二十行确定性代码。它瞬间修复了编码异常和格式错误的标签,处理每个文件的成本为零,且具有完美的复现性。
那次经历让我明确了提取流水线应当如何组织。这其中有三个截然不同的任务,它们不应混为一谈。
- 模型负责理解混乱的文档。 带有扭曲表格、混合字体和手写内容的扫描版 PDF
