惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
I
InfoQ
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
小众软件
小众软件
B
Blog RSS Feed
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
Last Week in AI
Last Week in AI
H
Help Net Security
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Microsoft Azure Blog
Microsoft Azure Blog
L
LangChain Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
腾讯CDC

定の栈

AI 写得出功能,写不好 Dockerfile:Next.js 自托管踩坑记录 9 个 commit,没有一个人类作者:AI 独自写完一个项目之后 Next.js 渲染模式排查笔记:force-dynamic、hydration 与构建期没有数据库 让 Claude 少写错数据层:Prisma、Drizzle 和几条结构化做法 我的 CLAUDE.md 只有一行:约定文件到底该写多少 同一个月,把主页搬出 Next.js,又把 Hugo 站搬进来 macOS Tahoe 如何用自己的视频做动态壁纸(替换 Aerial 方法) Podman Compose 常用命令速查 创建 Swap 文件 Cursor 2025 Hugo 中文阅读时间计算模版 手动重加载不蒜子计数 入坑舞萌 DX 历时两月终抵 w0 与首鸟加 多邻国 600 天连胜 原神五周年纪念 Asuna 成年生日 告别绝区零 站长18周岁啦! 一位动漫迷的追番日志与热情之旅 起飞日志 Reflector 镜像列表更新常用命令与配置文件 个人自用 rsync 文件同步常用命令 历时千日原神,深渊终抵满星 Arch Linux 个人常用命令记录 《三体III:死神永生》 《三体II:黑暗森林》 《千恋*万花》与现代物理学概念奇妙碰撞后产生出的糟糕想法 Minecraft 15 周年骨折价补票入正 网页添加 Live2D 看板娘 记第二次清醒控梦体验
多个 AI 一起写代码时,我给它们划的边界
Asuna · 2026-07-21 · via 定の栈

最近在几个项目里,同一个仓库同时被两个 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 扫全仓。

对照下我自己几个仓库,门禁强度差得挺离谱:

项目门禁强度
mikiacgCI 七步(装依赖 → db:generate → Biome 格式检查 → ESLint → tsc → 单测 → build)+ pre-commit 走 lint-staged★★★★★
bbpslint 加了 --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 才加,而 typecheckcheck 要到一个月后的 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 buildclippy -D warningscargo 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.jsonfiles.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 列表,缺一个就是一个坑~

说到底这几条都不是代码问题,全是谁读到什么、谁能碰什么。工具再多,这两件事还是得人来定啦~