🤖 第六章 应用实战:采购订单智能识别与ERP自动化填报助手
📄 一句话概括

本章七个节点,但交接的东西和前几章完全不同:前几章交接的是一张【任务表】(每一篇都要人过一遍),本章交接的是一张【异常表】(只有出了问题的才要人看)。这个差别来自 6.1.1 那条规律——正常件太多,人看不完;而正常件本来也不需要人。

📝 本节和前面几节的关系

6.1.1 说清了"为什么难",6.1.2 算清了"值不值得",本节把两件事变成一张流程图和一张表。 与第二~五章的流程设计(2.2 / 3.2 / 4.2 / 5.2)并列来看,本章有两处结构性新东西:独立的业务校验格,和以异常为中心的交接台。

一、全章结构总图

七个节点、两个人的位置:

① 收单  →  ② 识别(Agent)  →  ③ 校验(规则)  →  分流
                                                    ├─ 放行 ─────────────────┐
                                                    ├─ 异常 → ④ 人工复核 ────┤
                                                    └─ 阻断 → 退回补件       │
                                                                             ↓
                                                        ⑤ 填报(RPA) → ⑥ 保存前确认(人)
                                                                             ↓
                                                        ⑦ 回挂归档 → 留痕对账

把两个"人"的位置标出来:

谁 什么时候 做什么
异常复核台 采购/财务 只有被挑出来的少数几条 看原图、定字段、改完放行
保存确认点 采购 每一单都过 确认后点保存(签字的那个动作)
📝 这两个位置是本章与前几章最大的不同

前几章的人工位置是"每一篇都看一眼"(2.4.3 的发布弹窗、5.4.3 的发布前预检)。 本章的人工位置是"大部分不看,少数看仔细"——因为单据的量大、行数多、而绝大部分毫无问题。 "每一单都点一次保存"看起来像是浪费——但它是本章唯一不能省的人工动作,理由见 6.1.2 的"合规账":账要有人签字。

二、七个节点各做什么

① 收单

从收件目录(或邮箱、共享盘)把所有待处理的单据文件捞出来,做三件事:

为什么第一步就要做去重? 因为重复送会生成两张采购订单——这不是"多花点时间"的问题,是账上凭空多了一笔。

② 识别(Agent 侧)

这是本章唯一需要 AI 的地方,做一件事:把图片/PDF 变成结构化字段。

输出必须包含三样: - 字段值(供应商、单号、明细行、数量、单价、税率……); - 置信度(每个字段一个,或整体一个); - "没看见"的标记(宁可说不知道,不许猜)。

第 ③ 样最容易被忽略,但它是整条链的地基——6.3.1 会专门讲。

③ 校验(规则侧)——本章新增的一格

这一格是本章与前几章最大的技术差异。 见下面第三节。

④ 人工复核

只处理被挑出来的少数几条。 人看到的东西是装配好的上下文(原图 + 识别结果 + 校验发现 + 主数据对照),不是一堆原始数据——这一块由 6.3.2 的智能体来装配。

⑤ 填报(RPA 侧)

在 ERP 里创建采购订单,填完停在保存前。6.4.3 会讲三个难点:ERP 的三种形态、元素定位、字段之间的依赖顺序。

⑥ 保存前确认(人)

点那一下"保存"。 这一步机器替不了,理由见"两个位置"那一段。

⑦ 回挂归档 + 留痕对账

三、本章的新东西:为什么"校验"要独立成一格

前面几章其实也有校验(2.3.1 的违禁词检查、4.3.2 的字段校验)。但本章的校验在性质上不同:

前几章的校验 本章的校验
查什么 格式:长度、有没有、合不合法 业务:系统里存不存在、算术平不平
谁能发现 人也能看出来 只有系统知道
例子 标题超 30 字、标签超 5 个 "M2999 这个物料编码,主数据里根本没有"
判据 看一眼就知道 要查表、要算

所以它必须独立成一格,而且必须是【规则】而不是【智能体】:

