《Skill 与定时任务:让 Agent 在没人说话时也能干活》

AgentHub 的 Skill 与定时任务:能力扩展与两条定时链路

本文是 AgentHub 架构系列的第 6 篇,讲两块能力:Skill (自包含、可拔插的能力扩展)和定时任务 (Cron 业务定时 + Heartbeat AI 巡检)。这两块表面不相干,底层却是同一个设计------都通过"伪造一条 InboundMessage 走统一 dispatch 链路"来触发 Agent ,并且在多 agent 演进后都带上了 agent 维度。工具层(能力怎么执行)见 05-工具层,护栏细节见 07-护栏层

AgentHub 的 Skill 是一层"给 Agent 看的操作手册 + 自带脚本"的能力扩展:宿主对具体业务零认知,只提供一个通用的 run_skill_script 工具去跑脚本,Skill 的索引在每次组装 system prompt 时按 agent 的白名单注入,Agent 据描述匹配后再读完整指令;而 Cron 和 Heartbeat 这两套定时机制,本质都是"特殊的消息生产者"------它们不直接调 Agent,而是伪造一条 InboundMessage 投进消息总线,复用与用户消息完全相同的处理链路,只是一个由用户配 cron 表达式驱动、一个由系统定时让 AI 自检。


一、Skill:给 Agent 的操作手册

1. Skill 为什么不做成"又一个内置工具"

先厘清 Skill 和工具的分层,这是理解整个设计的起点:

  • 工具(Tool) 是 Agent 的"手",是代码级 的能力:read_filefetch_urlrun_command 这些都在 AgentHub 代码里注册,粒度细。加一个新工具意味着改宿主代码、重新打包。
  • Skill 是更高一层的扩展:一坨"指令 + 脚本"的组合,告诉 Agent 在某类业务任务下该按什么步骤、调哪些脚本去干 。粒度粗,内部往往编排多个工具调用。装进 skills/ 目录即可用,不改宿主代码。

关键的设计抉择是:为一个业务能力(比如生成周报),到底是给 AgentHub 加一个专用工具,还是做成一个 Skill? AgentHub 选了后者,理由是可移植性和解耦:

  • 专用工具会把业务逻辑焊死在宿主里。周报怎么算、字段怎么取、邮件怎么发,全塞进 AgentHub 代码,宿主从此"认识"周报------这是耦合。
  • Skill 把逻辑全放进它自己的 scripts/ 下,宿主只提供一个通用的 run_skill_script 去执行脚本。宿主对周报零认知,只是个"能跑 Python 脚本的执行器" 。同一个 Skill 拿到任何提供"跑脚本 + 读写文件"能力的 agent 框架(Claude Code、Codex 等)都能跑,不跟 AgentHub 绑死。

教学案例说明:下文以 weekly-report(个人/团队日报周报的自动生成与邮件发送)为例讲解 Skill 的自包含模式。该 Skill 已从当前代码库移除 (skills/ 根目录当前只有 .gitkeep,全局位置无安装;已有的是按 agent 安装的 skills/<agentName>/<skill>/,如 skills/default/case-timeout-notification/------一个查询案件时效并定时推送的 Skill),以下机制描述基于它曾经的实现,原理适用于任何遵循同一模式的 Skill。

2. SKILL.md ------ Skill 的"身份证 + 说明书"

每个 Skill 根目录有个 SKILL.md,YAML front matter + Markdown 正文两部分:

yaml 复制代码
---
name: weekly-report
description: '个人日报/周报与团队周报的自动生成和邮件发送。触发:生成日报/生成周报/...'
metadata:
  author: <作者>
  version: "<版本号>"
---

# weekly-report
本技能自包含、可拔插:所有工具都是 scripts/ 下的纯 Python 3(仅标准库)脚本...

## 激活条件
- 收到消息包含"生成日报"
- 收到消息包含"生成周报"
...

## 脚本调用约定
- 工作目录:以本技能根目录为当前目录
- 解释器:Python 3.8+,仅用标准库
...
  • front matter :name + description + metadata。其中 description 是整个机制的关键------它是唯一进 system prompt 的内容,Agent 靠它判断"当前用户需求该不该用这个 Skill"。
  • 正文:详细指令------激活条件、脚本调用约定、硬纪律(如"禁止臆造数据""禁止伪造执行结果")、分步骤流程。

