Cron Scheduler - 定时调度机制

3673 字
18 分钟
Cron Scheduler - 定时调度机制

L14 要回答一个问题:如何在 L13 的基础上实现”将来某个时间再开始做事”,而不是”稍后回来拿结果”?

L13 给了我们骨架:后台执行线 + 通知队列 + 任务表分离。但真实 Agent 需要定时执行任务——每天早晨检查日志、每周一提醒备份、定时轮询外部状态。这些不是”等结果回来”,而是”等开始时机”。怎么做?

本章讲三个机制:

机制解决什么问题核心设计
三组件模型调度记录如何回到主循环调度记录 → 定时检查器 → 通知队列
关键字段防重复、跨会话、反复触发last_fired_at + durable + recurring
通知注入统一主循环处理都走通知队列,下一轮注入 messages

这三个机制叠加在 L13 的 Background Tasks 上,循环本身不变。理解 L14,就理解了 Agent 如何”定时触发回归主循环”。

一、本质定位:等开始 vs 等结果#

“定时调度解决的是’将来某个时间再开始做事’,而不是’稍后回来拿结果’。”

这句话概括了 Cron Scheduler 的本质。不是后台任务的延续——而是另一类需求。

与 L13 Background Tasks 的区别#

机制回答的问题关注点
后台任务(L13)“已启动的慢操作,结果什么时候回来?“等结果
定时调度(L14)“一件事应该在未来什么时候开始?“等开始

关键洞察:定时调度不是另一套 agent,最终还是回到同一条主循环。

三组件心智模型#

调度记录 → 定时检查器 → 通知队列
flowchart TB subgraph 创建阶段 A[用户请求: 定时任务] --> B[schedule_create] B --> C{durable?} C -->|true| D[写入磁盘] C -->|false| E[仅存内存] D --> F[返回 schedule_id] E --> F end subgraph 检查循环 G[后台检查器] --> H[每分钟检查一次] H --> I[遍历所有 jobs] I --> J{cron_matches?} J -->|yes| K{已触发过?} K -->|no| L[放入通知队列] K -->|yes| M[跳过] J -->|no| M L --> N[更新 last_fired_at] N --> O{recurring?} O -->|false| P[删除任务] O -->|true| Q[保留任务] M --> H Q --> H end subgraph 主循环处理 R[主循环下一轮] --> S[drain 通知队列] S --> T[构建 user message] T --> U["[scheduled:id] prompt"] U --> V[注入 messages] V --> W[模型处理] end F --> G L -.-> R

完整流程

  1. schedule_create(…) → 把记录写到列表或文件里
  2. 后台检查器每分钟看一次”现在是否匹配”
  3. 如果匹配,就把 prompt 放进通知队列
  4. 主循环下一轮把它当成新的用户消息喂给模型

验证你的理解#

Q1:定时调度为什么不能”后台默默执行”?

:::details 答案 系统行为不透明 + 模型不统一决策 + 主循环结构乱。

如果后台默默执行:

  1. 模型不知道定时任务触发 → 行为不透明
  2. 后台直接调用工具 → 权限检查缺失
  3. 多条执行线 → 主循环结构混乱

正确设计:走通知队列 → 主循环统一处理 → 模型决策 → 行为透明。 :::

二、关键数据结构:ScheduleRecord#

定时调度需要记录”未来要做什么、什么时候做”。

ScheduleRecord 定义#

schedule = {
"id": "job_001", # 唯一编号
"cron": "0 9 * * 1", # 定时规则(分 时 日 月 周)
"prompt": "...", # 到点后要注入主循环的提示
"recurring": True, # 是否反复触发
"durable": True, # 是否落盘保存(跨会话)
"created_at": 1710000000, # 创建时间
"last_fired_at": None, # 上次触发时间(防重复)
}

字段职责#

字段职责为什么要
id唯一标识查询、删除、更新需要
cron定时规则判断何时触发
prompt注入提示触发后做什么
recurring反复触发周期性 vs 一次性
durable跨会话保存重启后是否保留
last_fired_at防重复触发检查周期内避免多次触发

