Sentinel 自主编码 Agent 因边界条件产生"幽灵触发":无触发时机却消耗 token、外部 API 500 错误仍扣费。
构建一个自主智能体,有点像养一个非常听话、非常较真的孩子,还给他一张信用卡。你写规则,孩子照做。完美。问题是孩子执行的是你写的规则,而不是你想让他执行的规则。
我们运行着一个名叫 Sentinel 的自主编码智能体。它按计划醒来,在自己的代码库里挑选一个文件,判断该文件是否值得改进——如果本地分析不够自信——就花一个"代币"去咨询 LLM。代币有预算:每天几个,hard cap,每次开销都有审计。整个系统围绕一个原则构建:智能体只有在有理由时才行动。时间本身不是理由。无聊不是理由。
好几周这都运行得很漂亮。然后我们的运维review在日志里标记了两行按理说不应该存在的记录:
10月7日 9:55,Sentinel 花掉了 2 个代币——没有任何触发条件,比预定 autonomy 窗口早了 6 个半小时。
10月8日 15:01,OpenAI API 抛出了 500。代币还是被花掉了。结果:deferred(延期)。钱没了,什么都没决定。
每天 15:01 整,Sentinel 都会醒来问同一个关于 decrypto.js 的问题,得到相同的答案(SKIP),然后回去睡觉。每天 $0.08,永远持续,而这个问题答案永远不可能改变。
没有任何人入侵。没有任何规则被违反。每一行代码都做了它被编写去做的事情。但系统找到了一扇后门——因为我们自己写了一个却没注意到。
如果你在构建任何带有智能体的系统——智能体能做决策、重试或消费代币——无论是调度器、预算还是"稍后重试"机制,这个 bug 类型都是你必须面对的。它不是某个检查出了问题,而是两个好的检查互不知情。
Sentinel 的 autonomy 路径有两条门通往同一个房间:
┌─────────────────────────┐
scheduled wake → │ WakeGate.evaluate() │ "is there a reason to run at all?"
└───────────┬─────────────┘
│ autonomy_due (last resort, never ahead of
│ repo_changed / evidence / scheduled reviews)
▼
┌─────────────────────────┐
│ AutonomyBudget.admit() │ door #1 — the strict one
│ · needs ≥1 provable │ (libs/core/autonomy-budget.js:137)
│ trigger │
│ · daily budget window │
│ · per-file cooldown │
└───────────┬─────────────┘
▼
┌─────────────────────────┐
│ grant() → 1 token │ parked as `pending`, picked up
│ for the chosen target │ by claim() during evaluation
└───────────┬─────────────┘
▼
pipeline runs
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
inside the pipeline, when a file is about to SKIP locally:
┌─────────────────────────┐
│ AutonomyBudget.consider()│ door #2 — added in MR !107
│ "token re-opens a local │ (libs/core/autonomy-budget.js:234)
│ SKIP" │
│ · daily limit ✓ │
│ · module-locked ✓ │
│ · cycleOwned ✓ │
│ · provable trigger? ✗ │ ← never checked
│ · spacing window? ✗ │ ← never checked
└───────────┬─────────────┘
▼
spends a token
admit() 是那个偏执的门卫:它要求至少一个确定的、已记录的触发条件(运行时错误、未解决的 rollback、governance drift、open opportunity——"时间本身不是理由" 这句话就写在 autonomy-trigger.js 的 docstring 里),强制每日窗口限制,并精确选择一个目标。consider() 是后来加进来的,针对一个合法场景——"这个文件本来要 SKIP,但现在有个理由重新考虑"——它检查预算、锁和 cycle ownership。
只是它从来不检查自己为什么被调用。没有触发条件要求,没有 spacing window。consider() 里的每一个独立检查都是正确的。只是这扇门通往的是和那个严格门卫同一个房间——而没人告诉它门卫存在。
Anomaly 2 — 后门(10月7日 9:55): 合并 MR !116 后推送到 master,改变了 headSha → WakeGate 返回 repo_changed → pipeline 运行。在 pipeline 内部,一个文件触发了本地 SKIP——于是 MR !107 中的 consider() 路径花了 2 个代币来重新打开它。无需触发条件,不检查 window,而且因为代币是在 9:55 花掉的,每日计划发生了偏移。MR !113 监控独立 autonomy 唤醒;这个路径不属于其中。Sequence 验证:correct。允许这个行为发生的决策分支:就是 consider() 本身——它从结构上就无法因为"缺少理由"而拒绝,因为它根本不问理由。
Anomaly 1 — 永恒的理由(每天 15:01 / 15:17): 两个永远不会过期的触发条件:
decrypto.js:"2 siblings have rollback penalty"——那些 siblings 是带有夏季 rollback 记录的锁定模块。锁定模块被排除在所有检查之外(wake-gate.js:85 甚至跳过它们陈旧的 schedule)——只是它们的 penalty 记录仍然在喂给触发条件,每天唤醒一个不同的文件,永远如此。
pressure-engine.js:"10 security failures in the system"——一个累计计数器。计数器只会往上走。这个触发条件在宇宙热寂时也还是"新鲜的"。
结果:同一个问题,同一个 SKIP 答案,每天如此——2 个代币 + 每天约 $0.08,换来的结论是零信息增益。没有 stale 特性的信号不是信号,是你自己的 alarm 的 recurring subscription。
Bonus anomaly — 10月8日 15:01: OpenAI 返回 500。代币花了,结果 deferred(延期),没有重试。代币故意设计为不可退还( documented decision)——但"花在传输错误上"是我们从未打算资助的花费类别。
0 个 error,MR !112 hold 住了(2× EVOLVE proposals 正确结束 SKIP NOT IMPORTANT,零 commit churn),代币账本一致。Bug 不是混沌,而是规则之间的缝隙。
不管你的智能体管它叫什么——tool call、retry、escalation、budget spend——真正重要的检查是:
// For EVERY function that can authorize the expensive action:
// does it independently verify the precondition, or does it
// assume the caller checked?
admit() → checks: trigger? window? cooldown? budget? lock? ✓ all
consider() → checks: budget? lock? cycleOwned? ✓ — but trigger? window? ✗
我们现在为任何新的授权路径编写的具体测试用例:
Same precondition, different path: 用触发条件缺失的状态通过每个入口点调用这个 action。如果任何路径成功了,你就有了我们的 bug。
Trigger staleness: 对每种触发条件类型,问自己:"这个能永远为真吗?" 累计计数器或锁定模块的历史是"能"——而"能"意味着永恒的 wake。
Spend vs. outcome: 在代币被花掉后模拟一个 provider error。传输失败在你的账本里是否与"经过考虑的拒绝"无法区分?
根据后续计划:consider() 获得与 admit() 相同的 provable-trigger 要求,外加一个 trigger fingerprint——在 grant 时序列化 AutonomyTrigger.summarize();一个与之前产生 SKIP 时完全相同的 fingerprint 不算作理由。Rollback/security 触发条件获得一个 staleness horizon。测试固定:identical fingerprint + prior SKIP → refused;changed fingerprint → admitted;zero triggers → no-trigger。
为什么说"拟议"而不是"已修复":因为这篇文章的存在正是为了不声称我们尚未测量过的东西。修复是一个 diff 加一个测试文件。在 CI 变绿之前,它是一个姿态良好的假设。
你的智能体不需要多聪明才能发现你规则里的漏洞。它只需要保持一致——一致性是将缺失的检查变成可靠 exploit 的关键。"智能体做了意料之外的事"的危险版本不是随机性;而是确定性流经一扇你忘了自己留下的门。
你可以清点你所有的 guardrail。然后检查每一条通往这个 action 的路径是否真的经过了它们。
证据:libs/core/autonomy-budget.js(admit() vs consider())、libs/core/wake-gate.js、libs/core/autonomy-trigger.js · incident data from production autonomy logs, Oct 6–8 · fix pending CI.
我们的 bug 是一个更广泛挑战的其中一个例子:自主智能体可以完全按照设计运行,却仍然产生没人预期的结果。我们不是第一批遇到这些问题的人,也肯定不会最后一批。
以下是与 AI 智能体更广阔世界中三个相关的发现,每个都暴露了开发者意图与自主系统实际行为之间的不同裂缝。
1. When Agents Do Not Stop: Uncovering Infinite Agentic Loops in LLM Agents
当反馈路径缺乏有效停止条件时,智能体可以反复调用模型、调用工具或将工作交给其他智能体。一个在孤立情况下看起来无害的循环可以悄然变成失控的成本和重复的副作用。研究人员检查了 6,549 个代码库,确认了 47 个项目中的 68 个循环失败案例。
2. Token Budgets: An Empirical Catalog of 63 LLM-Agent Budget-Overrun Incidents
如果重试、并发任务或委托的智能体能够绕过预算限制或多次花费同一笔预算,那么预算限额是不够的。这项研究编目了 21 个编排框架中报告的预算超支事件,并研究了更强的执行机制如何防止超支。
3. The Authority Benchmark: Do AI Agents Follow the Rules They Are Given?
智能体可能有权访问某个工具,但并不意味着有权将其用于任何目的。这个基准测试研究智能体在压力下是否遵守委托的授权,并测试了一种外部策略检查,可以在执行前拒绝未授权的行动。其结果适用于所测试的场景,而非每个智能体或部署。