两个 AI 同时改代码,为什么一个人的成果消失了?

多开一个 Agent,也多了一个能改动现场的人。用文件覆盖、端口串线和无冲突合并三个例子,讲清怎样划分写入权、Git worktree 能隔开什么,以及为什么合并后的结果必须重新验收。

两个 AI 同时改代码,为什么一个人的成果消失了?

开两个 AI 编码窗口,一个改导出功能,一个补搜索框,看起来是最自然的提速办法。两边都报告完成,打开页面,却发现刚才明明出现过的导出按钮不见了。

第一反应容易是:哪个模型不够聪明?但我会先看另一件事——这两个窗口,是不是站在同一个目录里,动着同一份文件。

会话分开了,不等于现场分开了。两个 Agent 可以各自记着完整的任务说明,却共同使用一份源码、一个开发服务器、一套测试数据。只要它们都能改,彼此就会成为环境中的变量。很多所谓“AI 忘了刚做的事”,其实是另一位参与者把现场换掉了。

一个人的成果,是怎样被另一个人覆盖的

设想一个小型笔记应用。下面的过程是为了说明机制的假设案例,不是某次真实事故。

最初,页面只有搜索框。Agent 甲读了组件文件,准备加导出按钮;Agent 乙也读了同一个文件,准备调整搜索提示。两人手里的起点相同,都是旧版本。

甲先把按钮写回去。此时磁盘上有搜索框,也有导出按钮。乙随后根据自己先前读到的版本,重写整段组件:提示语改好了,按钮却没被带上,因为乙构造新内容时根本没见过它。

这个过程不需要两次写入恰好落在同一毫秒。只要“读取”和“写回”之间存在间隔,就可能出现。最后留在磁盘上的文件语法完全正确,构建也能通过,甚至没有任何错误消息。丢失的是另一项需求。

假设时间线:甲乙先后读到旧版,甲写入按钮,乙按旧版整体写回后覆盖按钮;问题发生在读取与写回之间

有些编辑工具会在旧文本不匹配时拒绝应用补丁,这能挡住一部分覆盖。可工具不同,写法不同,保护也不同:精确替换几行、重新生成整个文件、运行格式化器,碰到的边界并不一样。不能把“上一次工具报了冲突”推成“以后任何覆盖都会被拦住”。

这也解释了为什么事后查看 Git,未必能看到一场合并冲突。两个会话如果直接改同一份未提交文件,Git 看到的可能只是最终的一次修改。中间曾经出现过、后来又消失的按钮,没有自动变成一段可合并的历史。

所以我更愿意先给写入安排一个主人。甲拥有导出组件,乙拥有搜索组件;必须共同修改的入口文件,先约定由谁汇总。这里的“拥有”不是永远禁止别人碰,而是在这一轮任务中,修改之前需要交接。

分支是名字,工作目录才是现场

“那就让它们各建一个分支。”这句话还差半步。

如果仍然共用同一个工作目录,一个会话切换分支,会改变另一个会话接下来看到的文件。在编辑器里多开标签,在终端里多开窗口,都没有复制目录。分支能组织提交历史,不会自动给每个会话分配一张独立桌子。

Git worktree 正好可以把这一步补上:同一个仓库挂接多个工作目录,每个目录有自己的检出内容,也有自己的 HEAD 和暂存区。这样甲在自己的目录里改文件、暂存和提交,乙的工作目录不会因此变成甲的版本。

但这些目录仍然属于同一个仓库,分支引用和对象库等信息是共享的。它不是把整个机器复制一遍,也不是权限沙箱。Git 通常还会阻止同一个分支被同时检出到两个工作树,日常使用没必要强行绕开这个保护。

来源:Git 官方文档:git worktree

我会先固定一个已经确认的起点,再给每个任务分配不同分支和不同目录。例如下面只是示意命令,假设当前提交已经适合作为两个任务的共同起点;实际使用时先确认工作区状态与分支命名。

git worktree add --no-track -b demo/export ../demo-export HEAD
git worktree add --no-track -b demo/search ../demo-search HEAD

任务说明也要随之具体化:“只在 demo-export 中工作,负责导出组件及其测试;需要改搜索组件时先报告。”光把目录建好,却没告诉 Agent 进入哪里,仍可能让它在原目录继续执行命令。

还有一个细节:分工要考虑工具的实际写入范围。任务看起来只改一个按钮,运行格式化命令时却可能改遍整个目录;安装依赖也可能更新公共锁文件。与其等两个任务各交一份锁文件再猜该留谁的,不如把依赖升级集中交给一个人,其他任务基于同一份依赖开始。

目录隔离也不应被当作安全承诺。一个拥有同等文件访问权限的 Agent,仍可能通过绝对路径走进别人的目录。明确目录是协作约定;真要限制它能访问什么,需要工具权限或沙箱提供约束。这两层作用不同,不能因为路径名字不同,就以为误操作已经不可能发生。

这个安排还有一个常被忽略的收益:别人能检查甲交出的到底是哪一份结果。交付时附上分支、提交和测试结果,比“我已经改好了”多了一个稳定落点。之后环境继续变化,也可以回到那份提交讨论。

两个工作树各有文件和暂存区,下层仍共享仓库;端口、数据库和临时路径位于工作树边界之外,需要另外分配

文件隔开了,测试仍然可能串线

现在甲乙有了独立目录,代码不再直接互相覆盖。甲在本机启动预览服务,乙也启动一份。两份配置都写着同一个端口,于是新的问题出现了。

