Day 14 / 共 20 天 · 第 3 周 执行引擎

Scheduler 定时执行

让 Agent 定时/周期性自动运行("每天早上 8 点跑")。今天读 executor/scheduler.py:基于 APScheduler、持久化、cron 兼容、孤儿任务自愈。

📍 你在整门课的位置 · 第 3 周 执行引擎
D13 数据流/拓扑 D14 Scheduler D15 WebSocket D16 Credits 计费
💡 今天的类比世界观:定时执行 = 一个"会记事的闹钟" 让 Agent"每天 8 点自己跑",就像用闹钟:APScheduler = 智能闹钟(到点触发);cron 规则 = 你设的响铃时间表(每天 / 每周几点);持久化 JobStore = 把闹钟设置写进日程本,手机重启也不丢;同步 / 异步桥接 = 闹钟(同步)把异步流水线叫醒孤儿任务自愈 = 清掉那些没主人的过期旧闹钟。今天都用"闹钟 / 日程本"来想。

👶 小白:服务半夜重启了,我之前设的"每天 8 点跑"的定时任务会不会就丢了?

👨‍🏫 老师:不会。如果闹钟只记在内存里,一断电(重启)确实全没了;所以平台把定时任务持久化进 JobStore(数据库)——相当于写进了日程本。服务重启后会从日程本把闹钟重新加载回来,接着到点触发。这就是"持久化"要解决的核心问题:状态不随进程消失。

L01

为什么要定时

回忆 Day 01 的例子:"每天早上把新闻头条总结后发到我邮箱"。这个"每天早上"就靠 Scheduler——让 Agent 无需人工触发、按时间表自动跑。这正是"持续运行的 AI Agent"(AutoGPT 的定位)的关键。

定时 = "持续 Agent"的核心 手动触发的 Agent 是"工具"(你用它时才跑)。定时/周期触发的 Agent 是"员工"(它自己按时干活,你不用管)。比如"每小时检查一次竞品价格""每周一生成周报""每天备份数据"——这些自动化才是平台"continuous AI agents"承诺的价值。Scheduler 让 Agent 从"被动工具"变成"主动员工"。
L02

APScheduler 基础

🤔 痛点:"每天早上 8 点自动跑"这件事,谁在数着时间? 执行引擎(Day 12)是"你叫它才动"的——收到 MQ 消息才跑图。可"每天 8 点"没人发消息啊。总得有个角色一直盯着钟表,到点了主动去按"运行"键。自己写个 while True: sleep(60)?重启就全丢、错过了不补、时区还容易错。
💡 本质:Scheduler = 一个"闹钟服务",到点自动去按主链路的运行键 它是独立服务 class Scheduler(AppService)scheduler.py:1255),内部揣着一个 BackgroundSchedulerscheduler.py:22)。你用 cron 表达式登记一个闹钟(CronTrigger.from_crontabscheduler.py:1208),到点它就调 execute_graphscheduler.py:159)——最终汇入 Day 05 那条"发 MQ → 引擎执行"的主链路。闹钟只负责"掐点喊一嗓子",真正跑图还是执行引擎。

Scheduler 基于 APScheduler(Python 成熟的定时任务库)的 BackgroundScheduler,是一个独立的 RPC 服务(AppService)。它支持几种触发器:

  • cron 触发:按 cron 表达式("0 8 * * *" = 每天 8 点)。
  • interval 触发:按固定间隔(每 30 分钟)。
  • date 触发:在某个具体时刻跑一次。
📝 举个例子:一个 cron 表达式变成一串触发时刻 你给 Agent 设定时 "0 9 * * 1-5"(工作日每天 9:00):
from_crontab("0 9 * * 1-5")scheduler.py:1208)→ 生成一个 CronTrigger → APScheduler 算出下一次触发时刻 周一 09:00,到点调 execute_graph,跑完再算下一次 周二 09:00……
任务参数(跑哪张图、哪个用户、输入是什么)打包进 GraphExecutionJobArgsscheduler.py:1035)随任务存下来。
为什么用 APScheduler 而不自己写? 定时任务的坑很多:错过了怎么补、重启后任务丢不丢、时区、并发实例控制……APScheduler 是久经考验的成熟库,把这些都处理好了。成熟的基础设施直接用,别重造轮子——AutoGPT 用 litellm 接模型、用 APScheduler 做定时、用 RabbitMQ 做队列,都是这个原则。
L03

持久化 JobStore

定时任务用 SQLAlchemyJobStore 存进 Postgres(scheduler.py:1302,表 apscheduler_jobs)——进程重启不丢job_defaults:1308)有几个关键设置:

coalesce=True            # 错过的重复任务只补跑最新一次(别补一堆)
max_instances=1000
misfire_grace_time=None  # 永不因错过而丢弃
这几个设置解决什么真实问题? 持久化:定时任务存 DB,服务重启/宕机后任务还在(不会"重启后所有定时都没了")。coalesce=True:假设服务停了 3 小时、期间某个"每小时任务"本该跑 3 次——恢复后不补跑 3 次(那会瞬间挤爆),只补跑最新 1 次。misfire_grace_time=None:即使错过了执行时刻,也不丢弃、照样跑。这些设置保证"定时任务在各种异常(重启、卡顿)下仍可靠"——生产级定时的必备考量。
L04