⚠️ 一个必须记住的区别

"识别对了"和"数据是对的"是两件事。 6.2 的 Demo 里会有两条特别典型的记录:置信度 0.99、0.97(模型非常确定),但一条算术不平、一条税率越界。 模型越确定的时候,越容易让人放松——所以业务校验不能因为它"看起来很确定"就跳过。

四、【异常表】:本章最核心的交接台

这张表就是本节标题里"整体流程设计"的落点。

先看它和前几章的交接台有什么不同:

前几章:任务表 本章:异常表
表里有什么 所有待办 + 所有已完成 只有出问题的那几条
人要过多少 全部(每一篇) 少数(通常是 10%~20%)
表的性质 进度台账 待办清单 + 责任记录

为什么本章要换成异常表? 回到 6.1.1 的规律:本章的批量是【行数 × 单数】。一批 500 张单、每张 20 行 = 10000 行——正常的那 425 张如果也要人过一遍,自动化就白做了。

字段设计

字段 作用 本章新在哪
doc_id 单号 ——
file 来源文件 ——
recog 识别结果(JSON) ——
min_conf 最低字段置信度 ★ 本章第一个新字段
check 校验发现(可以有多个) ★ 业务校验特有的
level 分档:放行 / 异常 / 阻断 ——
status 待人 / 处理中 / 已处理 ——
owner 处理人 ★ 本章第二个新字段
result 处理结果(改成了什么) ★ 审计要的

两个新字段,各解决一个问题

min_conf(最低字段置信度) ——解决"哪个字段不确定"。

注意它是"最低"而不是"平均":一张单整体 0.95,但某一行某个字段只有 0.62——出问题的就是那一个字段。 报"平均"会把这个信号淹没掉。(Demo 里的 Z02 就是这样:整体 0.88 看着还行,最低 0.62。)

owner(处理人) ——解决"这条是谁定的"。

这是本章区别于内容侧的关键设计。内容发错了,删掉重发就行;单据改错了,要有人解释为什么改。 所以异常表的每一行都必须有归属——它既是工作流,也是审计底稿。

五、状态机:八个状态、一个可打回的分岔

已收单 → 识别中 → 已识别 → 已校验 ─┬─ 可填报 ──→ 填报中 → 待确认 → 已入账
                                    ├─ 待复核 →(人改完)→ 回到【已校验】
                                    └─ 已阻断(退回补件,不进 ERP)

两个设计说明:

  1. 待复核 是一个"回路",不是一个"状态"——人改完字段之后,要重新走一遍校验,而不是直接放行。理由:人改的时候也可能改错(比如把数量改成了另一个错数),重新校验是唯一的保障。
  2. 已阻断 是终点,不是异常——阻断的单根本不进 ERP,它走的是"退回补件"的另一条路(找供应商重新传真一张清晰的)。把"阻断"和"异常"混在一起,会让人误以为所有问题都能在系统里解决。

六、与前面几章流程的对照

2.2(小红书) 3.2(公众号) 4.2(头条) 5.2(知乎) 6.2(本章)
交接台 任务表 任务表 任务表 + 节奏 任务表 + 合规组 异常表
新增的一格 表当交接台 排版 节奏规则 补人味 业务校验
人工位置 发布前(每篇) 发布前(每篇) 出稿抽检 + 发布确认 补人味 + 发布确认 异常复核(少数)+ 保存确认(每单)
风险来源 平台(限流封号) 平台 + 合规办法 平台(信用分) 平台(处置五档) 审计与法规

看最后一行:前三章的风险都来自平台,本章第一次来自审计与法规——这就是为什么本章要多一个"处理人"字段,而前几章不需要。

七、动手 Demo:异常表体检器

它做的是本节第 ③ 格(校验)和第 ④ 格(分流)的合并:

# -*- coding: utf-8 -*-
"""6.2 Demo:异常表体检器(纯标准库)
检查一批识别结果该怎么分流:直接放行 / 进异常 / 阻断。
核心演示:置信度阈值与业务校验各自拦住什么。
"""

