触发规则排练
这是什么
一个靠事件自唤醒的Agent,其安全性完全取决于它的触发规则。Agent本身可以很谨慎,但仍然可能烧光一天的预算,因为问题通常出在更高一层:
- 规则对其自身工作产生的事件作出反应,从而反复触发;
- 两条规则通过第二个系统相互触发(Agent发表评论,评论同步,同步触发Webhook);
- 嘈杂的源在两分钟内发送十次编辑,每次编辑都启动一个新运行;
- 原本意为“偶尔检查”的cron行每天触发288次;
- 两条规则都处理同一个事件并重复工作;
- 执行退款、删除或消息通知的规则未经询问就运行。
当你在一个示例上测试Agent时,这些问题都不会显现。只有当真实流量到来时才会暴露。Trigger Rehearsal 让真实流量提前到来——在纸上:你将规则以统一的格式写下来,回放一天或一周的事件,在规则真正部署前就能看到时间线。
何时使用
- 在启用任何允许Agent无需人工输入提示即可启动工作的规则之前。
- 在向现有规则集添加新规则时(新规则可能关闭之前开放的循环)。
- 当后台Agent的运行频率远高于或远低于预期时。
- 在选择去抖窗口、并行限制和每日上限时,希望使用数据而非猜测。
- 当审核者问“如果这个仓库一小时内收到50个issue会发生什么?”时。
不适用于:检查Agent在运行内部做了什么(那是提示词或技能问题),或对事件源本身进行负载测试。
规则表
所有规则都会翻译成 rules.json。保持翻译的真实性:一个真实规则对应一个表规则,并且只写下实际存在的防护措施,而不是你打算添加的。
{
"tz": "Asia/Shanghai",
"run_s": 180,
"external_sources": ["github"],
"budget": {"runs_per_day": 40, "cost_per_run": 0.15, "cost_per_day": 6},
"rules": [
{
"name": "triage-new-issue",
"on": {"source": "github", "types": ["issues.opened", "issues.edited"]},
"where": {"repo": "me/app"},
"key": "subject",
"debounce_s": 300,
"parallel": 1,
"max_per_day": 20,
"unless_actor": ["my-bot"],
"unless_label": "triaged",
"emits": [{"source": "github", "types": ["issues.labeled"], "actor": "my-bot", "labels": ["triaged"]}],
"sensitive": [],
"asks": false
},
{"name": "morning-digest", "on": {"cron": "0 9 * * 1-5"}, "max_per_day": 1}
]
}
| 字段 | 含义 |
|---|---|
on.source + on.types | 哪些事件会启动规则。省略 types 则匹配该源的所有类型。 |
on.cron | 一个五字段的cron行,使用规则表的 tz 时区。一条规则只能有 source 或 cron。 |
where | 事件字段必须等于某个值(或属于某个列表)。 |
key | 事件字段,用于标识主题(issue编号、工单ID、URL)。用于合并重复项。 |
debounce_s | 对于同一个主题,在最后一次事件后等待这段时间,然后执行一次。 |
parallel | 此规则最多可以同时运行多少个实例。额外的运行将等待。 |
max_per_day | 每个本地日的硬上限。额外的运行将被丢弃。 |
run_s | 典型运行时长,用于排队。回退到规则表值,然后是120。 |
unless_actor, unless_label | 防护措施:跳过由这些执行者创建或携带此标签的事件。 |
emits | 运行本身产生的事件(评论、标签、状态变更、后续事件)。包括执行者和它们将携带的标签。 |
sensitive + asks | 需要人工操作的动作(删除、消费、重启、发送)。asks: true 表示运行会停止并先询问。 |
external_sources | 由外部世界提供的事件源。其他所有事件必须由规则发出。 |
budget | 可选的每日运行次数、每次运行成本和每日成本,用于最终裁决。 |
分步操作
- 收集真实规则。 阅读实际配置,而非其描述。Agent集线器规则文件、保存的日常程序、CI工作流
on:块、crontab行、无代码自动化屏幕。注意任何会写回到另一个规则监视的源的操作。 - 翻译为
rules.json。 从python3 scripts/rehearse.py new rehearsal/开始。对每一条规则,问三个问题并填写字段:什么唤醒它,一次运行会写回什么,以及什么阻止它在自己的写入上触发。如果第三个问题的答案是“没有”,就保留防护为空。排练就是为了捕捉这种情况。 - 检查。
python3 scripts/rehearse.py lint rehearsal/rules.json。在回放之前修复所有 HOLD 项;REVIEW 项可以等待数据。 - 收集事件。 最佳方式:从源中导出真实忙碌的一天(Webhook 投递日志、issue 时间线、工单审计日志)到
events.jsonl,每行一个 JSON 对象,至少包含t、source、type,以及你的key和where使用的字段。如果没有历史记录,可以刻意编写一个短的合成突发:两分钟内的十次编辑、同时两个主题、由你自己的机器人创建的事件。 - 回放整个几天。
python3 scripts/rehearse.py replay rehearsal/rules.json rehearsal/events.jsonl --from 2026-11-02T00:00:00Z --to 2026-11-03T00:00:00Z --md rehearsal/report.md。传递--from/--to以确保每日数据不会因窗口过短而被夸大。 - 阅读时间线,而不仅仅是裁决。 查找同一主题上相隔几分钟的运行(缺少去抖或去抖时间过短)、深度不断增加的链(循环)、长时间等待(并行度太低)、丢弃(上限太低无法完成实际工作,或表明规则因噪声而触发)。
- 调整并重复。 每次只改变一个参数,回放相同的事件,这样差异就能清楚地归因于那个变更。
- 决定。 只有当最后一次回放对现实一天给出 ARM 结果时,才部署规则。保留最终的
rules.json、事件文件和report.md,作为规则启用原因的记录。
裁决结果
- ARM: 没有 HOLD 或 REVIEW 发现。按原样启用是安全的。
- TRIM: 某些东西浪费了运行次数或可以跳过工作(缺少 key、没有上限、重叠、上限丢弃了真实事件、接近预算)。只有在修剪后,或者附有书面理由,才能启用。
- HOLD: 不要启用。自循环、未防护的循环、深度超过5的链、未询问的敏感操作、超出预算或cron行错误。
退出码与裁决结果对应(0、1、2;3表示错误输入),因此排练可以在CI中作为配置变更的门禁。
示例
一个支持团队希望Agent回答工单。他们的规则表(在 examples/loopy/ 中)有一个回答规则,该规则会发布回复,回复会更新工单,而更新又会匹配同一条规则。对普通一天的回放给出:
HOLD self-loop answer-ticket: 规则自身的输出匹配自身触发器,且没有 unless_actor/unless_label 防护
HOLD runaway answer-ticket: 链持续触发超过深度5(3次中断)
HOLD sensitive-unasked refund-check: 声明了敏感动作['refund']但 'asks' 不是 true
HOLD over-runs-budget -: 100.0/天 超过 30/天 的预算
REVIEW cron-tight inbox-poll: 最多720次触发/天
VERDICT HOLD
三次工单更新变成了18次回答运行。修复很简单:在回答规则中添加 "unless_actor": ["support-bot"],在退款规则中设置 "asks": true,并将两分钟一次轮询替换为只有当收件箱真正发生变化时才发出 inbox.changed 事件的脚本(参见 examples/loopy/rules.fixed.json)。接下来对同一天的回放从100次运行下降到4次,裁决结果变为 TRIM。剩下的一个发现是,回答规则和退款检查规则都看到了一个退款工单,这里是有意为之的,因此在报告旁边用一行书面理由接受。
提示
- 最好的单循环防护是运行后变为假的条件:运行添加的标签且规则排除的标签,或规则不再匹配的状态。
- 跨系统循环是最容易遗漏的。如果Agent写入系统A,而系统A同步到B,则将同步后的事件列在
emits下,并附上它显示的执行者。 - 去抖会故意增加延迟。根据一个人完成一系列编辑所需的时间来选择窗口,在某人等待回复的地方保持窗口较短。
- 去抖不是动作的去重。对于任何不可逆的操作,运行本身应该仍然检查“是否已完成?”然后再执行。
- 每日上限是保险丝,而不是计划。如果正常一天的回放碰到了上限,说明规则太宽泛。
- 每隔几分钟轮询一次通常用一个小脚本来比较状态并在有变化时发出一个事件会更便宜;Agent只在有新内容时才被唤醒。
- 每当添加规则时,重新运行排练。大多数循环是由第二条规则创建的,而不是第一条。