FDE Times 数据在招 FDE 职位 826▼35 近 7 天在招职位数的变化(同口径比较):不计入本周新开始追踪的公司的职位近 7 天新增 77在招公司 154支持远程 25%美国薪资中位数 $202k招聘最多 Databricks 104
ENVI

前线部署工程师(FDE)的专业媒体

技能指南 · 指南

实战:构建一个无权放款的三单匹配 Agent

发票匹配难的不是比对,而是异常;好的 Agent 必须知道何时停手。

分享微博

要点速览

  • 先用规则写好匹配逻辑,Agent 只接手规则处理不了的行
  • 发票早于收货单到达时,应设置可解除的付款冻结,而不是直接拒收
  • Agent 的工具只能写入状态,是否付款永远由人决定
文中 decide() 函数的决策树。各分支按顺序检查:没有标记得到 MATCHED;价格不符得到 ESCALATE;开票数量超过收货数量得到 ESCALATE;仅缺少收货单得到 HOLD;部分交货且采购订单未关闭得到 MATCHED;部分交货且采购订单已关闭得到 ESCALATE。没有任何结果是付款。底部的橙色框写着:付款仅由人工审批。
decide() 函数按固定顺序检查标记。三种输出都不表示“付款”:放款的决定权不在 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 个信源
路线图上的下一篇 · 阶段 3: AI 应用用假 MCP 服务器离线测试 Agent 的故障处理一个小型 Python 服务器按脚本注入超时、429 和字段改名,向客户演示系统出故障时 Agent 如何应对。