# 每条:单号 / 来源 / 整体置信度 / 最低字段置信度 / 主数据命中 / 合计是否平 / 税率合法
RECS = [
    {"id": "Z01", "src": "PDF 电子单", "conf": 0.98, "min_conf": 0.95,
     "master": "10/10", "total_ok": True,  "tax_ok": True},
    {"id": "Z02", "src": "传真扫描件", "conf": 0.88, "min_conf": 0.62,
     "master": "10/10", "total_ok": True,  "tax_ok": True},
    {"id": "Z03", "src": "微信截图",   "conf": 0.93, "min_conf": 0.90,
     "master": "9/10",  "total_ok": True,  "tax_ok": True},
    {"id": "Z04", "src": "Excel 报价单", "conf": 0.99, "min_conf": 0.97,
     "master": "10/10", "total_ok": False, "tax_ok": True},
    {"id": "Z05", "src": "手写送货单", "conf": 0.65, "min_conf": 0.41,
     "master": "8/8",   "total_ok": True,  "tax_ok": True},
    {"id": "Z06", "src": "PDF 电子单", "conf": 0.97, "min_conf": 0.96,
     "master": "10/10", "total_ok": True,  "tax_ok": False},
    {"id": "Z07", "src": "PDF 电子单", "conf": 0.99, "min_conf": 0.98,
     "master": "12/12", "total_ok": True,  "tax_ok": True},
    {"id": "Z08", "src": "Excel 报价单", "conf": 0.97, "min_conf": 0.96,
     "master": "5/5",   "total_ok": True,  "tax_ok": True},
    {"id": "Z09", "src": "PDF 电子单", "conf": 0.96, "min_conf": 0.94,
     "master": "8/8",   "total_ok": True,  "tax_ok": True},
    {"id": "Z10", "src": "微信截图",   "conf": 0.98, "min_conf": 0.97,
     "master": "3/3",   "total_ok": True,  "tax_ok": True},
    {"id": "Z11", "src": "传真扫描件", "conf": 0.96, "min_conf": 0.93,
     "master": "6/6",   "total_ok": True,  "tax_ok": True},
    {"id": "Z12", "src": "PDF 电子单", "conf": 0.99, "min_conf": 0.99,
     "master": "20/20", "total_ok": True,  "tax_ok": True},
]

T_PASS_OVERALL = 0.95   # 整体置信度:达到才可能直接放行
T_BLOCK       = 0.70    # 整体置信度:低于它直接阻断
T_FIELD       = 0.80    # 单字段置信度:低于它该字段要人看
TAX_OK = {"0", "1", "3", "6", "9", "13"}

def hit(frac):
    a, b = frac.split("/")
    return a == b

def classify(r):
    """返回 (档位, [原因])"""
    reasons = []
    # 先看硬阻断
    if r["conf"] < T_BLOCK:
        reasons.append("整体置信度 %.2f 低于阻断线 %.2f —— 这张图本身就不可靠" % (r["conf"], T_BLOCK))
    if not hit(r["master"]):
        reasons.append("主数据只命中 %s —— 系统里查不到这个编码,填进去就是脏数据" % r["master"])
    if reasons:
        return "阻断", reasons
    # 再看业务校验
    if not r["total_ok"]:
        reasons.append("行小计与合计栏不相等 —— 算术不平,这是不能猜的错")
    if not r["tax_ok"]:
        reasons.append("税率不在法定集合 %s 内 —— 越界的税率属于合规问题" % "/".join(sorted(TAX_OK, key=int)))
    if r["min_conf"] < T_FIELD:
        reasons.append("最低字段置信度 %.2f 低于 %.2f —— 指认出【哪一个字段】不该由机器定" % (r["min_conf"], T_FIELD))
    if reasons:
        return "进异常", reasons
    # 最后看整体置信度
    if r["conf"] < T_PASS_OVERALL:
        reasons.append("整体置信度 %.2f 未达放行线 %.2f —— 可以抽检,但不该免检" % (r["conf"], T_PASS_OVERALL))
        return "进异常", reasons
    return "直接放行", []

