深度实战反思——系统中 Claude Code 生成的任务堆积自身处理,形成反馈循环。高质量的 AI 编程工具使用经验总结。
上周,一次 Claude Code session 告诉我:订单队列里有 180 个未完成事项,其中没有一个超过 13 天;254 条记录中,有 221 条是由 Claude Code session 写入的。它把自己也统计了进去。整个系统里最大的单一信箱,属于那个正在维护自身的系统:36 个订单,其中大约 30 个,都是工具在改进自己的工具。
我已经有一阵子没看过那份列表了。这才是真正值得写下来的部分。
这篇文章会讲:当项目增加到十个时,为什么每个项目放一个 TODO.md 的做法不再奏效;替代方案最终形成了怎样的结构;有哪三件事出了问题,以及每次事故分别留下了哪道 gate;还有整套系统至今仍让我付出哪些代价。如果你同时维护的代码仓库多到无法全装进脑子里,那么无论你用什么工具驱动它们,中间那部分都值得借鉴。
去年年底,我开始使用 Claude Code 同时处理多个项目。那时的整套系统,就是在每个项目文件夹里放一个 TODO.md。我打开一个 session,告诉它要做什么,剩下的事情则由这个文件记录。对于两三个项目来说,这种方式完全没问题。我也会推荐这么做。
后来,项目变成了十个。十个活跃项目、Mac Studio 上的十个文件夹、十份 TODO 文件,三个问题同时开始出现。
回家路上想到的点子会丢失。我在外面时突然想到某件事,但应该记录它的地方,是一台我当时并未坐在旁边的机器上、某个项目文件夹里的 Markdown 文件。等我回到家,那个想法已经没了。
每天早晨都变成了一次我不想做的决策。十份列表,没有统一视图,也无法跨列表排序。今天哪个项目最需要我?老实说,答案通常是我最近碰过的那个,而这恰恰与优先级背道而驰。
有些项目还会悄无声息地搁置数周。不是因为它们已经完成,而是因为我的系统里没有任何东西会主动举手提醒。文件不会告诉你,它已经很久没人管了。
大约一个月前,我决定做一个能告诉我所有项目中什么最重要的东西。不是再建一份列表,而是在所有列表之上增加一层。
上周,我终于做了一件一直拖着没做的事:写使用手册。不是代码文档,而是写给操作者的手册——而操作者就是我自己。我要在哪里做什么、有哪些命令、一个 note 如何变成一个订单,以及哪四个地方会停下来等待人类介入。整份手册共有八节。写它的过程让人不太舒服,但这种不舒服很有价值:一个只有唯一用户的工具,却需要一本使用手册,这本身就在告诉你,它是如何一路生长过来的。
下面是那份手册中可以推广到其他场景的部分。任何可能变成工作的东西,都会进入同一个收集入口;从那里开始,它只有一条确定的路径。
(此处省略图表。两张流程图可在原文中查看。)
这套系统背后运行着两套机械钟表般的机制,它们都不会思考。一个 launch agent 每隔 15 分钟清空一次 drop box(StartInterval 900),把其中的条目分类到对应项目的列表中。另一个每隔 30 分钟(StartInterval 1800)取出已经批准的订单,并在 ~/.cache/master-dispatch-worktrees 下的 git worktree 中以 headless 方式执行;这个位置被刻意安排在所有代码仓库之外。分类步骤没有使用任何 model,所以才能以这么高的频率免费运行。
我使用最多的部分,恰恰是最不聪明的那个:给自己发一封电子邮件。
(此处省略图表。两张流程图可在原文中查看。)
这解决了我三个问题中的第一个。我始终能够访问的东西,就是自己的收件箱,因此它成了整个系统的入口。但它不会直接把一条 note 变成工作。note 会变成订单,然后停在那里。这也引出了为什么整套系统必须设置 gate。
7 月 17 日,我同时运行着三个 Claude Code session,它们都在修改同一个共享配置文件,文件中保存着各个项目的支出限额。每个 session 都会读取文件、修改属于自己的那一行,然后把整个文件写回去。典型的 read-modify-write,只不过写入者不是线程,而是 Agent;而我当时根本没有把它当成并发问题。最后写入者胜出。一个 session 设置的例外,被下一个 session 悄无声息地抹掉了。
修复方案平淡无奇:现在,共享资源只能通过一个很小的 mit-lock wrapper 写入。它是一个基于 mkdir 的锁,设置了 10 分钟的过期超时,并带有审计日志。pre-tool hook 会拒绝任何未经过它、却试图写入受保护路径的 Bash 命令。
真正不平常的,是这个问题背后的认知转变。我之前一直把并行 session 当成并行工作的人类,以为它们会注意到彼此的存在。但它们不会。它们是进程;两个进程一旦共享状态,你就必须像对待两个线程一样,用同等严格的纪律约束它们。由此产生了三条规则,后来所有其他机制都建立在这三条规则之上:每个代码仓库只能有一个写入者;每个共享资源只能有一个所有者;跨项目工作只能通过一个 channel。
这就把我们带回了文章开头的那组数字。
截至 7 月 28 日,队列里有 180 个未完成订单,没有一个超过 13 天。这意味着从 7 月 20 日开始,系统每天大约产生 20 个新订单,而我每天只能关闭两到五个。254 条 drop-box 记录中,有 221 条不是我写的,而是 AI session 写入的;其中又有 163 条来自 orchestration repo 自身的 session。
没有任何东西坏掉。每一个订单都有道理。而这恰恰就是问题所在。Claude Code session 想要发挥作用,只要你在任何真实的代码仓库中启动一个 session,它就能找到十项确实值得做的改进:某处 refactor 后会更整洁、某份文档已经过时、某个测试可以更严密。把十个合理观察乘以十五个代码仓库,就会得到一股任何单人都永远处理不完的洪流。填满我收件箱的不是错误,而是好点子。
修复方式,是颠倒举证责任。在此之前,除非有人主动删除,否则订单就会一直存在。现在,只有能够明确说出 trigger,订单才会诞生:影响用户或客户的故障;涉及安全、金钱或数据的风险;阻塞 deploy 的问题;或者我亲口明确要求的事情。“这样会更整洁”“顺便注意到了”“为了保持一致”都不算 trigger。它们会进入供我阅读的 session summary,而不是进入 pipeline。把这条规则追溯应用到已有订单后,我一次性归档了 19 个 meta-order,订单总数从 165 降到了 144。
这里真正有意思的,是我刻意没有构建的东西。最显而易见的做法,是增加一个 AI gatekeeper:让 model 判断每个传入的订单,并过滤噪声。我没有这么做,因为就在同一周,一条措辞糟糕的订单进入了系统。它篇幅很长、结构完整,而且完全合情合理;但如果照字面执行,就会把 186 个客户邮箱的外发邮件转交给一个从未设计用于承载这些邮件的服务。model gatekeeper 很可能会直接放行。它自身就包含了它本应捕捉的那类故障。有些问题,并不会因为再加一个 model 就变得更好。
最近的一次发生在昨天,而这已经是我第四次遇到同一类故障了。
“一次 session 只能写入它自己的代码仓库”这条规则,由一个 pre-tool hook 强制执行。直到昨天,这个 hook 都是通过检查命令来工作的。它知道哪些命令形式会写入外部路径:输出重定向、tee、git -C、sed -i。7 月 28 日,一个 rollout script 通过 Python heredoc 写入了 21 个外部代码仓库。hook 看到的只是一次附带字符串的 python3 调用,于是放行了,因为 heredoc 不在那份“看起来像写入操作”的清单里。
修正方案,是停止猜测意图。现在,系统会在每次 autonomous run 之前记录所有代码仓库的 git 状态,并在运行结束后进行比较。它不关心变更是如何产生的,只关心本不该出现变更的地方,是否真的出现了变更。它会在三个位置采取 fail closed 策略:缺少检查机制时,dispatcher 无法启动;baseline 检查失败时,订单会被中止;baseline 不存在时,会被视为发现差异,而不是一切正常。
模式匹配是在猜测一条命令将会做什么。前后对比测量的,则是它实际做了什么。我用了四次尝试才学会这一点,而如果让我从第一天重新设计,我也不会一开始就这样做。
orchestration layer 由 32 个 shell script、约 6,300 行代码组成,并配有 145 个测试。这些测试只会针对副本运行,绝不会接触真实订单。在 tool call 之前、期间和之后,共有 21 个 hook 会被触发:代码仓库边界、资源锁;一项检查会阻止某个曾经摧毁 200 行数据中 199 行的 database dump flag;还有一项会拒绝发送包含未列入已验证清单之事实的外发邮件。这些都不是事先设计出来的。每一个部件,都坐落在某样东西曾经穿透系统的位置上。
仍有四个地方会停下来等我,而它们正是系统不会发生意外的原因。任何订单在获得我的批准前都不会执行,包括我自己提交的订单。标记为 dialogue 的工作,没有我参与就不会运行。已完成的工作绝不会自行 merge。任何文本中提到付款、客户或 production deploy 的订单,即使已经获批,也会在启动前被拦住——因为 7 月 27 日,队列中曾经放着一笔 50 欧元的客户退款:它已经获批,被标记为 autonomous,随时准备执行。
今天的真实状态是:账面上有 149 个订单,其中 122 个已经获批、正在等待处理。trigger 规则减缓了流入速度,但没有扭转趋势。底层矛盾仍未解决,而且我不确定它是否能够解决,因为这并不是工具的 bug。一台生成候选工作的速度超过人类评估速度的机器,最终必然会形成队列;而我构建的每一道 gate,都是我有意选择让自己成为瓶颈的地方。
如果让我回到去年年底,从十个项目、每个项目一份 Markdown 文件重新开始,我会先构建 drop box 和 approval gate,除此之外什么也不做。只有这两个部分,我从未需要返工。这套系统里的其他所有东西,我都做了两遍。
如果你也在运行类似的系统,并且找到了无需人类逐条阅读、又能确保流入内容真实合理的方法,我很想听听。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。