编辑
2026-09-05
默认
00

目录

我给AI团队造了个任务队列,七天后又亲手拆了它

我给AI团队造了个任务队列,七天后又亲手拆了它

2026年9月4号晚上,我把 queue-cli.py 挪进了 archive 目录。3008行,22个子命令,配套的回归测试脚本写了10万字节。从8月28号晚上立项到9月4号退役,这个工具满打满算活了一个星期。

这不是一次失败的收尾,恰恰相反,我觉得这是一次挺完整的工具生命周期。趁着记忆还热,把整个过程写下来:为什么造它、怎么造的、为什么又不用了。

一、理念:任务不丢,完成必达

先交代背景。我在服务器上养了一队AI agent,有管调度的、写代码的、做审查的、采情报的,日常产生大量后台任务:派子代理去改代码、跑CLI命令、定时巡检。任务一多,问题就来了:谁在跑?跑完没有?结果发给谁?挂了谁管?

我对后台任务管理就三条要求:

  1. 任务不丢。任何派出去的任务都要有账可查,网关重启也不能失忆。
  2. 完成必达。任务做完,结果必须自动送回发起它的会话,不能指望agent的记性。
  3. 挂了要喊。进程死了、状态卡住了,得有东西巡逻发现并且告警。

在 queue-cli 之前,这三条靠一个共享JSON文件撑着。所有agent都能读写它,没有锁,没有备份,没有校验。能一直没出大事,纯属运气好。

二、压垮骆驼的一场事故

8月28号傍晚6点22分,主会话切换任务时把 .active-task.json 整体覆写了,串行队列里排着的三批任务全部冲掉。19点05分人工恢复,丢的账捡回来了,但我坐不住了。

复盘根因,三层叠一起:

  1. 单文件双职责。当前任务和排队任务挤在同一个文件里,改任何一处都是整体覆盖,误伤半径无限大。
  2. 没有写前备份。写坏了就是写坏了,回不去。
  3. 没有写入拦截。谁来都能写,格式对不对、会不会把别人的字段冲掉,没人管。

当晚拍板:不缝补丁,重做。方向五层:文件拆分(当前任务和队列分成两个文件各管各的)、写前快照、原子写、单写通道、心跳巡逻。当天夜里走完内部评审,第二天实现落地。

三、实现:把人人都能写,变成只有一个人能写

核心思路一句话:所有写操作收口到一个命令行工具里,就是 queue-cli.py。

任何agent想改任务状态,只能调它的子命令:入队、出队、登记、完成、改状态。工具内部流程固定:抢文件锁、读最新数据、业务校验、写前快照、原子写、读回校验。谁也别想再玩整文件覆盖那一出。

几个实现细节我自己挺得意:

  1. 原子写不是写进去就算。临时文件写全、fsync刷盘、rename原子替换、写完再读一遍核对,最后父目录也fsync。麻烦吗?麻烦。但被丢过一次任务的人不会嫌麻烦。
  2. 锁单独放一个文件。因为原子写靠rename换inode,锁要是写在数据文件里,会随着文件替换整个失效。这是个不踩一次不知道的暗坑。
  3. 幂等表。每个操作带唯一键,重复执行直接拒绝,防agent抽风重复登记。

在这个底座上,长出了当时看挺完整的一套编排能力:

  1. 串行队列。同类任务排队一个一个跑,不抢资源。
  2. 并行槽。不同仓库的任务最多3个并行,超了排队等。
  3. 同仓库互斥。执行器扫/proc,发现同仓库已有实例在跑直接拒绝,防写冲突。
  4. 峰谷排队。DeepSeek有峰谷定价,高峰贵、凌晨便宜。任务可以打延迟标记,高峰期自动压着不派,谷底自动放行。省钱。
  5. 心跳巡逻。每轮心跳检查队列健康,进程消失、状态卡住超过15分钟,立刻告警。每个任务登记时都记着report_to_session——完成后结果投递给谁,任务到终态就由心跳负责把结果送回去。没人收的完成事件等于白干。

然后它开始长大。

8月30号,加update子命令,改字段不用再整文件重写。

9月2号,我抓到有agent先把执行器跑了、回头才补登记,甚至有不登记的。顺着查时间线,发现登记和启动之间全是窟窿:登记完没启动的,启动了没登记的,进程都跑起来了登记还没落地的。于是加start子命令:一把锁内完成登记,锁外拉进程,2秒探活,起不来自动回滚重新排队。登记先于启动,从机制上保证,不再靠自觉。

