删症状≠修根因:collect_cron_results 连续三天假阴性的教训

本文最后更新于 2026年7月24日 凌晨

每天凌晨三点,我的 AI Agent 系统有四个 cron 任务同时启动:日记、AutoQuant、回测调度员、策略深挖。另外还有一个叫 collect_cron_results.py 的脚本,负责汇总所有 cron 的执行状态,每天报告谁成功谁失败。连续三天,它把刚启动的任务全标成了 ❌ 失败。

第一天的”修复”是删掉这些报错的 session 记录,再删掉对应的 cron 任务。第二天,同样的问题又出现了。第三天,我终于改了脚本本身。这篇记录的是这三天的弯路,以及一个教训:删症状不等于修根因。

问题是什么

collect_cron_results.py 是日记系统的数据采集脚本,从 state.db 提取当天所有 source='cron' 的 session 记录,判断每个任务是成功还是失败。

判断逻辑很简单——查最后一条消息有没有 “error”、”失败”、”exception”、”❌” 这些关键词:

1
2
3
4
row["has_error"] = any(
kw in (last_text or "").lower()
for kw in ["error", "失败", "exception", "traceback", "❌"]
)

看起来没毛病。但每天 03:00 有四个 cron 同时触发,collect_cron_results 在 03:00:23 跑起来时,其他三个 session 刚启动 23 秒。它们还没跑完,最后一条消息还是 cron 的 prompt 文本。而 prompt 里恰好包含”❌”字符。

结果:三个正在运行的任务全被标成 ❌。

第一天的”修复”:删掉报错记录

06-23 的日记记下了当时的处理方式:

08:00 cron 清理空 session + 删除对应的 cron 任务。

这是最直觉的反应——报告里有错误,那就把错误的东西删掉。但删完之后,collect_cron_results 第二天又报了同样的错。

因为问题不在 session 记录,也不在 cron 任务本身。它们是正常的,只是检测逻辑判断错了。

删症状的操作做了什么: 把”被误报的 session”和”对应的 cron 任务”都删了。相当于体温计显示发烧,就把体温计摔了。

第二天:同样的报错又来了

06-24 凌晨 03:00,四个 cron 照常同时启动。collect_cron_results 照常在 03:00:23 执行,照常把三个刚启动的 session 标成 ❌。

日记里记了一句很直白的话:

这个修复是错的——删了症状没修根因,今天同样 4 个 session 再次出现。

这时候问题链路已经清楚了:

1
2
3
4
5
6
7
8
9
4 个 cron 同秒触发

collect_cron_results 03:00:23 启动

其他 3 个 session 刚跑 23 秒,end_reason=NULL

最后一条消息是 cron prompt(含 "❌" 字样)

has_error() 检测到 "❌" → 标记为失败

根因不在数据,在检测逻辑。has_error() 没有区分 “任务还在跑” 和 “任务真失败了”。

修复前后判断逻辑对比:二值判断把运行中误判为失败,三值判断正确区分运行中状态

原来的逻辑把所有 session 都塞进 “成功” 或 “失败” 两个框。但运行中的任务既不该算成功也不该算失败,它需要第三个状态。修复后,ended_at IS NULL 的分支直接标为运行中,不再走到关键词检测那一步。

真正的修复:三行代码

改法很直接。在检查 error 关键词之前,先看 session 是否已经结束:

1
2
3
4
5
6
7
if row["ended_at"] is None:
row["has_error"] = None # None = still running, distinct from True/False
else:
row["has_error"] = any(
kw in (last_text or "").lower()
for kw in ["error", "失败", "exception", "traceback", "❌"]
)

ended_at IS NULL 意味着 session 还在运行。这时候不检查 error 关键词,直接标为 None(显示 ⏳ 运行中),和 True(❌ 失败)、False(✅ 成功)三种状态分开。

输出效果:

1
2
3
4
- ⏳ 03:00 AutoQuant 每日自动优化 | 5条消息, 3次工具
- ⏳ 03:00 回测调度员 | 2条消息, 1次工具

汇总:✅1 成功 / ❌0 异常 / ⏳2 运行中

不再有假阴性。运行中的任务就是运行中,不是失败。

还有一个坑:假阳性

同一个脚本还有相反方向的问题。AutoQuant 这类长 session(100+ 消息、80+ 工具调用),跑完之后最后一条消息里可能提到 “❌→✅ 修复过程”。关键词检测看到 “❌”,又标成失败了。

这次的修法是加一个强成功信号覆盖:

1
2
3
4
5
6
7
if (
row["has_error"]
and row["end_reason"] == "cron_complete"
and (row["tool_call_count"] or 0) > 5
and (row["message_count"] or 0) > 10
):
row["has_error"] = False

cron_complete 是 Hermes 的正常退出标记。如果一个 session 正常退出、做了 5 次以上工具调用、产生了 10 条以上消息,那它大概率不是失败的——即使最后一条消息恰好含了个”❌”。

教训

这三天踩下来,有三条:

第一,删症状不等于修根因。 报告里有错误,第一反应是删数据。但数据本身没问题是检测逻辑有问题。删数据只能让今天的报告好看,明天同样的问题还会出现。

第二,状态机不能只有两个值。 原来的逻辑只有”成功”和”失败”。但现实中还有第三种状态——“还在跑”。把运行中的状态硬塞进二分类,必然误判。加一个 None 值,三行代码就解决了。

第三,关键词匹配是脆弱的。 用”❌”这种 emoji 做错误标记,在 cron prompt 里、在修复过程汇报里都会出现。纯关键词匹配分不清上下文。最终方案是关键词 + 结构化信号(end_reason + 工具调用数 + 消息数)的组合判断。

如果你也在做类似的 cron 监控或 session 状态检测,检查一下你的判断逻辑:有没有区分”运行中”和”失败”?有没有被 emoji 级别的关键词匹配坑过?


参考文献

  1. Hermes Agent 官方文档 — cron session 生命周期与 end_reason 字段定义
  2. 非定时任务衰减律:’记得做’等于’不会做’ — 同一时期关于 cron 任务管理教训的另一篇文章

删症状≠修根因:collect_cron_results 连续三天假阴性的教训
https://normdist.com/2026/07/24/ND-20260724-001-collect-cron-false-negative/
作者
小瑞
发布于
2026年7月24日
许可协议