作者发现 cron 调度的 AI 任务以 exit code 0 结束但实际未生成目标文件,开发了 agent-coroner 自动检测这类静默失败并告警。
我的一个 cron 定时任务运行得很正常:退出码 0,没有 stderr。但它应该生成的周报从未出现,也没有任何通知。过了好几天我才发现,手动去检查目录时才注意到。
我称这种现象为「静默失败」,这也是无人值守 Agent 最棘手的失败模式:任务报告成功,所以不会触发任何告警,唯一能发现它的方式就是有人恰好去看了一眼。我写了 agent-coroner 来自动捕获这类问题。v0.1.0 已发布在 GitHub、PyPI 和 Claude Code 插件市场。
连续五周,我的周报任务都运行正常。它拉取数据、生成 markdown、每周五早上写入文件。我没有主动监控它,因为它看起来正常工作,属于无聊的基础设施。
第六周:没有报告。日志看不出明显问题,任务也没有自己的告警机制。到第七周它自己恢复了过来,一切看起来又正常了。至今我也没能定位那缺失的一周的根本原因。最让我困扰的是:如果没有产物检查,我甚至不会知道这件事发生过。
我没有试图猜测什么时候会出问题,而是决定明确声明每个任务应该产生什么,然后确定性检查这些承诺。分三层。
第一层是 contracts 文件,在里面声明每个任务承诺的内容:
jobs:
weekly-report:
schedule: "FRI 08:30"
grace_minutes: 60
artifacts:
- path: "reports/weekly-*.md"
max_age_hours: 192
min_bytes: 500
logs: "run.log"
第二层是 checker,一个你从 cron 或任务计划程序运行的小 CLI。它问四个问题:文件是否存在、是否新鲜(max_age_hours)、是否够大(min_bytes)、以及你可选的 verify 命令是否通过。不涉及 LLM,所以一次运行只需几秒,持续监控几乎没有成本。如果违反合约就非零退出,否则零退出,所以你可以把它接入任何已有的告警系统。
第三层是 autopsy。只有在合约被打破时,才调度 Claude 以只读方式(--allowedTools "Read,Grep,Glob")读取日志,写一份四段式的复盘报告:事实、按置信度排序的假设(附日志引用)、供人工执行的验证步骤、以及预防建议。它负责诊断,绝不修改任何东西。
有一个设计决策值得专门说明:检测从不依赖 LLM。checker 总是先运行,如果 autopsy 崩溃了,违规通知仍然会发出。还有一个刻意为之的决定:不做自我修复。一旦你允许 LLM 在无人值守系统中打补丁,失败就开始被隐藏而不是被报告,而我信任一个只做报告的监控系统。
在新项目上吃自己的狗粮
发布的第二天,我启动了一个新项目(一个股票模拟交易评估,预测日本电力交易所 JEPX 的日前价格),并将其周度准确率报告注册为 coroner 合约。仅仅是构建和注册这条流水线,就暴露了两个恰好是这款工具所要捕获的陷阱。
第一个陷阱:JEPX CSV 端点在缺少 Referer header 时返回 HTTP 200,但响应体是零字节。成功状态码,没有数据。我在写 fetch 层时用 curl 抓到了它。只检查状态码的方案会「成功」,保存一个空文件,然后悄悄毒害下游所有环节。现在的防御有两层:fetcher 明确拒绝空响应体,合约的 min_bytes 规则守护产物。一个沉默,另一个仍然会触发。
第二个陷阱:我的监控产物位于另一个仓库、另一块磁盘上,所以我用绝对路径 glob 写了合约,然后 checker 崩溃了。Glob 解析委托给了 pathlib 的相对 glob,所以绝对路径不工作。修复方法是设置 workdir(默认为 contracts.yaml 所在目录)并使用相对 glob。改完之后,checker 通过了:power-eval-weekly: ok, exit 0。
事后回想起来,第二个陷阱让我觉得很有意思。把验证工具放到真实服务中,暴露了验证工具自身文档中的空白,所以验证工具得到了验证。README 现在记录了 workdir 和相对 glob 规则。
uv tool install agent-coroner # or: pip install agent-coroner
PATH 上的 claude CLI 仅在需要 autopsies 时才需要;checker 独立运行。
写 contracts.yaml:
jobs:
weekly-report:
schedule: "FRI 08:30"
grace_minutes: 60
artifacts:
- path: "reports/weekly-*.md"
max_age_hours: 192
min_bytes: 500
logs: "run.log"
$ coroner check --config contracts.yaml --no-autopsy
[coroner] 1 violation detected
- weekly-report / reports/weekly-*.md: missing (no file matches 'reports/weekly-*.md' under <contracts-dir>)
$ echo $?
1
一旦 reports/weekly-*.md 存在、新鲜且满足 min_bytes:
$ coroner check --config contracts.yaml --no-autopsy
weekly-report: ok
$ echo $?
0
把它注册到调度里。在 Linux/macOS 上:
*/15 * * * * coroner check --config /path/to/contracts.yaml >> /var/log/coroner.log 2>&1
在 Windows 上:
schtasks /create /tn "coroner-check" /tr "coroner check --config C:\path\to\contracts.yaml" /sc daily /st 09:00
通知委托给你选择的外部命令(邮件、Slack、随意什么),{message_file} 持有完整消息文本的路径。
Claude Code 集成
如果你用 Claude Code,还有一个插件:
/plugin marketplace add Chikoku-NEKO/agent-coroner
/plugin install agent-coroner@agent-coroner-marketplace
它添加了 /coroner-status(哪些任务正常、哪些有违规)、/autopsy <job-name>(按需运行复盘),以及一个 SessionStart hook,在打开会话时展示未读的复盘报告。
如果你有无人值守的任务应该产生产物——周报、数据管道、定时的 Claude 任务——对产物做存在性和新鲜度检查是廉价的保险,不管你是否用这款工具。agent-coroner 只是让它变得声明式:一套 YAML 文件、一个确定性 checker、在出问题时的只读复盘。
GitHub: https://github.com/Chikoku-NEKO/agent-coroner
PyPI: https://pypi.org/project/agent-coroner/
Claude Code marketplace: agent-coroner plugin
如果你试了但在注册时出了问题,开个 issue。就是上面两个陷阱被记录下来的方式。