写这篇之前先看了一眼构建机:5 个 tmux 会话在跑,~/.local/state/ai-jobs 下面 372 个任务目录、3.1 GB,load average 2.90 而且一直在二到七之间飘。这台机器 128 核、开机四天半。数字是采样的一瞬间,写完这段又变了——这本身也是个说明。

同一时刻手上的项目:一个 Rust + SQL 的站点在改统计查询、一个 Next.js 应用在退役 service worker、一个 Bun + Hono 的服务在修隧道 origin、一个 Prisma + tRPC 的 monorepo、一个 C# 的游戏私服在补资料字段、一个 C/C++ 的 SDL3/WASM 移植、两个 Zola/Hexo 的文档站。~/Code 下 64 个目录、36 个 git 仓库。

这不是"我同时开了很多窗口"的那种并行。窗口谁都会开,难的是同时推进而不互相污染。

先说清楚为什么值得

单开一个项目的时候,AI 协作最贵的是等待:跑一次全量测试十分钟、等一次构建五分钟、等浏览器回归跑完。这段时间人能做的只有干等,或者切去看别的——一切换,回来又要重新装载上下文。

八个项目并行的第一个收益就是这个:等待被别的线填满了。A 的构建在跑,B 的失败日志正好回来了;C 等一次人工确认,D 的测试红了要修。整机的吞吐被拉起来,人脑不再有空档。

第二个收益是同一个问题能在别的线上复用。今天在 av-site 想明白「建索引必须和取锁待在同一个事务块里」,在 AstroDX 就自然想到「退役 worker 的删除顺序」——两件事都是「两步之间不能被别人插进来」。这类跨项目迁移的直觉,只有在几条线同时在手上时才发生。

但收益有前提,下面是这些前提。

规则要分层,不能复制

~/.agents/AGENTS.md 是唯一正本,~/.codex/AGENTS.md、~/.dsh/AGENTS.md 是指向它的软链接,~/.claude/CLAUDE.md 用 @ 引用。要改规则只改这一份——复制三份必然会漂。

正本里只有跨项目通吃的:中文对话、Conventional Commits、本机不是构建机、远程走哪台、超 30 秒的任务怎么持久化、代理在哪。

项目级 AGENTS.md 只写这个项目特有的,而且真的有人写得很窄:

  • av-site/AGENTS.md:CI 五步全绿是合并前提;改 OpenAPI 类型要 bun run gen:client;带 #[ignore] 的数据库测试怎么起临时 postgres;从构建机 curl 要带 UA 否则 Cloudflare 403;生产排查前加 SET default_transaction_read_only = on;。
  • eagler-touhou-next/AGENTS.md:这台 Mac 只读写和 Git,构建机上的检出目录是哪个、bun 锁在 1.4.2、项目自己的 postgres:18 容器在哪。
  • th15/AGENTS.md 只有 15 行:两棵树的归属。

中间那类「构建机是哪台」其实两个文件都写了。看着重复,分工不同——正本说"有构建机、按优先级挑",项目文件说"这个项目已经定死是这台,检出目录在这里"。前者是规则,后者是事实。

还有一条硬的:每个项目都要能写清"在哪跑",写不清就说明它还没准备好被并行。

任务必须有身份

远程跑的东西不能是 ssh host 'bun run build' 就完事——SSH 一断就丢,重连也不知道跑到哪了。

正本里那条「远程 SSH 任务持久化与断线恢复」规定的是这样的(所有项目共用):

每个任务一个独立 ID、一个独立 tmux 会话、一个 ~/.local/state/ai-jobs/<任务ID>/ 目录。目录里 run.sh 开头就 set -uo pipefail、umask 077、exec > output.log、trap 把退出码写进文件、echo $$ > pid、date -Is > started,然后显式 export PATH 和 HTTP(S)_PROXY。

启动、日志、退出码、进程号、开始时间五样都在盘上。SSH 断了就按原 ID 回去读状态,不重新投递。tmux 会话没了但也没有退出码,状态算未知,继续核对进程和产物,不能当成功也不能当失败。

