Agent 开发进阶(十四):定时调度系统,让 Agent 学会安排未来的工作
本文是「从零构建 Coding Agent」系列的第十四篇,适合想让 Agent 按时间自动触发任务的开发者。
先问一个问题
当你希望 Agent 在特定时间执行任务时,你是怎么做的?
- 手动设置闹钟,到时再提醒 Agent?
- 每次到时间都重新告诉 Agent 该做什么?
- 希望 Agent 能自己记住并按时执行?
如果你的答案是前两者,那么你需要一个定时调度系统。
时间管理的「未来困境」问题
到了这一阶段,你的 Agent 已经具备了多种能力:
- 核心循环运行
- 工具使用与分发
- 会话内规划(TodoWrite)
- 子智能体机制
- 技能加载
- 上下文压缩
- 权限系统
- Hook 系统
- Memory 系统
- 系统提示词组装
- 错误恢复
- 任务系统
- 后台任务系统
但当面对需要在未来特定时间执行的任务时,你会遇到明显限制:
- 无法安排未来:Agent 只能响应当前的请求
- 重复操作:每次到时间都需要手动提醒
- 缺乏持久性:程序重启后,计划就丢失了
- 时间精度:无法精确控制执行时间
所以到了这个阶段,我们需要一个定时调度系统:
把一条未来要执行的意图,先记下来,等时间到了再触发。
定时调度系统的核心设计:持久化调度与通知机制
用一个图来表示定时调度系统的工作流程:
schedule_create("0 9 * * 1", "Run weekly report")
->
把调度记录写到文件里
->
后台检查器每分钟看一次“现在是否匹配”
->
如果匹配,就把 prompt 放进通知队列
->
主循环下一轮把它当成新的用户消息喂给模型
关键点只有两个:
- 持久化调度:把未来的任务记录下来,即使程序重启也不会丢失
- 通知机制:时间到了,通过通知队列把任务送回主循环
几个必须搞懂的概念
调度器(Scheduler)
调度器,就是一段专门负责“看时间、查任务、决定是否触发”的代码。
Cron 表达式
cron 是一种很常见的定时写法。
最小 5 字段版本长这样:
分 时 日 月 周
例如:
*/5 * * * *- 每 5 分钟0 9 * * 1- 每周一 9 点30 14 * * *- 每天 14:30
持久化调度
持久化,意思是:
就算程序重启,这条调度记录还在。
调度通知
调度通知是一条包含触发信息的消息,用于将定时任务送回主循环。
最小实现
1. Cron 解析器
import datetime
def cron_to_minutes(cron_expr):
"""简化版 cron 解析,只支持每分钟检查"""
parts = cron_expr.split()
if len(parts) != 5:
raise ValueError("Cron expression must have 5 fields")
minute, hour, day, month, weekday = parts
return {
"minute": minute,
"hour": hour,
"day": day,
"month": month,
"weekday": weekday
}
def cron_matches(cron_expr, now):
"""检查当前时间是否匹配 cron 表达式"""
try:
parts = cron_to_minutes(cron_expr)
except ValueError:
return False
# 简化版匹配,只处理通配符和数字
def matches(field, value):
if field == "*":
return True
if field == "*/5" and value % 5 == 0:
return True
return str(value) == field
return (
matches(parts["minute"], now.minute) and
matches(parts["hour"], now.hour) and
matches(parts["day"], now.day) and
matches(parts["month"], now.month) and
matches(parts["weekday"], now.weekday() + 1) # cron 周日是 0,Python 是 6
)
2. Schedule Manager
import os
import json
import threading
import time
import uuid
from pathlib import Path
class ScheduleManager:
"""定时调度管理器"""
def __init__(self, schedule_dir=".schedules"):
self.schedule_dir = Path(schedule_dir)
self.schedule_dir.mkdir(exist_ok=True)
self.jobs = {}
self.notifications = []
self.lock = threading.Lock()
# 加载已存在的调度
self._load_existing_schedules()
# 启动检查线程
self._start_check_thread()
def _load_existing_schedules(self):
"""加载已存在的调度"""
for schedule_file in self.schedule_dir.glob("*.json"):
try:
schedule = json.loads(schedule_file.read_text(encoding="utf-8"))
self.jobs[schedule["id"]] = schedule
except Exception as e:
print(f"加载调度失败 {schedule_file}: {e}")
def _start_check_thread(self):
"""启动检查线程"""
thread = threading.Thread(
target=self._check_loop,
daemon=True,
)
thread.start()
def _check_loop(self):
"""检查循环"""
while True:
now = datetime.datetime.now()
self._check_jobs(now)
time.sleep(60) # 每分钟检查一次
def _check_jobs(self, now):
"""检查任务"""
with self.lock:
to_remove = []
for job_id, job in self.jobs.items():
if cron_matches(job["cron"], now):
# 检查是否已经触发过(避免重复触发)
last_fired = job.get("last_fired_at")
if last_fired:
last_time = datetime.datetime.fromtimestamp(last_fired)
if last_time.date() == now.date() and last_time.hour == now.hour and last_time.minute == now.minute:
continue
# 添加通知
self.notifications.append({
"type": "scheduled_prompt",
"schedule_id": job_id,
"prompt": job["prompt"],
})
# 更新最后触发时间
job["last_fired_at"] = now.timestamp()
self._save_job(job)
# 如果不是重复的,标记为删除
if not job.get("recurring", True):
to_remove.append(job_id)
# 清理非重复任务
for job_id in to_remove:
del self.jobs[job_id]
job_file = self.schedule_dir / f"{job_id}.json"
if job_file.exists():
job_file.unlink()
def _generate_job_id(self):
"""生成任务 ID"""
return f"job_{str(uuid.uuid4())[:8]}"
def _save_job(self, job):
"""保存任务"""
job_file = self.schedule_dir / f"{job['id']}.json"
job_file.write_text(json.dumps(job, indent=2, ensure_ascii=False), encoding="utf-8")
def create(self, cron_expr, prompt, recurring=True):
"""创建调度任务"""
job_id = self._generate_job_id()
job = {
"id": job_id,
"cron": cron_expr,
"prompt": prompt,
"recurring": recurring,
"created_at": time.time(),
"last_fired_at": None,
}
with self.lock:
self.jobs[job_id] = job
self._save_job(job)
return job
def list(self):
"""列出所有调度任务"""
with self.lock:
if not self.jobs:
return "No scheduled tasks"
lines = ["# Scheduled Tasks\n"]
for job_id, job in self.jobs.items():
lines.append(f"- **#{job_id}** {job['cron']} {'[Recurring]' if job.get('recurring', True) else '[One-time]'}")
lines.append(f" Prompt: {job['prompt']}")
if job.get('last_fired_at'):
last_fired = datetime.datetime.fromtimestamp(job['last_fired_at'])
lines.append(f" Last fired: {last_fired.strftime('%Y-%m-%d %H:%M:%S')}")
return "\n".join(lines)
def delete(self, job_id):
"""删除调度任务"""
with self.lock:
if job_id in self.jobs:
del self.jobs[job_id]
job_file = self.schedule_dir / f"{job_id}.json"
if job_file.exists():
job_file.unlink()
return f"Scheduled task {job_id} deleted"
else:
return f"Scheduled task {job_id} not found"
def drain_notifications(self):
"""排空通知队列"""
with self.lock:
notifications = self.notifications.copy()
self.notifications.clear()
return notifications
3. 调度工具
def create_schedule_tools(schedule_manager):
"""创建调度相关的工具"""
def schedule_create(cron, prompt, recurring=True):
"""创建定时调度任务"""
job = schedule_manager.create(cron, prompt, recurring)
return f"调度任务创建成功: #{job['id']}\nCron: {job['cron']}\nPrompt: {job['prompt']}\n{'重复任务' if recurring else '一次性任务'}"
def schedule_list():
"""列出所有调度任务"""
return schedule_manager.list()
def schedule_delete(job_id):
"""删除调度任务"""
return schedule_manager.delete(job_id)
return {
"schedule_create": schedule_create,
"schedule_list": schedule_list,
"schedule_delete": schedule_delete,
}
4. 集成到 Agent Loop
def agent_loop_with_schedule(state):
"""带定时调度系统的 Agent Loop"""
# 初始化调度管理器
schedule_manager = ScheduleManager()
# 创建调度工具
schedule_tools = create_schedule_tools(schedule_manager)
state["tools"] = state.get("tools", []) + [
{
"name": "schedule_create",
"description": "创建定时调度任务",
"parameters": {
"cron": {"type": "string", "description": "Cron 表达式"},
"prompt": {"type": "string", "description": "触发时的提示"},
"recurring": {"type": "boolean", "description": "是否重复", "optional": True}
}
},
{
"name": "schedule_list",
"description": "列出所有调度任务",
"parameters": {}
},
{
"name": "schedule_delete",
"description": "删除调度任务",
"parameters": {
"job_id": {"type": "string", "description": "任务 ID"}
}
}
]
# 主循环
while True:
# 1. 排空通知队列(包括后台任务和调度通知)
notifications = []
# 排空调度通知
schedule_notifications = schedule_manager.drain_notifications()
notifications.extend(schedule_notifications)
# 排空后台任务通知(如果有的话)
if "background_manager" in state:
background_notifications = state["background_manager"].drain_notifications()
notifications.extend(background_notifications)
if notifications:
for notification in notifications:
if notification["type"] == "scheduled_prompt":
text = f"[定时任务 #{notification['schedule_id']}] {notification['prompt']}"
elif notification["type"] == "background_completed":
text = f"[后台任务 #{notification['task_id']}] {notification['status']} - {notification['preview']}"
else:
text = f"[通知] {notification}"
state["messages"].append({"role": "user", "content": text})
# 2. 调用模型
response = call_model(state["messages"])
if response.stop_reason != "tool_use":
return response.content
results = []
for block in response.content:
if hasattr(block, "type") and block.type == "tool_use":
tool_name = block.name
tool_input = block.input
# 执行调度工具
if tool_name in schedule_tools:
output = schedule_tools[tool_name](**tool_input)
else:
# 执行其他工具
output = run_tool(tool_name, tool_input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})
if results:
state["messages"].append({"role": "user", "content": results})
核心功能说明
1. 创建调度任务
创建重复任务:
schedule_manager.create("0 9 * * 1", "Run weekly status report")
# 每周一 9 点触发
创建一次性任务:
schedule_manager.create("30 14 * * *", "Remind me to check the build", recurring=False)
# 今天 14:30 触发一次
2. 管理调度任务
列出所有任务:
schedule_manager.list()
# 查看所有调度任务
删除任务:
schedule_manager.delete("job_12345678")
# 删除指定调度任务
3. 通知机制
通知格式:
notification = {
"type": "scheduled_prompt",
"schedule_id": "job_12345678",
"prompt": "Run weekly status report",
}
通知处理:
- 调度器每分钟检查一次时间
- 时间匹配时,生成通知并添加到队列
- 主循环在下一轮调用模型前,排空通知队列
- 通知被转换为用户消息,注入到上下文中
4. 持久化
调度任务会被保存到 .schedules/ 目录,每个任务一个 JSON 文件:
.schedules/
job_12345678.json
job_87654321.json
这样即使程序重启,调度任务也不会丢失。
定时调度 vs 后台任务的边界
| 特性 | 定时调度 | 后台任务 |
|---|---|---|
| 关注点 | 未来何时开始 | 现在开始,稍后拿结果 |
| 触发方式 | 时间触发 | 立即触发 |
| 生命周期 | 长期(可重复) | 短期(执行完成后可清理) |
| 作用 | 安排未来工作 | 处理慢命令 |
| 存储 | 持久化存储 | 运行时临时存储 |
使用建议:
- 对于需要在未来特定时间执行的任务:使用定时调度
- 对于需要立即执行但耗时较长的任务:使用后台任务
- 两者可以结合使用:定时调度触发后台任务
新手最容易犯的 5 个错
1. 一上来沉迷 cron 语法细节
# ❌ 错误
# 花大量时间研究复杂的 cron 表达式
cron_expr = "0 0 * * 0,6" # 周末午夜
# 但还没理解调度系统的核心机制
# ✅ 正确
# 先理解核心机制,再学习语法
# 核心:调度记录 -> 时间检查 -> 通知队列 -> 主循环
2. 没有 last_fired_at 字段
# ❌ 错误
# 没有记录最后触发时间,可能导致重复触发
job = {
"id": "job_123",
"cron": "*/5 * * * *",
"prompt": "Check status"
}
# ✅ 正确
# 记录最后触发时间,避免重复触发
job = {
"id": "job_123",
"cron": "*/5 * * * *",
"prompt": "Check status",
"last_fired_at": None
}
3. 只放内存,不支持落盘
# ❌ 错误
# 任务只存在内存中
class InMemoryScheduler:
def __init__(self):
self.jobs = {}
# ✅ 正确
# 任务持久化到磁盘
class ScheduleManager:
def __init__(self, schedule_dir=".schedules"):
self.schedule_dir = Path(schedule_dir)
self.schedule_dir.mkdir(exist_ok=True)
self._load_existing_schedules()
4. 把调度触发结果直接在后台默默执行
# ❌ 错误
# 直接在后台执行,跳过主循环
def _check_jobs(self, now):
for job in self.jobs:
if cron_matches(job["cron"], now):
# 直接执行,跳过主循环
subprocess.run(job["prompt"], shell=True)
# ✅ 正确
# 通过通知队列送回主循环
def _check_jobs(self, now):
for job in self.jobs:
if cron_matches(job["cron"], now):
# 添加通知,由主循环处理
self.notifications.append({"type": "scheduled_prompt", "prompt": job["prompt"]})
5. 误以为定时任务必须绝对准点
# ❌ 错误
# 追求秒级精度,增加系统复杂度
def _check_loop(self):
while True:
now = datetime.datetime.now()
self._check_jobs(now)
time.sleep(1) # 每秒检查一次
# ✅ 正确
# 分钟级检查,满足大多数需求
def _check_loop(self):
while True:
now = datetime.datetime.now()
self._check_jobs(now)
time.sleep(60) # 每分钟检查一次
为什么这很重要
因为一个真正智能的 Agent,应该能够安排未来的工作。
定时调度系统让你能够:
- 自动执行:按时间自动触发任务,无需手动提醒
- 持久化:程序重启后,调度任务依然存在
- 灵活性:支持重复任务和一次性任务
- 集成性:与主循环无缝集成,共享相同的执行环境
- 可扩展性:可以与任务系统、后台任务系统配合使用
推荐的实现步骤
- 第一步:实现基本的 cron 表达式解析和匹配
- 第二步:实现 ScheduleManager 类,支持任务的创建、管理和持久化
- 第三步:实现后台检查线程,定时检查任务是否触发
- 第四步:实现通知机制,将触发的任务送回主循环
- 第五步:创建调度相关的工具,暴露给模型
- 第六步:集成到 Agent Loop,统一处理通知
定时调度系统与后续章节的关系
- s14 定时调度:解决任务如何按时间触发的问题
- s15 Agent 团队:会利用定时调度系统来协调团队工作
- s17 自主智能体:会利用定时调度系统来规划长期任务
所以定时调度系统是构建智能 Agent 系统的重要组件。
下一章预告
有了定时调度系统,你的 Agent 已经具备了安排未来工作的能力。下一章我们将探讨 Agent 团队系统,让多个 Agent 能够协同工作,共同完成复杂任务。
一句话总结:后台任务是在“等结果”,定时调度是在“等开始”。
如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。