print("=" * 78)
print("一、逐条分流")
print("=" * 78)
buckets = {}
for r in RECS:
    lvl, rs = classify(r)
    buckets.setdefault(lvl, []).append(r["id"])
    print()
    print("  [%s] %-12s 置信度 %.2f / 最低 %.2f → 【%s】" % (
        r["id"], r["src"], r["conf"], r["min_conf"], lvl))
    for x in rs:
        print("        · %s" % x)

print()
print("=" * 78)
print("二、汇总")
print("=" * 78)
for lvl in ["直接放行", "进异常", "阻断"]:
    ids = buckets.get(lvl, [])
    print("  %-6s %d 条:%s" % (lvl, len(ids), "、".join(ids) if ids else "(无)"))

print()
print("=" * 78)
print("三、阈值在这里的作用(去掉会怎样)")
print("=" * 78)
n = len(RECS)
n_ok = len(buckets.get("直接放行", []))
n_hand = n - n_ok
print("  不设阈值、全部自动过:%d 条全过 —— 包括手写 0.65 那条、编码查不到那条。" % n)
print("  不设阈值、全部交人看:%d 条全看 —— 人一天也就看几十条,等于白搭。" % n)
print()
print("  有阈值的分流:%d 条免检,只有 %d 条进人工。" % (n_ok, n_hand))
print("  (提醒:这批样本是刻意加浓过的教学样本,问题类型各占一条;")
print("    真实批次里【异常 + 阻断】通常只占 10%~20%,也就是免检率 80% 以上。)")
print()
print("  换句话说:阈值的价值不是【判得准】,而是【把人的注意力集中到少数几条上】。")

print()
print("=" * 78)
print("四、两条容易被忽略的观察")
print("=" * 78)
print("  · Z04 与 Z06 的置信度都很高(0.99 / 0.97),但一条算术不平、一条税率越界。")
print("    说明【模型很有把握】与【数据是对的】是两件事 —— 所以业务校验不能省。")
print("  · Z03 整体 0.93 看着不错,却因主数据未命中被阻断。")
print("    说明【字认得对】与【编码真的存在】也是两件事 —— 识别准确率再高也管不了这一层。")

实际运行输出:

==============================================================================
一、逐条分流
==============================================================================

  [Z01] PDF 电子单      置信度 0.98 / 最低 0.95 → 【直接放行】

  [Z02] 传真扫描件        置信度 0.88 / 最低 0.62 → 【进异常】
        · 最低字段置信度 0.62 低于 0.80 —— 指认出【哪一个字段】不该由机器定

  [Z03] 微信截图         置信度 0.93 / 最低 0.90 → 【阻断】
        · 主数据只命中 9/10 —— 系统里查不到这个编码,填进去就是脏数据

  [Z04] Excel 报价单    置信度 0.99 / 最低 0.97 → 【进异常】
        · 行小计与合计栏不相等 —— 算术不平,这是不能猜的错

  [Z05] 手写送货单        置信度 0.65 / 最低 0.41 → 【阻断】
        · 整体置信度 0.65 低于阻断线 0.70 —— 这张图本身就不可靠

  [Z06] PDF 电子单      置信度 0.97 / 最低 0.96 → 【进异常】
        · 税率不在法定集合 0/1/3/6/9/13 内 —— 越界的税率属于合规问题

  [Z07] PDF 电子单      置信度 0.99 / 最低 0.98 → 【直接放行】

  [Z08] Excel 报价单    置信度 0.97 / 最低 0.96 → 【直接放行】

  [Z09] PDF 电子单      置信度 0.96 / 最低 0.94 → 【直接放行】

  [Z10] 微信截图         置信度 0.98 / 最低 0.97 → 【直接放行】

  [Z11] 传真扫描件        置信度 0.96 / 最低 0.93 → 【直接放行】

  [Z12] PDF 电子单      置信度 0.99 / 最低 0.99 → 【直接放行】

