让 Agent 从被动变为主动:定时任务和 KAIROS 模式

10 min

一、定时任务

设计定时任务,让 Agent 到点就执行,将执行结果发送给我们,这个也是一种让 Agent”主动”的方式之一,只不过是由定时任务驱动的,下面是一个简单的定时任务的设计思路,主要是核心设计:Agent 生产,轮询调度器消费

给 Agent 添加定时任务,主要是三种核心设计:

  1. 定时任务的存储:一个 JSON 文件存储定时任务,由轮询调度器读取来执行定时任务
  2. 轮询调度器:每秒读取定时任务 JSON 文件,达到条件的任务就开始执行
  3. 三种定时任务工具:创建、查询、删除这三种工具提供给 Agent 使用
定时任务设计
定时任务设计

定时任务的存储可以采用 JSON 文件的格式,里面的任务对象时间表示使用的 cron 格式

NOTE

Cron 时间格式可以轻松的表达一次性任务和循环任务,非常的方便,并且格式统一方便调度执行

任务的创建有两种方式:用户输入和/loop 指令

  • 用户输入:用户输入由模型解析任务的时间和任务指令,调用相应的创建任务的工具,将任务写入到 JSON 文件里
  • /loop 指令:这个方式会比用户输入更加精确,会约束好一个完整的时间解析规则和指令后面的输入一起注入给 LLM,并且任务创建之后会立即执行一次,并且该指令创建的任务都是循环任务

/loop 的解析规则这里详细说明一下吧:

  1. 前置间隔:指令的第一个空格分隔匹配出来的数字就是 cron 的循环时间,例如:/loop 30m check deploy
  2. 尾部的 every:如果输入结尾有 every N,那么 N 就表示循环的时间,例如:/loop run tests every 5 minutes
  3. 默认规则:如果上面两种规则都没有匹配到,那么循环时间默认就是 10m

定时任务的文件存储的内容可以参考下面这个对象:

{
    "tasks": [
      {
        "id": "a1b2c3d4",
        "cron": "*/5 * * * *",
        "prompt": "检查部署状态",
        "createdAt": 1712830000000,
        "lastFiredAt": 1712830300000,
        "recurring": true
      }
    ]
  }

关于调度器的读取,如果你觉得每秒都要读取文件带来的 IO 开销影响性能,可以考虑缓存,每秒读取缓存,每 5 秒重新读取文件

二、KAIROS 模式

在 ClaudeCode 设计思路中,有一个功能非常有意思,叫做KAIROS,是让 ClaudeCode 从交互式转变为常驻后台运行,让 Agent 从之前的被动交互,变为了主动运行,这里的有一些设计思路非常有意思,我们一起来学习解读一下

Kairos 源自古希腊语,是一个关于时间的哲学概念,指代恰好的时机,关键的瞬间

将 Agent 处理的所有任务统一放入到队列中去,我觉得这样可以让用户”单线程”的专注处理任务,

这一点的处理我很喜欢,在同一个会话中,Agent 后台运行的助手应该以不打断我的思路为前提,主动的去处理一些任务,并且 KAIROS 也指代”时机”这个意思,在恰当的时候去主动运行

队列的任务是有优先级的,并不是按照插入顺序取出,而是按照优先级来取出,用户输入的优先级是最大的

并且 KAIROS 模式的持续运行,不是简单的通过代码的 while 循环控制的,而是通过事件驱动的,通过上下文中的 tick 消息来控制的,非常灵巧

KAIROS 模式设计
KAIROS 模式设计
  1. 每一次 Agent 运行,首先从队列中按照优先级取出任务,判断任务类型是用户输入还是 KAIROS 模式的 tick 任务
  2. 任务是用户输入的话,就进入到正常的任务处理流程,和之前的方式一样,Claude 模型调用相应的工具,对上下文进行推理来完成用户的任务
  3. 任务是 tick 的话,那么更换系统提示词,使用 kairos 模式专属的提示词,Agent 根据实际情况有两种表现:执行任务和进入睡眠
  4. KAIROS 模式下执行任务,像正常模式下一样,根据上下文推断执行任务,例如:跑测试、探索不熟悉的代码块、做小重构
  5. 有意思的是 KAIROS 模式下进入睡眠,当模型根据上下文推理得到目前暂时没有任何任务需要处理,就会主动进行”睡眠状态”,睡眠多久也是由模型自己决定的,这个的实现是通过一个 sleep 工具来实现的,在”睡眠状态”,可以随时被用户输入打断,用户输入在整个任务执行队列中是”一等公民”
  6. 无论是用户输入的任务执行完成,还是睡眠状态结束,都表示一轮调用结束,调用结束之后,会有一个判断的流程
  7. 判断流程就是根据队列的状态是否为空判断,如果队列不为空,那么就什么都不做,正常进行队列的下一轮执行,如果队列为空,那么就向队列中添加一条 tick 消息,以此驱动 KAIROS 模式下 Agent 的持续运行

KAIROS 的实现有很多巧思,我这里重点梳理两点我自己很喜欢的设计

🌴第一点:睡眠状态的实现

相比定时任务的实现,固定一段时间之后运行,例如:固定每 30 分钟之后启动运行一次这种方式,

我始终能感受到有点牵强,在这种设计下实现的”Agent 主动”,其实本质没什么说服力,也是用户主动设定的运行时间,感受下来还是有点被动的味道

但是 ClaudeCode 的设计中,**将什么时候睡眠,睡眠多久完全交给模型自己,**用户只负责给 Agent 开机

  • 在系统提示词中添加判断条件:“当发现没有任务可做的时候,可以调用 sleep 工具进入睡眠状态”,将睡眠时机交给模型自己决定
  • 在工具参数中添加 duration 参数:sleep 工具要定时多久使用参数来控制,将睡多久通过工具参数交给模型控制

这种设计思路下,“主动运行”的味道更浓了一些,比定时任务赋予给 Agent 的权限更大了

sleep 工具定义的代码:

export const SleepTool = buildTool({
    name: 'Sleep',
    description: '等待指定时长,用户可随时中断',
    inputSchema: z.strictObject({
      duration_ms:z.number().nonnegative().int().describe('睡眠时长(毫秒)')
    }),
    interruptBehavior: 'cancel',
    async call({ duration_ms }) {
      await new Promise(resolve => setTimeout(resolve,duration_ms))
      return { data: { slept_ms: duration_ms } }
    }
  })

🌴第二点:tick 消息的使用

tick 是 KAIROS 模式下的事件驱动循环的触发源,该模式可以持续运行的原因是 Agent 每一次任务完成之后,都可能向任务队列中添加 tick,这样任务队列中会一直存在 tick,那么 KAIROS 模式下的 Agent 就可以不断的循环启动

tick 本质上就是一条消息,里面加一个动态时间变量

<tick>14:20:15</tick>

加入到模型上下文中的时候,是一条 user 消息

{"role":"user","content":"<tick>14:20:15</tick>"}

KAIROS 模式的设计,我觉得可以作为 Agent 主动运行的实现范式之一,核心思路是:Sleep 睡眠工具和 tick 事件驱动

这种方式比定时任务更加灵活,实现也足够优雅,开发难度会比定时任务大一点,主要是在队列状态的维护,如果你希望 Agent 的运行更主动灵活一些,那么这种设计是值得一试的