最近在几个项目里,同一个仓库同时被两个 AI 编码工具改过,加上几个人自己也在推代码… 一开始觉得没什么,反正都是写代码嘛,结果踩了一圈才发现问题基本都不在代码本身,而在「谁读到了哪份规则」「谁能碰机器上的什么东西」这类协作机制上。这里记录下我最后划出来的几条边界。
先说个反转:所谓的「多方」
翻我另一个仓库时撞上件好玩的事。git shortlog -sn 出来两行、两个名字,看着像俩人在推;一查邮箱是同一个,多出来那个只是我在网页端点 merge 时的账号名。而 78 个 commit 里 63 个带 Co-Authored-By: Claude——真正的「多方」压根不是多个人,是一个人加好几个 AI 会话。并行的痕迹也不在 .claude/worktrees/(那目录空的,git worktree list 只有主仓一行),而在分支名上:五个 claude/ 前缀分支,有一次是两条同期开着最后互相合并的。
所以下面这些边界,我原本以为是给人和人定的,回头看更多是给「我和我自己开的几个会话」定的——会话之间没有共同记忆,这点比同事还严重哦。
规则文件是会分裂的
其中一个团队仓库的约定写得挺全,禁止清单、版本差异、提交规范都有——但全写在其中一个 AI 工具的规则目录下,还都设成始终生效。问题是另一个工具根本不读那个目录,它那边只有一行 import 加脚手架注入的通用提示。两个 AI 的上下文完全不同,一个知道「这台机器上有别的生产项目」,另一个啥也不知道…
我自己仓库里也有现成的反面教材:mikiacg 同时维护三套约定文件,我只改了一份,另外两份的版本号和命令说明就原样停在旧值上——细账我在讲约定文件那篇里算过,不重复了。
结论很干脆:只留一份真实来源,其余全部用 import 指过去。vns-next 和 thdl 的 CLAUDE.md 都只有一行,内容全在 AGENTS.md 里。改一处两边同时生效,没有漂移的余地。
但这招只解决「同一个人维护两份」。多 AI 的场景还多一层:另一个工具压根不读你指的那个文件。所以真正要确认的不是「规则写全了没」,而是「每个 agent 启动时到底读进去了什么」——只能一个个工具去试,没有捷径。
第一条边界:不许碰机器上别的东西
团队那台跑 agent 的机器上同时有好几个项目在跑,所以专门写了条「生产环境安全约束」,设成始终生效。禁令大意是这几条:
- 不许按端口号杀进程
- 不许
killall/pkill这类批量杀进程的命令 - 不许改系统级配置目录下的东西
- 不许
sudo任何命令 - 端口冲突只报告给人,不许自行处理
- 数据库操作只限本项目那个库
- 动手前先检查影响范围,有疑问先问
这几条我现在会抄到每台装了 agent 的机器上… agent 看到端口被占,第一反应就是把占用进程干掉,根本不知道那是隔壁项目的生产服务。
第二条边界:不许凭记忆写 API
这个坑太常见了,训练数据里的 API 和你 package.json 里那个版本压根不是一回事… 怎么把版本差异写成 ❌→✅ 对照表,我在另外两篇里举过例子,这里只说协作层面更要紧的那半。
对照表在那份规则文件里其实是压轴,开头先摆的是一条三步 SOP:先 resolve-library-id,再 query-docs,以文档结果为准写代码,并列明哪些场景「必须查文档」。先给流程再给错例,这顺序挺关键——错例有限、会过期,流程才能覆盖你没预见到的那些库。你不可能替所有人把所有库的坑都列一遍。
还有个更省事的路子:新版脚手架自己会往仓库里塞一段「别信你的训练数据,去读 node_modules 里的文档」,连 BEGIN/END 标记都带着,摆明了留给工具覆写。约定不用人写,维护成本归零,这种便宜能占就占~
反过来说,禁令也得带理由。开头那个仓库的 docs/ARCHITECTURE.md:115 夹在技术栈表格里有一行:客户端类型由后端 OpenAPI 生成,不引入 tRPC/Prisma,后面直接跟着理由——后端是 Rust axum 的 REST,根本不存在 JS 后端契约。这俩是 TS 生态的默认答案,AI 顺手就装上;光写「禁止用 tRPC」拦不住,下一个会话总能觉得这条过时了,附了理由它才不是戒律而是事实判断。同文档 :148 连镜像方的权限都限了:另一侧用 pydantic 镜像 DTO「仅用于提交,不持久化结构」——允许有副本,但副本不许变成事实来源。
第三条边界:AI 大改必须能被人读完
另一个团队仓库的 PR 模板顶部有一行注释,大意是:多人加 AI 协同的场景下,PR 是坏代码进生产前唯一的人类闸门,请如实勾选。自测清单里两条专门冲着 AI 去:一条要求控制 PR 体积、大改动拆开,理由是人得能在几分钟内读完;另一条要求提交人对里面 AI 写的部分逐行看过、并且认账。
这两条比任何 lint 规则都有用。lint 只能抓语法层面,抓不到这段逻辑跑通了但是错的…
门禁挂 pre-commit 还是 CI
同一个仓库的选择是 pre-commit 就跑全量类型检查 + 全量单测。理由写在配置注释里:CI 只在合主干时触发,而预发分支是直推不开 PR 的,只靠 CI 破坏要拖到合并才暴露…
有个细节值得抄——类型检查是全工程的,不能只喂单个文件,所以钩子里返回固定命令、直接忽略匹配到的文件名。另外还挂了层密钥扫描,本地扫暂存、CI 扫全仓。
对照下我自己几个仓库,门禁强度差得挺离谱:
| 项目 | 门禁 | 强度 |
|---|---|---|
| mikiacg | CI 七步(装依赖 → db:generate → Biome 格式检查 → ESLint → tsc → 单测 → build)+ pre-commit 走 lint-staged | ★★★★★ |
| bbps | lint 加了 --max-warnings 0,但 typecheck 是一个月后才补的 | ★★ |
| vns-next | 只有一条 "check": "biome check && tsc --noEmit",没 CI 没 hook | ★★ |
| acgn-flow | 连 typecheck 脚本都没有,唯一静态校验是 eslint . | ★ |
bbps 那条最能说明问题:lint 2025-11 就有,--max-warnings 0 拖到 2026-02 才加,而 typecheck 和 check 要到一个月后的 1d953fe9 才补进 package.json… 补上当天就抓到一个类型问题——comments-section.tsx 里那个第三方 CSS 没类型声明的导入,当场就得加注释豁免。
那个仓库还有一道我没想到的门,专门守跨语言契约:CI 里重跑一遍代码生成,再拿结果跟已提交的比。
- run: pnpm gen:client
- name: Fail if OpenAPI/TS client drifted from committed source
run: git diff --exit-code apps/api/openapi.json packages/api-client/src/schema.d.ts差一个字节就红灯。docs/ARCHITECTURE.md:176 写「改后端 = 一处改、客户端自动更新」是句原则,这两行才是让它没法被违反的东西。跟 bbps 一个套路:引入这道门那次提交顺手写着「补回此前漂移的某几个端点」——上线当场就抓到存量漂移。
顺便说下 Biome 和 ESLint 怎么分工不打架:mikiacg 的 biome.json:20-25 把 linter 和 assist 整个关掉,Biome 只管格式,lint 全交 ESLint,CLAUDE.md:109 也写明了,免得下次 AI 看到 Biome 又去开 linter~
翻车之后 28 分钟,教训就成脚本了
这是我在那个仓库里看到最漂亮的一段。那天下午先是后端崩了:评论结构自己引用自己(replies: Vec<CommentOut>),Rust 类型系统里完全合法,cargo build、clippy -D warnings、cargo test 全绿,但启动时生成 OpenAPI 会无限递归、栈溢出、崩溃循环,从 502 一路到整站 500——纯文档层的东西打挂了生产哦。九分钟后第二个:新加的顶层页面没进中间件白名单,被 i18n 当成本地化路由,运行时 404,而 next build 显示路由存在、CI 全绿。
再过十九分钟,第三笔提交把这两条教训变成了部署脚本里的两道冒烟,13 行 shell:
# ①:真跑一次二进制,拦「build 全绿但启动就崩」
SQLX_OFFLINE=true timeout 30 target/release/api --openapi >/dev/null 2>&1 \
|| { echo "✗ 冒烟失败——中止部署,未传镜像"; exit 1; }
# ②:顶层页必须在 middleware 白名单里,否则运行时 404
for _d in apps/web/src/app/*/; do
_n=$(basename "$_d"); case "$_n" in "[locale]"|dash) continue ;; esac
[ -f "${_d}page.tsx" ] || continue
grep -q "\"/$_n\"" apps/web/src/middleware.ts || { echo "✗ /$_n 未放行"; exit 1; }
done土得掉渣,一个跑二进制一个 grep,但秒级、确定性、不用起服务不用连库,拦截点还在传镜像之前——坏产物根本出不了本机。上面那些 tsc、clippy、lint 一层比一层正规,可真把站点打挂的两次,全是这十三行拦下来的。
AI 产出要怎么 review
那边两个例子印象很深。一次是 AI 大改后专门找茬的那种 review,一口气揪出十来个确认缺陷:服务端请求伪造,一个可配置的地址被服务端直接 fetch;批量导入不是原子的,失败的记录也被计进成功数;数据看板混用了滚动 168 小时和自然日两种七天窗口;缓存写入后没打标签,后面就读到旧数据。这种东西 tsc 和 lint 一个都拦不住哦。
另一个是有个状态值从没被任何代码写入过,多步骤进度条于是永远走不到最后一步——渲染了四个圈,状态最多推进到第三个。查生产库里用这个值的行数,是 0。
也有挺难得的一次:需求几乎没描述,AI 做了防御性解读,还在提交说明里写「若后续规格与此解读不符,本次改动等于无关动作」。这比改对了还难得~
自己仓库里也有靠人 review 捞回来的。mikiacg 的 c2193c3:一次 PR 把本地开发的 fallback 端口从 :3000 改成 :4000,但同一个 PR 内其他 8 处加上 .env.example 仍是 :3000,判定是「顺手 commit 进来的脏改动」,回退保持全仓一致。
AI 占比八成那个仓库做得更细:多视角独立审同一份代码,产出是带编号分级的清单(H1/M1/L1),结论还不是二元的过或不过,有个 go-with-notes 档位——能合,但这几条得跟进。抓到的都是 lint 够不着的时序问题:并发下先查后写互相覆盖、effect 重跑冲掉没保存的编辑、乐观更新失败了不回滚。
反面也在这儿:这套流程在仓库里一个字都找不到,没有常驻规则文件、没有 CI job、没有 agent 定义,它只活在几条 commit message 里——写得再三段式、再带「教训:」段落和复现命令,也只有下一个会话恰好翻 git log 才拿得到哦。
worktree 并行的几个副作用
thdl 里 git worktree list 能看到三行:主仓在 main 上,另外两个是 .claude/worktrees/heuristic-bardeen-567381 和 .claude/worktrees/sweet-poincare-f3b7bc,随机背景那个功能就是在其中一个里做的。好用,但副作用有三个。
worktree 是完整 checkout,会被 lint 和格式化扫到
mikiacg 的 eslint.config.mjs:22-25 直接把它加进忽略了:
// IDE local history
".history/**",
// Claude Code worktrees
".claude/**",biome.json 的 files.includes 里同样排除。不加的话跑一次 lint 等于同一份代码扫两遍…
多 lockfile 会让打包器选错工作区根目录(worktree 只是成因之一)
bbps 的 449d72b3 在 next.config.ts 里加了 turbopack: { root: process.cwd() },注释是「避免多 lockfile 时 Turbopack 根目录推断警告」,那会儿跟 worktree 还没关系;home 的 54b4590 撞的是同一类问题但方向反过来,要让 Next 别从父目录的 pnpm-lock.yaml 推断工作区根。worktree 只是又一个来源。
容易误提交
home 有一次把 .claude/worktrees/ 下的某个子目录提交成了 gitlink,顺手还带进去 12 个 .history/ 文件和一个 .env——git add -A 的经典副产品,commit message 里一个字都没提。后来虽然把 .history 加进了 .gitignore,但 gitignore 不会 untrack 已跟踪的文件,到今天那批东西还躺在版本库里。搞什么飞机,明明每次都跟自己说别用 -A 的…
所以 worktree 目录记得同时加进 .gitignore 和各种 ignore 列表,缺一个就是一个坑~
说到底这几条都不是代码问题,全是谁读到什么、谁能碰什么。工具再多,这两件事还是得人来定啦~
