本章七个节点,但交接的东西和前几章完全不同:前几章交接的是一张【任务表】(每一篇都要人过一遍),本章交接的是一张【异常表】(只有出了问题的才要人看)。这个差别来自 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 的"合规账":账要有人签字。
二、七个节点各做什么
① 收单
从收件目录(或邮箱、共享盘)把所有待处理的单据文件捞出来,做三件事:
- 登记:记下文件名、大小、哈希、到达时间;
- 去重:同一张单可能被送来两次(供应商也是人)——按哈希判;
- 可追溯性检查:文件名能不能看出这是哪张单(6.4.1 的 Demo 就是这段)。
为什么第一步就要做去重? 因为重复送会生成两张采购订单——这不是"多花点时间"的问题,是账上凭空多了一笔。
② 识别(Agent 侧)
这是本章唯一需要 AI 的地方,做一件事:把图片/PDF 变成结构化字段。
输出必须包含三样: - 字段值(供应商、单号、明细行、数量、单价、税率……); - 置信度(每个字段一个,或整体一个); - "没看见"的标记(宁可说不知道,不许猜)。
第 ③ 样最容易被忽略,但它是整条链的地基——6.3.1 会专门讲。
③ 校验(规则侧)——本章新增的一格
这一格是本章与前几章最大的技术差异。 见下面第三节。
④ 人工复核
只处理被挑出来的少数几条。 人看到的东西是装配好的上下文(原图 + 识别结果 + 校验发现 + 主数据对照),不是一堆原始数据——这一块由 6.3.2 的智能体来装配。
⑤ 填报(RPA 侧)
在 ERP 里创建采购订单,填完停在保存前。6.4.3 会讲三个难点:ERP 的三种形态、元素定位、字段之间的依赖顺序。
⑥ 保存前确认(人)
点那一下"保存"。 这一步机器替不了,理由见"两个位置"那一段。
⑦ 回挂归档 + 留痕对账
- 回挂:把单据原图挂到这张订单上(6.1.2 合规账第 2 条);
- 留痕:记下谁、什么时候、哪张单、填了什么、订单号是多少;
- 对账:
收单数 = 放行数 + 异常数 + 阻断数——三个数加起来对不上,就说明有单丢了。
三、本章的新东西:为什么"校验"要独立成一格
前面几章其实也有校验(2.3.1 的违禁词检查、4.3.2 的字段校验)。但本章的校验在性质上不同:
| 前几章的校验 | 本章的校验 | |
|---|---|---|
| 查什么 | 格式:长度、有没有、合不合法 | 业务:系统里存不存在、算术平不平 |
| 谁能发现 | 人也能看出来 | 只有系统知道 |
| 例子 | 标题超 30 字、标签超 5 个 | "M2999 这个物料编码,主数据里根本没有" |
| 判据 | 看一眼就知道 | 要查表、要算 |
所以它必须独立成一格,而且必须是【规则】而不是【智能体】:
- 独立成一格——因为它不能用"识别准不准"来衡量。识别 100% 正确,也可能过不了业务校验(因为单据上写的东西系统里本来就没有);
- 用规则而不是智能体——因为它全是确定性判断:查表(编码在不在)、比大小(合计等不等)、查集合(税率合不合法)。确定性的事交给规则,便宜、可靠、可复现。
"识别对了"和"数据是对的"是两件事。 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)
两个设计说明:
待复核是一个"回路",不是一个"状态"——人改完字段之后,要重新走一遍校验,而不是直接放行。理由:人改的时候也可能改错(比如把数量改成了另一个错数),重新校验是唯一的保障。已阻断是终点,不是异常——阻断的单根本不进 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 看着不错,却因主数据未命中被阻断。
说明【字认得对】与【编码真的存在】也是两件事 —— 识别准确率再高也管不了这一层。
读结果,五个值得注意的地方:
-
分流不是"排出优先级",是"分成三种处置方式"。放行是免检、进异常是人看、阻断是退回——三条路的后半段完全不同。 所以顺序很重要:脚本里先看硬阻断(不可靠的图、查不到的编码),再看业务校验,最后看放行线。这个顺序不能颠倒——如果先看放行线,Z05(整体 0.65)可能因为某次阈值调松而"放行"了一张根本不可靠的手写单。
-
Z02 与 Z05 的区别,就是"进异常"与"阻断"的分界。 Z02 整体 0.88(够),只是某一个字段 0.62(不够)→ 人看一眼那个字段就能定,所以进异常; Z05 整体 0.65(根本不够)→ 不是某个字段的问题,是这张图本身不可靠,所以阻断。 一句话判据:"某处不确定"进异常,"整体不确定"阻断。 前者人能救,后者只能退回重来。
-
Z03 是被"业务校验"拦住的最典型一条——而它的置信度高达 0.93。 供应商、物料、数量、单价,模型全都认得很准;但那个编码在系统里不存在。 这不是识别问题,是主数据问题。 可能是供应商写错了编码,也可能是这个物料还没建档。两种情况都要人查,机器判不了。 这就是本节第三点说的"识别对了 ≠ 数据是对的"。
-
Z04 与 Z06 那两条(置信度 0.99 / 0.97,却有算术不平、税率越界)应该让人背后一凉。 因为它们看起来最"稳"——如果校验只依赖置信度,这两条会以最高的置信度通过。 这就是为什么校验必须是独立的规则,而且必须跑在识别之后、不看识别的脸色。
-
第二段那句"人一天也就看几十条,等于白搭"不是夸张。 6.1.2 的时间账里,异常处理按 5 分钟/条计——12 条就是 1 小时。如果不设阈值全部人工,一批 500 张单就是 40 多个小时,自动化毫无意义。 阈值的真正价值在这里:它不是"判得更准",而是"把人的注意力集中到少数几条上"。
30 秒实验
把 Demo 里 T_FIELD 从 0.80 调到 0.95,再跑一次。
会发生什么:
- Z09(最低 0.94)和 Z11(最低 0.93)从"直接放行"掉进"进异常";
- 放行从 7 条降到 5 条,人工量从 5 条升到 7 条;
- 值得注意的是:Z05 和 Z03 的位置完全没动——因为拦住它们的是阻断线,不是字段线。
结论:调阈值不是"调准确率",是调【漏检】和【人工量】之间的平衡。
- 调高 → 更安全,但人要看的更多(自动化收益变小);
- 调低 → 更省人,但风险件可能被放过去(而 6.1.1 说过:数据错了是延迟暴露的)。
顺着这个思路想一层:那这个阈值该怎么定?没有标准答案,但有一个原则——先定【可接受的漏检率】,再倒推阈值。比如"宁可多让人看 10%,也不能放过一条有问题的单据",那就调高;如果"单据量太大、人工扛不住",那就调低,但必须同时加强抽检(不放行的不代表不查,只是抽查比例低一些)。
再想一层:这个实验还说明一件事——阈值应该按【来源】分别定。手写单和干净 PDF 用同一个阈值是不合理的:手写单的置信度天然低,一刀切会让手写单全部进人工(那还不如不自动化)。
常见误区
-
【照搬前几章的任务表就行】——错,本章的表形态变了。 前几章是任务表(每一篇都要人过),本章是异常表(只有出了问题的才要人看)。理由在 6.1.1:本章的批量是【行数 × 单数】,一批 500 张单里正常的那 425 张如果也要人一遍,自动化就白做了。
-
【校验就是检查格式对不对】——错,本章第一次有"业务校验"。 前面几章查的是长度、有没有、合不合法(人也能看出来);本章查的是"系统里存不存在""算术平不平""税率合不合法"(只有系统知道)。这也决定了它必须用规则实现,不能用智能体——全是确定性判断,交给规则更便宜、更可靠。
-
【置信度是模型自己吹的,没什么用】——错,它是决定"谁来看"的主要依据。 没有它,就只剩两个极端:要么全自动(坏数据也过)、要么全人工(等于白搭)。而且注意 Demo 用的是
min_conf(最低字段置信度)而不是平均值——因为要定位"是哪个字段不确定",平均值会把这个信号淹没。 -
【阈值定死一个就行】——错,阈值是调参,而且应该按来源分别定。 30 秒实验演示了:调高(0.80→0.95)会让人工量从 5 条升到 7 条,但阻断面完全不动。而且手写单的置信度天然偏低——用同一个阈值,手写单会全部进人工,自动化就失去意义。
-
【异常表里不用记是谁处理的】——错,这是本章区别于内容侧的关键设计。 内容发错了删掉重发就行;单据改错了要有人解释为什么改。所以异常表的每一行都要有
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 说过,**数据错了是延迟暴露的**,代价在几天后才显现。 定法:**先定可接受的漏检率,再倒推阈值**。另外**应该按来源分别定**——手写单的置信度天然低,一刀切会让手写单全部进人工。- ☐ 下一节(6.3.1 采购订单智能识别工作流)
相关笔记
- 采购订单智能填报概述 —— "错误延迟暴露"是本节所有设计的出发点
- 知乎图文自动化创作和发布助手整体流程设计 —— 上一章的流程设计,本节多了"业务校验"这一格
- 今日头条批量发布助手工作流程设计 —— 上一个讲"批量"的流程设计,本节批量的含义不同
- 🤖 Agent-RPA实战 · 索引 —— 全课路线图