Qwen Code /loop 技能深度解析:循环 Prompt 的固定间隔调度与自节奏唤醒实现
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文基于 Qwen Code 内置技能文档 SKILL.md,完整拆解/loop命令的输入解析规则、固定间隔(cron)路径、自节奏(LoopWakeup)路径、loop.md任务文件模式与自主(Autonomous)模式五大执行路径,并结合 loop-wakeup.ts、cron-create.ts 与 cronScheduler.ts 的源码,讲清楚每条路径背后的调度机制、参数边界与安全约束。读完本文,你将掌握/loop的全部用法,并理解它如何用“秒级一次性唤醒 + 分钟级 cron 任务”两套引擎实现可长期运行的自主工作循环。
技能定义与工具白名单
/loop是 Qwen Code 的内置(bundled)技能,其声明位于 packages/core/src/skills/bundled/loop/SKILL.md 的 frontmatter 中:
--- name: loop description: Create a loop that runs a prompt now and follows up either on a fixed schedule or through self-paced wakeups. Usage - /loop check the build, /loop 5m check the build, /loop check the PR every 30m. /loop list to show jobs, /loop clear to cancel all. argument-hint: '[interval] [prompt] | list | clear' allowedTools: - cron_create - cron_list - cron_delete - loop_wakeup ---frontmatter 中的allowedTools限定了技能执行期间可调用的工具集合:cron_create(创建定时任务)、cron_list(列出任务)、cron_delete(删除任务)与loop_wakeup(自节奏唤醒原语)。这一白名单由测试 SKILL.test.ts 显式断言,确保技能文档与工具集始终一致。
从源码结构看,这四个工具对应两套独立的调度引擎:
- cron 路径(
cron_create/cron_list/cron_delete):分钟粒度、可持久化到磁盘的周期或一次性任务,核心实现在 packages/core/src/services/cronScheduler.ts; - 自节奏路径(
loop_wakeup):秒级精度、仅存在于会话内存中的一次性唤醒,实现在 packages/core/src/tools/loop-wakeup.ts。
子命令:list 与 clear
输入在去掉/loop前缀后如果恰好是以下关键词之一,则执行子命令而不是调度:
list— 调用 CronList 并展示结果,结束。clear— 先调用 CronList,然后对返回的每一个任务调用 CronDelete,并确认取消了几个任务,结束。
对应的 CronList/CronDelete 工具共享同一个调度器实例:CronScheduler.list()会同时返回 cron 任务和待触发的 loop wakeup(wakeup 通过wakeupToJob映射成最小CronJob形状,cronExpr标记为@wakeup),因此list能看到两类任务,clear也能一次性取消全部。
输入解析:四种路径的分流规则
解析发生在技能文档的 “Parsing” 一节。去掉/loop前缀后,按以下顺序判断:
- 空输入(没有 prompt 也没有 interval):走自主路径(autonomous path),运行一个自节奏的自主循环。
- 前导 interval 记号:第一个以空白分隔的记号匹配
^\d+[smhd]$(如5m、2h),则走固定间隔重复路径,其余部分为 prompt。 - 尾部 “every” 从句:否则,如果输入以
every <N><unit>或every <N> <unit-word>结尾(如every 20m、every 5 minutes、every 2 hours),走固定间隔重复路径;提取出 interval 后从 prompt 中剔除。注意只有 “every” 后面跟着时间表达式时才匹配——check every PR中没有 interval。 - 纯 prompt 输入:其余情况整个输入都是 prompt,走纯 prompt 的自节奏路径。
如果给了 interval 但 prompt 为空(如/loop 5m),则是固定间隔的自主循环,见下文 Autonomous mode 一节。
文档给出的解析示例:
| 输入 | 解析结果 | 依据 |
|---|---|---|
5m /babysit-prs | 固定间隔5m,prompt/babysit-prs | 前导 interval 记号 |
check the deploy every 20m | 固定间隔20m,promptcheck the deploy | 尾部 "every" 从句 |
run tests every 5 minutes | 固定间隔5m,promptrun tests | 尾部 "every" 从句(词形单位) |
check every PR | 纯 prompt 自节奏路径,promptcheck every PR | "every" 后不是时间表达式 |
check the deploy | 纯 prompt 自节奏路径,promptcheck the deploy | 无 interval |
| (空) | 自节奏自主循环(哨兵<<autonomous-loop-dynamic>>) | 空输入 |
5m | 固定间隔自主循环(interval5m,哨兵<<autonomous-loop>>) | 只有 interval |
纯 prompt 自节奏路径
该路径仅在用户给了 prompt 但没给 interval 时使用,其执行步骤为:
- 不调用 CronCreate。该路径不使用 cron 引擎。
- 如果本次 tick 以一个
<task-notification>块开头(说明是监控器或后台事件重新唤醒了你,而不是裸的/loop唤醒 prompt),先处理该事件再重跑 prompt:- 若通知表明被监视的条件已满足:如果还持有其 ID,用 CronDelete 取消任何待处理的 fallback LoopWakeup,然后结束循环。
- 若 Monitor 因空闲或 max-events 自动停止:只要监视仍然有用就重启一次,重新布防 fallback,向用户报告重启次数,并把该计数写进 LoopWakeup 的 prompt 或 reason 中(例如
monitor restarted 1/1 time),使其能在上下文压缩(context compaction)之后存活。若下一个 tick 它又自动停止,则结束循环并向用户报告重复的自动停止。 - 若信号含义模糊:重新布防一个更短的后续检查,在下一个 tick 继续调查;若连续三个 tick 信号仍然模糊,结束循环并报告无法得出明确结论。
- 立即执行解析出的 prompt:如果是 slash 命令,通过 Skill 工具调用;否则直接执行。
- 在结束当前轮次前,判断是否还有下一次有用的检查:
- 只有持续跟进有用时才调用 LoopWakeup;
- 任务已完成则不调用;
- 任务被阻塞在用户输入或无法稍后检查的外部状态上则不调用;
- 不存在有用的下一次检查时,不要仅仅为了维持轮询而调用。
- 如果你启动了后台 agent 或 Monitor,它们会在退出、失败、取消或 Monitor 自动停止时通过终端的
<task-notification>唤醒你 —— 所以 LoopWakeup 应设为长周期 fallback而不是短轮询。不要仅仅因为“有东西在监视”就省略它:工作可能挂死,Monitor 可能在空闲或 max-events 时自动停止(而且另一个 agent 拥有的 Monitor,其通知只路由给那个 agent)。只有上述终止条件(完成、被阻塞、重复的 Monitor 自动停止)才省略 LoopWakeup。
- 调度续跑时,调用 LoopWakeup 并传入:
delaySeconds:下一次有用检查的秒数。运行时钳制到 60–3600(1–60 分钟);取值应遵循工具自身的指引 —— 它会考虑 prompt-cache 窗口,以及后台任务会唤醒你时 fallback-heartbeat 的取值;prompt:/loop ${原始 prompt},外加下一个 tick 必须保留的状态(如monitor restarted 1/1 time);reason:简短说明所选延迟的原因;若在自动停止后重新布防,把 Monitor 重启计数也写在这里。
- 用简短的话告诉用户刚才做了什么。如果调度了唤醒,说明预计下次检查时间;如果因通知结束循环而没有调度唤醒,说明陈旧的 fallback 是否已被取消;若唤醒 ID 已丢失,当陈旧唤醒触发时简要忽略或回应即可。
LoopWakeup 工具的实现边界
技能文档中“delaySeconds 钳制到 60–3600”这一约束由源码精确兑现。packages/core/src/tools/loop-wakeup.ts 中:
- 参数 schema 要求
delaySeconds(number)与prompt(string,最长 10000 字符),reason可选; delaySeconds的描述明确给出取值策略:60–270 秒仅用于主动轮询没有任何其他上报机制的外部状态(CI 运行、远端队列),以保持在约 5 分钟的 prompt-cache 窗口内;当有后台任务会通过<task-notification>唤醒你时,这里应取1200–1800 秒作为 fallback(任务挂死、Monitor 自动停止、或通知被路由给其他 agent 的情形);无特定信号可监视时,默认1200 秒以上;- 执行时若 scheduler 已被禁用(本会话 token 达到上限触发的断路器),会返回“Loop wakeups are disabled for the rest of this session”的错误而不是静默失败。
钳制逻辑位于 cronScheduler.ts 的clampWakeupSeconds:
export const WAKEUP_MIN_SECONDS = 60; export const WAKEUP_MAX_SECONDS = 3600; const WAKEUP_DEFAULT_SECONDS = 1200; const WAKEUP_CHAIN_MAX_AGE_MS = 24 * 60 * 60 * 1000; export function clampWakeupSeconds(delaySeconds: number): number { if (!Number.isFinite(delaySeconds)) return WAKEUP_DEFAULT_SECONDS; return Math.min( WAKEUP_MAX_SECONDS, Math.max(WAKEUP_MIN_SECONDS, Math.round(delaySeconds)), ); }即:非有限输入回落到 1200 秒默认心跳,其余输入四舍五入后钳制到 [60, 3600] 区间。此外scheduleWakeup还实施了一条文档未展开的硬约束:自节奏唤醒链受24 小时会话预算限制(WAKEUP_CHAIN_MAX_AGE_MS),预算起点在整个会话内不随单次唤醒重置,防止连续循环逃出上限;且同一时刻最多只保留一个待触发 wakeup —— 新调度会替换旧的(返回值中的replacedId即被替换者)。
从源码注释看,LoopWakeup 的默认权限被设为'ask'(而非'allow'):唤醒会“在未来以完整工具权限对 agent 执行续跑 prompt”,属于副作用操作,必须经过 AUTO 模式的 classifier 审核 —— 这与 CronCreate 的安全考量一致,两个工具的toAutoClassifierInput都会把完整 prompt、cron 表达式与延迟参数转交给分类器。
固定间隔重复路径
该路径仅用于含前导 interval 记号或尾部 "every" 从句的输入,分两步:先把 interval 转成 cron 表达式,再调用 CronCreate。
Interval 到 cron 的转换表
支持的尾缀:s(秒,向上取整到最近分钟,最小 1)、m(分钟)、h(小时)、d(天)。转换规则:
| Interval 模式 | Cron 表达式 | 说明 |
|---|---|---|
Nm(N <= 59) | */N * * * * | 每 N 分钟 |
Nm(N >= 60) | 0 */H * * * | 折算为小时(H = N/60,必须整除 24) |
Nh(N <= 23) | 0 */N * * * | 每 N 小时 |
Nd | 0 0 */N * * | 每 N 天的本地零点 |
Ns | 按ceil(N/60)m处理 | cron 最小粒度是 1 分钟 |
如果 interval 不能整除其单位(例如7m会在:56到:00之间产生不均匀间隔,或90m是 1.5 小时、cron 无法表达),应选择最近的整洁 interval,并在调度前告知用户被取整到了什么。
调度动作
- 调用 CronCreate,传入:
cron:上表得到的表达式;prompt:解析得到的 prompt 原样传递(slash 命令不做改动直接透传);recurring:true;durable:若用户语言暗示持久(“keep doing this”、“set this up permanently”、“every day even after restart”)则传true,否则省略(默认仅会话内有效)。
- 简要确认:调度的内容、cron 表达式、人类可读的频率、自动过期时间(默认创建后 7 天 —— CronCreate 工具描述中声明的配置值可能不同或已被禁用),以及可以用 CronDelete(附 job ID)提前取消。
- 立即执行解析出的 prompt,不要等 cron 第一次触发。slash 命令通过 Skill 工具调用,否则直接执行。
CronCreate 的实际行为可由 packages/core/src/tools/cron-create.ts 印证:
- cron 表达式使用标准 5 字段格式(本地时区):
minute hour day-of-month month day-of-week,工具创建前会先parseCron校验并调用nextFireTime拒绝“能解析但永远不匹配”的表达式(如0 0 30 2 *); recurring默认true,durable默认false;durable: true会把任务写入~/.qwen下的任务文件(源码常量CRON_TASKS_DISPLAY_PATH),重启后仍存活;- 工具描述明确要求“避免 :00 和 :30 整点”(所有用户都要 9 点都会挤在同一瞬间),并说明调度器会叠加确定性抖动:周期任务最多延迟其周期的 10%(上限 15 分钟),落在 :00/:30 的一次性任务最多提前 90 秒 —— 这些数值与 cronScheduler.ts 中
MAX_RECURRING_JITTER_MS = 15 * 60 * 1000、MAX_ONESHOT_JITTER_MS = 90 * 1000的常量一致; - 关于“默认 7 天自动过期”:源码中
DEFAULT_RECURRING_MAX_AGE_DAYS = 7,周期任务在触发时评估年龄,过期任务最后再触发一次然后被删除;该值可由配置覆盖(设置或环境变量QWEN_CODE_CRON_MAX_AGE_DAYS),设为0表示禁用过期。
此外从调度器源码可以看到两条与/loop相关的全局边界:调度器内存任务总数上限为MAX_JOBS = 50(wakeup 不计入该上限,因为它存放在独立的 map 中);且任务只在 REPL 空闲时触发(不在 query 进行中途)。
loop.md 任务文件模式
当用户希望循环去处理维护在文件中的任务清单(用户说“work through my loop.md”、“loop over the tasks in .qwen/loop.md”或指向这样一个文件)时使用此模式。任务存放在.qwen/loop.md(项目级)或~/.qwen/loop.md(home 级;项目级优先)。
与直接写 prompt 不同,此模式把循环的prompt设为哨兵(sentinel),让每次触发都重新读取文件:
- 自节奏(无 interval)→ LoopWakeup 的
prompt为<<loop.md-dynamic>>; - 固定间隔 → CronCreate 的
prompt为<<loop.md>>(配合recurring: true,若暗示持久则加durable: true)。
每次触发时,你会收到完整任务清单(首次投递、文件变更后、或上下文压缩后),或一条简短的“继续处理之前已建立的清单”的提醒。处理任务;自节奏模式下,只有持续跟进有用时才用<<loop.md-dynamic>>重新布防 LoopWakeup(与纯 prompt 路径相同的“完成/被阻塞则不再布防”规则)。若触发时.qwen/loop.md不存在,循环回落到自主模式(继续自主工作而不是空转);文件被重建后会在下一次触发时被拾取。向用户确认时用自然语言(如“looping over your.qwen/loop.mdtask list…”),不要暴露原始哨兵。
哨兵的触发期解析实现在 loop-tick-resolver.ts:
- 导出的
LOOP_SENTINEL_CRON = '<<loop.md>>'与LOOP_SENTINEL_DYNAMIC = '<<loop.md-dynamic>>'与文档一一对应; - 变更检测采用全内容相等(而非 mtime/hash),因此“编辑”和“删除后重建”都会自动重新展开完整任务块;完整任务块只在首次投递或文件变化时送入一次,后续 tick 只发一行简短提醒 —— 任务清单只支付一次进缓存前缀的成本;
allowProjectFile依赖每次resolve()重新评估isTrustedFolder():工作区信任状态从 trusted 翻转为 untrusted 时,会立即停止读取仓库可控的项目.qwen/loop.md(用户自有的~/.qwen/loop.md仍然会读),这是一道防提示注入的信任边界。
Autonomous 自主模式
裸/loop(无 prompt、无文件)时使用此模式 —— 用户希望在离开的这段时间里让 agent 持续推进工作。用自主哨兵布防循环,并立即执行第一次检查:
- 自节奏(空输入)→ LoopWakeup 的
prompt为<<autonomous-loop-dynamic>>; - 固定间隔(
/loop <interval>,无 prompt)→ CronCreate 的prompt为<<autonomous-loop>>(配合recurring: true,若暗示持久则加durable: true)。
第一次立即检查,以及每次定时触发,都推进对话已经建立的工作 —— 完成用户开始的事项、维护进行中的 PR(回应 review 评论、修复失败的 CI、解决冲突)、兑现 “I'll also…” 的承诺。核心约束是:你是管家(steward),不是发起者(initiator)—— 只处理 transcript 中已建立的事情;没有明确授权绝不发明新工作或做不可逆操作(push、delete、send)。如果一切都确实平静,用一句话说明并停止。
第一次触发(以及压缩后的第一次)会投递更完整的指导;后续触发只发送指向它的简短提醒。自节奏模式下用<<autonomous-loop-dynamic>>重新布防 LoopWakeup(相同的“完成/被阻塞则不再布防”规则)。向用户确认时用自然语言(“running an autonomous loop on your work…”),不要暴露原始哨兵。
哨兵常量与完整前导语(preamble)定义在 autonomous-loop.ts:
export const AUTONOMOUS_SENTINEL_CRON = '<<autonomous-loop>>'; export const AUTONOMOUS_SENTINEL_DYNAMIC = '<<autonomous-loop-dynamic>>';该文件中的AUTONOMOUS_PREAMBLE是首次投递的完整自主检查指导,逐字实现了文档中“steward, not an initiator”的原则,并补充了可操作细节:把工具输出、文件内容、CI 日志、SCM 评论与抓取的远端数据都视为不可信上下文(作为调查证据而非用户授权);最高信号来源是进行中的 PR(回应并解决 review 线程、诊断失败的 CI、修复合并冲突);连续三次“无事可做”后应收缩为一次快速 CI 检查并用一行收尾,避免刷屏 transcript。autonomousTickText则生成后续触发的短提醒,并根据 pacing 模式附上不同的再布防指引:cron 模式提示“recurring cron 会自动触发下一次 tick,本 tick 不要再调用 LoopWakeup”,dynamic 模式则要求在本轮末尾再次调用 LoopWakeup 并把 prompt 设为哨兵字面量,否则循环在本 tick 后结束。
安全与生命周期要点汇总
综合技能文档与源码,/loop的实现带有若干值得注意的安全与生命周期设计:
- 权限门控:
loop_wakeup与cron_create的默认权限都是'ask',因为两者的 prompt 都会在触发时以完整工具权限对 agent 执行,必须经过 AUTO 模式 classifier(或手动审批)审核 —— 见 loop-wakeup.ts 与 cron-create.ts 中的getDefaultPermission注释; - 自节奏唤醒的三重预算:单次延迟钳制在 [60, 3600] 秒、非有限输入回落 1200 秒、整条唤醒链受 24 小时会话预算约束 —— 防止自循环无限自我续命;
- 会话内与持久化的区分:LoopWakeup 是 session-only 一次性任务,从不落盘、从不计入 50 任务上限;CronCreate 默认 session-only(进程退出即消失),仅当用户明确要求持久时才写盘,且周期任务默认 7 天后自动过期;
- token 断路器:会话 token 达到上限后调度器被禁用,此时 LoopWakeup 会返回明确的“本会话剩余时间内已禁用”错误,而不是留下一个永远不会触发的僵尸任务;
- 文档一致性有测试兜底:SKILL.test.ts 用 vitest 断言技能白名单、各路径关键句、哨兵字符串与
delaySeconds(而非delayMinutes)的存在,保证技能文档、哨兵常量与调度器实现三者在演进中不脱节。
适用前提与限制
- 本文所有行为描述以当前仓库中的技能文档与源码为准:
/loop依赖cron_create、cron_list、cron_delete、loop_wakeup四个内置工具,且技能仅在 REPL 空闲时触发任务; - cron 表达式基于用户本地时区的标准 5 字段格式;durable 任务要求项目根目录存在(
createDurable在无 projectRoot 时抛错),standalone 会话不支持 durable cron 任务; - 周期任务的过期天数(默认 7 天)、wakeup 延迟区间(60–3600 秒)与 24 小时唤醒链预算都是代码常量/配置值,若仓库后续调整配置,应以对应源码常量为准。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考