禁止的是共用固定的 ai-task 会话——几条线往同一个会话里 send-keys,等于把命令往一个不认识的 shell 里扔。

近期任务的退出码有 0 也有 1(adjust-download-policy-red 是 1、adjust-prod-cold 是 1、adjust-consent 一批 1/0 混着)。并行不是八条线都绿,是八条线的结果各自落盘、各自可查、红的不会把别的拖下水。

一次只写一处

同一个文件两条线同时在改,是这个工作流的头号事故来源。

分派的单位不是项目,是文件。今天四条线的写集是:av-site/infra/migrations/*、AstroDX/apps/web/public + src/lib、acgn-flow/src/server/*、drama-site/apps/web/src/*。四组互不相交。

反过来,如果要改同一个 package.json 或者根 tsconfig,就把它们排成前后依赖,先做完一条再开另一条。硬并行在这里只会产生 git 冲突和互相覆盖的编辑。

跨项目必须改共用配置的时候,我宁可停下手上的线,等它落地。

验证按改动范围分级

并行时最容易松掉的是验证,因为每条线都想快点收工。

规则是按改动分级,不是按时间宽裕程度:

  • 改一个文件 → 只 lint 那个文件、只跑那个测试文件,本机就能做。
  • 改到跨模块 → 上去构建机跑该项目的整库检查(AstroDX 的 scripts/remote/verify.sh 会跑 739 个测试 + 719 个文件 lint)。
  • 有副作用的(迁移、部署、数据修复)→ 加上真机验收,比如 av-site 改播相关必须跑 check-live-playback.py 且含一轮限速,smoke 全绿不算数。

降级验证的理由只能是"这个改动不值那个成本",不能是"现在不空"。 不空恰恰是并行带来的,用不空去换便宜验证,就是把并行省下的时间又赔回去。

代价,记在明处

上面都是方法,下面是账。

上下文会互相稀释。 八条线在手,每条的记忆都变薄。所以每条线落地必须立刻把"为什么这么改"写进代码注释或那个项目的文档——不能指望下次还记得。0055 那个迁移文件 86 行里有 52 行注释、AstroDX 的 docs/verification/service-worker-retirement-2026-10-30.md 里连脚本自己出问题都记下来,都是这个原因。

review 是瓶颈,而且不随并行度增长。 八条线各自产出,合并判断全压在一个人身上。所以宁可让 AI 慢一点、产出少一点,也不要让它一次改 20 个文件——分解不了的巨型改动,看完一遍比自己做还久。

构建机资源是共享的。 load average 二到七看着不高,但那是 128 核的余量;同一时刻几个 tmux 会话里有两三个在跑浏览器回归,抢内存。跑全量测试的线要排在空闲核上,taskset -c 0 那种单核用例还会被别的线拖慢,慢 CPU 上的时序问题只在那个条件下暴露——这是好事也是坑。

任务目录不清理会涨。 372 个目录、3.1 GB、开机四天半。跑完之后 --keep 留证据,一周后该扫一遍;现在还没扫,已经算欠账。

还有一条常被忽略的:并行会让"完成"变得廉价。 一条线红了没人管,另一条线绿了照样往下走,红的就被埋了。所以每天收工要看一遍退出码非 0 的目录,不是看"今天交付了几个"。

谁适合这么干

不适合的人:改动强线性、一次只能想一件事、不喜欢写规则文件。

适合的前提只有一个:项目已经能被远程形态验证。有构建机、有 verify 脚本、有写在文件里的"在哪跑"——这三样齐了才能拆线,缺一样,并行度提高只是把"卡住"从本地挪到远程。

至于 AI 那一侧,它不知道也不关心有别的线在跑。它是靠规则文件里"别动那个目录"的约束、靠任务目录的身份、靠写集不相交,被隔开的。这些隔断都是人事先搭好的,AI 只是跑在里面。

八条线同时跑的时候,我在干的事其实很无聊:读退出码、看日志、判断哪条该继续、哪条该停、把原因写进文件。真正的并行发生在磁盘和 CPU 上,人的部分仍然是串行的——只不过串的是判断,不是敲键盘。