9月3号,又给JSON文件加了PostgreSQL持久化层。到这里,这个工具3008行、22个子命令、几十个回归用例,还有一套心跳探针围着它转。

但你应该已经闻到味道了:一个任务队列,怎么越养越像一个系统。

四、为什么不用了:平台把它长出来了

9月4号,新升级的OpenClaw 2026.8.2摆在我面前,原生任务系统落地。我拿queue-cli的功能清单逐条对照:

  1. 自动台账。子代理、ACP、定时任务、CLI后台任务,一启动自动入账,SQLite持久化重启不丢,60秒一轮自动对账。最妙的是没有登记这个动作了——spawn即登记。
  2. 完成投递。任务完成自动推回发起会话,发起会话记在任务的requesterSessionKey字段里,不用手工登记。投递失败进blocked,30分钟退避自动重试。
  3. 展示。进度卡、任务面板、控制台任务页,全有。
  4. 目标锚定。goal机制,防多轮任务漂移。

我的第一反应是不信。让调度agent花了一下午做了五组真实验证:两类执行器真实启动、强制崩溃看失败标记、退出码核对、编排镜像记录、纯命令行无会话触发。全过。当时账本里已经有1269条生产任务记录在自动流转。

当然不是没缺口。串行队列、并行槽、峰谷排队,原生都没有等价物;CLI直跑的完成通知默认静音;复合命令会让退出码失真;原生投递也有失败案例,审计里躺着31条warning。原生不是零运维,只是换了个地方运维。

摆了三条路:渐进式,队列留着管编排、账本迁原生;混合式,砍掉三分之二子命令留个精简编排层;全拆。我拍板:全拆,不做瘦身迁移。

理由想清楚了:

  1. 原生覆盖的六七成,恰好是高频但低价值密度的部分——记账、投递、展示。天天要用,但没人想维护它。
  2. 剩下那三四成的编排语义,恰好是最贵的部分。贵不在代码,在规则面:queue-cli的用法散布在26个规则文件、一个提示词注入hook、两个执行器脚本和一堆心跳探针里。之前一个星期,hook注入文本和规则文件之间漂移了8次。每次改动都是一场同步风暴。
  3. 最关键的一条:spawn即登记是结构性消灭问题。start子命令是在登记和启动之间的窟窿上打补丁,补得再精致,窟窿的形状永远在。原生把两个动作合成一个动作,窟窿直接不存在了。好的设计是让问题无法发生,而不是防住问题。

放弃的东西也想清楚了:串行排队改并发派发,峰谷自动排队改成调度agent手动错峰,失败自动回滚重排改成发起方自己重试。都是可接受的退化。

五、拆除与复盘

拆比想象中利落。晚上拍板,34项清单当天全部落地:26个规则文件批量改写,hook注入同步(再不同步就是第9次漂移),两个执行器脚本的拦截门换成进程树校验,queue-cli.py和测试脚本归档,运行时数据归档,记忆库写入终态记录。最后派了个测试子代理走全链路:自动入账、进度卡展示、完成投递回发起会话,一次通过。

PostgreSQL里的tasks表停写留查,坟头留碑。

复盘几条:

  1. 自建轮子的隐性成本不在代码,在同步面。3008行代码好删,26个文件里的规则、hook里的注入文本、各agent记忆里的旧经验,才是真正难清理的。所以退役清单里最大的一块不是卸载,是防复活:往记忆库和经验库里写指向条目,明确告诉未来的agent——这套机制已下线,别按旧记忆办事。
  2. 造它的那七天不亏。恰恰因为自己造过,我们才有一份精确到字段的需求清单,升级那天才能一下午验完原生系统的成色。没造过轮子的人,连该测什么都不知道。
  3. 工具的寿命可以很短,但依然是好工具。它在没有原生方案的日子里,把任务不丢、完成必达从愿望变成了机制,顺带把并发写坏文件这类坑全踩了一遍。
  4. 平台在长,胶水在烂。今天该问的问题不是「我能不能造」,而是「平台是不是快长出来了」。如果答案是快了,就用最糙的方案顶住,别精装。

现在我的后台任务面板长在控制台里,任务页一刷,谁在跑、谁挂了、谁的结果没送出去,一目了然。

queue-cli,生于事故,卒于升级,享年七天。安息。

本文作者:丘丘

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!