并行协作与项目演进
一个人干活时,版本管理只是存档;几个人或几个 Agent 同时干活时,它决定了活能不能干成。
- 多个 Agent 同时改一份目录,具体会怎么互相覆盖;
- 工作树能隔开什么、隔不开什么;
- 上锁与合并两种协作策略,各自要付什么代价;
- 人数一多,为什么沟通关系会按平方增长;
- 最小可行产品、技术债,以及从零开始为什么比改成熟项目更难。
一、两个 Agent 改同一份目录
先看一个具体场面。你让 Agent A 改界面,让 Agent B 补后端接口,两个都开在同一个项目目录里。它们各自读文件、改文件、跑测试,看起来各干各的。
三分钟以后问题开始冒出来:
- A 读完
config.py,正在推断格式;B 在这期间把这份文件重写了一遍,A 后面基于旧内容写的补丁一提交,B 的修改就没了; - A 正在跑测试,B 改好了被测的那个模块,测试结果既可能通过也可能失败,而 A 看到的输出已经说不出对应哪一版代码;
- 两个都执行了同一个「整理目录」的命令,后执行的把先执行的成果又挪了一遍位置。
这三种麻烦的共同点:它们都不报错。没有冲突提示,没有异常报错,只是结果悄悄变成了另一个样子。人做同样的动作会互相看见、互相说一句,Agent 之间没有这个通道。
再往下想一层,覆盖不只发生在文件层面。两个 Agent 各自基于「现在的状态」作出判断,判断之间互相作废,可它们都不知道。举一个不带任何代码的例子:项目规定任何时候至少有一名值班员在岗,现在甲、乙都在岗。Agent A 看到乙在岗,把甲改成离岗;Agent B 同时看到甲在岗,把乙改成离岗。两处修改落在不同文件里,文本合并毫无冲突,结果却是没人值班。各自都按规则行事,组合起来违反了规则。
二、工作树:一个仓库,几个工作目录
给每个 Agent 各克隆一份仓库是一个办法,代价是空间与同步:几个目录各自维护远端、各自 fetch,中间还多了一层搬运。Git 提供了一个更适合本机并行的机制,叫工作树(worktree,也叫工作区检出)。
同一个仓库可以同时关联多个工作目录。每个目录有自己的工作区、自己的 HEAD、自己的暂存区;它们共享仓库里的对象和大部分引用。新建一个工作目录只要一条命令:
PS> git worktree add -b agent-ui ../project-ui
Preparing worktree (new branch 'agent-ui')
HEAD is now at 7d1e4a2 记下第一次练习的目标
PS> git worktree add -b agent-memory ../project-memory
Preparing worktree (new branch 'agent-memory')
HEAD is now at 7d1e4a2 记下第一次练习的目标
PS> git worktree list
C:/Users/你的用户名/gse-practice 7d1e4a2 [main]
C:/Users/你的用户名/project-ui 7d1e4a2 [agent-ui]
C:/Users/你的用户名/project-memory 7d1e4a2 [agent-memory]
三个目录,一个仓库。A 在 project-ui 里改界面,B 在 project-memory 里改记忆模块,两人提交的内容都直接进了同一个仓库,不需要互相 push、pull。要汇合的时候走常规的 merge 或 rebase,冲突标记仍然会出现。
flowchart TB R["同一个仓库 共享对象与引用"] --> W1["工作目录 project-ui 分支 agent-ui"] R --> W2["工作目录 project-memory 分支 agent-memory"] W1 --> MG["合并 或 变基 汇合"] W2 --> MG MG --> C["共同的主线"]
工作树隔开的东西很明确:文件、暂存区、当前分支。它隔不开的东西同样明确,这些才是并行时真正容易出事的地方:
| 共享资源 | 冲突表现 | 缓解办法 |
|---|---|---|
| 端口 | 两个 Agent 抢同一个 3000 端口,后起的那个起不来 | 用环境变量给每个目录分配不同端口 |
| 数据库 | 一个跑了迁移,另一个的测试数据被污染 | 每个目录独立数据库,或者各自容器化 |
| 依赖与缓存 | 每个目录都要重装一遍依赖 | 共享包缓存,把安装写进初始化脚本 |
| 被忽略的文件 | 新目录里没有 .env,因为这类文件本来就不进版本历史 | 初始化脚本自动复制配置模板 |
三、上锁与合并:两种协作策略
环境的隔离解决了「互相覆盖文件」,解决不了「谁先动共享的东西」。协作策略有两个基本方向。
1. 先上锁,再做
由主 Agent 规划,把任务拆成边界明确的部分。子 Agent 要修改某项共享资源时,先去取一把互斥锁;取到之后重新读一遍最新状态,改完、跑完必要检查、留下可恢复的快照,再释放锁。锁在这里是一套约定:同一时刻只允许一个参与者修改约定范围内的状态。
以上面两个改记忆格式的任务为例。先取锁的那一个完成一次前后一致的修改并存下快照;后取锁的那一个拿到锁以后重新读取最新内容再动手。谁都没有基于过期状态写入,冲突从「事后处理」变成「事前避免」。
2. 各做各的,汇合时检查
另一种方向是让所有人基于各自的版本往前推进,等到汇合的时候再处理差异,也就是 merge 与 rebase 那一套。日常写代码大体上就是这么干的,它允许并行探索,代价是把矛盾推到汇合那一刻集中爆发。
3. 锁的代价
上锁听起来干净,代价一点不少,而且这些代价在操作系统课和数据库课里都出现过:
- 锁粒度:锁住整个项目最简单,但会把本来能并行的活重新排成一条队。只锁一个文件更灵活,却盖不住跨文件的不变量——接口定义和它的调用方必须一起改,只锁其中一个文件等于没锁。
- 死锁:任务甲要资源一和资源二,任务乙要资源二和资源一,各自拿到一半,然后互相等。人遇到这种情况会打个电话,Agent 会安静地卡住。
- 锁的回收:Agent 中途崩溃、被中断、或者自己以为取到了锁其实没取到,锁就留在那里。谁来回收,怎么判断持有者已经死了,都需要约定。
- 拿到锁之前读到的旧内容:一个 Agent 排队等锁的时候顺手读了状态,拿到锁以后继续用那份旧内容,锁就白上了。
- 锁与锁之间的规则也会冲突:一个任务同时需要两项资源时,取锁顺序不统一,死锁就有了土壤。
四、人多以后,困难从技术转到了沟通
把参与者数量往上加,会发生一件数学上很确定的事。若 n 个人彼此都可能需要直接沟通,潜在的沟通关系有 n(n−1)/2 条:3 个人是 3 条,6 个人就是 15 条,10 个人是 45 条。翻一倍的人,翻四倍的线。
flowchart LR A["3 人 3 条关系"] --> B["6 人 15 条关系"] B --> C["10 人 45 条关系"]
这个公式数的是潜在关系,实际耗时不会严格按它增长,因为清楚的接口和明确的分工能把大量「必须当面说清」的关系消掉。但它把一件事说得很清楚:人一多,卡住项目的环节从「写不出来」挪到了「说不清楚」。
几个典型场面,对照自己的小组作业经历看:
- 每个人都觉得自己的部分很清楚:界面已经能展示模拟数据,后端已经通过了自己的测试,可两边对字段的含义、失败时怎么办的理解并不相同;
- 每个人都说「基本完成」:有人说代码写完了,有人说测试过了,有人以为已经能上线了;
- 有人一直在等上游决定,不断改计划,上游完全不知道自己挡住了谁;
- 代码量翻了一倍,项目离可用的距离没有缩短,因为新增的部分都在各自假设上生长;
- 分歧发生在目标上:一个人想赶紧做出来给人用,另一个人想趁机把结构理清楚,两个人都觉得自己在为项目好。
这些场面里没有一行代码写错,它们全都是「同一件事在不同人脑子里成了两件事」。版本管理能保存状态、保留历史和教训、允许多条探索路线存在,它做不了的是替团队确定优先级、替团队找到那个愿意把整件事接起来的人。
五、最小可行产品:先做一个能用的
上面那些麻烦里有一大半的来源是同一个:大家都在对着想象做东西。对付想象最有效的办法是把它变成一个能看见、能点、能批评的东西。
最小可行产品(Minimum Viable Product,缩写 MVP)指的是:做出足够小、又能真正投入使用的版本,用它检验最关键的假设。开工之前先写下两个问题:
- 这个版本要验证什么?
- 出现什么结果,就说明我们想错了?
第二问最容易被跳过。只有事先说明需要什么证据,第一版做出来之后才判断得了应该继续完善还是改方向。
拿「会成长的个人助手」举例。第一版不必包揽邮件、日程、浏览器和长期记忆,可以先挑一个你每天都会遇到的场景:记录一项偏好,下一次相关任务里用上它,并且允许你纠正。这个版本可以很粗糙,但你能亲自观察它有没有帮上忙、记忆有没有被正确取用、纠正之后行为有没有真的变化。
一旦开始使用,藏在形容词里的问题就会变成具体的分歧:「更主动一点」可能意味着每天固定时间提醒,也可能意味着只在发现明确遗漏时提示;「「更了解我」可能需要记住长期偏好,也可能先要让人学会忘掉一次性的指令。真实使用产生的这些分歧,会反过来检验早期的规格和结构。
MVP 有它盖不住的东西。原型走通正常路径,转专业、课程替代、规则追溯、人工批准这些角落可能还没露出獠牙;试用期里没人碰的边界情况,恰恰是日后最贵的地方。更硬的一关是需求本身变了:把「一门交换课程替代一门本校课程」改成「允许两门交换课程合抵一门必修课」,受牵连的地方遍布下拉框之外:课程对应关系、学分计算、毕业审核、成绩单显示以及一串测试都要动;已经认定过的学分要不要重算,已经批准的特例还算不算数,也都要给出答案。快速做出原型很有价值,需求冲突、历史包袱和持续变化仍然要有人处理。
六、技术债与认识的进步
第一版做完、用起来之后,多半会发现当初的设计有点别扭。为了赶进度先凑合的写法、临时加的一个字段、绕过去的一段逻辑,都会在后续修改时收利息。这笔利息有个名字:技术债(technical debt),指将来修改系统时必须额外偿还的成本。
值得区分两种来源,它们的处理方式完全不同。
| 来源 | 典型情形 | 处理方式 |
|---|---|---|
| 图省事欠下的 | 明知有更好的写法,为了先跑起来先凑合,且没记下来 | 记进待办,安排时间偿还 |
| 认识进步欠下的 | 当初的设计在当时有充分理由,做出来用起来之后才明白概念应该怎么分 | 承认它,写下新的理解,找时机调整结构 |
第二种更值得留意。即使团队认真设计,也可能在学习之后发现原来的结构已经落后于新的理解。这件事说明认知往前走了一步,不必当成失误。真正要紧的是把这个发现记下来,并且知道什么时候该动手调整。
这也解释了为什么「没有什么不能变」是一条实用准则。落到软件上,它问的是系统能不能承受需求变化:能不能替换一项记忆策略而不用重写全部界面?能不能增加一种输入方式而不把核心行为复制一份?围绕已经看见的变化组织边界,再用实际反馈修正设计,比一次把未来想清楚更现实。
七、从零开始,为什么比改成熟项目更难
这一条听起来反直觉:新项目没有历史包袱,代码干净,文件少,应该更好干。实际经验常常相反。
Agent 干活有一份隐藏燃料:仓库里已有的模式。它修一个已有页面时,会照着旁边另一个页面写,命名、目录、错误处理全跟着走。空项目里一条先例都没有,它只能按训练数据里最常见的写法来,而那套写法与你想要的东西未必一致。更要紧的是,空项目里没有测试可以照抄,没有检查工具告诉你什么算对,没有配置文件说明这个项目要往哪个方向长。
还有一个更根本的原因:成熟项目已经替你回答了大量问题。用户是谁、代码怎么分层、什么叫兼容、测试怎么跑,这些约束都已经固定,做局部修改时沿着它们走就行。空项目里约束少、自由度大,意图、规格与实现之间的缺口有更多机会被默认选择悄悄填满。
「会成长的个人助手」至少留下了几个必须自己回答的决定:成长指的是记住事实、修正偏好,还是学会执行新任务?助手什么时候该主动行动?记错了以后怎么纠正?如果一开始就把所有记忆设计成永久有效,后来才发现大量信息有时间和场景限制,早期那个看起来很确定的数据结构,就把一项需求误解固定在实现里了。
| 对比项 | 成熟项目 | 从零开始 |
|---|---|---|
| 可模仿的样本 | 多,照着写就行 | 没有,只能靠训练数据里的常见写法 |
| 验证骨架 | 已有测试与检查命令 | 要自己搭,不搭就无从判断对错 |
| 成文约定 | 写在配置与文档里 | 只存在于你的脑子里 |
| 历史包袱 | 多,约定不统一 | 少,但也没有依据 |
所以从零起一个项目时,最划算的顺序是先把三样东西立起来:一条能跑起来的构建与测试命令,一个最简单但能通过的测试,一份说明目录结构的配置文件。三样加起来半小时能写完,它们决定了后面几十次协作的质量。
八、把成功的做法沉淀下来
项目跑顺之后,会积累出一批「每次都得这么做」的动作:新产品从哪一步开始、并行开几个工作目录、合并之前跑哪几条检查、Agent 犯过的哪类错误要提前叮嘱。这些做法如果只留在记忆里,换一个人、换一个会话、换一个项目就全部重来一遍。
把它们写成文件,是让一次成功变成可复用资产的唯一办法。可用的载体有好几种,粗细不同:
- 项目配置文件:写在仓库根目录,说明构建命令、目录地图、约定和禁区,每次开工都会读到;
- 任务模板:把「目标、背景、约束、可自动执行的验收标准」四段写成模板,派活时填空;
- 检查清单:合并前的固定动作,一条一条对照,能勾选的那种;
- 错误记录:哪一类问题反复出现、上次怎么解决的,写在容易看到的地方。
判断一份沉淀值不值得写下来,有个简单标准:同一件事说到第三遍,就该写进文件。写的时候遵守一条纪律,默认否定——自动加载的每一条内容都要证明自己能改变参与者的行为。证明不了就删掉。上下文窗口有限,无关内容会稀释注意力、增加成本,还会让整份文件被当成「大概无关」而整体淡着处理。
多人多 Agent 的场景里,这份沉淀还有一个额外的好处:它把只有老成员才知道的隐含规矩变成了公开可查的东西。所谓部落知识——那种「大家都知道要用新的那个写法」的经验——恰恰是并行协作里最贵的一种信息,因为它没有写在任何地方,却决定了每一次改动对不对。
① 在一个已有提交的仓库里执行
git worktree add -b try-parallel ../try-parallel,在新目录里改一个文件并提交,回到原目录用 git log --oneline --all 找到这条新提交,说出两个目录共享了什么;② 写下你的小项目的 MVP 两问:这个版本要验证什么、出现什么结果就说明想错了,每问一句就够;
③ 打开项目根目录,新建或补写一份配置文件,只写三行:构建命令、测试命令、一条禁区。写完问一个 AI 助手「怎么验证这个项目的改动是对的」,看它能否只靠仓库里的东西答上来。