==============================================================================
二、汇总
==============================================================================
  直接放行   7 条:Z01、Z07、Z08、Z09、Z10、Z11、Z12
  进异常    3 条:Z02、Z04、Z06
  阻断     2 条:Z03、Z05

==============================================================================
三、阈值在这里的作用(去掉会怎样)
==============================================================================
  不设阈值、全部自动过:12 条全过 —— 包括手写 0.65 那条、编码查不到那条。
  不设阈值、全部交人看:12 条全看 —— 人一天也就看几十条,等于白搭。

  有阈值的分流:7 条免检,只有 5 条进人工。
  (提醒:这批样本是刻意加浓过的教学样本,问题类型各占一条;
    真实批次里【异常 + 阻断】通常只占 10%~20%,也就是免检率 80% 以上。)

  换句话说:阈值的价值不是【判得准】,而是【把人的注意力集中到少数几条上】。

==============================================================================
四、两条容易被忽略的观察
==============================================================================
  · Z04 与 Z06 的置信度都很高(0.99 / 0.97),但一条算术不平、一条税率越界。
    说明【模型很有把握】与【数据是对的】是两件事 —— 所以业务校验不能省。
  · Z03 整体 0.93 看着不错,却因主数据未命中被阻断。
    说明【字认得对】与【编码真的存在】也是两件事 —— 识别准确率再高也管不了这一层。

读结果,五个值得注意的地方:

  1. 分流不是"排出优先级",是"分成三种处置方式"。放行是免检、进异常是人看、阻断是退回——三条路的后半段完全不同。 所以顺序很重要:脚本里先看硬阻断(不可靠的图、查不到的编码),再看业务校验,最后看放行线。这个顺序不能颠倒——如果先看放行线,Z05(整体 0.65)可能因为某次阈值调松而"放行"了一张根本不可靠的手写单。

  2. Z02 与 Z05 的区别,就是"进异常"与"阻断"的分界。 Z02 整体 0.88(够),只是某一个字段 0.62(不够)→ 人看一眼那个字段就能定,所以进异常; Z05 整体 0.65(根本不够)→ 不是某个字段的问题,是这张图本身不可靠,所以阻断。 一句话判据:"某处不确定"进异常,"整体不确定"阻断。 前者人能救,后者只能退回重来。

  3. Z03 是被"业务校验"拦住的最典型一条——而它的置信度高达 0.93。 供应商、物料、数量、单价,模型全都认得很准;但那个编码在系统里不存在。 这不是识别问题,是主数据问题。 可能是供应商写错了编码,也可能是这个物料还没建档。两种情况都要人查,机器判不了。 这就是本节第三点说的"识别对了 ≠ 数据是对的"。

  4. Z04 与 Z06 那两条(置信度 0.99 / 0.97,却有算术不平、税率越界)应该让人背后一凉。 因为它们看起来最"稳"——如果校验只依赖置信度,这两条会以最高的置信度通过。 这就是为什么校验必须是独立的规则,而且必须跑在识别之后、不看识别的脸色。

  5. 第二段那句"人一天也就看几十条,等于白搭"不是夸张。 6.1.2 的时间账里,异常处理按 5 分钟/条计——12 条就是 1 小时。如果不设阈值全部人工,一批 500 张单就是 40 多个小时,自动化毫无意义。 阈值的真正价值在这里:它不是"判得更准",而是"把人的注意力集中到少数几条上"。

30 秒实验

把 Demo 里 T_FIELD 从 0.80 调到 0.95,再跑一次。

会发生什么:

结论:调阈值不是"调准确率",是调【漏检】和【人工量】之间的平衡。

顺着这个思路想一层:那这个阈值该怎么定?没有标准答案,但有一个原则——先定【可接受的漏检率】,再倒推阈值。比如"宁可多让人看 10%,也不能放过一条有问题的单据",那就调高;如果"单据量太大、人工扛不住",那就调低,但必须同时加强抽检(不放行的不代表不查,只是抽查比例低一些)。

