← 返回导航站首页

AI Agent规则绑定实战指南:为什么「自报成功」会掩盖零产出

你的自动化任务已经连续 16 天什么都没做出来,但它的状态一直是绿的。

这不是玄学,是验收位置问题:规则写在提示词里(读侧),产物长在外部世界(写侧),两个计数器各自涨各自的,中间没有对账。本文讲怎么把这个缺口补上:读侧计数 vs 写侧计数、三档证据、四象限判定,并附一个可跑的工具 tools/bind/bind_audit.py。

文中所有输出块都是本机实跑结果(2026-10-02),包括对真实定时任务清单的脱敏扫描、真实 URL 核验和 19 项自检。


一、先说结论

  1. 「任务成功」有三个完全不同的含义,混在一起用就是事故。 自报成功(last_status=ok / exit 0 / 文本说"已完成")、有进程跑过(脚本真的执行了)、外部留下了可核验的产物(文件 / URL / 账本条目)——只有第三档能支撑"做成了"。前两档在失败时不会报警,因为信号源和执行者是同一方。
  2. 规则出现在上下文里,不等于规则被执行。 读侧计数器(规则被提到几次)和写侧计数器(外部产物几件)如果没有绑定,会出现 12:0 这样的分布:规则读了 12 遍,产物 0 件,而每一次都自报成功。
  3. 验收必须挪到执行者写权限之外。 不要问"任务说它成功了吗",要问"外部世界能不能查到它做过的痕迹"。判别式只有一条:失效时谁先发现。执行者先发现 = 没绑定。

二、两种计数器:在场的量和绑定的量

一个 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": "..."}:

退出码就是门禁结论: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)

三个可以直接读出来的信号:

  1. job-publish-verify 的绑定率是 0.25,静默 12.2 天。 4 次运行里 3 次是绿灯无产物——这正是最危险的形态:它曾经成功过一次(所以你会相信它还能成功),之后连续 3 次什么都没产出而状态全绿。
  2. job-url-declared 的"证据"全是声明(V/D/B = 0/1/0)。 账本里有 URL,但离线审计不联网,所以它按未绑定处理。这不是保守,是准确:声明不是产物。
  3. 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

判定: 仅自报的任务在失败时不会报警——它们的信号源与执行者是同一方。

几个读法:

这份扫描不解释"任务该不该跑",只回答一个问题:如果它今天失败了,谁会发现? 答案是 18 个任务里没人会发现。


八、实跑三:URL 证据核验(--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 字节"这种空壳。


九、自检输出(19 项断言)

工具自带 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 而不是成功。审计工具的默认值必须偏保守——"没数据"永远不能被读成"没问题"。


十、五个反模式

  1. 用状态字段当验收。 last_status=ok 是被告的证词,不是证据。至少要在旁边放一列"外部产物时间戳"。
  2. 绿灯没有过期时间。 停摆两个月的任务仍然显示 ok。凡是状态字段,都要配一个"距上次产物多久"的独立读数,并对它设阈值。
  3. 把 URL 当产物却从不请求。 声明 ≠ 核验。离线审计就该记 DECLARED 并拒绝计入绑定率,而不是含糊地算过。
  4. 只盯着自报失败去告警。 真正的损失都在自报成功的那一侧。告警要盯的是"窗口内该有的产物不存在"(缺勤检测),不是"任务报错了"。
  5. 让审计工具自己成为假绿灯源。 用默认 UA 请求、把 0 字节文件算成产物、把空账本算成通过——这三条都能让审计变成新的心理安慰。

十一、落地清单(12 项)


十二、边界声明(这个工具不做什么)


十三、怎么跑起来

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