关键字段详解#

last_fired_at:防止短时间内重复触发同一任务。

def should_fire(schedule: ScheduleRecord, now: datetime) -> bool:
# 1. cron 匹配
if not cron_matches(schedule["cron"], now):
return False
# 2. 未触发过
if schedule["last_fired_at"] is None:
return True
# 3. 上次触发不是同一分钟
last_minute = schedule["last_fired_at"].minute
current_minute = now.minute
return last_minute != current_minute

没有这个字段,系统容易在检查周期内多次触发。

durable:是否落盘保存。

行为适用场景
true写磁盘,重启后保留跨会话任务(每天提醒)
false仅内存,重启后丢失临时任务(本次会话有效)

recurring:是否反复触发。

行为适用场景
true触发后保留,下次再触发周期性任务(每周备份)
false触发后删除一次性任务(明天提醒)

验证你的理解#

Q2:为什么 durable=true 需要落盘,而不只是内存存储?

:::details 答案 跨会话保留——程序重启后调度记录仍然存在。

如果只存内存:

  1. 用户设置”每天早晨检查日志”
  2. 程序重启(用户关闭应用)
  3. 调度记录丢失 → 不再触发
  4. 用户困惑:为什么定时任务消失了?

正确设计:durable=true → 写磁盘 → 重启后重新加载 → 定时任务继续有效。 :::

三、Cron 表达式:5字段版#

Cron 表达式定义何时触发。教学版用5字段,不是6字段或7字段。

5字段格式#

分 时 日 月 周
字段含义范围
分钟0-59
小时0-23
日期1-31
月份1-12
星期0-7 (0和7都是周日)

示例解析#

Cron含义触发时间
*/5 * * * *每5分钟0, 5, 10, 15, …
0 9 * * 1每周一9点周一早晨9:00
30 14 * * *每天14:30下午2:30
0 0 1 * *每月1号0点月初午夜

特殊符号#

符号含义示例
*任意值* * * * * = 每分钟
*/N每N单位*/5 * * * * = 每5分钟
N-M范围0-30 9 * * * = 9点0-30分每分钟
N,M列表0,30 9 * * * = 9:00和9:30

注意:初学者常见错误——沉迷 cron 语法细节。主线是”调度记录如何回到主循环”,不是语法。

验证你的理解#

Q3:0 9 * * 1 表示每周一9点,那 0 9 1 * 1 表示什么?

:::details 答案 每月1号且是周一的9点。

解析

  • 分=0,时=9 → 9:00
  • 日=1 → 1号
  • 月=* → 任意月
  • 周=1 → 周一

触发条件:同时满足”1号”和”周一”。

这意味着不是”每月1号或周一”,而是”既是1号又是周一”。如果某月1号不是周一,就不会触发。

设计决策:日和周是”AND”关系,不是”OR”。这是 cron 标准行为。 :::

四、检查周期与通知注入#

定时检查器需要决定”多久检查一次”和”如何通知主循环”。

检查周期:分钟级而非秒级#

# 后台检查循环
def cron_checker_loop():
while True:
sleep(60) # 每分钟检查一次
check_all_schedules()

为什么分钟级?

原因说明
精度够用大多数 cron 任务不需要秒级精度
资源消耗低秒级检查消耗高(每秒遍历所有 jobs)
后台独立检查是后台循环,不是每轮对话触发

通知注入:统一主循环处理#

def check_all_schedules():
now = datetime.now()
for schedule in schedules:
if should_fire(schedule, now):
# 1. 放入通知队列
notification = {
"type": "scheduled_prompt",
"schedule_id": schedule["id"],
"prompt": schedule["prompt"],
}
notifications.append(notification)
# 2. 更新 last_fired_at
schedule["last_fired_at"] = now
# 3. 处理 recurring
if not schedule["recurring"]:
schedules.remove(schedule)

为什么走通知队列?