再想一层:这个实验还说明一件事——阈值应该按【来源】分别定。手写单和干净 PDF 用同一个阈值是不合理的:手写单的置信度天然低,一刀切会让手写单全部进人工(那还不如不自动化)。

常见误区

  1. 【照搬前几章的任务表就行】——错,本章的表形态变了。 前几章是任务表(每一篇都要人过),本章是异常表(只有出了问题的才要人看)。理由在 6.1.1:本章的批量是【行数 × 单数】,一批 500 张单里正常的那 425 张如果也要人一遍,自动化就白做了。

  2. 【校验就是检查格式对不对】——错,本章第一次有"业务校验"。 前面几章查的是长度、有没有、合不合法(人也能看出来);本章查的是"系统里存不存在""算术平不平""税率合不合法"(只有系统知道)。这也决定了它必须用规则实现,不能用智能体——全是确定性判断,交给规则更便宜、更可靠。

  3. 【置信度是模型自己吹的,没什么用】——错,它是决定"谁来看"的主要依据。 没有它,就只剩两个极端:要么全自动(坏数据也过)、要么全人工(等于白搭)。而且注意 Demo 用的是 min_conf(最低字段置信度)而不是平均值——因为要定位"是哪个字段不确定",平均值会把这个信号淹没。

  4. 【阈值定死一个就行】——错,阈值是调参,而且应该按来源分别定。 30 秒实验演示了:调高(0.80→0.95)会让人工量从 5 条升到 7 条,但阻断面完全不动。而且手写单的置信度天然偏低——用同一个阈值,手写单会全部进人工,自动化就失去意义。

  5. 【异常表里不用记是谁处理的】——错,这是本章区别于内容侧的关键设计。 内容发错了删掉重发就行;单据改错了要有人解释为什么改。所以异常表的每一行都要有 owner——它既是工作流,也是审计底稿。这也正好呼应 6.1.2 那条合规账:账要有人签字。

自测三问(点开对答案) 1. **本章的交接台为什么是"异常表"而不是"任务表"?这个变化对人工量的影响有多大?** 因为**本章的批量是【行数 × 单数】**:一批 500 张单、每张 20 行 = 10000 行,**正常的那 425 张如果也要人过一遍,自动化就白做了**。 影响是数量级的:**任务表要人过 100%,异常表只过人 10%~20%**。Demo 里 12 条样本有 7 条免检,真实批次免检率通常 80% 以上。 **代价是**:异常表必须把"上下文"装配好交给人(原图 + 识别结果 + 校验发现 + 主数据对照)——**否则人看一条异常比原来手工录一遍还慢**,这正是 6.3.2 要解决的问题。 2. **"识别对了"和"数据是对的"有什么区别?举一个 Demo 里的例子。** **识别对 = 模型把图上的字看对了;数据对 = 这个数据在业务上也站得住。** Demo 里的 **Z03** 最典型:供应商、物料、数量、单价**模型全认得很准(置信度 0.93)**,**但那个物料编码在系统主数据里根本不存在**。 还有 **Z04 / Z06**:置信度 0.99 / 0.97(**模型非常确定**),却**一条算术不平、一条税率越界**。 **结论:识别准确率再高,也管不了"这个编码存不存在""合计对不对""税率合不合法"**——所以业务校验必须**独立成一格,且用规则实现**。 3. **置信度阈值调高或调低,分别会带来什么后果?该怎么定?** **调高** → 更多人被挑出来看(**更安全,但自动化收益变小**):30 秒实验里 0.80→0.95 让放行从 7 条降到 5 条。 **调低** → 更省人,但**有问题的单据可能被放过去**——而 6.1.1 说过,**数据错了是延迟暴露的**,代价在几天后才显现。 定法:**先定可接受的漏检率,再倒推阈值**。另外**应该按来源分别定**——手写单的置信度天然低,一刀切会让手写单全部进人工。

相关笔记