ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

并行AI Agent工作区隔离:Git Worktree与Worktrunk实战

并行AI Agent工作区隔离:Git Worktree与Worktrunk实战 周末我同时开了四个 AI 编程 Agent 帮我改一个开源项目——一个在加新接口一个在重构数据库迁移脚本一个在补测试还有一个在处理文档里的错误链接。各自跑起来都很顺畅结果下午一合并就崩了两个 Agent 在同一个文件里改了相邻的代码第三个 Agent 不知道第二个改了接口签名四个 Agent 的上下文里全是过期的代码快照。那一刻我意识到Agent 写代码的能力早就不是瓶颈真正的瓶颈是怎么给每个 Agent 一块互不干扰的独立工作区。Git Worktree 是解决这个问题的标准姿势但裸用git worktree命令在多个 Agent 并行、会话频繁创建销毁的场景下管理成本很快就会失控。于是我把 Git Worktree 管理做成了 Worktrunk 这个 CLI 工具专门服务于并行 AI Agent 工作流。这篇文章会把从第一次踩坑到最终跑顺的完整过程拆开讲为什么并行 Agent 工作流需要 worktree、Worktrunk 的核心命令和生命周期设计、三种真实接入方式以及实践过程中处理过的现场问题。内容不挑 IDE也不绑定某家 Agent 服务适合正在被多任务并行和代码冲突折磨的个人开发者也适合准备把多个编码 Agent 纳入日常流程的团队。1. 并行 Agent 撞车现场问题不在模型能力在工作区隔离1.1 一次真实的四路并发事故那天的项目是一个 Node.js 服务端仓库本地已经积压了三十多个分支改动没整理。我开了四个会话各自任务很清晰Agent A 在routes/user.ts里增加一个/v2/profile接口Agent B 把数据库迁移脚本从一套旧方案换成新的迁移工具Agent C 给todosController补集成测试Agent D 负责修 README 里的过期安装命令。这四个 Agent 单独拎出来看推进能力都挺强但问题出在它们全部工作在同一个 checkout 下。到中午我看了一眼git status工作区里已经是四个 Agent 交替改动的混合状态README 的改动和测试文件搅在一起routes/user.ts被 A 改了一半B 在迁移目录里删掉了 A 可能依赖的文件。更麻烦的是我没法单独把 A 的改动挑出来提交因为同一批文件里残留着 D 留下的空白修改连 code review 都不知道该以谁为准。这是并行 Agent 工作流里最典型的事故模型多个 Agent 共享同一个工作区改动互相污染。Agent 本身不知道别人改了什么它的上下文建立在我以为文件是这种状态的基础上一旦真实状态变了它后续每一步操作都在往错误的地基上盖楼。1.2 为什么不能把多个 Agent 塞进同一个工作区表面上看Agent 只是改文件、跑命令共享工作区好像没什么大不了。但实际跑下来至少有三个层面的问题。第一个是文件级冲突。两个 Agent 就算改的是不同文件构建产物、锁文件、缓存目录依然会互相覆盖。举个最简单的例子一个 Agent 执行了依赖安装package-lock.json被更新另一个基于旧锁文件做依赖分析的 Agent后续所有判断就都建立在错误前提上。这种冲突非常隐蔽因为代码本身没语法错误CI 一跑才发现行为不对。第二个是上下文失效。AI 编程 Agent 的能力建立在准确的当前状态感知上文件的真实内容、最近的 git 历史、分支状态每一项都是它的地图。共享工作区意味着这张地图随时随地可能被别人改掉尤其是 Agent 用长会话跑任务时它看到的状态往往已经过期。我事后去翻对话日志能看到 Agent 一本正经地基于旧代码重构一个已经被改掉的函数——它没有偷懒只是手里那份世界快照不对了。第三个是不可追溯性。多个 Agent 的改动混在同一个工作区事后想搞清楚这段代码是谁写的、当时基于什么目标、中间经历了哪些尝试会非常困难。而如果每个 Agent 在独立分支上工作这条链路就清晰得多出了事故可以精准定位到某个 Agent 某个时间点提交的某个操作。所以结论很清楚要让多个 Agent 真正并行每个 Agent 必须有一块独立的工作区、独立的分支、独立的提交历史。这正是 Git Worktree 的用武之地。1.3 Git Worktree 为什么是标准答案可能有人说那我开多个仓库副本不就行了也行但代价是要处理多份.git元数据、多个远端连接以及从副本目录回到主项目之间的各种状态不同步。Git Worktree 的精妙之处在于它在同一个仓库内维护多个工作目录共享同一个对象库和引用库但每个工作目录有自己独立的 HEAD、索引和工作区文件。用一句话理解Git 的对象仓库是硬盘worktree 是插在硬盘上的读卡器你想在同一个仓库里同时浏览和修改多份内容就多插几个读卡器而不是复制整块硬盘。每个 worktree 关联一个分支。这意味着我可以给 Agent A 一个feature/v2-profile分支给 Agent B 一个refactor/migration分支它们在各自的目录里自由操作互不阻塞。合并的时候各分支独立走 merge 流程主工作区全程不受影响。这套机制从 Git 2.5 开始已经非常成熟但裸用起来依然有痛点——这也是我做 Worktrunk 的直接原因。2. Worktrunk 的核心设计把 worktree 生命周期变成流水线2.1 原生 git worktree 少了什么原生方法的日常操作其实就四条命令git worktree add ../feat-a -b feat/a git worktree list git worktree remove ../feat-a git worktree prune单独看每一条都不难但放到并行 Agent 工作流里缺了四个关键能力。第一没有 Agent 会话绑定。git worktree list只能看到路径和分支看不到这个 worktree 是给哪个任务、哪个 Agent 用的任务上下文全靠人在其他地方另记一份。第二生命周期不完整。创建 worktree 只是一小步后面还有运行 Agent、收集改动、合并分支、清理目录、删除分支原生命令把这一长串全推给使用者手工串。第三状态不可量化。哪几个 worktree 已经合并、哪几个还在跑、哪几个是 Agent 崩溃后留下的孤儿需要人肉判断。第四缺少批量操作和策略多个 Agent 同时启动时要逐个创建、逐个判断没法一条命令统一处理。2.2 核心命令与状态机设计Worktrunk 的设计目标很直接把 worktree 的整个生命周期压缩成一条可观察、可追溯的流水线。所以核心命令围绕三条链路来设计——创建、查看、收尾。# 创建为指定 Agent 分配独立 worktree worktrunk create feat/v2-profile --from main --agent agent-a # 查看观察所有 worktree 的状态 worktrunk ls # 收尾合并 清理一条命令完成 worktrunk ship feat/v2-profile --to develop worktrunk drop feat/v2-profile每个 worktree 在我这套设计里有明确的状态机created→running→ready→shipped/dropped。created是刚创建完Agent 还没接手running是 Agent 正在里面干活Agent 跑完并提交后自动或手动标记为readyship会做合并检查把分支合并到目标分支后自动删除 worktree 并回收磁盘drop则是放弃当前 worktree 里的全部改动。这套状态机的价值在于你可以随时知道现在到底有几个 Agent 在工作、各自的进展如何而不是靠记忆在十几个命令行窗口里翻找。2.3 目录约定让 Agent 和 IDE 都找得到路很多并行方案失败在人找得到 worktreeAgent 找不到。所以我要求所有 worktree 统一创建在仓库根目录下的.worktrees/name目录里而不是散落乱放。这个约定的好处有三个。第一路径可预测Agent 的 Prompt 里可以直接写你的工作目录是.worktrees/feat/v2-profile不用每次动态发现。第二IDE 可以按固定规则打开窗口不会出现开错仓库副本的情况。第三.worktrees目录写进.gitignore之后主工作区的git status不会被一堆 worktree 的产物弄脏。实际使用中我还给每个 worktree 生成一个.agent-context文件里面记录了 Agent 名称、任务描述、创建时间、基础分支等元信息。Agent 在开始干活前如果加载这个文件就能快速理解我在哪、我要干嘛、如何回退。2.4 配置文件与细粒度控制Worktrunk 支持在仓库根目录放一个.worktrunk.yml定义项目级策略。下面是我这边的实际配置# .worktrunk.yml root_dir: .worktrees base_branch: main merge_strategy: no-ff auto_cleanup: true agents: default: command: codex exec paths: exclude_from_copy: [node_modules, vendor, .venv, dist]exclude_from_copy是我特别看中的一个字段。worktree 的工作区文件是独立 checkout 出来的如果基础分支上带着大量依赖产物每个 worktree 都会白白吃掉一份磁盘。Worktrunk 对这个字段里的目录统一建立符号链接指向主工作区对应的共享依赖目录加速初始启动的同时省下大量空间。不过这个优化要小心使用稍后我会在踩坑部分详细说它的副作用。3. 十分钟跑通第一个并行 Agent 工作区安装与上手3.1 环境准备前置条件不复杂Git 2.15 以上就支持基础 worktree 功能建议用 Git 2.30 以上Windows 上体验更好。支持 macOS、Linux以及 Windows 的 Git Bash / WSL。Windows 下如果要用符号链接共享依赖建议开启开发者模式或者把 Worktrunk 的链接模式配成目录联接junction避免权限问题。安装方式有三种从 GitHub Releases 下载对应平台的二进制直接放到 PATH 里用 Homebrew 装brew install worktrunk或者源码构建因为我当前用 Go 实现一条go build就能出干净的单文件二进制没有任何运行时依赖。3.2 初始化并创建第一个 worktree在仓库根目录执行worktrunk initinit会检查当前仓库是否有未提交改动有的话提示你先处理避免把脏状态带进并行环境、确认 Git 版本、生成.worktrees目录和默认的.worktrunk.yml。接着创建第一个并行工作区我会带--agent参数标明归属worktrunk create feat/v2-profile --from main --agent agent-a这条命令内部实际做了四件事从 main 拉出feat/v2-profile分支在.worktrees/feat/v2-profile下完成 checkout生成.agent-context元信息处理符号链接。命令结束时输出一行AGENT_WORKSPACE/path/to/.worktrees/feat/v2-profile,这一行是关键——Agent 启动时可以直接拿它作为工作目录。3.3 让 Agent 在隔离工作区干活不管你用 Codex CLI、Claude Code、Trae CLI 还是手写脚本调模型只要保证 Agent 启动时的工作目录指向 worktree 路径就可以cd .worktrees/feat/v2-profile codex exec 实现 GET /v2/profile 接口保持与现有 v1 接口的兼容性对 AI Agent 来说它眼里的世界被限定在这个目录内git 历史是这个分支的历史文件状态是这个 worktree 的状态跑测试也只影响这个目录。即使另一个 Agent 在主工作区疯狂提交它也不会感知到——这正是互相隔离、各自推进的核心。3.4 合并与清理Agent 干完活、把改动提交到自己的分支之后回到主目录执行worktrunk ship feat/v2-profile --to developship的逻辑是检查 worktree 是否干净 → 切换到 develop → 合并feat/v2-profile按配置走 no-ff 或 squash→ 推送远端 → 删除 worktree → 删除已合并分支。一条命令把整个收尾链路跑完。如果 Agent 中途跑飞了改动不想要直接worktrunk drop feat/v2-profile --forcedrop 会放弃整个 worktree 的改动并回收磁盘分支默认保留但会打上abandoned标记方便以后追溯为什么存在这条分支、当时发生了什么。4. 三种实战接入方式把 Worktrunk 装进现有工作流4.1 多 Agent 并行实现多个 Feature假设一个迭代里有三个互不依赖的业务功能想同时让三个 Agent 并行开发。传统做法是手动开三个终端、三个分支、三个目录然后精神高度紧张地避免搞混。用 Worktrunk 的做法是三条命令worktrunk create feature/payment --from main --agent agent-1 worktrunk create feature/export --from main --agent agent-2 worktrunk create feature/notification --from main --agent agent-3然后 dispatch 到不同终端去跑 Agent。中途用worktrunk ls看进度做完一个ship一个。最直观的变化是你不再需要在三个仓库副本之间人肉同步每个 Agent 的提交历史天然分开review 时一次看一个分支逻辑非常干净。这种模式下我强烈建议把merge_strategy配成squash。因为 Agent 在开发过程中很容易产生十几次小提交里面不少是fix typo、retry这类噪音squash 成一条干净提交后develop 的提交图会好维护得多。4.2 高危实验与依赖升级隔离第二种场景是我个人最喜欢的——把 worktree 当成实验舱。有一次我要升级一个核心依赖的大版本这种任务风险很高一旦失败可能会污染主工作区好几天。我把实验放到独立 worktreeworktrunk create experiment/upgrade-http-client --from mainAgent 在里面做依赖升级、跑迁移、修测试。一种情况是实验顺利数据都验证过了再决定合并回主分支另一种是实验结果不理想一条worktrunk drop experiment/upgrade-http-client --force就把整个现场清理干净主工作区毫发无伤。这类场景尤其适合握让 Agent 做大规模重构——在强拆主分支之前先在隔离区跑通全量测试再决定要不要带回来。对配置中心、构建脚本这类牵一发动全身的文件单独开一个 worktree 去改比在主干上赌运气稳妥得多。4.3 与 Codex CLI / Claude Code 这类本地 Agent 工具组合最近社区讨论热度很高的 Codex CLI、Claude Code、Trae CLI本质上都是本地命令行 Agent 环境。它们解决的是怎么让模型读写代码、执行命令、自我迭代的问题但并解决多个这种环境同时跑的时候怎么互不干扰的问题——而 Worktrunk 恰好补的是这一层。我的用法是每次要起一个新的 Codex 会话先worktrunk create一个 worktree再把路径传给codex exec -C会话结束无论成败worktrunk ship/drop收尾。这样即使我同时在跑四五个编码 Agent每个 Agent 都活在独立世界里提交、回滚、复盘都有清晰的边界。对我来说Worktrunk 和这些 Agent CLI 不是竞争关系而是上下游配合它们负责思考与执行Worktrunk 负责空间隔离与生命周期管理。5. 跑了一段时间 Worktrunk我处理的五个现场问题5.1 分支被 worktree 占用git branch -D 删不掉第一次遇到时很容易愣住。fatal: branch feature/xxx is used by worktree at /path/to/.worktrees/xxx我明明加了-D强制删除Git 还是拒绝。这个其实是 Git 的保护机制一个分支同一时刻只能被一个 worktree checkout防止两个位置对同一分支的引用互相覆盖。解决方式并不难先git worktree remove或worktrunk drop再删分支。但里面有个容易忽略的点如果 worktree 里有未提交改动git worktree remove也会拒绝执行。Worktrunk 在drop时如果检测到未提交内容会先打印 diff 摘要让你确认这能有效防止手滑丢掉一整个下午的工作。我在改进前曾经直接删过 worktree 目录导致一堆未提交代码彻底找不回来只能靠编辑器缓存哭着重写——这个坑希望大家不要踩。5.2 依赖目录与磁盘空间问题把 Worktrunk 分享给同事用的时候反馈最多的就是磁盘。一个中型前端仓库 node_modules 动辄几个 GB不开符号链接的话五个 worktree 就是五个 GB 起步。我的解法就是配置里的paths.exclude_from_copyWorktrunk 对这些目录统一创建符号链接指向主工作区依赖。Windows 下符号链接权限麻烦建议配成link_mode: junction用目录联接实现。这样每个 worktree 的依赖都走主工作区那一份磁盘占用几乎不增加。但有一副作用必须提醒如果两个 Agent 同时在跑依赖安装命令它们会写到同一个真实目录互相踩。所以这个优化只适合依赖相对稳定、Agent 主要改业务代码的场景如果 Agent 的任务本身就包含依赖变更、版本升级那还是要让它用自己的 node_modules 更安全。取舍规则很简单看任务是否会动依赖会动就别共享。5.3 IDE 打开的还是旧工作区用 VS Code 或者某些带 IDE 联动能力的 Agent 工具时有个很隐蔽的坑你先打开了主工作区窗口再创建新的 worktree新窗口开错了路径Agent 的改动看起来就像消失了。实际上不是消失是你盯错了工作区。我现在的习惯是给每个 worktree 指定固定无误的绝对路径用一个终端统一跑worktrunk ls查看需要用 IDE 打开时从输出里直接复制路径而不是凭印象输入。新项目一律先worktrunk init让.worktrees从第一天就存在避免后续补建时路径不一致。这个习惯养成后我再也没出现明明改完了却找不到文件在哪的尴尬。5.4 多个 Agent 合并冲突的现场恢复多个 Agent 并行开发最终往 develop 合并时撞车几乎是不可避免的。Worktrunk 的ship命令遇到冲突会中途停下来不删 worktree不强推合并状态这点非常重要——它给你留了完整的现场。我最常遇到的冲突模式是两个 Agent 都修改了同一个配置文件比如package.json或某个公共常量文件。Git 的冲突标记会把双方改动标出来但让 Agent 去改冲突标记实际体验很别扭——它看到的是一堆、、很容易在里面绕晕。我的推荐做法是中立方 worktree 处理冲突单独开一个 worktree把两个分支都拉进来用三方合并工具解决确认没问题后再把解决结果提交回去最后走 ship。这个 worktree 用完就 drop不占用正线资源又能把冲突处理从多人抢修变成一个隔离区慢慢解。5.5 孤儿 worktree 与历史追溯Agent 偶尔会崩溃或者某个会话被人为遗忘。这时 worktree 不会自己消失它会一直占着磁盘和分支引用时间一长就变成一堆鬼魂目录。Worktrunk 的做法是给每个 worktree 打上明确的状态和归属标记。worktrunk ls里能看到创建时间、最近活跃时间、Agent 名称、状态是 running 还是 abandoned。配合auto_cleanup配置超过设定时间仍处于 abandoned 状态的 worktree 会在下次执行安全回收并输出一条回收记录。我在实际使用里发现只要回收记录里留有分支名和 Agent 名称隔很久回查时依然能还原当时的工作轨迹这个能力对团队协作特别有价值。6. 进阶玩法与团队落地从单机工具到协作约定6.1 把 worktree 注入 Agent prompt 模板很多 Agent 工作流的失败都源于上下文里没有路径概念。你让 Agent 干活却没告诉它自己的准确定位它只能模糊地猜。我建议 Worktrunk 在 create 之后把一段上下文直接吐出来方便粘进 prompt你正在 /path/to/repo/.worktrees/feature/payment 目录工作。 当前分支是 feature/payment基础分支是 main。 你的任务是实现支付回调功能。 可以执行任意 git 命令但请在当前 worktree 内提交改动。这段上下文对 Agent 的隔离感建立非常重要。实测下来Agent 在明确知道我的世界是哪个目录之后误改其他区域文件的情况大幅减少因为它不再需要靠猜测来定位自己。如果你用的是支持系统提示词定制的 Agent CLI可以把这段写成模板变量每次创建后自动替换路径和任务描述。6.2 批量启动多个 Agent 的脚本化如果要一次起五个 Agent每个管一个模块纯手工敲命令显然不现实。我一般在 shell 里这么跑for task in auth billing notify search report; do worktrunk create feature/$task --from main /dev/null \ echo 启动 $task $(worktrunk ls --path feature/$task) done启动完五个 worktree 之后再用 tmux 或者在多个终端里分别 dispatch 给 AI CLI。全部工作区信息在worktrunk ls里一眼看完哪个 Agent 完成了、哪个还没开始状态一目了然。这条链路还能继续封装成更上层的调度脚本比如按任务类型自动选择 Agent 模型、任务结束后自动收集测试结果并通知你都是水到渠成的事。6.3 合并策略与团队协作约定最后聊一下合并策略。squash 适合业务迭代型并行任务优点是可以有效过滤 Agent 在开发过程中产生的噪音提交让主干历史保持简洁no-ff 适合需要保留完整工作轨迹的场景比如评审时要看中间状态、或者你希望把 Agent 从开始到完成的全过程 保留在 git 历史里。如果团队多人协作建议把 Worktrunk 的约定固化到.worktrunk.yml里并提交到仓库这样不管谁在哪个环境打开行为和命名规范都一致。worktree 的命名也值得定一套规范比如类型/业务名,避免出现test1、test2这种完全不可读的目录。命名规范一旦沉淀下来配合worktrunk ls看板整个团队对一个仓库正在并行推进的所有工作就能做到一目了然。最后再分享一点个人体会工具本身命令不多但把 worktree 生命周期当成流水线来管理之后并行 Agent 工作流才真正变得可管、可控、可回溯。我现在已经默认每个 Agent 一个 worktree没有例外。如果你也在同时跑多个编码 Agent正被代码互相污染搞到崩溃我建议先从给 Agent 分配独立 worktree 开始——有时候一个干净的空间比模型换大一号更解决问题。
返回列表