不走的问题走的好处
后台直接执行 → 权限检查缺失主循环统一处理 → 权限检查完整
模型不知道触发 → 行为不透明通知注入 → 模型看到触发原因
多条执行线 → 结构混乱单一主循环 → 结构清晰

验证你的理解#

Q4:为什么定时检查器是后台独立循环,而不是每轮对话触发?

:::details 答案 检查频率固定,与对话轮次无关。

如果每轮对话触发:

  1. 用户不说话 → 不检查 → 定时任务错过
  2. 用户频繁说话 → 检查过多 → 资源浪费
  3. 检查频率不可控 → 行为不确定

正确设计:后台独立循环 → 每分钟固定检查 → 不依赖对话 → 行为确定。 :::

五、回顾:L14 在循环上叠加了什么#

回到开头的问题:如何在 L13 的基础上实现”将来某个时间再开始做事”?

答案:

叠加机制代码位置循环的变化
调度记录ScheduleRecord 数据结构不变——只是存储层扩展
定时检查器后台独立循环不变——只是触发源增加
通知注入notifications 队列不变——只是注入内容增加

循环本身仍是 L01 的 while True + stop_reason + messages[]。L14 只是在”触发层”叠加了定时机制。

系统整合#

到了 L14,系统有两条外部事件输入:

事件源内容统一方式
后台任务完成background_completed通知队列 → drain → messages
定时调度触发scheduled_prompt通知队列 → drain → messages

统一方式:都走通知队列,在下一轮模型调用前统一注入。

与后续课程的关系#

课程关系
L13 Background Tasks前置:理解”等结果”vs”等开始”的区别
L15 Agent Teams后续:定时调度可用于团队任务轮询
L06 Context Compact调度记录在 system prompt 不在 messages,不受压缩影响

核心洞察:Cron Scheduler 是”等开始时机”的机制。理解 L14,就理解了 Agent 如何”定时触发回归主循环”。后续课程都在扩展调度机制——团队轮询、跨会话保留。

六、心智模型升级:从 L0 到 L2#

读完这篇文章,你的理解经历了怎样的升级?

L14 核心概念的理解升级路径#

概念L0(表面)L1(关联)L2(深层)
本质定位”定时任务""等开始 vs 等结果""定时调度解决的是’将来某个时间再开始做事’,而不是’稍后回来拿结果’。不是另一套 agent,最终回到同一条主循环。类比闹钟:设定时间后等待触发,不是立刻执行”
三组件模型”调度记录 → 检查器 → 通知""存储 → 触发 → 注入""调度记录(存储意图)→ 定时检查器(后台独立循环)→ 通知队列(统一注入)。类比 Git:commit(记录)→ push(触发)→ remote(通知)。每个组件职责单一”
关键字段”cron/prompt/durable""定时规则 + 提示 + 跨会话""last_fired_at 防重复——检查周期内避免多次触发。durable 跨会话——重启后保留。recurring 反复触发——周期性 vs 一次性。三个字段缺一不可”
通知注入”时间到放队列""后台检查 → 通知队列 → 主循环""统一走通知队列——后台任务完成和定时调度触发都走同一通道。下一轮 drain → 注入 messages → 模型统一决策。系统行为透明,主循环结构不乱”

如何检验你达到了 L2?#

用这个问题自测:

如果定时检查器每秒检查一次(秒级),会有什么问题?完整链条是什么?

:::details L2 层级回答 资源消耗高 + 重复触发风险 + 检查频率不可控。

完整链条

  1. 每秒检查一次 → 每秒遍历所有 jobs
  2. 100个 jobs → 每秒100次检查 → CPU消耗高
  3. cron 匹配后 → 1分钟内可能触发60次(每秒检查一次)
  4. last_fired_at 需要更精细(秒级而非分钟级)→ 字段设计复杂
  5. 检查频率固定 → 与对话轮次无关 → 但资源消耗不可控

正确设计:分钟级检查 → 资源消耗可控 → 大多数任务精度够用 → last_fired_at 分钟级防重复。 :::

