最近我在折腾并行 AI Agent 开发时,遇到一个特别拧巴的问题:同一个代码仓库,一个 Agent 重构核心模块,一个 Agent 补测试,还有一个 Agent 在改文档。听起来分工挺明确,可真跑起来你会发现,一个工作目录根本塞不下三个 Agent。改着改着,git status 里全是别人的半成品;A Agent 刚格式化完的代码,B Agent 一小时后又按自己的规矩改了回来;跑测试的时候,两个 Agent 抢占同一个构建缓存,直接把队友一脚踢翻。
Git Worktree 是解决这个问题的标准答案,但它裸用起来还是有点难受:路径要自己想、分支名要靠记忆、清理的时候还得先回忆一堆命令。于是我把日常操作整理成了一个叫 Worktrunk 的 CLI 工具,专门把 Git Worktree 包装成“任务 = 工作区”的心智模型。这篇就聊聊 Worktrunk 解决的是什么问题、我为什么这么设计、以及怎么用它把多个 AI Agent 真正并行跑起来。想搭一套干净并行开发流的朋友,都可以直接参考,即使你是刚接触 AI Agent 和命令行工具的新手,这套思路也能帮你少踩不少坑。
1. 为什么并行 AI Agent 工作流会卡在“共享一个目录”上
1.1 三个 Agent 挤在一个工作区,会发生什么
很多人一开始对“并行 AI Agent”的理解是:把几个 Agent 丢进同一个项目目录,给它们分别交代任务,它们就会各自干活、互不干扰。实际情况完全不是这样。我第一次试的时候,两个 Agent 同时操作同一个工作目录,结果惨不忍睹:
- 上下文污染最致命。Agent 看到的是整个工作区的状态,它会无差别地把其他 Agent 留下的半成品、临时修改、未完成的接口当成“现状”。有一次重构 Agent 在改一个模块的返回值,测试补充 Agent 已经基于旧返回值写了十几个测试用例,两边一叠,代码全都对不上。
- 文件互相覆盖是常态。两个 Agent 可能同时格式化同一个文件,或者一个改了公共配置,另一个又从旧缓存里读了一遍,直接覆盖回去。哪天你没有 commit 的习惯,改着改着连哪个版本是谁写的都分不清。
- 构建产物和缓存会互相踩踏。前端项目里的 .next、node_modules/.cache,后端项目里的 target、pycache,第一个 Agent 刚跑完一轮验证,第二个 Agent 一跑测试,缓存直接失效,全都被重建一遍,白白浪费算力。
这就好比一个很小的厨房,两个厨师共用一把刀、一个案板。不是切到手,就是菜被拿错,再不就是刚做好的半成品被另一个人当成垃圾倒掉。你需要的不是让厨师们“注意一点”,而是直接把厨房隔成几个独立的操作台。
1.2 Git Worktree 的机制:一份 .git,多张“工作桌”
Git Worktree 就是那种“把厨房隔开”的天然解法。它允许你在同一个仓库里同时维护多个工作目录,每个工作目录都有自己的文件、自己的 index、自己的 HEAD,可以互不干扰地 checkout 不同分支。
它的原理其实很巧妙:所有 worktree 共享同一份 Git 元数据(objects 目录、refs 引用),但每个 worktree 的工作目录是独立的。也就是说,你在 main 分支上正常开发,同时还能开一个 ../agent-a 目录专门跑重构分支,再开一个 ../agent-b 目录专门跑测试补充分支,三个目录共存,互不影响。
裸用 git worktree 的关键命令并不复杂:
# 在指定目录创建一个新 worktree,并基于默认分支创建新分支 git worktree add ../agent-a -b feat/agent-a # 查看所有 worktree 列表 git worktree list # 删除一个 worktree git worktree remove ../agent-a当多个 Agent 并行开发时,这套机制的价值非常大:每个 Agent 有独立目录,git 状态不会被别人污染;每个 Agent 可以 checkout 自己想要的分支,不会互相阻塞;构建产物、依赖目录相对独立,测试时不会抢同一份缓存。
不过这里要强调一点:Git 对象是共享的,所以仓库本体不会因为多开几个 worktree 就成倍膨胀,但 node_modules、venv 这类依赖目录在每个 worktree 里都会单独出现。这个开销问题下面实操部分我会详细讲。
1.3 裸用 Git Worktree 的隐形摩擦成本
既然 Git Worktree 本身就能实现隔离,为什么还要再做一层封装?因为我实际用了两周之后发现,裸用这套命令在并行 Agent 场景下有五个很现实的痛点:
- 路径和分支之间的关系全靠人脑记忆。我开了四五个 worktree 之后,git worktree list 的输出又长又乱,很难一眼看出“哪个目录对应哪个 Agent、哪个 Agent 正在处理什么任务”。
- 任务信息完全丢失。worktree 只记录路径和分支,不记录“这个 Agent 的目标是什么、基于哪个分支拉的、当前进展怎么样”。Agent 一多,你根本不知道它到底在干嘛。
- 生命周期管理太繁琐。Agent 干完活之后,要合并、要删 worktree、要确认没有未提交内容,每一步都得手动执行;中途 Agent 进程没有正常退出时,删除还会失败,报一堆让人摸不着头脑的错误。
- 并行场景下缺少统一视图。多个 Agent 同时跑,你很难快速回答“现在有哪几个 Agent 在工作、他们分别在哪个目录、分支状态如何”。
- Agent 本身也不知道自己该在哪干活。给 Agent 一个裸的 worktree 目录,它没有“任务上下文”,还得靠你写一堆 prompt 反复交代。
Worktrunk 就是为了解决这五个问题而生的:它不重新发明 Git,只是把“任务”和“worktree”绑定在一起,让所有操作都围绕任务展开。
2. Worktrunk 的设计思路:从“管分支”变成“管任务”
2.1 为什么是 CLI,而不是 IDE 插件或一堆 Shell 脚本
在设计 Worktrunk 之前,我评估过三种实现方式:纯 shell 脚本、IDE 插件、独立 CLI。最后选了独立 CLI,核心原因是它和 AI Agent 工作流的契合度最高。
先看一张对比表:
| 方案 | 上手成本 | 可脚本化/Agent可调用 | 不绑定编辑器 | 团队统一入口 |
|---|---|---|---|---|
| 纯 Shell 脚本 | 低 | 中(容易散落各处) | 是 | 差,每个人改自己的 |
| IDE 插件 | 低 | 差 | 否(每个编辑器都要装) | 一般 |
| 独立 CLI | 中 | 强 | 是 | 好,命令统一 |
AI Agent 的运行环境和人不一样,它不一定会打开你的 IDE,但它一定能在终端里执行指令。如果你的工作区管理工具是一个 CLI,那么人可以用,脚本可以用,Agent 也可以直接调用。这是 IDE 插件很难替代的优势。
但做成独立 CLI 并不意味着要重写 Git。Worktrunk 的核心非常薄,它只是把 git worktree 命令包了一层,加上任务元数据、命名约定、安全检查。换句话说,底层还是 Git,上层只是让人和 Agent 用一种更接近“任务管理”的方式去操作它。
假设你已经编译安装好了 Worktrunk(如果你构建的是自己的版本,安装方式和普通 Go CLI 一致):
go install example.com/worktrunk/cmd/worktrunk@latest worktrunk --version2.2 核心概念:Task 即 Worktree,Worktree 即 Agent 工作区
Worktrunk 最核心的设计,是把“worktree”抽象成“任务(Task)”。你不需要告诉它“帮我开一个 worktree,路径叫 xxx,分支叫 yyy”,你只需要说“我要开始一个叫 task-a 的任务”,剩下的事情由 CLI 按约定完成。
一套最基础的命令体系是这样设计的:
# 开始一个任务:创建 worktree + 创建任务元数据 worktrunk start <task> [--base <branch>] [--agent <name>] # 查看所有任务状态 worktrunk list # 输出某个任务的工作区路径 worktrunk path <task> # 切换到某个任务的工作区 worktrunk switch <task> # 结束一个任务:清理 worktree 和任务元数据 worktrunk stop <task> [--merge --target <branch>] # 清理所有已失效的 worktree worktrunk prune你执行worktrunk start agent-a --base main --agent codex的时候,它内部实际做了三件事:
- 调用
git worktree add,把新的工作区创建在统一的约定目录下(比如.worktrunk/workspaces/agent-a); - 基于指定的 base 分支,创建并切换到一个以任务名命名的分支(比如
feat/agent-a); - 在
.worktrunk/tasks/下写一个任务元数据文件,记录任务的完整信息。
元数据文件是 Worktrunk 和裸 Git Worktree 最大的区别,它的 JSON 结构大概长这样:
{ "task": "agent-a", "branch": "feat/agent-a", "base": "main", "worktree": ".worktrunk/workspaces/agent-a", "agent": "codex", "created_at": "2025-01-12T14:30:00+08:00", "status": "running" }有了这个文件,你随时可以回答前面提到的五个问题:哪个 Agent 在哪个目录、基于什么分支、什么时候创建的、当前状态是什么。路径统一放在.worktrunk/workspaces/下面,而不是散落在一堆别名的目录中,管理和清理都会简单很多。
2.3 给 Agent 一个能带进上下文的任务卡
我用的 AI Agent 工具不止一个,有 Claude Code、Codex CLI,还有一个内部接的 Harness。它们各有各的启动方式,但在一点上是共通的:都需要一个清晰的“任务上下文”。如果一个 Agent 进到工作区之后还要靠猜“我现在在哪个仓库、该改什么、怎么验证”,那它大概率会跑偏。
Worktrunk 的export子命令就是为了解决这个问题的,它把任务元数据渲染成一段可以直接喂给 Agent 的上下文。
worktrunk export agent-a --format md输出结果大概是这样:
# Task: agent-a - Worktree: .worktrunk/workspaces/agent-a - Branch: feat/agent-a - Base: main - Agent: codex - Verify: npm test把这个任务卡作为启动 prompt 的一部分喂给 Agent,它一进来就知道自己该在哪个目录工作、基于哪个分支、验证命令是什么,不再需要你一遍遍在 prompt 里重复交代细节。
更进一步,如果 Agent 的 Harness 支持自定义工具(也就是常说的 MCP、skill 这一层能力),你完全可以把 Worktrunk 暴露成一个工具给 Agent 调用。Agent 自己执行worktrunk start和worktrunk stop,等于它自己管理自己的工作区,母 Agent 只需要负责拆任务。这个扩展方向后面我会单独讲。
3. 实操:用 Worktrunk 把两个 Agent 并行跑起来
3.1 初始化仓库与约定配置
在使用 Worktrunk 之前,前提条件很简单:你有一个已经存在的 Git 仓库,代码默认在 main 分支上。如果你是从零开始的项目,先正常把仓库初始化好、提交第一版代码。
进入仓库后,可以执行一次worktrunk init来做基础约定配置,它会读取当前仓库信息,生成一份默认配置,比如默认的 base 分支、默认的 Agent 类型、工作区目录命名规则。不执行 init 也可以,Worktrunk 会用一套合理的默认值:base 分支是当前分支,Agent 类型留空,工作区统一放到.worktrunk/workspaces/。
这里有一个小建议:.worktrunk目录默认应该在.gitignore里,任务元数据是给人和 Agent 看的运行状态,不需要提交到仓库版本库。除非你想做团队级的任务审计,那可以单独考虑提交一份匿名化的任务日志,但千万不要把包含本地路径和临时状态的文件塞进主仓库。
3.2 启动第一个 Agent 工作区
现在假设我要跑一个重构任务,让一个 Agent 去重构核心模块。执行:
worktrunk start refactor-agent --agent claude --base main执行成功后,输出会告诉你这段信息:
Task refactor-agent started. Worktree: .worktrunk/workspaces/refactor-agent Branch: feat/refactor-agent Base: main此时,Worktrunk 已经帮你完成了原本需要手动执行的git worktree add、分支创建、路径切换这一整套流程。你唯一需要做的就是进入这个工作区,然后把 Agent 跑起来:
cd $(worktrunk path refactor-agent) claude注意,cd $(worktrunk path refactor-agent)这个用法很实用,它可以让你在任何位置快速跳到对应任务的工作区,不用自己记那一长串路径。进入工作区后,Agent 看到的是一个干净、独立、只属于它自己的目录,没有其他 Agent 的干扰。
3.3 启动第二个 Agent 工作区
第一个 Agent 跑起来之后,你再开一个终端,启动第二个任务:
worktrunk start test-agent --agent codex --base main这时候你就有了两个并行的 Agent 工作区:refactor-agent 在.worktrunk/workspaces/refactor-agent,test-agent 在.worktrunk/workspaces/test-agent。两个工作区基于同一个 main 分支的不同新分支运行,彼此完全隔离。
实际操作的时候,最直观的感受就是:两个 Agent 可以同时跑同一套测试命令,各自的构建产物在自己的目录里,不会再因为共享目录导致缓存被对方冲掉。如果两个 Agent 需要共享一些基础工具链(比如同一个编译缓存、同一个依赖下载缓存),那可以把这些缓存配置成外部公共目录,但代码和构建输出一定不能互相串。
这里还要提一个并行协作的关键场景:如果第二个 Agent 需要等第一个 Agent 的代码完成之后再继续,那不要让它基于 main 拉分支,而是基于第一个任务的分支来创建任务:
worktrunk start test-agent --agent codex --base feat/refactor-agent这样 test-agent 的工作区里一开始就是 refactor-agent 的最新代码,两个 Agent 的工作有上下游依赖关系。不过要注意,这要求第一个 Agent 定期把代码提交并推送,否则第二个 Agent 拉到的还是一个陈旧快照。
3.4 查看总览与切换上下文
并行跑起来之后,最常用的命令就是worktrunk list。它会输出一张表格,把当前所有任务的状态列清楚:
Task Branch State Changes Agent Worktree refactor-agent feat/refactor-agent running 12 claude .worktrunk/workspaces/refactor-agent test-agent feat/test-agent running 8 codex .worktrunk/workspaces/test-agent我实际使用中,这个命令用的频率比任何其他命令都高。因为我经常是开着好几个终端,跑着两三个 Agent,一旦任务多起来,脑子是记不住“哪个终端对应哪个工作区”的。worktrunk list一执行,所有状态一目了然,哪个任务改了哪些文件、用了什么 Agent、在哪个目录,全部摆在那里。
要切换上下文,直接用worktrunk switch test-agent,然后cd $(worktrunk path test-agent)进入对应目录。整个过程不需要 git stash、不需要手动记路径,比裸用 Git Worktree 省心太多。
3.5 合并与清理:任务结束后的收尾工作
Agent 把活干完之后,第一件事不是直接跑stop,而是先检查改动是否合理、测试是否通过,然后手动或让 Agent 提交代码。确认没问题后,执行:
worktrunk stop test-agent --merge --target main这个命令做的事情比较多,内部逻辑是:检查 worktree 是否有未提交的改动和未推送的提交;如果有,提示你先处理或确认是否强制结束;把当前分支合并到目标分支 main;删除 worktree;清除任务元数据。
如果不想自动合并,只想把工作区清理掉,直接执行:
worktrunk stop test-agent停止之后,你可以用git worktree list验证一下,之前多出来的测试 Agent 工作区应该已经消失了,仓库恢复到一个相对干净的状态。
4. 常见问题与排查技巧实录
4.1 “我明明没在用这个 worktree,为什么删除失败”
这是并行 Agent 场景里最容易踩的坑。你以为 Agent 已经退出了,实际上它的进程还挂在后台,或者某个编辑器的终端还停留在那个目录里,文件句柄被占用,Git 就会拒绝删除 worktree。常见的报错信息里有 “contains modified or untracked files” 或 “being used by process” 之类的提示。
排查思路:先用worktrunk list看任务状态,确认是不是有 Agent 还在运行;再用系统命令检查目录占用,比如lsof +D .worktrunk/workspaces/test-agent;确认没有活动进程后,再执行删除。
如果确实有进程占用但你想强制清理,Worktrunk 提供了--force参数:
worktrunk stop test-agent --force这个命令等价于git worktree remove --force,会跳过安全检查直接删除。使用前一定要确认没有未保存的重要改动,强制删除是不可逆的。
根据我个人经验,比较稳妥的流程是:先让 Agent 正常退出,再等一两秒让它释放文件句柄,最后才执行 stop。如果你用的 Agent 经常异常退出,建议把 stop 命令接入 Agent 的退出回调里,避免每次手动处理僵尸进程。
4.2 并行跑测试时端口和缓存互相冲突
两个 Agent 各自跑测试,编译产物互相不影响了,但端口冲突依然会出现。比如两个 Agent 都在各自的 worktree 里启动一个测试服务,默认都监听 3000 端口,第二个 Agent 一启动就会报 “port already in use”。
解决办法是给每个任务注入唯一的运行时环境变量。Worktrunk 支持给任务记录环境变量:
worktrunk env test-agent PORT 43210写入之后,这个环境变量会出现在任务元数据里,并且在worktrunk export test-agent --format md时作为一条上下文提示输出。Agent 读取到任务卡后,就知道该用哪个端口启动服务了。
更完整的做法是:不仅服务端口要隔离,临时文件目录、数据库文件、缓存目录也最好按任务名隔离。你可以约定一个规则,所有运行时临时文件都放在.worktrunk/workspaces/<task>/tmp/下面,这样即使两个 Agent 同时跑同一套系统,也不会把对方的临时数据覆盖掉。
4.3 两个 Agent 基于同一个 base 并行改动,合并时冲突满天飞
并行开发必然伴随合并冲突,这是物理规律,不是工具能完全消除的。两个 Agent 同时改了同一个业务模块的同一个函数,最后无论是往 main 合并,还是把第二个 Agent 的分支合并到第一个 Agent 的分支上,都会遇到一堆冲突标记。
我的经验是:先解决任务划分问题,再解决合并技术问题。任务拆分时,尽量让每个 Agent 负责独立的模块或独立的代码层,比如一个 Agent 只改后端接口层,另一个 Agent 只改前端展示层,它们之间即使在同一时间点同时合并,冲突概率也会小很多。
如果两个任务确实有上下游依赖,不要等最后再合并,而是在中途就让下游 Agent 定期 rebase 到上游分支的最新版本。Worktrunk 虽然不帮你做冲突决策,但它能帮你快速搞清楚任务之间谁基于谁,通过worktrunk info <task>查看任务的 base 信息,就能判断两个分支之间的关系,方便规划合并顺序。
4.4 多 Worktree 磁盘占用过大的问题
有朋友问过我:开五个 worktree,是不是等于仓库复制了五份?磁盘会不会爆炸?这里解释一下:Git 对象库是共享的,仓库本体的代码历史不会重复存储,所以 git 层面的开销不会成倍放大。
真正占空间的是每个 worktree 里的依赖目录。一个 Node.js 项目,node_modules 动辄几百兆;一个 Python 项目,venv 也是差不多的体量。开三个 worktree,光依赖就是三个完整副本。
解决方案有几种:
- 依赖管理器层面的共享缓存。比如 pnpm 有全局 store,配合 workspace 配置可以大幅降低磁盘占用;npm 和 yarn 也有 cache 目录可以复用。
- 把依赖目录排除在 Git 管理之外,然后用符号链接指向一个公共目录。例如
ln -s /shared/venv .worktrunk/workspaces/test-agent/venv,这样多个 worktree 共用一套依赖。不过要注意,依赖版本一变,链接会失效,而且共享依赖时不同 Agent 同时更新依赖库也可能互相干扰,建议只在紧急节省空间时用。 - 构建产物和临时文件一定不要提交到 Git,同时配置好 .gitignore。两个 Agent 各自生成的 build 目录如果被 Git 跟踪,工作区之间会频繁出现冲突。
4.5 快速排查速查表
| 现象 | 原因 | 处理建议 |
|---|---|---|
| worktree 无法删除 | 进程占用 / 有未提交改动 | 检查 lsof,确认无进程后stop --force |
| 端口冲突 | 多个任务默认端口相同 | 用worktrunk env注入唯一端口 |
| 合并冲突集中爆发 | 两个任务改动区域重叠 | 重新规划任务边界,按模块拆分 |
| 磁盘占用明显增大 | 依赖目录被重复创建 | 启用包管理器共享缓存,或符号链接依赖目录 |
worktree 删了但git worktree list还有记录 | 元数据不一致 | 执行worktrunk prune清理失效记录 |
worktrunk list显示的状态和实际不符 | 手动在外部执行了 git 操作 | 尽量所有操作都走 Worktrunk,避免绕过封装 |
5. 我的使用心得与可扩展方向
5.1 不要盲目追求并行度,2 到 3 个 Agent 是舒适区
用过一段时间后,我最大的体会是:并行不是目的,隔离才是。很多项目刚上手时会觉得“既然能并行,那就多开几个 Agent,任务拆得越细越好”。实际上我踩过坑之后发现,同时跑 2 到 3 个 Agent 是比较舒适的范围;超过 5 个之后,维护心智负担会暴涨,你很难快速判断每个 Agent 当前的状态,两个任务同时改同一处代码的概率也大幅上升,最后合并成本可能远超并行节省的时间。
所以我现在的工作习惯是:大任务优先拆成“重构、测试、文档”这类边界清晰的小任务,每个任务对应一个独立 worktree,三个 Agent 并行跑;核心业务逻辑的修改,宁可按顺序串行执行,也不要同时让两个 Agent 去改同一个层。
5.2 把 Worktrunk 暴露成 Agent 可调用的工具,实现自我管理
这算是我最近在试验的一个方向。如果你的 Agent 框架支持 MCP 或自定义 skill,你可以把worktrunk start、worktrunk stop、worktrunk list这几个命令注册成 Agent 可调用的工具。这样母 Agent 收到一个复杂需求时,它可以自己拆解子任务,为每个子任务创建独立 worktree,然后派生出子 Agent 到对应工作区干活,等子 Agent 完成后自动执行 stop 和 merge。
这套模式的效果是:工作区的生命周期管理不再依赖人盯着,Agent 自己就知道“我应该开一个工作区、干完活、合并代码、清理现场”。我实际跑下来,比让一个 Agent 在一个目录里连续处理多个子任务要稳定得多,每个子任务的前后文不会被别的任务污染。
5.3 团队落地时的三个约定
如果你想把 Worktrunk 这套思路引入团队或引入 AI Agent 开发流程,我建议先定好三个约定:
- 命名约定:任务名必须是 Agent 可读的唯一 ID,不要用“test”“fix”这种过于通用的名字,最好带上模块名,比如
refactor-user-service、test-payment-api。 - 分支约定:每个 worktree 对应一个分支,分支名带上任务名,比如
feat/refactor-user-service,这样从远程仓库看分支名就能知道是哪个任务的产出。 - 生命周期约定:Agent 完成任务后必须先提交、再合并、再清理,不允许把 worktree 丢在那里不管。CI 层面可以加一个检查任务,定期扫描那些长期未合并的 worktree 分支,提前预警。
最后再分享一个小技巧:任务卡文件不要只生成完就丢,建议把它放在 worktree 根目录下,比如.worktrunk/task-card.md。这样不仅是 Agent 启动时能读到,每次打开工作区的人或 Agent 也能快速了解这个任务的前因后果,复盘的时候特别有用。
我自己实际用了几个月,最大的感受不是工具本身多炫,而是它把“哪个 Agent 在哪个目录干什么”这个问题从脑内记忆变成了一条命令。尤其是同时开着两三个 Agent 时,worktrunk list扫一眼,所有状态清清楚楚,比裸用 Git Worktree 安心得多。如果你也被并行 Agent 搞得头疼,不妨从一个薄薄的 CLI 开始,核心就一条:给每个 Agent 一张自己的桌子。