同步/异步桥接

APScheduler 跑在线程池(同步),但平台业务逻辑是 async 的。run_asyncscheduler.py:148)用 run_coroutine_threadsafe 把协程丢到一个常驻 event loop 线程执行——搭起"同步调度器 ↔ 异步业务"的桥。

为什么需要"桥接"? APScheduler 到点了会在它的线程池里调你的函数(同步世界)。但你的业务函数(发起图执行)是 async 的(异步世界)。两个世界不能直接互调——同步线程不能直接 await 协程。run_coroutine_threadsafe 就是那座桥:把异步协程"提交"到一个专门的异步事件循环去跑,同步这边等它结果。混合同步库(APScheduler)和异步业务是常见场景,这个桥接模式很实用。(CrewAI 的调度器也是同样处理。)
L05

触发即回主链路

定时时刻到了,APScheduler 调 execute_graphscheduler.py:159)→ _execute_graph → 调 add_graph_execution——又回到 Day 05 那条"发 MQ → 引擎执行"的主链路

三种触发源 → 同一个执行入口 ① 用户点"运行" ② API 调用 ③ Scheduler 定时 execute_graph :159 add_graph_execution 唯一执行入口 RabbitMQ 执行引擎跑图
无论"用户点击""API"还是"定时器",最终都汇聚到 add_graph_execution 发 MQ、由执行引擎消费——Scheduler 只是"按时间自动调用"那个入口,执行逻辑完全复用。
读法:定时触发和用户手动点"运行"殊途同归——最终都调 add_graph_execution 发 MQ、由执行引擎消费执行。Scheduler 只是"按时间自动调用"那个入口,执行逻辑完全复用。
复用主链路的好处 无论触发来源是"用户点击""API 调用"还是"定时器",它们都汇聚到同一个执行入口add_graph_execution)。这意味着:定时执行和手动执行走完全一样的流程(扣费、拓扑执行、实时推送、计费对账)——不用为定时执行单独写一套逻辑。"多种触发源 → 同一执行入口"是清晰架构的标志。定时任务参数用 kind 字段区分(graph 执行 vs copilot 任务),是"多态判别器"设计。
L06

cron 兼容坑

一个真实的坑(scheduler.py:1143_normalize_cron_day_of_week):APScheduler 的星期编号(0=周一)和 Unix cron(0=周日)不一致,需要转换,还要处理范围回绕(如 6-2 跨周末)。

这种"兼容坑"为什么值得讲? 因为它是真实工程里最容易出错、最难查的那类 bug——两个系统对"同一个概念"(星期几)有不同约定。如果不转换,用户设"每周日跑",实际却在周一跑(差一天),而且很难发现(要等一周才知道)。集成第三方库/系统时,务必核对"约定是否一致"(编号从 0 还是 1、时区、大小端…)。这类"边界约定不一致"的坑,是老手才会警惕的。AutoGPT 专门写了转换函数处理它。
L07

孤儿任务自愈

定时任务运行时,如果它要跑的图已经被删了(或移出库、校验失败),怎么办?Scheduler 会捕获异常并自动删除这个孤儿定时任务scheduler.py:189_cleanup_orphaned_schedules_for_graph:372)。

"自愈"为什么重要? 想象你设了个"每天跑 Agent X"的定时,后来删了 Agent X。如果不处理,这个定时任务每天都会触发、每天都失败报错——产生无穷无尽的错误日志、浪费资源。自愈 = "发现要跑的图没了,就自动把这个定时任务也删掉"——系统自己清理掉不再有效的任务。另外添加定时任务时会先 validate_and_construct_node_execution_input 校验图能跑(:1556)——防止"定时任务运行时才炸",把错误提前到设置时。这些自愈/前置校验让定时系统健壮、不留垃圾。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 定时为什么是"持续 Agent"的核心?
  • 持久化 JobStore + coalesce + misfire_grace 各解决什么异常?
  • 同步/异步桥接为什么需要?
  • 定时触发和手动触发为什么殊途同归?
  • cron 星期编号坑是什么?孤儿任务怎么自愈?

✋ 动手

P=autogpt_platform/backend/backend
grep -n 'SQLAlchemyJobStore\|coalesce\|misfire_grace\|run_async' $P/executor/scheduler.py | head
grep -n 'def _execute_graph\|def add_graph_execution_schedule\|_normalize_cron_day_of_week\|_cleanup_orphaned' $P/executor/scheduler.py
明天预告 · Day 15(第3周收官)WebSocket 实时更新——执行过程如何实时推送到前端画布。鉴权、消息路由、Redis Pub/Sub → WebSocket 转发。你在画布上看节点实时亮起的原理。
← Day 13 数据流 Day 15 · WebSocket →