本章目标:让每个核心概念都从 L0 升级到 L2。理解 L14,不只是记住 cron 语法,而是掌握”如何在循环上叠加定时触发”的设计原则。

七、复习可视化#

调度器核心流程#

flowchart TB subgraph 创建阶段 A[用户请求: 定时任务] --> B[schedule_create] B --> C{durable?} C -->|true| D[写入磁盘] C -->|false| E[仅存内存] D --> F[返回 schedule_id] E --> F end subgraph 检查循环 G[后台检查器] --> H[每分钟检查一次] H --> I[遍历所有 jobs] I --> J{cron_matches?} J -->|yes| K{已触发过?} K -->|no| L[放入通知队列] K -->|yes| M[跳过] J -->|no| M L --> N[更新 last_fired_at] N --> O{recurring?} O -->|false| P[删除任务] O -->|true| Q[保留任务] M --> H Q --> H end subgraph 主循环处理 R[主循环下一轮] --> S[drain 通知队列] S --> T[构建 user message] T --> U["[scheduled:id] prompt"] U --> V[注入 messages] V --> W[模型处理] end F --> G L -.-> R

与后台任务的对比#

flowchart LR subgraph L13 Background Tasks A1[用户请求] --> B1[bash/run_in_background] B1 --> C1[新线程执行] C1 --> D1[结果写入队列] D1 --> E1[主循环 drain] E1 --> F1[注入 tool_result] end subgraph L14 Cron Scheduler A2[用户请求: 定时] --> B2[schedule_create] B2 --> C2[写入调度表] C2 --> D2[后台检查循环] D2 --> E2[时间匹配触发] E2 --> F2[放入通知队列] F2 --> G2[主循环 drain] G2 --> H2[注入 user message] end style A1 fill:#e1f5fe style A2 fill:#fff3e0

系统整合:两条外部事件#

flowchart TB subgraph Events["外部事件源"] E1[后台任务完成] E2[定时调度触发] end subgraph Queue["通知队列"] Q1[notifications] end subgraph MainLoop["主循环"] M1[drain_notifications] M2[注入 messages] M3[模型处理] end E1 --> Q1 E2 --> Q1 Q1 --> M1 --> M2 --> M3 style E1 fill:#e1f5fe style E2 fill:#fff3e0 style Q1 fill:#f3e5f5

关键字段职责#

flowchart TB subgraph Fields["ScheduleRecord 字段"] F1["id<br/>唯一标识"] F2["cron<br/>定时规则"] F3["prompt<br/>注入提示"] F4["recurring<br/>反复触发?"] F5["durable<br/>跨会话?"] F6["last_fired_at<br/>防重复"] end F1 --> U1[查询/删除/更新] F2 --> U2[判断何时触发] F3 --> U3[触发后做什么] F4 --> U4[周期性 vs 一次性] F5 --> U5[重启后是否保留] F6 --> U6[检查周期内避免多次触发] style F6 fill:#ffebee style F5 fill:#fff3e0 style F4 fill:#e1f5fe

durable + recurring 组合#

flowchart TB subgraph Combinations["四种组合"] C1["durable=true<br/>recurring=true"] C2["durable=true<br/>recurring=false"] C3["durable=false<br/>recurring=true"] C4["durable=false<br/>recurring=false"] end C1 --> S1["跨会话周期性<br/>每天提醒"] C2 --> S2["跨会话一次性<br/>明天提醒一次"] C3 --> S3["本次会话周期性<br/>每5分钟检查"] C4 --> S4["本次会话一次性<br/>临时提醒"] style C1 fill:#e8f5e9 style C2 fill:#fff3e0 style C3 fill:#e1f5fe style C4 fill:#fce4ec

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Cron Scheduler - 定时调度机制
https://firefly.cuteleaf.cn/posts/learn-claude-code/14-cron-scheduler/
作者
AltumSisy
发布于
2026-04-23
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
AltumSisy
Hello, I'm AltumSisy.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
3
标签
25
总字数
68,347
运行时长
0
最后活动
0 天前

文章目录