s14:定时调度(Scheduled Tasks)
s14:定时调度(Scheduled Tasks)
与 s13 的关系
- s13(后台任务):手动触发的命令 → 异步执行,但不会自动触发
- s14(定时调度):到点自动入队、空闲自己跑、人不用在场
一句话概括区别:s13 解决"命令太慢",s14 解决"人不在也要干活"。
1. 四层解耦模型(本章灵魂)
时间判断(生产者)和执行(消费者)彻底分开。调度器自己跑自己的,不管 agent 在不在忙;agent 忙完空闲了,queue processor 才把活递过去。互不阻塞。
| 层 | 角色 | 干什么 |
|---|---|---|
| Scheduler | 独立 daemon 线程 | 每 1s 轮询,判断"现在到点了吗" |
| Queue | cron_queue |
调度线程把触发的 job 写进去(生产者缓冲区) |
| Queue Processor | 另一 daemon 线程 | 队列非空且 agent 空闲时,拉起一轮 agent_loop |
| Consumer | agent_loop |
从队列取出 job,注入 messages 执行 |
解耦的好处:
- Scheduler 不关心 agent 忙不忙,只管判断时间
- Queue 做缓冲,削峰填谷
- Processor 只在 agent 空闲时才消费,不打扰用户交互
- 四层各自独立线程,互不阻塞
2. CronJob 数据结构
@dataclass
class CronJob:
id: str # 唯一标识
cron: str # 触发条件 —— 五段式 cron 表达式,如 "0 9 * * *"
prompt: str # 触发时注入给 agent 的消息/指令
recurring: bool # 是否重复:True=周期性, False=一次性
durable: bool # 是否持久化:True=写磁盘跨重启保留, False=仅内存
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
str |
任务唯一标识符 |
cron |
str |
Cron 表达式,定义触发时间规则 |
prompt |
str |
触发时作为 user 消息注入给 agent 的文本指令 |
recurring |
bool |
True=周期重复执行,False=只执行一次 |
durable |
bool |
True=持久化到磁盘(重启后恢复),False=仅存在于内存 |
3. Cron 表达式速查
Cron 是 Linux 里跑了 50 年的定时任务语法。"0 9 * * *" 就是一张"时间表",表示每天早上 9 点执行。
五段式结构如下:
| 字段 | 取值范围 | 含义 | 示例值 |
|---|---|---|---|
| 分(Minute) | 0–59 | 第几分钟 | 0 = 整点 |
| 时(Hour) | 0–23 | 几点(24小时制) | 9 = 上午9点 |
| 日(Day of month) | 1–31 | 每月几号 | * = 每天 |
| 月(Month) | 1–12 | 几月 | * = 每月 |
| 星期(Day of week) | 0–7(0和7都代表周日) | 星期几 | * = 每星期 |
常用示例:
| 表达式 | 含义 |
|---|---|
0 9 * * * |
每天早上 9:00 |
*/15 * * * * |
每 15 分钟 |
0 9 * * 1-5 |
周一到周五早上 9:00 |
30 8 1 * * |
每月1号早上 8:30 |
0 0 * * 0 |
每周日午夜 00:00 |
注意:
*表示"每一个"(通配符),即该字段的所有可能值。
4. 各组件详解
4.1 cron_scheduler_loop — 独立调度线程
调度线程每秒读取一次本地时间,对每个已注册的 CronJob 执行 cron_matches() 判断:
- 到点了 → 把任务塞进
cron_queue - 没到点 → 啥也不干,等下一秒
# 伪代码示意
def cron_scheduler_loop():
while running:
now = datetime.now()
for job in all_cron_jobs:
if cron_matches(job.cron, now): # 判断当前时间是否匹配 cron 表达式
cron_queue.put(job) # 匹配则入队
sleep(1) # 每秒轮询一次
4.2 cron_matches() — 时间匹配
遍历 cron 表达式的五个字段与当前时间的各分量进行比对。被调度线程每秒调一次,返回 True 就被塞进 cron_queue,之后等 agent 空闲时执行即可。
4.3 queue_processor_loop() — 交付端(消费者线程)
后台线程,职责很简单:
- 检查队列是否有待办任务
- 检查 agent 当前是否空闲
- 两个条件同时满足 → 自动拉起一轮
agent_loop去处理
使用 agent_lock 避免定时任务与用户正在进行的回合同时修改会话(防止冲突)。
4.4 agent_loop — 执行端
从队列取出已经触发的任务,将它变成一条普通 user 消息注入对话,让 LLM 去执行:
fired = consume_cron_queue() # 从队列取出并清空
for job in fired:
messages.append({
"role": "user",
"content": f"[Scheduled] {job.prompt}" # 加上 [Scheduled] 前缀标记来源
})
错误恢复:如果模型调用失败,这些消息会从当前会话中移除,任务重新放回队列等待下次执行。
4.5 durable 持久化
将任务定义写入/读出磁盘,让定时任务跨重启保留:
save_durable_jobs():只挑durable=True的任务,序列化写入.scheduled_tasks.jsonload_durable_jobs():启动时读回文件,恢复任务到内存
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 茯茶养生人的博客!










