Agent 开发进阶(十四):定时调度系统,让 Agent 学会安排未来的工作

Agent 开发进阶(十四):定时调度系统,让 Agent 学会安排未来的工作

本文是「从零构建 Coding Agent」系列的第十四篇,适合想让 Agent 按时间自动触发任务的开发者。

先问一个问题

当你希望 Agent 在特定时间执行任务时,你是怎么做的?

  • 手动设置闹钟,到时再提醒 Agent?
  • 每次到时间都重新告诉 Agent 该做什么?
  • 希望 Agent 能自己记住并按时执行?

如果你的答案是前两者,那么你需要一个定时调度系统。

时间管理的「未来困境」问题

到了这一阶段,你的 Agent 已经具备了多种能力:

  • 核心循环运行
  • 工具使用与分发
  • 会话内规划(TodoWrite)
  • 子智能体机制
  • 技能加载
  • 上下文压缩
  • 权限系统
  • Hook 系统
  • Memory 系统
  • 系统提示词组装
  • 错误恢复
  • 任务系统
  • 后台任务系统

但当面对需要在未来特定时间执行的任务时,你会遇到明显限制:

  • 无法安排未来:Agent 只能响应当前的请求
  • 重复操作:每次到时间都需要手动提醒
  • 缺乏持久性:程序重启后,计划就丢失了
  • 时间精度:无法精确控制执行时间

所以到了这个阶段,我们需要一个定时调度系统:

把一条未来要执行的意图,先记下来,等时间到了再触发。

定时调度系统的核心设计:持久化调度与通知机制

用一个图来表示定时调度系统的工作流程:

rust 复制代码
schedule_create("0 9 * * 1", "Run weekly report")
  ->
把调度记录写到文件里
  ->
后台检查器每分钟看一次"现在是否匹配"
  ->
如果匹配,就把 prompt 放进通知队列
  ->
主循环下一轮把它当成新的用户消息喂给模型

关键点只有两个:

  1. 持久化调度:把未来的任务记录下来,即使程序重启也不会丢失
  2. 通知机制:时间到了,通过通知队列把任务送回主循环

几个必须搞懂的概念

调度器(Scheduler)

调度器,就是一段专门负责"看时间、查任务、决定是否触发"的代码。

Cron 表达式

cron 是一种很常见的定时写法。

最小 5 字段版本长这样:

复制代码
分 时 日 月 周

例如:

  • */5 * * * * - 每 5 分钟
  • 0 9 * * 1 - 每周一 9 点
  • 30 14 * * * - 每天 14:30

持久化调度

持久化,意思是:

就算程序重启,这条调度记录还在。

调度通知

调度通知是一条包含触发信息的消息,用于将定时任务送回主循环。

最小实现

1. Cron 解析器

python 复制代码
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

python 复制代码
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. 调度工具

python 复制代码
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

python 复制代码
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. 创建调度任务

创建重复任务

python 复制代码
schedule_manager.create("0 9 * * 1", "Run weekly status report")
# 每周一 9 点触发

创建一次性任务

python 复制代码
schedule_manager.create("30 14 * * *", "Remind me to check the build", recurring=False)
# 今天 14:30 触发一次

2. 管理调度任务

列出所有任务

python 复制代码
schedule_manager.list()
# 查看所有调度任务

删除任务

python 复制代码
schedule_manager.delete("job_12345678")
# 删除指定调度任务

3. 通知机制

通知格式

python 复制代码
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 语法细节

python 复制代码
# ❌ 错误
# 花大量时间研究复杂的 cron 表达式
cron_expr = "0 0 * * 0,6"  # 周末午夜
# 但还没理解调度系统的核心机制

# ✅ 正确
# 先理解核心机制,再学习语法
# 核心:调度记录 -> 时间检查 -> 通知队列 -> 主循环

2. 没有 last_fired_at 字段

python 复制代码
# ❌ 错误
# 没有记录最后触发时间,可能导致重复触发
job = {
    "id": "job_123",
    "cron": "*/5 * * * *",
    "prompt": "Check status"
}

# ✅ 正确
# 记录最后触发时间,避免重复触发
job = {
    "id": "job_123",
    "cron": "*/5 * * * *",
    "prompt": "Check status",
    "last_fired_at": None
}

3. 只放内存,不支持落盘

python 复制代码
# ❌ 错误
# 任务只存在内存中
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. 把调度触发结果直接在后台默默执行

python 复制代码
# ❌ 错误
# 直接在后台执行,跳过主循环
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. 误以为定时任务必须绝对准点

python 复制代码
# ❌ 错误
# 追求秒级精度,增加系统复杂度
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,应该能够安排未来的工作。

定时调度系统让你能够:

  1. 自动执行:按时间自动触发任务,无需手动提醒
  2. 持久化:程序重启后,调度任务依然存在
  3. 灵活性:支持重复任务和一次性任务
  4. 集成性:与主循环无缝集成,共享相同的执行环境
  5. 可扩展性:可以与任务系统、后台任务系统配合使用

推荐的实现步骤

  1. 第一步:实现基本的 cron 表达式解析和匹配
  2. 第二步:实现 ScheduleManager 类,支持任务的创建、管理和持久化
  3. 第三步:实现后台检查线程,定时检查任务是否触发
  4. 第四步:实现通知机制,将触发的任务送回主循环
  5. 第五步:创建调度相关的工具,暴露给模型
  6. 第六步:集成到 Agent Loop,统一处理通知

定时调度系统与后续章节的关系

  • s14 定时调度:解决任务如何按时间触发的问题
  • s15 Agent 团队:会利用定时调度系统来协调团队工作
  • s17 自主智能体:会利用定时调度系统来规划长期任务

所以定时调度系统是构建智能 Agent 系统的重要组件。

下一章预告

有了定时调度系统,你的 Agent 已经具备了安排未来工作的能力。下一章我们将探讨 Agent 团队系统,让多个 Agent 能够协同工作,共同完成复杂任务。


一句话总结:后台任务是在"等结果",定时调度是在"等开始"。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。

相关推荐
飞龙14775657467502 小时前
Agent 开发进阶(十五):Agent 团队系统,让多个智能体协同工作
agent
飞龙14775657467502 小时前
Agent 开发进阶(十三):后台任务系统,让慢命令不阻塞主循环
agent
Pkmer2 小时前
工程师眼中的Prompt提示词
llm·agent
Pkmer2 小时前
LLM应用的“外挂大脑”:Embedding、向量数据库与RAG
llm·agent
前进的李工2 小时前
智能Agent实战指南:从入门到精通(工具)
开发语言·人工智能·架构·langchain·agent·tool·agentexecutor
chaors2 小时前
LangGraph 入门到精通0x00:HelloLangGraph
langchain·llm·agent
打酱油的D2 小时前
Claude Code Harness Agent 架构深度解析
agent
阿维的博客日记3 小时前
了解哪些其他的 Agent 设计范式?
agent
霪霖笙箫3 小时前
「JS全栈AI Agent学习」六、当AI遇到矛盾,该自己决定还是问你?—— Human-in-the-Loop
前端·面试·agent