cron/,14 个文件,其中 scheduler.py 有 367 KB。这一章讲让智能体在没有人的时候自己跑起来 —— 以及这件事带来的一整类新问题。
cron 是 Unix 系统里的定时任务工具,名字来自希腊语 chronos(时间)。它的核心是一个表达式格式:
0 9 * * 1-5 每周一到周五的早上 9:00
*/15 * * * * 每 15 分钟
0 0 1 * * 每月 1 号午夜
字段顺序:分钟 小时 日 月 星期
| 场景 | 做什么 |
|---|---|
| 每日简报 | 早上 8 点扫一遍邮件、日历、待办,生成摘要发到聊天工具 |
| 持续监控 | 每 15 分钟检查服务健康度,异常时告警 |
| 定期维护 | 每周清理日志、更新依赖、跑安全扫描 |
| 长任务 | 把一个需要几小时的任务拆成多次执行 |
「定时智能体」和「定时脚本」的本质区别:
定时脚本做的事是固定的 —— 同样的输入产生同样的行为。
定时智能体会读取外部内容并据此决定做什么。它读的邮件、网页、日志,都可能包含恶意指令。这就把「定时任务」变成了一个安全问题。
class CronPromptInjectionBlocked(...)
「提示词注入」(prompt injection)是智能体系统最核心的安全威胁。先用一个具体例子说清楚它是什么。
| 交互式会话 | 定时任务 | |
|---|---|---|
| 人在不在 | 在。看着屏幕 | 不在。凌晨 3 点 |
| 异常行为 | 用户立刻发现「它怎么在读我的 SSH 密钥?」 | 没人看到 |
| 审批 | 弹出确认框,用户拒绝 | 自动批准或自动拒绝(第 10 章的困境) |
| 发现时间 | 当场 | 可能几天后,或者永远不会 |
所以定时任务必须有一道专门的注入防线,而不是复用交互式会话的那一套。CronPromptInjectionBlocked 这个异常类型的存在,说明系统在这一层做了显式的检测和拦截 —— 检测到疑似注入时,直接中止整个定时任务,而不是「警告一下继续跑」。
这是「fail-closed」(失败即关闭)的选择:不确定的时候,宁可任务不执行,也不执行一个可能被劫持的任务。
def _resolve_cron_disabled_toolsets(...)
呼应第 4 章:定时任务运行时,某些工具集被禁用。
这是防御的第二层。即使注入检测被绕过了,被劫持的智能体也无法执行最危险的操作 —— 因为那些工具根本不在它的工具列表里。
三层防御在定时场景下的组合:
① 注入检测 —— 尽量识别恶意内容,识别到就中止
② 工具收窄 —— 就算没识别到,也没有危险工具可用
③ 执行环境(第 6 章)—— 就算工具被滥用,破坏也被限制在容器内
没有任何单独一层是可靠的。注入检测必然有漏网(因为它本质是在猜「这段文字是数据还是指令」);工具收窄会限制功能;容器隔离有性能代价。三层叠加才能达到可接受的风险水平。
def _failure_streak_nudge(...) # 连续失败提醒
def _upsert_incident_for_failure(...) # 为失败创建/更新事件记录
_failure_streak_nudge:连续失败的提醒「streak」(连续)这个词是关键:它统计的是连续失败次数,成功一次就重置。这样偶发失败不会累积成告警,而持续性故障会很快达到阈值。
_upsert_incident_for_failure:事件记录的去重「upsert」= update + insert,意思是「有就更新,没有就插入」。
这是运维告警系统的标准做法,叫「告警聚合」或「事件去重」。
判断两次失败是否属于「同一个事件」的依据通常是:任务 ID + 错误类型 + 是否还未解决。
没有去重的告警系统,最终会因为噪声太大而被所有人关掉。而一个被关掉的告警系统,等于没有告警系统。
def try_register_running_job(...)
函数名里的 try_ 是关键:「尝试注册」—— 如果已经有一个同样的任务在跑,注册失败,这次就跳过。
这是定时任务系统里最经典的一个坑,几乎每个团队都踩过一次。
症状很有迷惑性:系统平时好好的,某天突然雪崩。因为触发条件是「单次执行时间 > 调度间隔」,这个条件在数据量小的时候永远不成立。
try_register_running_job 就是解法:每次执行前先声明「我要跑了」,如果发现已经有人在跑,就安静地跳过这一次。
还有一个细节:这个注册记录必须是持久化的、带过期时间的。
「按时间跑任务」听起来是一个 while True: sleep(); run() 的事情。367 KB 在做什么?
| 类别 | 内容 |
|---|---|
| 时间计算 | cron 表达式解析、时区处理、夏令时切换(这一天可能有 23 或 25 小时)、闰秒 |
| 错过的执行 | 机器关机了 8 小时,错过的 96 次执行怎么办?全补跑?只跑最后一次?跳过? |
| 并发控制 | 防重复执行、多任务并发上限、任务之间的依赖 |
| 失败处理 | 重试策略、退避、连续失败告警、事件去重 |
| 安全 | 注入检测、工具收窄、审批策略 |
| 状态管理 | 任务的启用/禁用、暂停/恢复、动态增删 |
| 可观测 | 每次执行的耗时、成本、结果、日志 |
| 结果投递 | 跑完了结果发到哪儿?发失败了怎么办?(呼应第 1 章的投递台账) |
这张图是整个 Hermes 架构的一个浓缩:
核心循环只有一个(不为每种触发方式写一套逻辑),但安全策略是按触发源分层的(不用同一套权限对待所有来源)。
这两句话看起来矛盾 —— 统一 vs 分化。实际上它们分别作用在不同的维度:「怎么做」统一,「能做什么」分化。
如果反过来(每个触发源一套循环逻辑,但共享同一套权限),你会得到一个既难维护、又不安全的系统。