让 Agent 从被动变为主动:定时任务和 KAIROS 模式
一、定时任务
设计定时任务,让 Agent 到点就执行,将执行结果发送给我们,这个也是一种让 Agent”主动”的方式之一,只不过是由定时任务驱动的,下面是一个简单的定时任务的设计思路,主要是核心设计:Agent 生产,轮询调度器消费
给 Agent 添加定时任务,主要是三种核心设计:
- 定时任务的存储:一个 JSON 文件存储定时任务,由轮询调度器读取来执行定时任务
- 轮询调度器:每秒读取定时任务 JSON 文件,达到条件的任务就开始执行
- 三种定时任务工具:创建、查询、删除这三种工具提供给 Agent 使用

定时任务的存储可以采用 JSON 文件的格式,里面的任务对象时间表示使用的 cron 格式
NOTECron 时间格式可以轻松的表达一次性任务和循环任务,非常的方便,并且格式统一方便调度执行
任务的创建有两种方式:用户输入和/loop 指令
- 用户输入:用户输入由模型解析任务的时间和任务指令,调用相应的创建任务的工具,将任务写入到 JSON 文件里
/loop指令:这个方式会比用户输入更加精确,会约束好一个完整的时间解析规则和指令后面的输入一起注入给 LLM,并且任务创建之后会立即执行一次,并且该指令创建的任务都是循环任务
/loop 的解析规则这里详细说明一下吧:
- 前置间隔:指令的第一个空格分隔匹配出来的数字就是 cron 的循环时间,例如:
/loop 30m check deploy - 尾部的 every:如果输入结尾有 every N,那么 N 就表示循环的时间,例如:
/loop run tests every 5 minutes - 默认规则:如果上面两种规则都没有匹配到,那么循环时间默认就是 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 消息来控制的,非常灵巧

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