多个 agent 同时改文件老是打架,什么时候该给它开独立工作区
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
多个 agent 一起干活出乱子,绝大多数不是”它们同时写了同一行代码”,而是它们共用了同一份不该共用的状态:同一个工作目录、同一个构建缓存、同一个本地端口、同一个数据库、同一份 .env。真正的文本冲突反倒是最容易发现、最容易修的一类,因为 git 会明确告诉你哪里冲突了。让你半天定位不出来的,是那些没有任何报错、只是结果不对的状态污染。所以隔离策略不该按”开几个 agent”来定,而该按”它们碰的是哪一层共享资源”来定。
先划清这篇的边界:同一时间多个会话抢着改、后写的把先写的盖掉那类现象,在 多会话并发冲突排查 里讲得更细;agent 明明想写却被环境挡回来、提示没有权限,属于 沙箱权限拒绝 的范围。本篇只管一件事:当你确实需要多个 agent 并行推进时,工作区该怎么切、切到什么粒度、什么时候切了反而更亏。
一、先分清三类打架,不要一上来就限并发
把”agent 互相打架”拆成三类,后面的动作完全不同。
第一类是编辑冲突。 两个 agent 改同一个文件、甚至同一个函数。表现是 git 冲突标记 <<<<<<<、=======、>>>>>>> 出现在文件里,或者你发现某个 agent 的改动凭空消失了。这类冲突有明确证据,处理起来是纯 git 问题,具体合并手法可以看 git 冲突让 AI 解决。
第二类是状态冲突。 代码本身没冲突,但两边共用了运行期资源:同一个 dev server 端口、同一个 node_modules、同一个构建缓存目录、同一个本地数据库、同一份日志文件。表现往往很怪——代码看着是对的,跑出来的结果却是另一个分支的行为;或者构建时好时坏,重跑一次又好了。这类最难查,因为没有任何一处报错指向根因。
第三类是认知冲突。 两个 agent 各自读到了不完整的现状。A 刚把某个函数改名,B 还按旧名字写调用;A 新增了一个配置项,B 的改动里没有它。最后代码能合并、能编译,逻辑却是断的。这类的根源不是隔离不够,恰恰相反——是隔离得太彻底,两边谁也不知道对方干了什么。
判断顺序建议是:先看有没有 git 冲突标记或改动丢失(编辑冲突),没有的话看两个任务是否碰同一份运行期资源(状态冲突),都没有再怀疑认知冲突。反过来查会浪费很多时间。
二、判别表:现象对成因,先验证再动手
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
文件里出现 <<<<<<< / >>>>>>> 标记,或代码里混着两套写法 | 编辑冲突,合并未收尾 | git status 看 unmerged 路径,git diff --name-only --diff-filter=U 列冲突文件 | 人工裁决冲突段落,不要让 agent 盲选一边 |
| 某个 agent 说改完了,文件里却没有它的改动 | 后一次写入覆盖了前一次,或改错了目录 | 先在各个副本里跑 git status --short 确认改动是不是落在别的目录;确实提交过就用 git log --oneline -5 -- <文件> 和 git reflog 找节点 | 提交过的从 reflog 或历史节点找回;从没提交过的 git 里没有备份,只能靠编辑器本地历史,之后把两个任务拆到不同工作区 |
| 代码没改却行为变了,重跑一次又正常 | 状态冲突,共用构建缓存或产物目录 | 清空缓存目录后单独跑一遍,看现象是否消失 | 给每个工作区独立的缓存与产物路径 |
| 启动服务提示端口被占用 | 多个工作区抢同一个端口 | 用系统命令查占用进程,例如 lsof -i :3000(Linux/macOS)或 netstat -ano | findstr :3000(Windows) | 给每个工作区分配不同端口,用环境变量传入 |
| 测试在 A 工作区绿、B 工作区红,代码却一致 | 依赖树不一致或 lockfile 未同步 | 对比两边锁文件的哈希,或删掉依赖目录重装一遍 | 每个工作区独立安装依赖,别做软链接共享 |
| 数据库迁移执行两次或报”已存在” | 共用同一个本地数据库实例 | 查迁移记录表里的版本行 | 每个工作区一个独立数据库文件或 schema |
| 合并后编译过、逻辑断(调用了不存在的字段) | 认知冲突,两边基于不同快照 | 合并后跑一遍完整测试与类型检查,别只看编译 | 缩短并行窗口,先合接口层再并行细节 |
| agent 反复说自己改好了但每次都从头再来 | 它读到的是旧快照,改动落在别处 | 让它先输出文件绝对路径,再核对该路径的 mtime | 明确工作目录,禁止相对路径漂移 |
这张表的用法是从上往下扫现象,先做”怎么验证”那一列,验证过再执行动作。跳过验证直接开隔离,很容易把一个认知冲突当成状态冲突去治,隔离得更彻底、问题更严重。
三、隔离有四档,别一上来就上最重的
工作区隔离不是开关,是强度梯度。成本从低到高,选够用的那一档。
第零档:同一目录,靠任务边界隔离。 不做任何物理隔离,只在分派任务时把文件范围切开——A 只碰 src/api/,B 只碰 src/ui/,共享的类型定义文件由你自己先改完。这一档成本为零,适合改动面小、边界清晰的活。它的失效点很明确:只要有一个 agent 越界去改公共文件,隔离就破了。所以配套动作是把改动范围写死在任务描述里,并在合并前核对实际改了哪些文件——改动还没提交时用 git status --short,已经提交了就用 git diff --name-only <开工时的提交>,只敲 git diff --name-only 看到的仅是未暂存的部分,很容易漏判。越界立刻打回。
第一档:独立分支,同一目录串行切换。 两个任务各自一个分支,但共用一个工作目录,来回 git switch。这一档其实不解决并行问题——切分支时另一个 agent 正在读文件,读到的就是错的。我的判断是:除非你能保证任意时刻只有一个 agent 在跑,否则这一档不如第零档安全,因为它给了你一种”已经隔离了”的错觉。
第二档:独立工作副本。 每个任务一个物理目录,各自独立的依赖、缓存、产物、端口。git 原生的 worktree 就是干这个的:
# 从当前仓库派生一个独立工作目录,同时新建分支
git worktree add ../proj-featA -b feat/a
git worktree add ../proj-featB -b feat/b
# 随时查看有哪些工作副本
git worktree list
# 任务结束后清理
git worktree remove ../proj-featA
worktree 共享同一份 .git 对象库,所以磁盘占用比整仓克隆小,分支和提交在几个副本之间是互通的——A 提交完,B 直接 git merge feat/a 就能拿到,不用走远端。但要注意:worktree 只隔离了受版本控制的文件,node_modules、.env、本地数据库、构建缓存这些被忽略的东西不会自动跟过去,需要你手动准备。多个副本并行的实操细节,Claude Code 用 worktree 并行开发 里有更具体的展开。
配套的端口与环境隔离,通常这样处理:
# 每个工作副本一份独立配置,通过环境变量注入差异
# 数据库连接串的写法各框架不同,重点是路径里带上副本名、互不重合
PORT=3001 DB_FILE=/tmp/dev-a.db npm run dev
PORT=3002 DB_FILE=/tmp/dev-b.db npm run dev
这种前置环境变量的写法在 bash/zsh 里直接可用;Windows 的 PowerShell 需要先 $env:PORT=3001 再单独执行启动命令,或者干脆给每个副本准备一份自己的本地配置文件。要点不在写法,在于差异必须从外部注入,而不是让两个副本去改同一份被版本控制的配置。
第三档:独立运行环境。 容器、虚拟机或另一台机器,连操作系统级的资源都不共享。适合两个任务会改系统级配置、要装不同版本的运行时、或者其中一个任务有破坏性操作(清库、批量删文件)的场景。代价是环境搭建时间、镜像体积、以及 agent 访问路径变复杂。
选档的经验判断:改动集中在应用层代码、任务之间不碰同一批文件——第零档;任务要各自启服务、跑测试、装依赖——第二档;任务会动系统配置或有破坏性写入——第三档。第一档基本可以跳过。
四、什么时候开独立副本才划算
开一个独立工作副本不是免费的。你要付出的成本包括:依赖重装的时间、环境变量重新配一遍、agent 上下文里要重新交代目录结构、以及最后那次合并。所以要有明确的判断依据。
值得开的信号:
- 任务之间存在真实的文件重叠,且重叠的是核心文件(路由表、类型定义、配置中心)。重叠面越大越该分。
- 任务的时长差异大。一个五分钟改完,另一个要跑二十分钟测试——短任务不该被长任务的工作目录状态卡住。
- 任务需要各自启动服务或跑集成测试。这是最硬的信号,共享端口和共享数据库几乎必然出事。
- 你打算让其中一个任务在后台无人值守地跑。后台任务的失败往往没有声音,具体表现可以看 后台 agent 静默失败,共享目录会让这类静默失败污染到另一条线。
- 任务有较高概率被整体废弃。独立副本可以直接删目录,不用在主目录里一点点回滚。
不值得开的信号:
- 两个任务本来就有强依赖,B 必须等 A 的接口定下来。这种情况开副本只会制造认知冲突,不如串行做。
- 单个任务的改动不超过几个文件,且不需要跑服务。开副本的准备时间比任务本身还长。
- 项目的依赖安装极慢或体积极大。每个副本重装一遍的代价可能超过冲突本身的代价,这时更划算的是把任务改成串行。
- 你还没搞清楚上一轮到底为什么打架。隔离是治手段,不是治病因。带着没查清的问题去开副本,只是把同一个 bug 复制到两个目录里。
有一条我认为比并发数更重要的原则:并行的粒度应该匹配可独立验证的粒度。如果两个任务不能各自跑通测试、各自证明自己是对的,那它们本来就不该并行——合并的时候你还是得整体验证一次,前面省下的时间会在合并阶段还回来。
五、什么情况下别再折腾了
并行调试最大的坑是沉没成本:已经开了三个副本、装了三遍依赖,出问题时第一反应总是”再修一下就好”。下面几条是我的止损线。
同一个冲突修了两轮还在复发,停。 冲突复发说明成因判断错了——你在治编辑冲突,实际是状态冲突或认知冲突。这时候正确动作是收敛:关掉所有并行任务,只留一条线,把这个改动完整做一遍。花二十分钟串行做完,好过花两小时并行救火。
工作副本之间的差异你已经说不清了,停。 判断方法很直接:在每个副本跑 git status --short 和 git log --oneline -3,如果你看完还是拼不出”谁改了什么、基于哪个提交”的完整图景,那就不要再往前推。此时应当回滚点归零——把还有价值的改动用 git diff HEAD > /tmp/featA.patch 导出成补丁存起来(加 HEAD 才会连暂存区的改动一起带上,只写 git diff 会漏掉已 git add 的部分;全新的未跟踪文件补丁里没有,要么先 git add -N <文件> 让它进入索引,要么单独拷贝一份),删掉副本,从一个干净的基线重新开始。补丁文件后面用 git apply 打回去,比在混乱的目录里理线索快得多。
合并冲突的规模超过改动本身,停。 如果 A 和 B 的冲突文件数量接近它们各自改动的文件数量,说明这两个任务从一开始就不该拆开。此时的最优解通常是保留其中一边、另一边整个重做——让 agent 在已经合并的新基线上重新实现一遍,往往比人工逐段调和快,而且结果更干净。
基线本身在动,停。 如果并行期间主分支还在被别人推新提交,几个副本的基线会持续漂移。要么先把主分支冻结一段时间,要么让所有副本先 rebase 到同一个提交再继续。基线不稳的情况下做并行,冲突会源源不断地来。
换条路的信号: 当你发现自己花在协调 agent 上的时间已经超过了它们节省的时间,就该退回串行。并行的收益上限是任务时长的重叠部分,而协调成本是随并行数量超线性增长的。两个副本通常还划算,四个以上基本只有在任务完全解耦(比如给十个独立模块各写测试)时才成立。
六、避坑清单:每条都写清为什么会踩
依赖目录用软链接共享。 为什么会踩:看到每个副本装一遍依赖很慢,很自然想到链接过去省时间。为什么出事:两个副本的锁文件一旦不同,共享的依赖树就只能满足其中一个,另一个会以一种没有报错的方式跑出错误结果。怎么避:宁可慢也各装各的;真嫌慢就用包管理器自带的全局缓存机制,那是安全的共享层,软链接工作副本目录不是。
忘了 .env 和本地配置不受版本控制。 为什么会踩:新建的工作副本里代码齐全,看起来一切正常。为什么出事:环境变量缺失时程序可能不报错,只是走了默认分支,于是你调了半天逻辑其实是配置没读到。怎么避:建副本后第一件事是把配置文件复制过去并逐项核对,不要只复制不看内容——数据库地址、端口这些必须改成副本专属值。
多个副本连同一个本地数据库。 为什么会踩:数据库地址通常写在配置里,复制配置的时候顺手就带过去了。为什么出事:一个副本跑了迁移,另一个副本的代码还是旧 schema,读写立刻错位,而错误信息往往指向业务代码,很难联想到根因。怎么避:数据库文件路径里带上副本名,每个副本独立初始化。
让 agent 自己决定在哪个目录干活。 为什么会踩:你在一个会话里说”去另一个副本改一下”,它理解成了相对路径。为什么出事:改动落在错误的目录里,两边都不对,而且很难发现——因为文件确实被改了,只是改错了地方。怎么避:始终给绝对路径,并在任务开始前让它把目标文件的完整路径列出来核对一遍。
并行做重构和做新功能。 为什么会踩:重构和新功能感觉是两件事,可以同时推。为什么出事:重构会大面积改动函数签名和文件位置,新功能分支的每一行几乎都会冲突。怎么避:重构期间不并行,或者把重构切成小步、每步立即合回主线,让新功能分支频繁 rebase。
副本用完不清理。 为什么会踩:任务做完就切走了,目录还在那儿。为什么出事:过几天你再回来,分不清哪个是最新的,容易在废弃副本上继续改。怎么避:任务合并后立即 git worktree remove,并且养成用 git worktree list 做收尾核对的习惯。
把隔离当成质量保证。 为什么会踩:隔离之后确实不打架了,很容易以为改动就是对的。为什么出事:隔离只解决了互相干扰,不解决单个 agent 改错。副本里跑得再绿,合并后仍可能因为认知冲突而逻辑断裂。怎么避:合并后必须再跑一次完整验证,把合并本身当成一次独立的改动来对待。
收尾
工作区隔离的核心判断只有一句:你要隔离的不是 agent,是它们共享的那份状态。搞清楚共享的是文件、是运行期资源、还是对现状的认知,隔离的强度自然就定了。
开并行之前过一遍这份自检:
- 这两个任务的改动文件集合重叠吗?重叠的是不是核心文件?
- 它们各自需要启服务、跑测试、装依赖吗?需要的话端口和数据库分开了吗?
- 每个任务能不能独立验证自己是对的?不能就别并行。
- 副本里的
.env、缓存目录、产物目录是不是各自独立的? - 合并点和回滚点想好了吗——出问题时你从哪个提交重来?
- 这次并行预计省下的时间,比协调和合并的成本多吗?
六条里有两条答不上来,就先串行做一遍。并行是优化手段,不是默认姿势。