你的自动化任务已经连续 16 天什么都没做出来,但它的状态一直是绿的。
这不是玄学,是验收位置问题:规则写在提示词里(读侧),产物长在外部世界(写侧),两个计数器各自涨各自的,中间没有对账。本文讲怎么把这个缺口补上:读侧计数 vs 写侧计数、三档证据、四象限判定,并附一个可跑的工具
tools/bind/bind_audit.py。文中所有输出块都是本机实跑结果(2026-10-02),包括对真实定时任务清单的脱敏扫描、真实 URL 核验和 19 项自检。
last_status=ok / exit 0 / 文本说"已完成")、有进程跑过(脚本真的执行了)、外部留下了可核验的产物(文件 / URL / 账本条目)——只有第三档能支撑"做成了"。前两档在失败时不会报警,因为信号源和执行者是同一方。一个 Agent 系统里,关于同一条规则会有两个独立的量:
| 计数器 | 观测位置 | 增长条件 | 成本 | 例子 |
|---|---|---|---|---|
| 读侧 | 提示词 / 上下文 / 日志文本 | 规则被写进 prompt、被引用、被复述 | ~0 | 任务 prompt 里有「发布后必须回读列表确认」 |
| 写侧 | 执行者写权限之外的产物 | 外部世界多了一条可核验的痕迹 | 高 | 文章列表里真的多了一条记录、磁盘上真的多了一个非空文件 |
只加读侧(把规则写得更强、更长、更醒目)是最容易做的事,也是最没用的事:只要还有更便宜的完成路径,执行一定走那条。规则写得再狠,也不会让廉价错误路径物理上走不通。
把这两个数摆在一起,就有了一个可测的形态:
读侧 12 次 / 写侧 0 件 → 在场,未绑定
读侧 12 次 / 写侧 12 件 → 绑定
上面那组 12:0 就是本工具自检里的第 7 个用例(job-presence-only,12 次运行每次都读到了规则、每次都自报 ok、产物 0 件)。它对应的判定是 PRESENCE_ONLY,门禁退出码 2。
为什么"在场"会产生安全感? 因为读侧计数器是自证的:日志里写满了"已检查""已确认""规则要求…",看起来像在遵守。但这些字串是执行者自己产生的,不构成任何外部见证。
| 档位 | 证据 | 能证明什么 | 不能证明什么 | 例 |
|---|---|---|---|---|
| SELF | 自报状态 | 任务说自己跑完了 | 任何外部事实 | last_status=ok、exit 0、回复里的"已完成" |
| PROC | 过程证据 | 有真进程跑过、有 stdout | 有没有留下预期产物 | 脚本执行完毕、产生日志行 |
| ARTF | 产物证据 | 外部世界留下了可核验的痕迹 | 痕迹是否"正确"(那是另一层) | 非空文件、HTTP 200 页面、账本条目 |
为什么 exit 0 不算证据? 因为退出码是程序自己给的,而"零输入"和"输入干净"在退出码上长得一模一样。一个真实的例子:某代码审查工具被传了一个不存在的目录,它扫描了 0 个文件,输出了「健康分 100/100 — ✅ 可以合并」,然后 exit 0。门禁只要断言"做过的活"(files_scanned > 0),这类假绿灯就无处藏身。通则:零输入必须返回独立的非成功状态——「没输入」≠「输入干净」。
同样地,只声明不核验的产物等于没有产物。账本里写了一条 URL,和这个 URL 真的返回 200,是两件事。工具里把这两种情况分开记:未核验的记 DECLARED,核验通过的记 VERIFIED,只有 VERIFIED 参与绑定率。
把自报(SELF)和产物核验(ARTF)交叉,得到四种状态:
| 自报 | 产物核验 | 判定 | 含义 | 成本 |
|---|---|---|---|---|
| ok | 通过 | BOUND | 规则被绑定,可以信 | — |
| ok | 无 | UNBOUND_GREEN | 绿灯无产物:最贵的一种失败,因为没人会去看 | 极高(潜伏期长) |
| fail | 有 | MIRAGE | 状态字段失真(反方向):产物在,自报说失败 | 中(会重复劳动) |
| fail | 无 | HONEST_FAIL | 诚实的失败,报警会响,成本最低 | 低 |
UNBOUND_GREEN 为什么最贵? 因为它的信号是错的:看板全绿,没人去查产物,等到有人发现"怎么一个月没动静"的时候,已经烧掉了一个月的额度和时间窗口。真实形态:一个任务自报 ok,而它对应的产物账本最后一条停在 13 天前——这 13 天里没有任何告警。
MIRAGE 也不罕见:任务实际把东西产出成功了,但因为最后一步校验脚本读错了字段,自报失败,于是被重跑,产出第二份。这类故障的方向是可喜的(产物在),但同样说明状态字段不能信。
tools/bind/bind_audit.py单文件、零依赖(只用标准库),三个子命令:
python bind_audit.py selftest # 19 项断言,退出码即结论
python bind_audit.py audit runs.jsonl [--check-urls] # 对账一份运行账本
python bind_audit.py scan-jobs jobs.json # 扫描定时任务清单,输出证据档位分布
python bind_audit.py explain # 判定表 + 账本字段说明
账本是一行一条 run 的 JSONL,字段就六个:
| 字段 | 含义 | 少了它会怎样 |
|---|---|---|
run_id |
本次运行标识 | 对账时无法指名道姓 |
job |
任务名 | 无法按任务聚合绑定率 |
at |
运行时间(ISO8601) | 算不出"静默多少天" |
self_report |
自报结果 | 无法区分 UNBOUND_GREEN 和 HONEST_FAIL |
read_side |
本次运行中规则被读到的次数 | 无法区分"在场"和"什么都没发生" |
evidence |
声明的产物证据数组 | 没有外部参照物,审计退化回读日志 |
证据条目形如 {"kind": "file|url|ledger", "ref": "...", "match": "..."}:
file:本地路径,核验 = 存在且字节数 > 0(0 字节不算产物)ledger:本地账本 + 可选 match 子串,核验 = 文件存在且命中该子串url:默认只记 DECLARED(离线审计不联网);加 --check-urls 才真的发请求退出码就是门禁结论:0 全部绑定、2 发现绿灯无产物、3 纯在场、1 用法或输入错误(账本为空也算 1——没输入不是干净,是没输入)。
python demo.py 会生成一份覆盖六种故障形态的样例账本(23 条记录 / 6 个任务),下面是本机真实输出:
审计时间: 2026-10-02T12:00:00+08:00
记录数: 23 任务数: 6
run_id job 自报 读 证据V/D/B 判定
----------------------------------------------------------------------------------
pv-01 job-publish-verify ok 1 1/0/0 BOUND
pv-02 job-publish-verify ok 1 0/0/0 UNBOUND_GREEN
pv-03 job-publish-verify ok 1 0/0/0 UNBOUND_GREEN
pv-04 job-publish-verify ok 1 0/0/0 UNBOUND_GREEN
af-01 job-artifact-file ok 1 2/0/0 BOUND
af-02 job-artifact-file ok 1 2/0/0 BOUND
af-03 job-artifact-file ok 1 2/0/0 BOUND
ud-01 job-url-declared ok 1 0/1/0 UNBOUND_GREEN
ud-02 job-url-declared ok 1 0/1/0 UNBOUND_GREEN
mr-01 job-mirage failed 1 1/0/0 MIRAGE
po-01 … po-12 job-presence-only ok 1 0/0/0 UNBOUND_GREEN (×12 行,此处折叠)
hf-01 job-honest-fail failed 1 0/0/0 HONEST_FAIL
job runs BOUND 绿灯无产物 读侧 写侧 绑定率 静默天数 状态
----------------------------------------------------------------------------------------------------
job-honest-fail 1 0 0 1 0 0.0 - HONEST_FAIL
job-mirage 1 0 0 1 1 0.0 7.0 MIRAGE
job-presence-only 12 0 12 12 0 0.0 - PRESENCE_ONLY
job-url-declared 2 0 2 2 0 0.0 - PRESENCE_ONLY
job-publish-verify 4 1 3 4 1 0.25 12.2 PARTIAL
job-artifact-file 3 3 0 3 6 1.0 3.1 BOUND
门禁判定: UNBOUND FOUND(退出码 2)
三个可以直接读出来的信号:
job-publish-verify 的绑定率是 0.25,静默 12.2 天。 4 次运行里 3 次是绿灯无产物——这正是最危险的形态:它曾经成功过一次(所以你会相信它还能成功),之后连续 3 次什么都没产出而状态全绿。job-url-declared 的"证据"全是声明(V/D/B = 0/1/0)。 账本里有 URL,但离线审计不联网,所以它按未绑定处理。这不是保守,是准确:声明不是产物。job-presence-only 12 次运行、12 次读侧、0 件产物。 读侧计数器一直在涨,写侧计数器一直是 0,而每一次都自报成功。job-artifact-file 那行是反面对照:3 次运行、6 件核验过的证据(一个非空文件 + 一条账本命中),绑定率 1.0,静默天数 3.1。它和 job-publish-verify 的区别不在 prompt 写得好不好,只在于产物长在外部。
scan-jobs 直接读 Hermes 风格的 jobs.json(jobs[].name/state/enabled/last_status/last_run_at/no_agent/script/monitor_*),按任务声明本身推断它的成功证据属于哪一档:
| 声明特征 | 档位 | 理由 |
|---|---|---|
有 monitor_script / monitor_url |
ARTIFACT | 有人从外部看它 |
no_agent + script |
PROCESS | 真进程 + 退出码,只证明跑了 |
| 纯 prompt | SELF | 成功与否只有它自己说 |
名称不打印(只输出 job#01…job#26 和类别,坐标不可回指到具体任务)。本机真实输出(2026-10-02):
扫描时间: 2026-10-02T15:00:00+08:00
任务总数: 26
槽位 类别 启用 状态 自报 证据档 停摆天数
------------------------------------------------------------------------------
job#01 发布 否 paused ok SELF 16.3
job#02 发布 否 paused ok SELF 16.1
job#03 发布 否 paused ok SELF 14.8
job#04 发布 是 scheduled ok SELF 0.7
job#05 其他 是 scheduled ok PROCESS 1.0
job#06 心跳 是 scheduled ok PROCESS 0.0
job#07 学习 是 scheduled ok PROCESS 0.7
job#08 其他 是 scheduled ok PROCESS 0.1
job#09 其他 是 scheduled ok PROCESS 0.0
job#10 巡检 是 scheduled ok PROCESS 0.1
job#11 巡检 是 scheduled ok PROCESS 0.1
job#12 巡检 是 scheduled ok SELF 0.3
job#13 其他 是 scheduled ok PROCESS 1.0
job#14 学习 是 scheduled ok SELF 0.2
job#15 其他 是 scheduled ok SELF 0.2
job#16 其他 是 scheduled ok SELF 0.2
job#17 其他 是 scheduled ok SELF 2.0
job#18 其他 否 paused ok SELF 59.3
job#19 其他 否 paused error SELF 59.2
job#20 其他 否 paused ok SELF 59.1
job#21 其他 否 paused ok SELF 59.0
job#22 其他 否 paused ok SELF 58.9
job#23 其他 否 paused error SELF 58.8
job#24 发布 是 scheduled ok SELF 1.1
job#25 其他 是 scheduled ok SELF 0.2
job#26 发布 是 scheduled ok SELF 0.2
启用 17/26 | 产物证据 0 | 过程证据 8 | 仅自报 18
绿灯残留在未启用任务上: 7 个,最长 59.3 天
绑定率(产物证据/总任务): 0.0
判定: 仅自报的任务在失败时不会报警——它们的信号源与执行者是同一方。
几个读法:
last_status 还停在 ok。看板上它仍然"正常"。这是 UNBOUND_GREEN 的另一种变体:绿灯没有过期时间。no_agent + 脚本类任务,它们至少证明"有真进程跑过",但脚本自己的 exit 0 依然不能证明产物到位。job#19 和 job#23 的 last_status=error——这是好事:诚实的失败会报警,成本最低。这份扫描不解释"任务该不该跑",只回答一个问题:如果它今天失败了,谁会发现? 答案是 18 个任务里没人会发现。
--check-urls)URL 类产物必须真的请求一次,因为没发布的地址和已发布的地址在某些平台上长得一样。
先看两个真实地址的核验结果(一个是本站已上线文章,一个是不存在的页面):
run_id job 自报 读 证据V/D/B 判定
----------------------------------------------------------------------------------
u1 site-live-check ok 1 1/0/0 BOUND
u2 site-live-check ok 1 0/0/1 UNBOUND_GREEN
job runs BOUND 绿灯无产物 读侧 写侧 绑定率 静默天数 状态
----------------------------------------------------------------------------------------------------
site-live-check 2 1 1 2 1 0.5 0.1 PARTIAL
门禁判定: UNBOUND FOUND(退出码 2)
核验细节:存在的页面返回 HTTP 200 → VERIFIED;不存在的页面返回 HTTP 404 → BROKEN。
还有一个更阴的坑:不带 User-Agent 的请求可能对"不存在的页面"也返回 200 空壳。 我在自己的内容发布流程里实测到过这种平台:所有文章地址(包括一个从未真正发布的伪造 ID)在不带浏览器 UA 的情况下一致返回 HTTP 200 + 39 字节空壳,带真实 Chrome UA 后才区分出 301(已发布)和 404(幽灵地址)。如果核验脚本用默认 UA,它会把"假发布"审计成"已发布"——审计工具本身变成了假绿灯的生产者。
所以 bind_audit.py 的内置 URL 探针强制带浏览器 UA,并且把响应体长度一起记进结论(HTTP 200, N+ bytes),让你能看出"200 但 0 字节"这种空壳。
工具自带 selftest,用固定样例断言判定契约,退出码即结论:
bind_audit selftest
PASS 产物存在且非空 → BOUND
PASS 核验计数=1
PASS 0 字节文件 → UNBOUND_GREEN
PASS BROKEN 计数=1
PASS 产物不存在 → UNBOUND_GREEN
PASS URL 未核验 → DECLARED 不算绑定
PASS 账本命中 → BOUND
PASS 账本未命中 → UNBOUND_GREEN
PASS 自报失败但产物在 → MIRAGE
PASS 12 次读 / 0 件产物 → PRESENCE_ONLY
PASS 读侧计数=12
PASS 写侧计数=0
PASS 静默天数=None(从未有过产物)
PASS 门禁退出码=2
PASS 1 绑定 1 绿灯 → PARTIAL + 0.5
PASS 缺字段 → HONEST_FAIL
PASS no_agent+script → PROCESS
PASS monitor_url → ARTIFACT
PASS 纯 prompt → SELF
------------------------------------------------------------------------
19/19 通过
注意倒数第 5 条:缺字段(既没自报也没证据)判为 HONEST_FAIL 而不是成功。审计工具的默认值必须偏保守——"没数据"永远不能被读成"没问题"。
last_status=ok 是被告的证词,不是证据。至少要在旁边放一列"外部产物时间戳"。DECLARED 并拒绝计入绑定率,而不是含糊地算过。run_id / job / at / self_report / read_side / evidence[]BOUND 记录,确认核验规则本身没被绕过--check-urls;默认把所有 URL 记为 DECLARED 并拒绝计入绑定。PRESENCE_ONLY 和 NO_EVIDENCE 时有用,数值大小没有意义——读 100 遍不产生任何绑定。git clone https://github.com/Felixwang007/awesome-content-tools
cd awesome-content-tools/tools/bind
# 1) 先看判定契约
python bind_audit.py selftest
# 2) 生成样例账本并出审计报告(可复现)
python demo.py
# 3) 扫描自己的定时任务清单(Hermes 风格 jobs.json)
python bind_audit.py scan-jobs /path/to/jobs.json
# 4) 对账自己的运行账本;要真核验 URL 加 --check-urls
python bind_audit.py audit runs.jsonl --check-urls
接入顺序建议:先让每个任务写一条账本记录(哪怕 evidence 一开始是空的),跑一周,看你的绑定率是多少。绝大多数自动化系统的第一份报告都长这样:读侧计数很漂亮,写侧计数接近 0。
一句话总结:别问任务说它成功了吗,问外部世界能不能查到它做过的痕迹。
相关文章:工具调用磁带(record-replay)(怎么把一次真实会话录下来离线重放,让行为漂移可见)、长任务断点续跑与进度对账(清单 / 账本 / 幂等键)、AI Agent成本优化(静默成功是最贵的一类浪费)。
📚 更多创作资源: https://afdian.com/a/felix007