本质上 SKILL.md 正文是一段按需加载的 prompt 片段

3. 两阶段存储:下载缓存 vs 安装

Skill 在磁盘上有两个位置,不能混:

阶段 位置 是什么
下载缓存 workspace/cache/<repo>/... 技能市场拉取后的暂存,仅落盘,不生效
安装(全局) <项目根>/skills/<skillName>/ 真正生效的位置之一,所有 agent 共享
安装(agent 专属) <项目根>/skills/<agentName>/<skillName>/ 真正生效的位置之二,仅该 agent 可见(查找时优先于全局)

设计意图:下载与生效解耦 。从市场拉取只是把文件放进 cache,不影响运行中的 Agent;只有显式"安装"(把 Skill 从 cache copy 到 skills/)后才会被 Agent 看到。安装时市场模块(target: "agenthub" 分支)还会做规范化------把嵌套子目录里的 SKILL.md 提到 Skill 根(src/skills/market.ts)。

安装分两个作用域:loadSkill(repo, skill, "global") 装到 skills/<skillName>/,loadSkill(repo, skill, <agentName>) 装到 skills/<agentName>/<skillName>/------后者正是 findSkillMd"agent 专属优先"那一档的来源。卸载语义跟着细化:unloadSkill(skill, agentName) 只删 agent 专属副本、全局安装保留;不传 agentName 才删全局。

安装目标还不止 AgentHub 自己:InstallTarget 是五枚举(market.ts)------agenthub / claude-code(~/.claude/skills)/ codex(~/.codex/skills)/ zcode(~/.zcode/skills)/ opencode(~/.config/opencode/skill),getInstallTargets 按各宿主目录是否存在报 available。装到外部工具时还会做安装后校验(目标目录找不到 SKILL.md 即报错)并补写插件元数据(claude-code 写 marketplace 元数据、codex 写 plugin 元数据)。这正是"同一个 Skill 拿到其他 agent 框架也能跑"的落地------市场直接替你装过去。顺带一提,loadSkill/unloadSkill 入口都有 assertSafeName 净化(拒绝 /、``、..、盘符前缀),防止用 Skill 名做路径穿越。

另一个容易误判的文件:项目根有 skills-lock.json(记录 source/skillPath/computedHash),但 src 下无任何代码读写它------疑似外部安装器的产物,不是 SkillMarket 维护的,别把它当成本套机制的一部分。

所以"装一个 Skill" = 市场下载到 cache + copy 到 skills/ 两步,Agent 只认后者。

4. Skill 怎么被 Agent 发现 ------ system prompt 按 agent 白名单注入索引

这是 Skill 机制里最需要讲清楚的一环,也是多 agent 演进后变化最大的一环

单 agent 时代 的做法是:ContextBuilder.buildSystem() 每次组装 system prompt 时,扫描整个 skills/ 目录,把所有已安装 Skill 的索引注入。

多 agent 时代 改成了按 agent 白名单查找,不再盲扫目录ContextBuilder 持有该 agent 的 enabledSkills(来自 agent.jsonskills 字段),buildSystem 只遍历这份清单逐个查找(src/agent/context-builder.ts):

javascript 复制代码
buildSystem(channel) {
  const soul = readOptional(join(this.agentDir, "SOUL.local.md"))
            || readOptional(join(this.agentDir, "SOUL.md"));

  const skills: SkillSummary[] = [];
  for (const name of this.enabledSkills) {      // ★ 按 agent.json.skills 清单,不扫目录
    const s = this.findSkillMd(name);
    if (s) skills.push(s);
  }
  const skillsBlock = buildSkillsBlock(skills, projectDir);

  const parts = [soul];
  parts.push(`## 当前时间\n\n${now.toLocaleString(...)}(${时区},星期x)...`);
  // ★ 当前时间注入:soul 之后、skillsBlock 之前
  //   LLM 无实时时钟,"五分钟后/明早九点"等相对时间及 cron 表达式换算须以此为准
  if (skillsBlock) parts.push(skillsBlock);      // Skill 索引拼在当前时间后面
  // ... system_allowed_paths 注入、platform-policy 追加 ...
  return parts.filter(Boolean).join("\n\n");
}

