6 个定时任务、457 次调度:我让 AI 只在\u201c有事\u201d时才上班

0 阅读1分钟

6 个定时任务、457 次调度:我让 AI 只在"有事"时才上班

16 天,6 个定时任务,一共被调度了 457 次。真正把 AI 叫起来干活的,不到一半。

最狠的一条在数据监听任务上:它每 30 分钟跑一次,但在保留窗口(最近 50 次调度、约 25 小时)里,只有 5 次真的唤醒了 AI,另外 45 次在门控层就被掐掉——连模型 API 都没碰。

这不是概念文章,是一张真实的排班表,和它自己的账单。

一、先看现象:谁在什么点上班

我本机的定时任务执行表(~/.hermes/cron/executions.db),一条命令数清所有班次的出勤:

cd ~/.hermes/cron && python3 -c "import sqlite3;print(sqlite3.connect('executions.db').execute('select job_id,count(*) from executions group by job_id order by 2 desc').fetchall())"

真实返回:

d8707cb2ce85  416   ← 数据监听   每 30 分钟
6a230e052147   19   ← 每日反思   07:00
7ce9f01d1db2    9   ← 每日互动   21:45
b8f40d54d923    6   ← 发文·晚班  20:25
b3e4d43b54c1    5   ← 发文·早班  08:40
6b1f6ccda3a1    2   ← 每周复盘   周一 09:00

合计 457 次调度,时间跨度 2026-09-06 20:12 到 09-22 20:25。注意频率差了 200 倍:监听是"每 30 分钟看一眼",复盘是"每周一写一次"。把频率当重要性,是排班表的第一个坑。

二、做法 1:把"有事"定义成一个哈希

监听任务的监控脚本,只输出一行确定性的字符串:

parts.push(`${id}:a${a.audit}:v${Math.floor(a.view / 10)}0:d${a.digg}:c${a.collect}:m${a.comment}`);

关键在 Math.floor(a.view / 10)——阅读数是按 10 分档的。真实数字从 172 涨到 179,算出来都是 v170,字符串一字不变、哈希不变,AI 根本不会被叫醒;只有跨到 180 才触发一次。

先验证门控的基准没漂:

cd ~/.hermes/cron/output/d8707cb2ce85
sha256sum monitor_last_output.txt | cut -c1-24
# 27ce2fdf0edab29d63ebff46
grep -o '"last_output_hash": "[a-f0-9]\{24\}' ~/.hermes/cron/jobs.json | head -1
# "last_output_hash": "27ce2fdf0edab29d63ebff46

两个哈希逐位相同,说明"上一次输出"就是当前这次,比对基准是对得上的。

再数一数到底抑制了多少次——被抑制的运行也会落一条记录,所以可数:

grep -l "agent run suppressed" *.md | wc -l   # 45
grep -l "## Response"          *.md | wc -l   # 5

45 : 5。这就是"变化才报"的真实兑现率。

三、做法 2:班次按"日期奇偶"轮换

固定时刻发文,机器味太重。我把两个发文班次挂在日期的奇偶上:

DOY=$(date +%j); DOY=$((10#$DOY)); PAR=$((DOY % 2))
# 早班在偶数日值班,晚班在奇数日值班
if   [ "$SLOT" = "morning" ] && [ $PAR -eq 0 ]; then ACTION=PUBLISH
elif [ "$SLOT" = "evening" ] && [ $PAR -eq 1 ]; then ACTION=PUBLISH
fi

今天 date +%j 返回 265(奇数)→ 晚班值班、早班静默。

效果是发布间隔彻底不规律:09-18 20:51 → 09-19 08:42 → 09-20 20:52 → 09-21 08:42 → 09-22 20:25,间隔在 11 小时 51 分36 小时 10 分 之间摆动,平均下来仍是每天 1 篇。

四、反直觉结论 1:哈希门控挡不住"状态跳变日"

我原以为"输出不变就不唤醒"能让静默日彻底安静。实测打脸。

发文班次 11 次调度里,6 次是空跑:早班 3 次(09-18 / 09-20 / 09-22)、晚班 3 次(09-17 / 09-19 / 09-21),只有 5 次真的发布了。

拆开看,6 次空跑里 2 次是冷启动(首跑没有基准哈希,必然放行),剩下 4 次全是状态跳变:发布日的监控输出是 ACTION=PUBLISH + 选题行,第二天变回 ACTION=SKIP,字符串变了 → 门控放行 → AI 白醒一次,读到 SKIP 后回一句 [SILENT],不推送、不打扰人。

所以规则是:门控能挡住"连续静默",挡不住"跳变的边界"。每次发布后,第二天被排到静默的那个班次一定会白醒一次。

改进方向(我还没上,先记下):让静默日的监控输出等于上一次的输出——用状态文件做粘滞,哈希恒定,永不唤醒。代价是脚本要读写状态,换来的是每天省一次模型调用。这里我没编"省了多少钱",上线后再补真实数据。

五、反直觉结论 2:被抑制 ≠ 没留痕

每个被掐掉的 tick,仍然写一个 172 字节的记录:

**Mode:** monitor
**Status:** no_change (agent run suppressed)

这 172 字节什么都不干,但它让"任务根本没跑"和"跑了但被门控抑制"变成了两件可区分的事。哪天数据不对,你能一眼分出是调度挂了还是门控误判。

五之二、做法 3:互动任务必须带"去重账本"

21:45 的互动任务跑了 9 次,每次读候选文章、写一条有信息增量的评论。它最容易翻车的地方不是写不好,而是重复评论同一篇——所以评论 ID 一律入表,跑之前先查表:

cat /tmp/juejin_audit/commented_ids.json | python3 -c "import json,sys;print(len(json.load(sys.stdin)),'条去重记录')"
# 18 条去重记录

9 次运行、18 条评论、零重复。有状态的任务,状态必须落盘,不能只活在上下文里。

六、数据对比:门控前 vs 门控后

指标无门控实测(有门控)
监听的 AI 唤醒每次 tick 都醒(48 次/天)5 次 / 25 小时
阅读 +1 是否产生扰动否(10 分档内不触发)
静默日是否推送到人是(空简报)
被抑制的运行是否留痕是(172 字节/次)
空跑率(发文班次)6/11 ≈ 55%

(第一列是无门控时的应然值,不是实测值——我不编数字。)

七、诚实账:唯一一直在失败的任务

6 个任务里,只有"每日反思"有失败记录:19 次调度 → 15 次成功、4 次失败,其中 2 条被登记成 config 类 incident,最长一条从 09-12 07:00 挂到 09-14 21:51 才闭合——跨了 3 天。

其余 5 个任务零失败。结论不是"反思任务写得差",而是:定时任务从来不是"配好了就一直在跑",而是"失败要留痕、留痕要有人看"。

八、可抄清单

  1. 监控脚本的输出必须确定性,且只包含"有意义的变化";数值先离散化(阅读按 10 分档)。
  2. 哈希比对决定是否唤醒 AI,而不是无条件轮询调用模型。
  3. 值班表用日期奇偶/星期排,不用固定时刻,让更新间隔自然不规则。
  4. 允许空跑,但必须统计空跑率——它是排班设计的最直接质检指标。
  5. 被抑制的运行也要落一条记录,保留"没跑"与"被抑制"的可区分性。
  6. 建一张 incident 表:错误签名 + 首次/末次出现 + 是否闭合,别让失败沉在日志里。
  7. 永远不要把"没报警"当成"没事"。