06 - 调度器集成与热重载机制
学习目标
理解 CowAgent 如何集成 Cron 定时任务引擎,以及 MCP、技能、配置的热重载机制。
项目实践
调度器集成
CowAgent 使用 croniter 库解析 Cron 表达式驱动定时任务:
关键设计:
- 模块级锁:
_scheduler_init_lock防止并发 session 初始化时重复创建 SchedulerService - 隔离 session:每个定时任务使用
scheduler_前缀的独立 session 执行,不污染用户对话
调度器任务执行流程
防递归设计:当 is_scheduled_task=True 时,scheduler 工具被排除,防止调度任务内部再次调用 scheduler 创建新任务导致无限递归。
MCP 热重载
MCP 配置变更检测使用文件签名(mtime + sha256):
def _read_mcp_json_signature(self): """返回 (mtime, sha256_of_bytes) 元组""" mtime = os.path.getmtime(path) with open(path, "rb") as f: digest = hashlib.sha256(f.read()).hexdigest() return (mtime, digest)
def refresh_mcp_if_changed(self): new_sig = self._read_mcp_json_signature() if new_sig == self._mcp_signature: return # 快速路径:未变更
# 解析新配置,对比旧配置 added = [n for n in new_by_name if n not in old_by_name] removed = [n for n in old_by_name if n not in new_by_name] changed = [n for n in new_by_name if n in old_by_name and new_by_name[n] != old_by_name[n]]
# 关闭已移除/编辑的,启动新增/编辑的技能热刷新
每次构建系统提示时调用 skill_manager.refresh_skills():
def refresh_skills(self): self.skills = self.loader.load_all_skills(...) # 重新扫描目录 self._sync_skills_config() # 同步配置技能的热刷新比 MCP 简单——只需重新扫描目录,不涉及后台进程。
配置热重载
EnvConfig 工具管理 API Key 和技能的动态配置:
def _refresh_conditional_tools(agent): # 重新加载 .env 文件 load_dotenv(env_file, override=True)
# 刷新所有 Agent 实例的技能和条件工具 agent.skill_manager.refresh_skills()
# 重新评估条件工具(如 web_search 依赖 API Key) if WebSearch.is_available() and not has_tool: agent.tools.append(WebSearch()) # 注入 elif not WebSearch.is_available() and has_tool: agent.tools = [t for t in agent.tools if t.name != "web_search"] # 移除问题与规避
| 问题 | CowAgent 的规避策略 |
|---|---|
| 调度任务递归创建新任务 | 执行时排除 scheduler 工具 |
| 调度任务 session 内存膨胀 | 更小的恢复窗口 + 滑动窗口修剪 |
| 并发初始化重复创建 SchedulerService | 模块级锁 + 双重检查 |
| MCP 文件签名变更但内容不变 | Diff 对比配置 dict,逻辑不变则不重载 |
| .env 修改后技能/工具未生效 | EnvConfig 工具触发全局刷新 |
| 调度任务结果注入用户 session 膨胀 | scheduler_inject_max_per_session(默认 3 条)滑动窗口 |
设计取舍
为什么调度器使用 croniter 而非 APScheduler?
croniter 是轻量级的 Cron 表达式解析库,无额外依赖。APScheduler 功能更丰富但引入重量依赖。CowAgent 定位为轻量部署,选择 croniter + 自定义扫描线程的方案足够满足需求。
为什么 MCP 重载使用 diff 而非全量重启?
diff 式重载确保未变更的 MCP 服务器不中断。如果用户编辑了 mcp.json 中的某个服务器配置,全量重启会导致所有服务器(包括未修改的)都断开重连。diff 式重载只影响变更的服务器。