这一步是"送料":RPA 把单据文件送进识别工作流,换回一个任务号。技术含量不高,但有两个地方决定成败:送之前的三查(在不在、对不对、重不重),和 Token 与请求体的卫生(它一旦泄露,别人就能拿你的额度跑你的工作流)。
6.3.1 造好了工作流,本节负责"把它调用起来"——这是 RPA 侧三步里的第一步。 与前面几章的同行对照:3.4.1 / 4.4.1 / 5.4.1 都是"提交任务",但它们送的是文字(选题、关键词、任务卡);本节第一次送【文件】——而"送文件"比"送文字"多了三件事:文件要先落在能被访问的地方、要防重复、要防大文件。
一、RPA 在这条链里的位置:中间那根传送带
先把位置摆清楚:
本地收件目录 ──RPA 送──→ 识别工作流(扣子) ──RPA 取──→ 结构化结果
↑ ↓
6.2 第①格 6.2 第③格(校验)
RPA 在这里做两件不聪明的事: 1. 把文件送进去(本节); 2. 把结果取回来(下一节)。
它不需要理解单据内容——"看懂"是工作流的活,"搬运"是 RPA 的活。 这条分工从第一章 1.6.1 讲的三种交互模式里就定下来了(RPA 调 Agent),本章只是把它用在了一个新的对象上:文件。
因为扣子这类平台访问不了你公司内网的 ERP。它能在云端识别图片,但"把单子填进用友/金蝶/SAP"这件事必须发生在你办公网里的一台机器上。 这就是本课从头到尾那条分工的物理基础:Agent 在云上想,RPA 在本地做。
二、送进去的是什么:文件必须先落到一个"两边都能访问"的地方
这是本章新增的第一件事。 前面几章送的是一段文字(写进请求体就行),本章送的是一个文件——而文件必须有一个【能被工作流读到的地址】。
两条路:
| 做法 | 怎么走 | 适合 |
|---|---|---|
| 先传文件服务,再传地址 | RPA 把图传到对象存储/文件服务 → 请求体里带 URL | 推荐。地址可留痕、可复用(合规账要的"影像关联"正好能用它) |
| 直接把文件作为参数传 | 取决于工作流的开始节点怎么定义 | 小量、临时 |
无论走哪条,一句话必须记住:文件的参数形式由工作流的开始节点决定(6.3.1 讲的 File-Image 类型就是它)。
扣子的工作流 API 是动态 schema——接口路径和鉴权是固定的,但【入参和出参】由你这个工作流自己生成。 所以不同工作流的参数名可能不同,唯一可靠的依据是部署页面上的参数说明和调用示例。 本课那条老规矩在这里又用一次:先看现场,再动手。
三、怎么发起:把工作流部署成 API
3.1 第一步:部署,并拿到 Token
2026 年在扣子上,这段流程是这样的:
- 在工作流开发页把工作流【部署】(没部署的工作流调不了,未发布调用会报错 4200——这个错误码值得记,它说明"你的工作流压根没上线");
- 部署完成后,平台自动生成一组 API 接口(同步执行、流式执行、异步执行、查询异步任务状态、查询工作流参数);
- 在部署总览页创建 API Token,调用时放在请求头
Authorization里。
关于 Token,有三条必须记住的:
| 性质 | 说明 |
|---|---|
| 只显示一次 | 创建后立刻复制——页面上不会再显示第二次 |
| 永久有效 | 没有自动过期 → 泄露了必须手动删 |
| 有数量上限 | 每个项目最多 10 个 → 别一个人一个,用完就删 |
域名:国内区 api.coze.cn,国际区 api.coze.com——用哪个取决于你的账号在哪个区。
3.2 第二步:选调用方式(同步 / 流式 / 异步)
工作流的 API 一般提供三种:
| 方式 | 行为 | 适合 |
|---|---|---|
| 同步执行 | 一次请求,等它全跑完再返回 | 快任务(几秒) |
| 流式执行 | 用 SSE 持续推事件 | 要"打字机效果"的对话 |
| 异步执行 | 先返回任务号,之后靠"查状态"取结果 | 长任务(官方文档说的是 24 小时以内) |
本章选【异步】。
为什么? 因为 6.1.1 已经算过:一批单会有几十张,一张图识别几秒,一批就是几分钟。同步调用会一直占着那条连接,而且一旦中途超时,前面白跑。 异步的好处是:提交和取回被拆成两个动作——提交很快(拿到任务号就完事),取回可以慢慢来(甚至分几次),中间断了也能重查。
同步调用只有两种结果(成功、失败);异步会多出第三种:还不知道。 所以本节结束时要落盘的是一个任务号列表,下一节(6.4.2)要围绕它做轮询、超时与对账。 这就是本课"回收要分档"那条纪律(4.4.2、5.4.1 都出现过)在本章的形态: 异步让链路更稳,代价是把"状态管理"从平台手里接了过来。
四、发起前的三查
这三查加起来不到一秒钟,但它挡掉的是后面整条链路的返工。
第一查:在不在
"文件真的落地了吗"——听起来像废话,但下载中的文件也有文件名、也有大小。 判据:文件的修改时间稳定了没有、能不能完整读出来。
为什么这一查最容易被跳过? 因为它不报错——你送一个"下载了 80% 的图"进去,工作流照样返回,只是识别出一堆乱码。
第二查:对不对
- 扩展名在可识别范围内吗(jpg / png / pdf / xlsx 之类);
- 体积在上限内吗;
- 文件名能看出这是哪张单吗(既无单号也无日期 → 三年后没人找得到)。
第三查:重不重
按内容哈希去重,不是按文件名。
理由:文件名不可靠——人会加"副本""最终版""(1)",而内容完全一样。而 6.2 说过:重复送会生成两张采购订单,那是账上凭空多一笔。
Demo 会专门演示这一条。
五、发起之后不要干等
异步调用的后半段有三条纪律:
- 记下任务号,落盘——它在内存里没有意义(流程一崩就没了);
- 别死等,也别轮得太密——隔一段时间查一次,给一个上限(比如 5 分钟);
- 超时不是失败,是"待定"——它属于 6.4.2 那个"重试"档(重跑一次多半就好)。
因为它不是"错了",是"还没好"。 如果把它当失败处理(比如直接标异常交人),人会把一张本来没问题的单子手工录一遍——而那些时间是因为"服务当时有点忙"白花的。 判据仍然只有那一条:重跑一遍能不能修好。
六、动手 Demo:送单预检 + 请求体装配
# -*- coding: utf-8 -*-
"""6.4.1 Demo:送单预检 + 请求体装配(纯标准库)
在调用扣子工作流之前,先把不该送的挡在门外,并把请求体拼出来。
"""
import json
import re
# 待送文件:文件名 / 大小(MB) / 扩展名 / 内容哈希前 8 位
FILES = [
{"f": "PO-20260913-01.jpg", "mb": 1.8, "ext": "jpg", "h": "a1b2c3d4"},
{"f": "PO-20260913-02.jpg", "mb": 2.2, "ext": "jpg", "h": "e5f6a7b8"},
{"f": "PO-20260913-02副本.jpg", "mb": 2.2, "ext": "jpg", "h": "e5f6a7b8"},
{"f": "quote_20260913.xlsx", "mb": 0.3, "ext": "xlsx", "h": "11223344"},
{"f": "采购订单模板.docx", "mb": 0.1, "ext": "docx", "h": "55667788"},
{"f": "扫描件_2026091301.jpg", "mb": 12.4, "ext": "jpg", "h": "99aabbcc"},
{"f": "wx_20260913_1147.png", "mb": 0.9, "ext": "png", "h": "ddeeff00"},
{"f": "微信图片_20260913114722.png", "mb": 1.1, "ext": "png", "h": "13579bdf"},
{"f": "po_20260915.pdf", "mb": 0.6, "ext": "pdf", "h": "2468ace0"},
{"f": "IMG_0421.jpg", "mb": 1.5, "ext": "jpg", "h": "0f1e2d3c"},
]
ALLOW_EXT = {"jpg", "jpeg", "png", "pdf", "xlsx"}
MAX_MB = 10.0
def traceable(name):
"""文件名里要能看出这是哪张单:带单号 PO-xxxx,或带 8 位日期"""
return bool(re.search(r"PO-\d+", name, re.I)) or bool(re.search(r"20\d{6}", name))
def precheck():
ok, skip, block = [], [], []
seen = {}
for r in FILES:
if r["h"] in seen:
skip.append((r, "与 %s 内容哈希相同 —— 同一张单被送了两次" % seen[r["h"]]))
continue
if r["ext"] not in ALLOW_EXT:
block.append((r, "扩展名 .%s 不在可识别范围内(%s)" % (
r["ext"], "/".join(sorted(ALLOW_EXT)))))
continue
if r["mb"] > MAX_MB:
block.append((r, "体积 %.1fMB 超过上限 %.0fMB —— 先压缩或分页" % (r["mb"], MAX_MB)))
continue
if not traceable(r["f"]):
block.append((r, "文件名里看不出是哪张单(既无单号也无日期)—— 出了问题回不到原单"))
continue
seen[r["h"]] = r["f"]
ok.append(r)
return ok, skip, block
ok, skip, block = precheck()
print("=" * 78)
print("一、送单预检")
print("=" * 78)
for r in ok:
print(" [通过] %-24s %5.1fMB" % (r["f"], r["mb"]))
for r, why in skip:
print(" [跳过] %-24s %s" % (r["f"], why))
for r, why in block:
print(" [阻断] %-24s %s" % (r["f"], why))
print()
print(" 合计:送 %d / 跳过 %d / 阻断 %d(共 %d 个文件)" % (
len(ok), len(skip), len(block), len(FILES)))
print()
print("=" * 78)
print("二、请求体装配(以第一份通过的文件为例)")
print("=" * 78)
body = {
"workflow_id": "7xxxxxxxxxxxxxxx",
"parameters": {
"doc_url": "https://<文件服务>/po-inbox/%s" % ok[0]["f"],
"task_type": "purchase_order",
"need_conf": True,
"schema_version": "po_v1",
},
}
print(json.dumps(body, ensure_ascii=False, indent=2))
print()
print(" 请求头里另需带:Authorization: Bearer <API Token>")
print(" 注意:API Token 第一次生成后【只显示一次】,且【永久有效】——")
print(" 它必须放在环境变量或凭证保险箱里,绝不能写进脚本。")
print()
print("=" * 78)
print("三、三查之后再说【发起】")
print("=" * 78)
print(" · 一查【在不在】:文件真的落地了吗(不是还在下载中)。")
print(" · 二查【对不对】:格式、体积、文件名标识 —— 就是上面这段。")
print(" · 三查【重不重】:哈希去重。同一张单重复送,会生成两张采购订单。")
print()
print(" 这三查加起来不到一秒钟,但它挡掉的是【后面整条链路的返工】。")
实际运行输出:
==============================================================================
一、送单预检
==============================================================================
[通过] PO-20260913-01.jpg 1.8MB
[通过] PO-20260913-02.jpg 2.2MB
[通过] quote_20260913.xlsx 0.3MB
[通过] wx_20260913_1147.png 0.9MB
[通过] 微信图片_20260913114722.png 1.1MB
[通过] po_20260915.pdf 0.6MB
[跳过] PO-20260913-02副本.jpg 与 PO-20260913-02.jpg 内容哈希相同 —— 同一张单被送了两次
[阻断] 采购订单模板.docx 扩展名 .docx 不在可识别范围内(jpeg/jpg/pdf/png/xlsx)
[阻断] 扫描件_2026091301.jpg 体积 12.4MB 超过上限 10MB —— 先压缩或分页
[阻断] IMG_0421.jpg 文件名里看不出是哪张单(既无单号也无日期)—— 出了问题回不到原单
合计:送 6 / 跳过 1 / 阻断 3(共 10 个文件)
==============================================================================
二、请求体装配(以第一份通过的文件为例)
==============================================================================
{
"workflow_id": "7xxxxxxxxxxxxxxx",
"parameters": {
"doc_url": "https://<文件服务>/po-inbox/PO-20260913-01.jpg",
"task_type": "purchase_order",
"need_conf": true,
"schema_version": "po_v1"
}
}
请求头里另需带:Authorization: Bearer <API Token>
注意:API Token 第一次生成后【只显示一次】,且【永久有效】——
它必须放在环境变量或凭证保险箱里,绝不能写进脚本。
==============================================================================
三、三查之后再说【发起】
==============================================================================
· 一查【在不在】:文件真的落地了吗(不是还在下载中)。
· 二查【对不对】:格式、体积、文件名标识 —— 就是上面这段。
· 三查【重不重】:哈希去重。同一张单重复送,会生成两张采购订单。
这三查加起来不到一秒钟,但它挡掉的是【后面整条链路的返工】。
读结果,五个值得注意的地方:
-
那第 3 个文件(
PO-20260913-02副本.jpg)是本 Demo 最重要的一条。 它和PO-20260913-02.jpg内容一模一样(哈希都是e5f6a7b8),只是名字后面多了"副本"两个字。 如果按文件名去重,它会被当成一张新单送进去——然后在 ERP 里生成第二张一模一样的采购订单。 这不是"多花点时间",是账上凭空多一笔。 而且它很难被发现——因为两张单确实都"有据可依"(都对应一张真实的图)。 -
IMG_0421.jpg被拦的理由,当时看不出任何问题。 它体积正常、格式正常、内容也可能很正常——它只是没有一个能说明"这是哪张单"的名字。 这一查防的不是今天,是三年后。 6.1.2 的合规账说过:原始凭证要存 30 年,影像要能跟订单对上。如果文件名是IMG_0421.jpg,到时候你只能在几千张图里翻。 -
采购订单模板.docx那条值得一笑,但它是个真问题。 收件目录里总会混进别的东西(模板、说明、聊天导出的文件)。如果不拦,它们会被送进识别、返回一堆没有意义的字段、然后变成"异常"占掉人的时间。 在入口拦住它,成本是 0;让它走到异常表,成本是人的 5 分钟。 -
体积那条(12.4MB)值得单独说一句:上限不是"技术上限",是【成本与稳定性的折中】。 图越大,识别越慢、越贵、越容易超时。所以这条线应该按你的实际情况定——但它必须存在,不能"反正都能传就都传"。
-
请求体里那个
schema_version字段看着多余,其实是本章最该抄走的一个设计。 为什么要版本号? 因为字段形状会变(今天要 8 个字段,明天可能要加"原产地")。带上版本号之后,老的脚本和新工作流可以并存——否则一次字段变更会让所有历史脚本同时失效。 这是"数据侧"特有的问题:内容侧换一版提示词就行,数据侧的接口一变,下游全要跟着改。
30 秒实验
把 Demo 里 precheck() 中去重的两行改一下——把 if r["h"] in seen: 改成 if r["f"] in seen:,并把 seen[r["h"]] = r["f"] 改成 seen[r["f"]] = r["f"](也就是改成"按文件名去重"),再跑一次。
会发生什么:
PO-20260913-02副本.jpg不再被跳过——因为它的文件名和原文件不同;- 合计从"送 6 / 跳过 1"变成"送 7 / 跳过 0"。
结论:"副本"这两个字,是一笔多付的钱。
顺着这个思路想一层:为什么人会加"副本"?因为他自己也怕覆盖原文件——这是个善意动作,但它的后果在另一头(账上多一笔)。 这提醒一件事:上游的善意习惯,必须在下游用技术手段兜住——因为你不能要求所有人改掉多年的习惯。
再想一层:那"按哈希去重"有没有代价?有——它会误杀"内容相同但确实要下两次的单"(比如同一个订单分两批交货,供应商把同一张图发了两遍)。但这种情况下,正确的做法是"补一张有区分的单据",而不是"凭同一张图下两次单"——所以这个误杀的代价,是可以接受的。
常见误区
-
【API Token 写在脚本里方便】——绝对不行。 它只显示一次、永久有效、泄露后别人就能拿你的额度跑你的工作流。正确做法:放在环境变量或凭证保险箱里。这也正是 6.1.1 现状三里那条"凭证加密管理:严禁在代码中硬编码密码"——同一个道理,只是对象从账密变成了 Token。
-
【调用工作流和调用智能体是一回事】——不是,接口族不同。 工作流的 API 一般是同步执行 / 流式执行 / 异步执行 / 查询异步任务状态 / 查询工作流参数这一组;调用智能体是另一组(如对话接口)。本章要的是工作流的那一组,而且入参出参是【动态 schema】——以部署页面的参数说明为准,不要凭记忆写。
-
【未发布的工作流也能调】——不能,会报错 4200。 这个错误码值得记住:它说的不是"参数错了",是"你的工作流压根没部署成 API"。遇到它先回去看部署页,别在请求体里找问题。
-
【发送前不用检查,失败了会报错】——错,失败发生在服务端,你的损失是时间和额度。 更要紧的是:有些"失败"根本不会报错——比如送一张"下载了 80% 的图"进去,工作流会正常返回,只是识别出一堆乱码。三查的价值就是把这类"不报错的失败"挡在门外。
-
【去重按文件名就行】——错,30 秒实验已经演示过了。 人会加"副本""最终版""(1)",而内容完全一样。只有内容哈希是可靠的——因为它不依赖任何人记得起名字的习惯。
自测三问(点开对答案)
1. **为什么"送文件"比"送文字"多了三件事?** 因为文字可以直接写进请求体,而**文件必须先有一个"能被读到的地址"**(本节第二节),所以多了—— ① **文件要先落到两边都能访问的地方**(对象存储/文件服务),并**在请求体里带上它的地址**; ② **要防重复**(同一张单被送两次 → ERP 里生成两张订单); ③ **要防大文件与异常文件**(体积上限、扩展名、以及"文件其实还没下载完")。 **根子在 6.1.1 那条**:本章的产出要落进 ERP,**而"多一张单"和"少一张单"都是账上的事**,不是"重试一下"能解决的。 2. **为什么本章选"异步执行"而不是"同步执行"?代价是什么?** 因为**一批单有几十张、一张图识别要几秒**,同步调用会一直占着连接,而且**中途超时前面白跑**。 异步的好处是**把"提交"和"取回"拆成两个动作**:提交很快(拿到任务号即可),取回可以慢慢来、甚至分几次、断了还能重查。 **代价是要自己管任务状态**:同步只有"成功/失败"两种结果,**异步多出第三种——"还不知道"**。所以本节结束时落盘的是一张**任务号列表**,下一节要围绕它做轮询、超时与对账。 3. **"文件还没下载完"为什么是最危险的失败?** 因为它**不报错**。 一个下载到 80% 的图,**有文件名、有大小、甚至能被"打开"**——工作流会正常接收、正常返回,**只是识别出一堆乱码或者一个空结果**。 而**这类错误会一路往下走**:变成"识别失败"→ 进异常 → **消耗人的 5 分钟**。**而真正的原因(上游还在写文件)根本不在异常表上。** 所以三查里"一查【在不在】"要放在第一条:**判据是文件的修改时间稳定了没有、能不能完整读出来。**- ☐ 下一节(6.4.2 RPA获取扣子识别结果)
相关笔记
- 采购订单智能识别工作流 —— 本节的调用对象,特别是那个 File-Image 类型的输入变量
- RPA发起会话并获取图文 —— 上一个"发起并取回",本节多了文件与异步两件事
- RPA与Agent交互 —— 三种交互模式的来处,本章用的是"RPA 调 Agent"
- 🤖 Agent+RPA实战 · 索引 —— 全课路线图