一种情况很好认:第二份服务启动失败,提示地址已被占用。另一种更容易误判:工具自动选了别的端口,Agent 却仍然打开旧地址。它在浏览器里看到的是甲的页面,却把结果写进乙的验收报告。页面能打开,并不能证明打开的是刚改的那份代码。

这是假设的第二个例子。应对它不需要复杂系统:给任务分配不同端口,记录实际启动地址,再在页面或响应里确认一个属于本次修改的特征。若工具允许,可以临时展示版本标记;至少核对新行为,不能只截一张“网页正常”的图。

同样的事也发生在数据里。两个目录都连接 demo_test,甲测试导出时创建几条记录,乙测试清理逻辑时把记录删了。甲的测试忽然失败,重跑又成功,容易被当成偶发问题。文件隔离没有帮上忙,因为两份程序写的是同一套数据库。

临时文件也一样。源码目录不同,但脚本都把结果写到固定的 /tmp/demo-result.json,后写的人仍会覆盖先写的人。把路径搬出源码目录,并不等于它自然变成私有的。

我会沿着“这项任务会写到哪里”往外查:文件、数据库、端口背后的进程、缓存、测试账号和输出目录。能独占的资源给一个任务;能按名字分开的,就加入明确的任务标识;确实必须共享的,指定写入顺序。只读资料通常可以共享,可变状态需要单独考虑。

两个任务拥有不同工作台,但若插向同一个测试数据库仍会相互影响;图中把独立目录、端口和测试数据作为一整套分配

还可以用一个很小的交叉检查找出串线:先让甲单独跑关键动作,再让乙单独跑,最后一起跑。若单独都稳定,只有同时运行才失败,资源竞争就值得优先调查。这个现象只是线索,不能直接认定根因;接下来仍要检查实际连接、进程和输出路径,找出哪一处状态被另一方改变了。

尤其要小心“失败后重试就好了”。重试可能只是碰巧避开另一项任务的写入时刻。把重试次数加大,表面通过率提高,原来的干扰却仍在。记录每次运行的任务标识、起止时间和资源名称,通常比只保存最后一张成功截图更有诊断价值。

这里不必一上来就给每个 Agent 配一套完整云环境。小项目也许两个端口、两个临时目录就够了;涉及数据库破坏性测试,再考虑独立库或独立实例。隔离的程度,应当跟任务实际写入的范围相称。

如果环境成本高到无法合理隔开,把那一段改成串行也是好办法。读文档、找调用点可以同时做,迁移数据库和验收共享环境轮流做。并行是一种安排,不是必须贯彻到每一分钟的目标。

没有合并冲突,也可能把功能合坏

各自改完后,第三个问题才真正开始:两份正确的修改,放在一起仍然正确吗?

再设想一个例子。甲把接口的金额单位从元改为分,同时更新了当时已有的展示位置和测试;乙从旧版本出发,新增一个报表视图,正确地按旧接口的“元”显示金额。甲没见过乙新加的视图,乙也还不知道单位变了。两份修改各自在原来的条件下成立,没有改到同一段文本,Git 也可能顺利合并。可合在一起,新报表直接展示“分”的数值,金额就可能差一百倍。

这属于约定发生了变化,不能指望文本合并替我们理解业务。Git 能整合提交中的改动,遇到无法自动处理的内容会留下冲突供人解决;“自动合并成功”本身没有承诺功能之间的契约仍然成立。

来源:Git 官方文档:git merge

所以分工之前,最好先写清连接处。返回字段叫什么、金额用什么单位、错误如何表示、导出读取哪一份筛选条件。共同接口若还在频繁变化,先由一个人确定最小约定,再让两边展开,会比同时自由发挥更省返工。

而合并之后,必须检查的是合并后的提交。甲报告的通过,证明甲当时那份代码在相应条件下通过;乙的也一样。把两张合格证放在一起,不能替合并版本补签第三张。

我倾向于保留两层验证。先看每项修改自己的关键行为有没有实现,再看跨边界的完整动作:改筛选条件后导出的内容对不对,前端发送的参数和服务端读取的是不是同一套约定。具体测哪些,取决于本次变化连接了哪些部分。

交接时,把结果和现场一起交出来

给 Agent 的任务可以写得很短,但要能回答几个实际问题:从哪个版本开始,在哪个目录做,允许改哪些文件,测试会占用什么资源,以及最后把什么交给汇总者。

比如“修复导出功能”仍然很宽。“在指定工作树修改导出组件和对应测试;搜索行为保持原约定;预览使用分配的端口;结果写入专属输出目录;交付提交编号、复现步骤和已知未覆盖项”,更容易让下一位接得住。

这不是为了让任务单显得专业。发生问题时,这些信息能缩小搜索范围:到底是实现错了,跑错了目录,访问错了服务,还是验证时的数据被另一个任务改了。没有这些边界,所有不确定性会一起落进一句“测试偶尔失败”。

收工时也要认清资源归属。停止自己启动的进程,清理自己的临时目录;不要用一个宽泛的进程名把所有同类服务都关掉,也不要看到目录名字像测试文件就一起删除。共享环境里,清理动作同样是一种写入。

如果两个任务不断要求改同一小段代码,我会重新考虑是否值得并行。把它们交给同一个实现者,再让另一个 Agent 做只读审查,往往更清楚。多开窗口的收益,取决于任务有没有真正可分开的部分。

我现在看多 Agent 的提速,先看的不是人数,而是现场:谁在改哪份东西,谁能影响谁,最后验的是哪一份结果。把这三件事说清楚之后,并行才不只是两个人同时忙起来。

评论