Worktrunk:用Git Worktree实现并行AI Agent工作区隔离 最近在做 AI Agent 工程化落地时我发现一个特别反直觉的现象很多人每天花大量精力调 Prompt、选模型、搭 MCP 服务但真正卡住并行 AI Agent 跑起来的往往不是模型能力也不是上下文窗口而是一个最不起眼的东西——Git 工作区。手头同时跑两三个 AI 编程代理比如 Codex CLI、Claude Code CLI、Gemini CLI时它们会同时修改同一个目录下的文件互相覆盖、互相踩踏。改完之后连 git status 都分不清哪行是谁改的更别提交代清楚让哪个 Agent 继续完成哪部分工作了。一次两次还能忍任务一多、仓库一大整个工作流就乱成一锅粥。我捣鼓了小半个月最后解决方案落地在一个叫 Worktrunk 的 CLI 工具上——它专门做 Git Worktree 的管理核心目标就是服务并行 AI Agent 工作流。这篇文章把它的原理、用法、以及我在实践中踩过的坑完整写出来希望能帮到同样被这个问题折磨的人。1. 并行 AI Agent 时代最大瓶颈竟然是 Git 工作区1.1 多 Agent 协作的文件踩踏困境先描述一个我实际经历过的典型混乱场景。某次我把一个中等规模的后端仓库交给两个 AI Agent 并行改Agent A 负责新增用户登录接口Agent B 负责把工具函数库从 lodash 迁移到原生实现。两个任务互不依赖按说完全能并行。结果跑了不到十分钟Agent A 发现src/utils/http.ts里的辅助函数被改得面目全非Agent B 则发现它启动时读的配置文件里多了一段不属于它的代码。这就是典型的文件踩踏多个 Agent 共享同一个工作目录彼此对文件的修改没有隔离谁后保存谁覆盖。更麻烦的是Agent 的上下文比如它读过的文件、做过的决策往往绑定在一组具体路径上一旦其他 Agent 改了同一个文件它的后续决策就会基于过期甚至错误的信息错误的代码像滚雪球一样累积。很多人第一反应是让 Agent 改不同目录不就行了。问题是你不能用目录层面去硬隔离代码——同一个仓库里服务层和工具层是互相引用的Agent A 改了接口定义Agent B 的代码很快就编不过了。真正的隔离不是在目录上做文章而是让每个 Agent 拥有独立的、完整的、可随时切换上下文的代码版本。1.2 为什么git branch切换解决不了本质问题那直接用 Git 分支呢Agent A 在feat/login分支上跑Agent B 在chore/lodash-migration分支上跑听起来很合理但用起来全是坑。第一个问题是物理工作目录只有一个。Agent A 正在改代码时你想让 Agent B 开工就必须先暂停 A、提交或暂存当前改动然后切到 B 的分支。可是 Agent A 可能正在读文件、分析代码强行切分支会导致它的文件句柄失效、状态错乱Agent B 再一跑两个会话就互相污染了。第二个问题是上下文丢失。AI Agent 不像人它不会记住刚才在想什么。每次切换分支回来它需要重新读文件、重新构建上下文。你让它切三次分支它的有效工作时间至少打七折。而且很多 CLI 工具的会话状态是绑定在绝对路径上的切了分支之后路径虽然没变但文件内容全变了它的记忆就全错位了。所以分支隔离解决了变更归属问题但没有解决工作目录独占问题。磁盘上同一时间只有一份工作目录它就是所有 Agent 共享的可变状态——只要这个共享状态存在并行就永远只是伪并行。1.3 Git Worktree被严重低估的原生并行机制Git 其实从 2.5 版本开始就提供了一个专门用来解决这个问题的原生机制git worktree。它允许你在同一个仓库下创建多个工作目录每个工作目录对应一个独立的分支彼此完全隔离。但它们共享同一个.git对象数据库和引用数据库所以不产生额外的远端仓库拷贝磁盘开销非常小。你可以把 worktree 理解成同一份代码仓库的多个互不干扰的副本但它们底层共享着同一个对象库。对我来说worktree 最舒服的一点在于每个工作目录都是一个完整的、独立的、可随时跑起来的分支副本。Agent A 在/workspace/repo-agent-a里改代码Agent B 在/workspace/repo-agent-b里改代码它们看到的是各自分支的最新状态互不可见、互不影响。两个人同时写同一个文件都不冲突因为它俩物理上就不在同一份文件上。这才是并行 AI Agent 工作流真正需要的底层能力。但就像很多好用的原生能力一样Git Worktree 的命令行交互在复杂场景下会变得很繁琐你要记住每个 worktree 的路径、记住哪个分支对应哪个任务、跑完之后要手动清理、稍不注意就会把 worktree 删错。这些重复劳动叠加起来恰恰是 Worktrunk 这类工具存在的价值。2. Worktrunk 的核心原理与设计思路2.1 底层机制拆解.git/worktrees/与 HEAD 隔离想用好 Worktrunk必须先理解git worktree在底层到底做了什么。当你执行git worktree add /path/to/wt -b feat/login时Git 会做三件事一是在.git/worktrees/wt目录下创建一个元数据子目录里面记录了这个 worktree 的 HEAD、index、以及它对应的.git文件路径二是把feat/login分支检出到/path/to/wt这个工作目录三是更新相关引用的 reflog。这个机制带来的两个关键隔离是第一每个 worktree 有独立的 HEAD 和 index。index是什么它是暂存区的数据库记录了你git add了哪些文件。主工作区暂存了 A 文件不会影响其他 worktree 的暂存状态。第二每个 worktree 共享对象库和远程引用的获取。你在 worktree A 里git fetch拿到的新提交在 worktree B 里执行git log也能看到因为 refs 是共享的。有一点需要特别注意同一个分支不能在两个 worktree 中同时检出。比如主工作区在main分支上另一个 worktree 想切到mainGit 会直接拒绝并提示main is already checked out at /path/to/main-worktree。这个限制看起来烦人实际上是为了保护并发写入的安全性逻辑是对的。Worktrunk 在设计上就是依托这套机制它负责管理 worktree 的生命周期、命名规则、分支映射和状态展示而底层的数据一致性完全交给 Git 自身的机制来保证。这种不重复发明轮子、把复杂状态管理交给底层原生能力的设计理念让工具本身非常轻量且可靠。2.2 Worktrunk 的 CLI 设计哲学声明式配置替代记忆负担如果用原生git worktree命令管理少量任务手动操作完全能应付。但当你同时管理五六个 Agent 任务时痛点就出来了每个 worktree 对应哪个分支哪个任务已经跑完了可以清理哪些 worktree 还有未提交的改动不能动Worktrunk 把这些问题收敛到了一套声明式配置里。你不需要记住每个 worktree 的命令历史只需要在一个配置文件中声明我有哪几个任务、每个任务用什么分支名、跑在哪个目录然后让 Worktrunk 帮你把目标状态和实际状态对齐。它的核心命令面我做了一张速查表命令作用对应原生 Git 操作worktrunk init在当前仓库初始化 Worktrunk 配置-worktrunk list展示所有 worktree 的状态分支、目录、dirty 状态git worktree listworktrunk plan对比配置文件声明的目标状态和当前实际状态给出待执行的差异列表-worktrunk up按配置文件批量创建/检出/切换 worktreegit worktree add/git switchworktrunk clean清理配置之外的残留 worktreegit worktree remove/git worktree pruneworktrunk exec -- cmd在全部或指定worktree 中批量执行命令for d in *; do (cd $d cmd); donelist和plan是我个人使用频率最高、也最推荐优先体验的两个子命令。list帮你一眼看清当前所有 worktree 的分布情况plan则是模拟运行它会告诉你如果执行up会发生什么避免误操作。这种先看计划再执行的思路和terraform plan的哲学很像对管理多个并发环境非常友好——你永远知道自己将要改变什么。2.3 为什么手写脚本管理 worktree 是个坑看到这里你可能会说这不就是几个git worktree命令套个壳吗我自己写个 shell 脚本也能做到。确实简单的创建和列出用脚本完全够。但我在实际项目里发现真正复杂的是边界情况而脚本很难把这些情况处理干净。举例来说当你想要清理一个还有未提交改动的 worktree 时git worktree remove会拒绝执行你必须先确认这些改动是否可以丢弃。在脚本里你要么写-f强删危险要么写一堆交互逻辑复杂。另一个麻烦是 worktree 的悬挂问题。如果你直接手动删除了 worktree 的目录比如rm -rfGit 的.git/worktrees里会残留一个失效的元数据记录。之后你执行git worktree list还能看到那个目录但访问它就会报错。git worktree prune可以清理这种悬挂记录但一般开发者根本不知道要跑这个命令。Worktrunk 的价值不是替代 Git 命令而是把多 worktree 生命周期管理这件事变成第一等公民。它处理悬挂目录、处理脏状态检查、处理配置漂移这些恰恰是脚本最容易踩坑、也最不显眼的地方。我的建议是如果你只是偶创建一两个 worktree直接用原生命令没问题如果工作流里已经出现了多个 Agent 并行跑的形态那一个专门的管理 CLI 带来的收益会非常明显。3. 从零到一安装、配置与多 Agent 工作区搭建3.1 环境要求与安装方式Worktrunk 对运行环境的要求很朴素一个是 Git 版本另一个是你本机的 CLI 环境。Git 方面git worktree从 2.5 版本开始引入早期版本有一些缺陷比如 worktree 内的git status性能较差、某些命令不支持--git-dir路径解析等。我建议至少使用 Git 2.30 或者更高版本在 Linux/macOS/WSL 上运行基本没有问题。Windows 原生环境也可以跑但路径分隔符对 worktree 元数据的处理有时会有一些小毛病更推荐在 WSL 下使用体验会顺畅很多。安装方面Worktrunk 本质上是一个单二进制 CLI 工具你可以从项目的 Release 页面下载对应平台的二进制也可以从源码构建。如果你的机器上有 Go 工具链一条命令就能装好go install github.com/worktrunk/worktrunklatest装完后执行worktrunk version确认安装成功。如果输出正常说明环境就绪。顺便说一句如果你之前安装过 Codex CLI 或者 Claude Code CLI应该对这类单文件 CLI 的安装套路不陌生。Worktrunk 和它们可以无缝协作——它不关心你跑的是哪个 Agent 客户端只关心你的 Git 仓库里有没有并行的 worktree。3.2 用配置文件声明你的并行任务Worktrunk 的核心用法是通过worktrunk.yaml配置文件来声明你的工作区目标状态。我自己在项目里用的配置是这样的# worktrunk.yaml project: my-app agents: - name: agent-login task: feature/login branch: feat/agent-login path: worktrees/agent-login - name: agent-utils task: refactor/utils branch: refactor/agent-utils path: worktrees/agent-utils - name: agent-fix task: fix/payment-timeout branch: fix/agent-payment-timeout path: worktrees/agent-fix这份配置声明了三件事每个 Agent 的标识name、它要处理的任务task、以及对应的 Git 分支和目录路径。path是相对于仓库根目录的所以worktrees/agent-login表示在仓库根目录下的worktrees/agent-login子目录中创建 worktree。有了配置之后先执行worktrunk plan看一下差异$ worktrunk plan - will create worktree: worktrees/agent-login (branch feat/agent-login) - will create worktree: worktrees/agent-utils (branch refactor/agent-utils) - will create worktree: worktrees/agent-fix (branch fix/agent-payment-timeout)确认没问题之后执行worktrunk up它会批量创建这些 worktree。创建完成后项目的目录结构大概是这样的my-app/ ├── .git/ ├── worktrunk.yaml ├── worktrees/ │ ├── agent-login/ # 分支 feat/agent-login │ ├── agent-utils/ # 分支 refactor/agent-utils │ └── agent-fix/ # 分支 fix/agent-payment-timeout └── src/ # 主工作区通常在 main 分支每个 worktree 都是一个完整的独立工作副本。你现在可以分别把三个目录交给三个不同的 AI Agent 去并行干活了。有一点要留意配置文件中不要写成path: worktrees/agent-login/这种带尾斜杠的形式Git worktree 在解析路径时把带尾斜杠的目录当作已存在目录处理有时会报一个奇怪的错。我在最初使用时就被这个问题卡了一次后来统一去掉尾斜杠就好了。3.3 日常操作与批量执行创建 worktree 只是第一步日常维护才是大头。一旦 worktree 建立起来我用得最频繁的几个命令是# 查看所有 worktree 的状态 worktrunk list # 在某个 Agent 的 worktree 里执行 git 操作 cd worktrees/agent-login git status # 在所有 worktree 里批量执行测试 worktrunk exec -- go test ./... # 清理已经合并完成的分支对应 worktree worktrunk clean --mergedworktrunk exec是我非常推荐的一个功能。在并行开发场景下让所有 Agent 的代码一起跑一遍完整测试是很常见的要求。原生做法是写一个 for 循环在目录间切换Worktrunk 把这一步收敛成了一个命令。它会把每一个 worktree 里的执行结果分别标注清楚方便你快速定位是哪个 Agent 的改动引起了测试失败。注意worktrunk clean我加了--merged参数它的意思是只清理那些分支已经被合并进主分支的 worktree保留还有独立开发的 worktree。这个参数非常有用避免了你手动判断哪些分支可以删、哪些必须留。4. 实战三个 AI Agent 并行改同一个仓库的完整流程4.1 任务拆分与 worktree 映射理论讲完说一个实际跑通的案例。我最近在维护一个 Go 后端服务同时有三个模块需要改动登录认证模块、工具函数库清理、以及一个支付超时 bug 修复。这三个任务在代码层面有少量文件重叠但大致是独立演进的。如果串行做每个任务要等上一个任务完成整体耗时至少是三倍如果并行做就必须解决工作区隔离问题。我的做法是先给三个任务分别建分支然后通过 Worktrunk 配置文件把它们映射到三个 worktree 上。接下来我分别用三个终端窗口启动不同的 AI Agent这里我实际用的是 Codex CLI 和 Claude Code CLI并告诉它们各自的工作目录和任务目标# 终端1Agent A 处理登录模块 cd /workspace/my-app/worktrees/agent-login codex 实现用户登录接口包含手机号验证码登录和 Token 刷新 # 终端2Agent B 处理工具库迁移 cd /workspace/my-app/worktrees/agent-utils claude 把 src/utils 下的 lodash 用法全部替换为原生实现 # 终端3Agent C 修复支付超时 cd /workspace/my-app/worktrees/agent-fix codex 修复支付回调偶发超时的问题重点排查数据库连接池配置关键点是每个 Agent 只在它自己的 worktree 目录下读写文件。Agent A 看不到 Agent B 的改动Agent B 也没法破坏 Agent C 的现场。三个 Agent 同时跑互不干扰。4.2 并行开发中的隔离策略与验收并行跑起来之后最有意思的现象是 git status 完全互不可见。如果你在agent-login目录下执行git status你只会看到 Agent A 的改动在agent-utils下只会看到 Agent B 的改动。这和分支隔离工作区共享时代是完全不同的体验——那种模式下 git status 往往是两个任务改动的混合体根本没法用。但这不代表你完全不用管隔离之外的依赖关系。这里有一个重要的实践建议共享文件的变更尽量控制在加函数/加接口层面避免大范围重命名。我在这个项目里虽然三个 Agent 在各自的 worktree 中开发但它们的改动最终要合并到同一份main分支上。如果 Agent B 把utils.go里的Foo()函数改名为FooV2()而 Agent A 的代码恰好调用了Foo()合并时就会产生编译冲突。这种冲突在 worktree 并行阶段完全不可见只有合并时才会暴露。所以我在每个 Agent 跑完后都会有一个验收前检查步骤cd worktrees/agent-login git diff --stat # 看改动范围 git diff --name-only # 看改了哪些文件 worktrunk exec -- go build ./... # 全工作区编译验证go build ./...在各自 worktree 里独立跑编译通过只是一个基础门槛。真正的验收是看是否影响了其他 Agent 的代码这个问题只能留到合并阶段去验证。4.3 合并取舍顺序、冲突与代码评审三个 Agent 都跑完后合并是一个技术活。这时 Worktrunk 的价值在于它把哪些分支已就绪展示得一清二楚你可以根据任务风险来决定合并顺序。我的合并策略是先从最底层、被依赖最多的模块开始合并。在这个例子中Agent B 改的工具函数库被另外两个 Agent 都依赖所以优先合并它。然后合并 Agent A 的登录模块最后合并 Agent C 的支付修复因为它的影响面最小、独立性强。合并过程git switch main git pull origin main git merge refactor/agent-utils git merge feat/agent-login git merge fix/agent-payment-timeout顺序合并的好处是如果 B 的工具库改动导致了 A 的编译失败这个冲突会在我合并 B 之后立刻暴露出来而不是等三个分支一起汇合时才能发现。一个一个小冲突地解决比三个大冲突一起堆在面前要轻松得多。合并时如果遇到多个分支同时修改了同一个函数我会先拉出三个 diff 对比一下谁改的是哪个逻辑片段再决定保留谁、或者手动找一个兼容方案。这里不建议让 AI Agent 自动解决合并冲突它们在冲突解决上的误判率相当高。我在实际中遇到过一次 Agent 自动把两个分支里完全不同的新功能融合成了一个不伦不类的实现编译能过但逻辑完全错误。从那以后所有合并冲突都坚持手动处理。5. 踩坑实录与工程化建议5.1 worktree 的边界条件分支互斥与清理陷阱第一类坑和 Git worktree 自身的规则有关。最典型的是同分支互斥。主工作区如果在main分支上那么任何其他 worktree 都不能再检出main。这本身是特性不是 bug但配合 Agent 工作流时有个隐性问题如果某个 Agent 的任务中途变成了直接改主分支你得先把主工作区切到其他分支或者专门为它新建一个 worktree。第二类坑是worktree 清理。git worktree remove会在目录里有未跟踪文件时拒绝删除这时你通常需要-f强制删除。但-f之后worktree 的元数据可能没有完全清理干净。在 Worktrunk 里worktrunk clean默认不做-f因为它希望你先确认这些文件是否真的可丢弃。如果要清理的对象是正在被某个 Agent 进程占用的目录删除会报 directory not empty 错误。解决方案是先停掉 Agent 进程再删除这个顺序问题在并行运行时特别容易踩到。第三类坑是路径大小写和符号链接在 macOS 上尤其明显。默认文件系统大小写不敏感如果你有两个 worktree 路径只是大小写不同WorkTREEvsworktreeGit 可能把它们当作同一个目录导致各种诡异错误。Worktrunk 的做法是在配置解析阶段检查路径冲突发现时直接报错。如果你绕开 Worktrunk 手写脚本这类问题排查起来非常费时。5.2 与 IDE、工具链的兼容问题Worktree 模式对命令行工具是透明的因为它们只关心当前目录但对 IDE 和守护进程并不是。以 VS Code 为例如果你同时打开了主工作区和worktrees/agent-login两个窗口第一次打开没有问题但在某些版本中会出现工作区信任提示混乱、以及调试器无法绑定正确工作目录的问题。我的习惯是只打开当前需要用到的 worktree 窗口其他 worktree 保持命令行状态不开启 GUI 编辑。另一个容易踩的坑是后台守护进程。如果你有一个文件监听器比如 Air、nodemon、go-watcher在主工作区跑着它监听的路径是固定的。当你切到 worktree 下开发时那个守护进程监听的还是原来的路径不会自动跟着切。结果就是你在 worktree 里的改动触发了自动重载但重载加载的是主工作区的代码——这个 bug 排查起来非常隐蔽。我现在的工程规范是每个 worktree 独立启动自己的守护进程监听路径直接指向 worktree 目录。Worktrunk 的exec命令在这里也能派上用场批量启动所有 worktree 的开发服务器worktrunk exec -- air虽然这个命令要求每个 worktree 里都有对应的二进制或脚本但一次拉起全部开发环境效率确实高。5.3 多 Agent 并行开发的规范建议最后分享一些我在多 Agent 并行开发中总结出的工程规范。第一约定文件所有权。虽然 worktree 从物理上隔离了改动但最终合并时仍然会冲突。建议在任务拆分阶段就明确谁负责哪些文件尽量避免两个 Agent 同时改同一个文件的不同位置。Worktrunk 配置文件里的task字段除了说明任务外其实也在文档化这个所有权约定。第二配置文件的版本管理。worktrunk.yaml本身应该提交到仓库里。团队成员拉取仓库后执行一个worktrunk up就能重现整个并行开发环境。我之前犯过一个错误是把 worktrunk.yaml 加进了.gitignore结果另一个同事怎么都复现不了我的环境排查半天才发现是这个文件没被提交。第三定期 prune。Worktrunk 在内部维护 worktree 元数据但如果你手动删除了 worktree 目录元数据会出现悬挂。建议每隔一段时间跑一次worktrunk clean和git worktree prune把所有陈旧状态一次性清理干净。这也是我通常在合并完一批分支后做的收尾工作。第四也是最重要的一条永远不要在 worktree 之间共享未跟踪文件。worktree 解决了已跟踪文件的隔离但未跟踪文件比如.env、本地配置文件、临时脚本默认是互不可见的。如果你在agent-login里创建了一个.envAgent B 那边是看不到的。你要么把这些文件纳入 Git 跟踪并做加密处理要么专门建一个公共的配置同步目录。这个坑我只踩过一次就学乖了代价是一个 Agent 因为没有环境变量跑了半小时全部失败。6. 下一步还能怎么扩展跑通这套流程之后我开始把 Worktrunk 往更深的自动化方向用。一个方向是把它接入 CI每次合并完一批分支后自动执行worktrunk clean --merged保持仓库整洁。另一个方向是把它和任务管理工具联动——每个 worktree 对应一个 Jira 或 GitHub Issue合并分支后自动关闭 issue。这些如果做成脚本会和 Worktrunk 的命令面配合得很自然。还有一个大的思路用 Worktrunk 把 Agent 会话容器化。每个 agent 不再直接接收去改src/services这种模糊指令而是接收去/workspace/xxx/worktrees/y目录完成 y 分支上的 y 任务这种带明确目录、明确分支、明确边界的指令。这样即使未来并行的 Agent 数量从 3 个变成 10 个也只需要在配置里多加几行工作流不会崩。我自己在这套方案下最直观的感受是并行 Agent 没再互相打架过git status 永远是干净的、可解释、可追踪的合并时的冲突从一坨变成了可数的、逐项解决的。这个体验差距用回单工作区后就再也回不去了。