模型说“记住了”,五分钟后谁来叫它?
“每五分钟检查一次 main.py,拿到结果再报告。”
模型回答“好的”,这轮响应就结束了。五分钟以后,如果没有新的调用,它不会自己从这句话里醒来。上一课的后台任务解决了程序已经启动之后如何等待;眼前的问题更早:没有新输入时,谁来产生下一份工作?
宿主保存一张时间表,到点产生一条待交付提示,再在 Agent 空闲时把提示交给原循环。时间检查、排队和执行分开以后,我们才能解释两种看似矛盾的现象:到点了却还没执行,执行结束了时间表却还在。
用户要求每五分钟检查main.py。宿主登记*/5 * * * *和检查提示,当前没有启动程序,也没有模型响应。
- 先保存“什么时候、做什么”:用户要求每五分钟检查main.py。宿主登记*/5 * * * *和检查提示,当前没有启动程序,也没有模型响应。
- 09:00,宿主检查时间表:调度器读取固定UTC+08:00时钟,本分钟满足五分钟刻度。检查时间由宿主负责,不需要模型在上一轮记住一个计时器。
- 先入队,再记住本分钟:产生cron-1@01:00Z对应的提示,暂未交付。09:00:40再检查仍是同一个UTC分钟,因此不会多加一项。
- Agent空闲,才交付这一项:宿主占用一个轮次位置,将触发ID和检查提示组成新问题交给原循环。到点没有授予程序许可,运行仍经过原检查。
- 本次回答保存真实程序结果:获准运行才得到exit_code=0和42;拒绝也可以得到answered,但回答必须说明没有执行。轮次结果记录后释放占用。
- 时间表仍在,等待下一次匹配:一次回答结束不删除重复时间表。下一次匹配会产生新的事件;关闭本课进程则表和队列一起消失,没有后台服务继续运行。
到点,只是队列里多了一项
时间表保存“什么时候、做什么”。本例是表达式 */5 * * * *,以及“读取预期,获准后运行程序,报告本次结果”这段提示。cron 是按日历刻度匹配的时间表;这里的分钟刻度为 0、5、10,直到 55。
宿主每次检查当前时间,匹配后产生一个事件。事件带时间表 ID、触发分钟和提示。例如 cron-1@01:00Z 表示 cron-1 在 UTC 的 01:00 触发,对应本例固定 UTC+08:00 的 09:00。
时间表可以重复,事件只代表其中一次触发。 如果把二者当成同一个对象,第一次消费就可能把整张表删掉;如果只看时间表还在,又可能在同一分钟不断追加重复事件。
因此需要保存最后成功入队的分钟。09:00:01 和 09:00:40 属于同一个分钟,只产生一项;09:05 是另一项。标识包含日期,第二天的 09:00 不会被前一天挡住。
Agent还在工作,就先别插话
到点时,Agent 可能正在等模型响应,也可能正在执行工具。如果调度器直接改这轮 messages,模型当前到底看到了什么就又变得含糊了。
本课让调度器只写队列。消费者检查宿主是否还有其他工作、是否已有轮次占用;空闲才取一项,组成新问题,交给原来的 run_loop。正在进行的轮次保持自己的消息和预算,定时提示不会从响应中间插进来。
| 对象 | 保存的内容 | 发生变化的时机 |
|---|---|---|
| 时间表 | cron、提示、最后触发分钟 | 登记、取消或成功入队后 |
| 触发队列 | 某张表在某个分钟产生的事件 | 到点追加,空闲时取首项 |
| 当前占用 | 正在交付的事件 ID | 锁内占用,轮次返回后释放 |
| 轮次结果 | answered、call_limit 或异常等状态 | 原循环返回或交付失败后 |
这个占用位置不是第十二课的持久任务负责人,也不是第十三课的后台进程 ID。它只负责保证本课消费者不会同时交付两轮。
排着队,也需要说明排队规则
假设 09:00 时 Agent 没有空闲,09:05 仍然没空。本例保留两条事件,空闲后依次交付。对“逐次检查”来说,这样容易核对;对“只看最新状态”来说,连续补做旧检查可能没有价值,此时应另加合并规则。
当前实现最多保留八条待交付事件。队列满时不提前写“本分钟已经触发”:如果本分钟释放了空间,下一次检查仍有机会入队。若等到之后的分钟才恢复,本课不会回头补发错过的时刻。去重、积压、满队列和停机补发分别是四个决策,不能靠一个 last_run 字段含糊带过。
一个轮次出错时,本课保存异常并释放占用,没有自动把事件放回队列。因为“没收到结论”不证明工具没有执行过;请求恢复可以沿用上一课已有机制,而整轮重跑需要另外核对副作用。
时间到了,许可也自动有了吗?
时间表保存的是待交付的工作要求,程序权限仍由执行入口决定。定时事件可以成功进入模型,也可以得到 answered,但回答内容可能是“没有许可,未运行”。判断程序是否运行,仍然要查本次工具结果。
本课把时间固定为 UTC+08:00,并只实现足以观察机制的五字段子集:*、单个数值,以及分钟字段的 */N。偏移始终保持八小时,也没有范围和列表语法。它按分钟刻度匹配,*/7 之类的表达式跨小时也不保证均匀相隔七分钟;需要固定经过时长时,应选择相应的间隔调度方式。
会话表与队列都保存在本进程内存中。关闭进程,时间检查就停止,重新运行也不会恢复旧表。若要求关闭应用后仍执行,应把唤醒职责交给持续运行的宿主或外部调度设施,再单独设计持久化、恢复和重复执行边界。
实践篇逐行展开“时间匹配 → 触发入队 → 空闲交付”,并用注入时钟验证完整路径,不必真的等五分钟。参考课程的 Cron Scheduler提供了这一拆分思路;Claude Code 的官方定时文档也说明提示先排队、在轮次之间交付。官方产品会继续演进,本文的会话内 Python 实现有自己的明确边界,不据此推断其内部源码。