实战:构建一个无权放款的三单匹配 Agent
发票匹配难的不是比对,而是异常;好的 Agent 必须知道何时停手。
要点速览
- 先用规则写好匹配逻辑,Agent 只接手规则处理不了的行
- 发票早于收货单到达时,应设置可解除的付款冻结,而不是直接拒收
- Agent 的工具只能写入状态,是否付款永远由人决定
供应商送来 50 箱中的 46 箱,却按 50 箱开了发票。剩下的 4 箱也许在下一趟车上,也许永远不会到。专做匹配自动化的公司 Lleverage 用这个例子说明,什么样的异常会让发票匹配系统失灵。
比对三个数字的代码,谁都能在十分钟内写完。真正的工作在别处。
供应商 Peakflo 援引 APQC 2025 年的数据称,采用规则匹配的组织,只有 52%–58% 的发票能实现无人工干预的直通处理。
剩下的近一半发票,正是 FDE 拿薪水要解决的问题。
本指南按照在客户现场应有的顺序,讲解如何构建一个三单匹配 Agent:先写规则,再处理异常,最后才是 Agent,并设一条硬边界——Agent 不得放款。下面的代码是纯 Python,为说明逻辑做了简化,不绑定任何 ERP 的 API。
要构建什么,需要准备什么
产出是一条小型数据管道。它接收三类单据(采购订单、收货单和发票),逐行比对,为每一行分配状态,只把规则无法解决的行交给 Agent。Agent 提出行映射方案并说明理由,由人审批。
你需要 Python 3.10 或更高版本、一份包含若干采购订单、收货单和发票的样例数据集(可以自己造),以及一个支持工具调用的大模型。最重要的是,你要清楚客户在 ERP 中配置了哪些容差。
这个顺序并非随意。Anthropic 在构建 Agent 的指南中建议,先找到尽可能简单的方案,只在确有必要时才增加复杂度。对发票匹配而言,最简单的方案就是一个比对函数。
第 1 步:统一为行结构
一切都在行级别进行,而不是单据级别。把三类单据转换成同一种结构:
from dataclasses import dataclass
@dataclass
class Line:
doc_id: str # 采购订单号、收货单号或发票号
po_line: str | None # 该行指向的采购订单行(可能缺失)
sku: str
qty: float
unit_price: float
检查:打印所有 po_line 为空的发票行。它们是第 4 步的首批候选,因为供应商要么漏填了引用,要么把多行合并成了一行。
第 2 步:用规则和容差匹配
大型 ERP 做的正是这件事。Sage X3 的文档描述了两项比对:发票价格对比采购订单价格,收货数量对比订购数量,各自带有可配置的容差阈值。
超出阈值,系统就会告警或阻断。你要重写这段逻辑,并为开票数量少于订购数量的发票增加一个标记:
def check_line(po: Line, gr_qty: float, inv: Line,
price_tol=0.02, qty_tol=0.0) -> list[str]:
issues = []
if gr_qty == 0:
issues.append("MISSING_GR")
elif inv.qty > gr_qty * (1 + qty_tol):
issues.append("QTY_OVER_RECEIPT")
elif inv.qty < po.qty:
issues.append("PARTIAL") # 开票数量恰好等于收货数量,但少于订购数量
if abs(inv.unit_price - po.unit_price) > po.unit_price * price_tol:
issues.append("PRICE_VARIANCE")
return issues
看一个假设的例子。采购订单为 50 件,单价 10.00,价格容差 2%。发票单价为 10.15,偏差 1.5%,通过。但发票按 50 件开票,收货单只记录了 46 件:这一行会被打上 QTY_OVER_RECEIPT 标记,正是 Lleverage 描述的情形。
检查:在全部样例数据上运行该函数,统计没有任何标记的行所占比例。这个数字就是你的基线,后续每一项改进都要以它为准来衡量。
第 3 步:每类异常该如何处理?
一串标记还不是决策。你需要把标记转换成状态,而每类异常都有各自的处理方式。
缺少收货单很常见:据 Lleverage 介绍,原因是仓库还没登记,或者登记到了错误的订单行上。SAP S/4HANA Cloud 为此专门设有容差键 DW,在收货数量为零时过账发票就会触发。
SAP 建议把 DW 配置为付款冻结而非错误,这样早于收货单到达的发票仍可在之后放行。思路保持一致:设为 HOLD 状态,新的收货单到达时自动重新检查。
部分交货需要仔细区分。Peakflo 认为,Agent 应当把正常的部分交货与真正的差异区分开,只有真正的差异才交给人工审批。
一种合理的划分方式:发票开票数量恰好等于已收货数量、且采购订单仍处于未关闭状态,属于正常情况,余下的货稍后会到。如果采购订单已关闭,短缺部分不会再交付,就需要人来处理。开票数量超过收货数量的发票,比如 50 箱只到 46 箱的情形,一律视为差异。
价格变化是容差救不了的情形。Lleverage 说得很直白:容差能应对四舍五入,却应对不了这种情况,因为错误出在采购订单本身——它已经过时了。这种情况需要人介入:要么修正采购订单,要么拒绝新价格。
def decide(issues: list[str], po_open: bool) -> str:
if not issues:
return "MATCHED"
if "PRICE_VARIANCE" in issues:
return "ESCALATE" # 采购订单可能已过时,即使收货单缺失也是如此
if "QTY_OVER_RECEIPT" in issues:
return "ESCALATE" # 开票数量超过收货数量
if issues == ["MISSING_GR"]:
return "HOLD" # 付款冻结,收货单到达后可解除
if issues == ["PARTIAL"]:
return "MATCHED" if po_open else "ESCALATE"
return "ESCALATE" # 任何不寻常的组合:交给人处理
分支顺序是有意安排的。一行既缺少收货单、又有价格偏差,结果是 ESCALATE 而不是 HOLD:等仓库登记收货修不好定价错误的采购订单,冻结发票只会拖延采购员必须做的工作。
检查:这个函数里没有任何状态表示“付款”。MATCHED 只表示三份单据一致。
第 4 步:只在规则失效处引入 Agent
规则真正失效的地方是行结构。据 Lleverage 介绍,行级规则默认一张发票行对应一条采购订单行,其他情况都会漏掉。
发票把三条采购订单行合并成一行,或把一行拆成两次交货,就需要有人读懂品名描述、SKU 和数量,再把每一行映射到正确的采购订单行和收货单。这正是 Agent 的工作。
Anthropic 建议对工具采用“防呆”(poka-yoke)设计,即通过设计参数让工具难以被误用。落到这里,Agent 只有两个工具,都不涉及付款:
TOOLS = [
{"name": "propose_line_mapping",
"params": {"invoice_line": "str", "po_line": "str",
"gr_id": "str", "reason": "str"}},
{"name": "record_match_result",
"params": {"invoice_line": "str",
"status": ["MATCHED", "HOLD", "ESCALATE"],
"reason": "str"}},
]
这是简化后的描述,你需要把它转换成所用大模型的工具格式。值得注意的细节是,status 是一个封闭枚举。Agent 无法凭空造出“APPROVED_FOR_PAYMENT”状态,因为根本不存在这个值。
Agent 提出映射方案后,在结果上再次运行 check_line 和 decide。阅读和理解交给 Agent,比对仍由代码决定。任何未通过规则的方案都会自动变为 ESCALATE。
第 5 步:用异常本身做测试
Lleverage 把界线划得很清楚:放款永远不由 Agent 决定。把这句话变成测试。写一小组用例,覆盖上述四类异常、既缺收货单又有价格偏差的情形,再加一条完全匹配的行,然后断言每个输出状态。
再加一条测试:扫描 Agent 可调用的所有工具,断言没有一个会写入付款表。这听起来理所当然,但当客户的审计人员问“你怎么证明机器不能自己付款?”时,你拿出来的就是它。
常见错误
第一个错误是一开始就引入 Agent,让它处理做个减法就能解决的行。成本更高,可解释性丧失,也没有可供衡量的基线。
第二个错误是调高容差来提升匹配率。数字好看了,但价格变化的情形会漏过去,到不了本该去修正采购订单的人手里。
第三个错误是直接拒收缺少收货单的发票。仓库只是晚了一天登记收货,供应商却得重新寄送,财务得重新录入。SAP 自己都建议采用可解除的冻结。
这项技能在客户现场如何体现
在客户现场,你很少需要从零写 check_line。容差早已配置在 ERP 里,第一项任务是读懂这些配置,并问会计人员为什么选这些数值。你的 Agent 必须遵守同样的阈值,否则两套系统会为同一张发票争执不下。
第二项任务是取一周的真实发票,跑一遍规则,然后和应付账款人员坐在一起,手工给每个异常分类。这份分类能告诉你 Agent 值得部署在哪里,也会成为你的第一个评测集。
如果你想转向前线部署工程师(FDE)岗位,留意那些提到“accounts payable”(应付账款)、“ERP integration”(ERP 集成)或“human-in-the-loop”(人在回路)的岗位描述。简历上不要写“为财务构建了 AI Agent”。
而要写:你按类型拆解了异常,测量了改造前后的直通处理率,并设计了工具,让 Agent 无从触及付款。FDE 招聘方想看到的是,你清楚机器在哪里可以自主决策,在哪里不可以。
至于那 4 箱始终没到的货,它们不需要更聪明的 Agent。它们需要的是一个愿意冻结发票、把原因说清楚、并等待有权限的人来决定的系统。
这篇文章对你有帮助吗?
感谢你的反馈!
6 个信源
- Three-way match — Procurement Glossary | LightSource
- Matching tolerance (Sage X3 documentation) · 2026-10-14
- Supplier invoice blocked or in error due to missing goods receipt in 3-way match in SAP S/4 HANA Cloud Public Edition
- Three-Way Matching Automation: The Exceptions That Break It · 2026-04-27
- Autonomous PO Matching: Can AI Agents Match Invoices Without Humans? · 2026-04-14
- Building effective agents · 2024-12-19