(顺带说一句,system prompt 里注入"当前时间"这个小细节很容易被忽视,但对定时任务场景是刚需:LLM 没有时钟,用户说"每天早上九点"、Agent 要生成 cron 表达式时,全靠这段注入换算,否则它会按训练数据猜时区。)

findSkillMd 的查找顺序是"agent 专属优先 > 全局" :

csharp 复制代码
private findSkillMd(skillName: string): SkillSummary | null {
  const agentPath  = join(skillsDir, this.agentName, skillName, "SKILL.md");  // agent 专属
  const globalPath = join(skillsDir, skillName, "SKILL.md");                   // 全局
  const skillMd = existsSync(agentPath) ? agentPath
                : existsSync(globalPath) ? globalPath : null;
  // ... 解析 front matter,拿 name + description ...
}

也就是同名 Skill,skills/<agentName>/<skillName>/ 覆盖 skills/<skillName>/------既能给某个 agent 定制专属版本,又能共享一套全局 Skill。

注入的索引只含 `name + description + SKILL.md 路径,不含正文:

swift 复制代码
## 可用技能(Skills)

以下为当前可用技能。当某技能与用户需求匹配时,用 `read_file` 读取其 SKILL.md 路径获取完整说明并严格执行;若没有匹配的技能,告知用户并建议联系管理员。

- weekly-report:个人日报/周报与团队周报的自动生成和邮件发送。触发:生成日报/...
  SKILL.md:skills/weekly-report/SKILL.md
...

只放摘要是为了省 token :system prompt 每轮都带,把所有 Skill 正文塞进去会迅速撑爆上下文;放摘要,Agent 匹配到哪个再 read_file 读那一个的全文。

5. Skill 发现的完整流程

串起来看 Agent 用 Skill 的真实路径:

scss 复制代码
1. buildSystem 组装 system prompt 时,按该 agent 的 enabledSkills 白名单
   逐个 findSkillMd(agent 专属优先 > 全局),把命中的索引
   (name + description + 路径) 注入 system prompt

2. 用户发"生成周报" → 走正常 dispatch → 该 agent 自己的 loop.process
   → Runner 跑 ReAct,LLM 看到 system prompt 里的 Skill 索引
   → LLM 发现 weekly-report 的 description 匹配"生成周报"

3. LLM 调 read_file("skills/weekly-report/SKILL.md") 读完整指令
   → 拿到激活条件、脚本调用约定、硬纪律等

4. LLM 按 SKILL.md 指令,调 run_skill_script 执行脚本:
   - run_skill_script(skills/weekly-report/scripts/get_config.py)
   - run_skill_script(skills/weekly-report/scripts/render_report.py, ...)
   - run_skill_script(skills/weekly-report/scripts/send_email.py, ...)

5. 脚本 stdout 喂回 LLM,LLM 组织成回复给用户

要点 :Skill 发现不是 Agent 主动 list_files 去扫目录(技术上能做),而是组装 system prompt 时就把索引给了 Agent,Agent 据 description 匹配后 read_file 读全文。ContextBuilder 替 Agent 把"有哪些 Skill 可用"这件事准备好了,Agent 只需匹配 + 读详情。

6. run_skill_script:自包含的执行边界

Agent 不直接跑脚本,而是通过通用工具 run_skill_script 执行,该工具本身就是"自包含"设计的落地(src/tools/builtin/run-skill-script.ts):

  • 路径约束 :脚本路径经 resolveSkillRoot 归一化后,必须位于 skills/ 之下 ,否则 Access denied。这既是安全边界(不能借此跑任意系统脚本),也定位出脚本所属的 Skill 根目录。
  • 工作目录 = Skill 根 :脚本以其所属 skills/<name>/ 为 cwd 执行,所以 Skill 内部可以用相对路径组织自己的文件。
  • 解释器探测 :优先读该 Skill 的 config.local.json/config.json 里的 python_path,没有则自动探测系统 Python(Windows 扫常见安装路径、查注册表 PEP 514 登记、试 PATH;类 Unix 试 python3/python)。
  • 注入操作人身份 :执行前读 auth/current_user.json,把当前用户名注入环境变量 AGENTHUB_OPERATOR,脚本据此知道"是谁在操作"(比如周报脚本据此定位是给谁生成)。
  • 输出回传:成功回 stdout,失败附带 stderr,统一截断到 16000 字符防止撑爆上下文。

配合 SKILL.md 里强调的"所有工具都是 scripts/ 下的纯 Python 3(仅标准库)脚本",自包含的三层含义就完整了:逻辑全在 Skill 自己的 scripts/ 下(宿主零认知)+ 仅用标准库(任何有 Python 的机器能跑,不用 pip)+ 只依赖通用的 run_skill_script(换宿主也能跑)

7. 另一条路径:Cron 触发时显式加载 SKILL.md 全文

除了对话场景的"索引 + 按需读全文",还有一条显式加载全文的路径------Cron 任务触发时(src/scheduler/cron.ts):

csharp 复制代码
// CronScheduler.loadSkills
private loadSkills(skillNames: string[]): string {
  const parts: string[] = [];
  for (const name of skillNames) {
    const skillPath = join(this.rootDir, "skills", name, "SKILL.md");
    if (existsSync(skillPath)) parts.push(readFileSync(skillPath, "utf-8").trim());  // 直接读全文
    else logger.warn("skill not found for cron job", { skill: name });
  }
  return parts.join("\n\n---\n\n");
}
// fire 里:const content = skillContent ? `${skillContent}\n\n${job.prompt}` : job.prompt;

两条路径的区别与互补:

  • system prompt 路径(对话场景) :只注入索引,Agent 有主动权按需 read_file 读全文------省 token。
  • Cron 路径(定时场景) :直接把指定 Skill 的 SKILL.md 全文 前置到 prompt------因为 Cron 是无人值守触发,必须一次把指令给足,不能指望 Agent 自己去发现该用哪个 Skill。

(一个可注意的细节:loadSkills 目前读的是全局路径 skills/<name>/SKILL.md,没有走 findSkillMd 的"agent 专属优先"逻辑,即使这个 Cron 任务归属某个非 default agent。这是 per-agent 化不彻底的一处,见后文"局限"。)


二、Cron 与 Heartbeat:同一个"伪造 InboundMessage"底座

这是本文的第二个核心。AgentHub 有两套定时机制,容易混:

机制 触发 干啥 谁定义任务
Cron cron 表达式(如 0 9 * * 1-5 工作日 9 点) 业务定时:跑某 Skill + prompt,结果推给指定 IM 用户/Agent 通过工具配置
Heartbeat 固定间隔(默认 30 分钟) AI 主动巡检:让 LLM 自问"有没有待处理的事" 系统内置,固定 prompt

它们最重要的共同点:都不直接调用 Agent,而是构造一条 InboundMessage 投进 bus.inbound,复用与用户消息完全相同的 dispatch 链路。 这是 AgentHub 的统一设计------Cron/Heartbeat 只是"特殊的消息生产者"。

Cron 的 fire(多 agent 版) :

typescript 复制代码
private fire(job: CronJob): void {
  const channel = /* 从 deliver "wecom:ZhangSan" 切出 */;
  const chat_id = /* ... */;

  const skillContent = this.loadSkills(job.skills);
  const content = skillContent ? `${skillContent}\n\n${job.prompt}` : job.prompt;

  this.bus.inbound.push({
    id: randomUUID(),
    session_key: `${job.agent}:${channel}:cron_${job.id}`,  // ★ agent:channel:cron_<id>
    channel,
    chat_id,
    user_id: "system",                                       // 系统发的,非真人
    content,                                                 // SKILL.md 全文 + prompt
    timestamp: Date.now(),
    agent_hint: job.agent,                                   // ★ 归属 agent 直达:不设 hint 会被
  });                                                        //   routeAgent 按 chat_id 通配路由到 default
}

CronJob 结构在多 agent 演进后新增了 agent 字段 (缺省 "default"),fire 时 session*key 从旧版的 ${channel}:cron*{id} 变成 {job.agent}:{channel}:cron_{id}------每个 Cron 任务不仅独立会话(不跟用户日常对话混),还明确归属某个 agent,由那个 agent 的 loop 来处理。agent_hint 字段是这条归属链路的最后一公里:routeAgent 拿到 hint 直接路由,不再按 chat_id 通配猜测(没有 hint 时,新建 agent 没有 wecom filter,它的任务触发消息会被通配路由全部落到 default)。

调度机制本身是 setTimeout 链式:每次算出"下次触发的 delay",setTimeout 到点 fire 再排下一次,而不是 setInterval 硬轮询。其中有两个工程细节:

  • cron-parser 按系统时区解析表达式(它默认按 UTC,否则 "0 9 * * 1-5" 会被当成 UTC 9 点)。
  • setTimeout 的 delay 是 32 位有符号整数,上限约 24.8 天,超出会被 Node/Bun 截断成 1ms 导致死循环;代码里保守取 2e9 ms(约 23 天)封顶,超限时不 fire、只重排,分段逼近目标时间(src/scheduler/cron.ts)。

manage_cron_jobs 工具的两个新语义 (src/tools/builtin/cronjob.ts):一是 deliver 可填 "current" 或省略------从 handler 收到的 sessionKey(agent:channel:chat_id)里解析出 channel:chat_id 作为推送目标,工具描述还提醒 LLM 参照系统提示里的当前时间做相对时间换算(与上面的当前时间注入呼应);二是任务归属隔离 ------list 只列当前 agent 的任务,update/delete 校验 existing.agent !== agentName 直接拒绝跨 agent 操作,agent 被删除时 removeJobsByAgent 连带清理它的全部任务和运行中 timer。

Heartbeat 的 tick(多 agent 版) :

php 复制代码
private tick(): void {
  for (const agentName of this.registry.listActive()) {       // ★ 遍历所有 active agent
    this.bus.inbound.push({
      id: randomUUID(),
      session_key: `${agentName}:cli:heartbeat`,               // ★ agent:cli:heartbeat
      channel: "cli",
      chat_id: "heartbeat",
      user_id: "system",
      content: "【定时巡检】请检查是否有待处理的任务或需要跟进的事项,如有请列出并说明下一步行动。",
      timestamp: Date.now(),
      agent_hint: agentName,                    // ★ 同样直达归属 agent,防通配路由到 default
    });
  }
}

Heartbeat 从"单个全局巡检"改成遍历所有 active agent 各推一条 ,session_key 从旧版固定的 cli:heartbeat 变成 ${agentName}:cli:heartbeat------每个上线的 agent 都定时自检一次,各查各的、各推各的。

"AI 驱动"体现在 prompt 的"如有"二字 :Agent 收到巡检消息后会真去调工具(list_files 看待办、或调业务工具查系统)检查------有事就列出来 + 说明下一步并 reply;没事就输出空内容。配合空响应静默规则 (GatewayCore 见 Agent 返回空串就跳过 reply,不推 IM),实现"有事才报、没事闭嘴",不骚扰用户。这跟传统"到点就推一条固定消息"的定时任务本质不同------推不推由 LLM 判断


三、几个值得展开的设计取舍

为什么 Cron/Heartbeat 触发时是伪造一条 InboundMessage,而不是直接调用 Agent?

因为复用统一链路 能一次性白拿到所有横切能力,不用重复实现。用户消息经过的 dispatch 链路上有:找 agent(按 session_key/agent_hint 路由到对应 agent)、命令路由、会话获取与历史加载、per-session 锁(串行化保证) 、Agent Loop/Runner、reply 分发、可观测性打点。

如果 Cron/Heartbeat 直接调 Agent,这些都要各自重写一遍------尤其是 :定时触发和用户消息可能同时命中同一会话,不走统一链路就绕过了串行化,会产生并发处理同一会话的竞态。伪造成 InboundMessage 投进总线后,它和用户消息在锁面前一视同仁,天然排队。代价只是构造一条 user_id: "system" 的消息,收益是整条链路的能力全部复用。这也是"消息总线解耦渠道与 Agent"这个架构决策的红利------任何东西只要能造出一条 InboundMessage,就能驱动 Agent

Cron 和 Heartbeat 为什么做成两套机制而不是一套?

Cron 是用户/Agent 配置的业务定时 :cron 表达式驱动(能表达"工作日 9 点"这种复杂时间),跑指定 Skill + prompt,结果推给配置的 IM 目标(企微/飞书)。Heartbeat 是系统内置的 AI 巡检:固定间隔(默认 30 分钟),固定 prompt 让 LLM 自问"有没有待处理的事",走 cli 渠道不推 IM。

不合并成一套,是因为两者的语义和主体不同:Cron 是"用户明确要在某时间做某件确定的事",prompt 和产出都是确定的、要外推给人;Heartbeat 是"让 Agent 定时自检"的兜底机制,推不推、推什么完全由 LLM 临场判断,产出更像 Agent 自己的巡检记录。硬把它们合并,要么 Cron 被迫接受"AI 决定要不要执行"的不确定性,要么 Heartbeat 被迫配置成确定任务,都别扭。分成两套,各自的触发方式(setTimeout 链式 vs setInterval)、prompt 来源、推送目标都能独立演化。

补充 Cron 任务的归属隔离 :manage_cron_jobs 的 list 只列当前 agent 的任务,update/delete 校验 existing.agent !== agentName 拒绝跨 agent 操作;deliver"current" 或省略时从 sessionKey 解析当前会话做推送目标;agent 被删除时 removeJobsByAgent 连带清理其任务。所以多 agent 下每个 agent 的定时任务互不可见、互不可改。

Skill 的索引为什么只放 description 不放正文?代价是什么?

收益是省 token :system prompt 每一轮 LLM 调用都要带,如果把所有 Skill 的完整 SKILL.md(激活条件、脚本约定、硬纪律、分步流程,动辄上千 token)都塞进去,几个 Skill 就能把上下文占满,挤掉真正的对话历史。只放 name + description + 路径 的一行摘要,再让 Agent 匹配到后用 read_file 读那一个的全文,是典型的"惰性加载"。

代价是多一次工具往返 :Agent 得先看索引、再 read_file 读全文、才能执行,比全量注入多一跳。而且 description 写得好不好直接决定匹配准不准------描述模糊,Agent 可能匹配不到或匹配错,所以 SKILL.md 的 description 字段要精心写(覆盖触发词)。对比之下,Cron 场景因为无人值守、必须一次给足,就反过来直接前置全文------两种加载策略的取舍,取决于"有没有人在场纠偏" :有人在场就惰性加载省 token,无人值守就全量前置保确定性。

Skill 的自包含设计具体怎么做到不依赖宿主?

三个层面:

  1. 逻辑全在 Skill 自己的 scripts/------业务逻辑(周报怎么算、邮件怎么发)全是 Skill 里的 Python 脚本,AgentHub 对具体业务零认知,只是个"能跑脚本的执行器"。
  2. 仅用 Python 标准库------不用 pip 装依赖,任何装了 Python 3 的机器都能直接跑。
  3. 只依赖通用工具 run_skill_script------宿主不为某个 Skill 注册专用工具,Skill 只要求宿主提供"跑脚本 + 读写文件"这种通用能力。

结果就是可移植:同一个 Skill 拿到 Claude Code、Codex 等其他 agent 框架也能跑,因为它们同样提供跑脚本的能力------市场模块的五枚举安装目标就是这个思路的落地。宿主与 Skill 通过一个极窄的接口(执行一个脚本、传参、拿 stdout)解耦,run_skill_script 还会把操作人身份注入环境变量,让脚本无需宿主传参就知道"是谁在操作"。


四、已知局限与演进方向

几处诚实的不足:

  1. Skill 隔离只做了配置层,没做执行层强制 。配置/可见性层是做了的:每个 agent 的 agent.jsonskills 白名单,ContextBuilder 只把白名单里的 Skill 索引注入该 agent 的 system prompt,查找时还遵循"agent 专属优先 > 全局"。但执行层没做强制校验 :run_skill_script 的校验只有一条------脚本路径必须位于 skills/ 之下,不校验这个脚本是否属于当前 agent 的白名单 。也就是说,一个持有该工具的 agent 理论上能跑 skills/任意 Skill 的脚本,哪怕那个 Skill 不在它白名单里------只要它知道(或猜到)脚本路径。白名单目前是"提示层"的软隔离(控制 Agent 看得见什么),不是"执行层"的硬隔离(控制 Agent 能跑什么)。要堵这个口,可以在 run_skill_script 里加一步:从 resolveSkillRoot 拿到 Skill 名后,校验它在当前 agent 的 enabledSkills 里,不在就 Access denied。实际场景下 agent 拿到的脚本路径大多来自自己 system prompt 里的索引,越权需要主动构造路径、动机不强,但从纵深防御角度这确实该补。
  2. Cron 的 loadSkills 没跟上 per-agent 化 :它读的是全局 skills/<name>/SKILL.md,没走 findSkillMd 的"agent 专属优先"。所以一个归属非 default agent 的 Cron 任务,加载的仍是全局版 Skill,拿不到该 agent 的专属定制版。
  3. 调度器状态不持久 :setTimeout/setInterval 的 timer 都在内存,进程重启后靠 loadJobs 从 jobs.json 重排------重启期间到点的 Cron 任务会漏触发(不会补跑)。Heartbeat 同理,重启即丢失当前周期。
  4. Heartbeat 随 active agent 数量线性放大:每个 tick 给每个 active agent 都推一条巡检消息。agent 一多,固定间隔下的巡检负载和 LLM 调用量会线性增长,且都挤在同一时刻投递。
  5. 空响应静默依赖 Skill/prompt 自觉:"无变化即静默"不是框架强制的,而是靠 SKILL.md 和 Heartbeat prompt 引导 Agent 在无事时输出空串。如果某个 Skill 写得不好、无事也啰嗦,静默就失效,用户会被定时消息骚扰。

按收益排序的演进方向:

  • 补执行层校验(优先) :run_skill_script 里加 agent 白名单校验,把 Skill 隔离从软(可见性)升级到硬(可执行性);同时把 Cron 的 loadSkills 也切到 findSkillMd 的 agent 优先查找,两处 per-agent 语义对齐。
  • 调度持久化 + 错过补偿:给 Cron 记录每个任务的"上次成功触发时间",重启后对比 cron 表达式判断是否有错过的窗口、按策略补跑(至少告警)。
  • Skill 依赖不再限死标准库 :当前"仅标准库"换来了零安装的可移植性,但也限制了 Skill 能力。可以给 Skill 加一个隔离的依赖安装机制(每个 Skill 独立 venv,run_skill_script 探测/创建),在保持自包含的同时放开三方库------代价是牺牲一部分"任何机器直接跑"的简单性,需要权衡。
  • Heartbeat 错峰 + 可 per-agent 关闭:给各 agent 的巡检投递加抖动避免同刻扎堆,并支持某些 agent 关掉 Heartbeat(不是所有 agent 都需要主动巡检)。

小结

Skill 用"宿主零认知 + 通用 run_skill_script + 仅标准库脚本"三板斧做到自包含可移植,索引按 agent 白名单惰性注入 system prompt(agent 专属优先于全局)、Agent 据描述匹配后读全文执行;Cron 和 Heartbeat 则共用"伪造 InboundMessage 走统一 dispatch 链路"这一个底座,白拿路由/锁/可观测性等全部横切能力,多 agent 演进后 Cron 任务带上 agent 字段、Heartbeat 遍历所有 active agent 各自巡检,且两者都用 agent_hint 把消息直达归属 agent、Cron 任务还有 per-agent 归属隔离(list 只见自己、update/delete 拒绝跨 agent)。两条加载策略的对比也很有意思------对话场景惰性注入、无人值守全量前置,取决于"有没有人在场纠偏"。整套设计的主线是用极窄的接口解耦、用统一的链路复用:Skill 通过"跑一个脚本"接进宿主,定时任务通过"投一条消息"接进总线,两者都只用一个最小的接触面,换来宿主侧几乎为零的认知负担。

相关推荐
事界见闻1 小时前
网页版AI 3D工具通常能完成哪些基础工作?生成、处理与导出流程对比
人工智能·3d
renhongxia11 小时前
数字孪生不止在工厂:能源、医疗与农业
人工智能·深度学习·算法·机器学习·数字孪生
济南凡维1 小时前
CODESOFT中如何将标签保存为PDF文件
网络·人工智能·打印机·codesoft
weixin_469273811 小时前
Image Translate AI:利用 AI实现图片翻译
人工智能
ysu_03141 小时前
边界条件的硬约束与软约束——从试函数构造到谱分析诊断
人工智能·pytorch·深度学习·物理信息神经网络·pinns·边界条件·ntk
会飞锦鲤1 小时前
基于 Mask R-CNN 的药片缺陷检测系统
人工智能·pytorch·python·神经网络·resnet-50
kyriewen1 小时前
GPT-6 拿下模型众测第一:我拆完 30 个主题的实时榜单,「最强 AI」得看你问哪个场景
人工智能·程序员·ai编程
jimmyleeee1 小时前
大模型安全之十六:AI Security Posture Management
人工智能·安全
人工智能AI技术1 小时前
2026老后端转AI Agent:技术体检+完整复习路线
人工智能