ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Worktrunk:用 Git Worktree 隔离并行 AI Agent 的工程实践

Worktrunk:用 Git Worktree 隔离并行 AI Agent 的工程实践 1. 为什么并行 AI Agent 需要一个专门的 Git Worktree 工具1.1 多个代理共用一个目录迟早要出大问题先说个我经常遇到的场景。你开了一个编码任务用 codex 或者 Claude Code 跑起来干一件重构觉得一个 Agent 不够又起了一个实例让它去修另一个模块的 bug。一开始大家都在往不同文件里写东西你觉得没事结果等第一次合并的时候就会发现两个 Agent 在同一个目录里互相踩踏改了同一个文件的相邻行提交历史拧成一团代码状态跟你脑子里那张任务清单完全对不上。更难受的是AI Agent 不是人。人瞟一眼周围状态就知道自己在哪个分支、改了什么、还有什么没做但 Agent 只会忠实于它的上下文窗口和磁盘文件。它读到什么就是什么。一旦两个 Agent 同时对同一个文件动手这种“你说你的、我改我的”的状态就会互相污染。最典型的翻车现场是Agent A 改了config.ts后Agent B 基于旧的config.ts做了二次开发然后 A 的写入把 B 的改动覆盖掉B 浑然不知继续往旧文件上补逻辑。你看提交记录能找到蛛丝马迹但过程已经脏了。这说明一个很朴素的需求并行跑多个 AI 编码代理时隔离工作目录不是可选项而是基本前提。每个 Agent 都应该有一个自己独占的目录有独立的分支、独立的索引、独立的文件状态任务结束以后再统一收拢。这个需求听起来很基础但它恰恰是 Git Worktree 最擅长的场景。1.2 Git Worktree 的隔离能力被很多人低估了Git Worktree 不是什么新东西很多老工程师都知道它但真正在并行 AI Agent 工作流里用好它的人并不多。它的核心能力一句话就能说清同一份 Git 仓库可以同时存在多个工作目录每个目录对应一个独立分支互不干扰。常规做法是git checkout -b feature-a然后开始工作但一个仓库在同一时刻只能有一个工作目录处于“已检出”状态。你要切分支就得把手头改动处理好否则就会遇到各种阻碍。对于人来说这还能忍毕竟我们切换上下文有成本、有记忆不会随便乱来。但对 AI Agent 来说切换分支这件事几乎是灾难性的它会因为上下文丢失搞不清自己到底在哪改到一半被强制切换回来以后接着改改完才发现分支都弄混了。用 Worktree 就完全不一样了。每个 Agent 一上来就进入一个独立的目录比如.worktrees/task-42里面是完整的一份代码检出但背后共享同一个 Git 对象库。Agent 不需要关心“我现在在哪个分支”因为它从进入这个目录开始就只在feature/task-42上工作。它不会碰到别人的改动别人也碰不到它的提交也天然隔离。任务结束后把这条分支合并回主线再把 worktree 删掉干干净净。这里有一个关键点很多人没注意到Worktree 之间虽然分支隔离但对象库共享。也就是说你在 worktree A 里创建的提交在 worktree B 里做git log是可以看到的。这意味着某 Agent 在任务中需要依赖另一个 Agent 的某个中间提交时只要两个 worktree 挂在同一个仓库上就能通过git merge或者git rebase进行精确的协作而不会像传统方式那样把整个工作目录都搅成一锅粥。1.3 为什么原生 Git Worktree 命令还是不够用既然 Git Worktree 已经解决了目录隔离那直接用原生命令不就行了说实话单个 worktree 用起来确实不复杂只需要两条命令git worktree add ../feature-a -b feature/a git worktree remove ../feature-a但一旦你同时挂五六个 Agent、每半小时开一个新任务、还需要知道每个 worktree 对应哪个 Agent、跑的是什么任务、状态怎样、分支是否干净这套手动操作就开始露馅了。我实际踩过几个坑都是原生命令给的教训第一目录和分支的对应关系靠人脑记。/repos/app-1、/repos/app-2、/repos/app-3分别是谁在用、在跑什么任务时间一长必然糊涂。尤其那些中途换了任务、临时新建又忘记删除的 worktree会像没人认领的共享单车一样堆在那里。第二创建流程太啰嗦。git worktree add虽然一条命令就能建但要想建得规范必须带上目录路径、分支名、基础分支、是否显式设置 upstream每次都要打一长串参数。你手动跑都觉得烦让 Agent 自己跑就更不现实了。第三没有“归属”和“状态”概念。原生命令只维护了 worktree 的存在性不关心它现在被哪个 Agent 占用、任务做到哪一步、分支是否已经可以合并这些都需要你自己去归纳和跟踪。而跟踪这类状态本质上就是一种 DevOps 工作一旦规模上来纯手动完全不可持续。所以我当时的判断是需要一个小工具把“创建 worktree 并给 Agent 分配任务”这个过程做成一个标准化的、可脚本化的、Agent 能直接调用的 CLI。这个想法就是 Worktrunk 的起点。2. Worktrunk 的核心设计从高频痛点反推需求2.1 命令设计如何用最少的动词满足最高频场景我一开始列需求的时候没有照着 Git 那套庞大命令集去复刻而是先反问了三个问题并行 Agent 工作流里我一天二十四小时要重复做哪些事答案是建 worktree、把 Agent 放进 worktree、看所有 worktree 的状态、任务做完以后合并清理。这四件事覆盖了我日常 95% 的操作。所以 Worktrunk 的命令集就围绕这四个动作设计不多不少命令作用对应的 Git 原生操作worktrunk create创建 worktree 并登记任务元信息git worktree add 一堆手工参数worktrunk attach为某个 worktree 绑定 Agent 并执行命令cd 手动 export 环境变量worktrunk list展示所有 worktree 的任务、分支、状态git worktree list 人工脑补worktrunk finish合并分支、清理 worktree、更新状态git mergegit worktree remove每个动词都是高密度、高复用的没有多余的概念。比如finish把合并和清理粘在一起这背后有个设计判断任务做完以后分支合并和目录清理是同一件事的两个阶段如果你把它们拆成两个命令人就会偷懒只合并不清理worktree 越积越多。粘起来以后你每次干净地收尾仓库目录就不会失控。2.2 状态与元数据一个专门给 Agent 看的“任务台账”Worktrunk 和原生 Git Worktree 最大的差别就是每个 worktree 除了有 Git 自己的 metadata 之外还有一份 Worktrunk 级别的状态台账。它记录这几个关键字段worktree 的唯一标识、对应分支、创建时间、当前归属的 Agent 类型、任务描述、基础分支、合并状态。这部分元数据我放在了.git/worktrunk/state.json里不污染代码目录也不影响 Git 的自身的对象存储。其实这个位置选择也踩过一个小坑一开始我放在项目根目录的.worktrunk/state.json结果 Agent 容易把它当成项目代码的一部分跑 lint 时会去格式化 JSON甚至有 Agent 会“好心”帮我清理掉这个文件。放到.git/里面以后Agent 默认不会碰它干净很多。这个台账的价值在什么时候体现当你有六个 Agent 并行跑其中一个 agent 崩溃了你需要知道它最后用了哪个 worktree、当时在哪个分支、任务描述是什么。手工查的话六七个目录逐个翻一遍费时费力。用worktrunk list直接就能看到$ worktrunk list ID BRANCH BASE AGENT TASK STATUS task-42 feature/task-42 main codex 登录页重构 active task-43 feature/task-43 main claude 修复 API 超时 waiting task-44 feature/hotfix-44 main zcode 修复线上 500 错误 merged这一刻你会觉得那个台账真是个好东西。2.3 输出设计一切以 Agent 可解析为首要目标这个 CLI 的用户不仅仅是人更多的其实是 AI Agent 本身。所以在设计输出格式时我给自己定了一个原则默认输出必须是一行一条、易于解析的纯文本格式而不是花里胡哨的人类友好表格。因为 Agent 解析表格的能力远不如你想象中那么强稍微复杂一点的美化输出它就容易读串。具体到实现层面我做了两类输出人看的时候用表格比如上面那个 display 格式可读性好。Agent 调用的时候用--json参数输出严格的 JSON字段固定没有冗余装饰。拿worktrunk attach举例当它在一个脚本里被调用时输出是这样{ worktree_id: task-42, dir: /workspace/project/.worktrees/task-42, branch: feature/task-42, pid: 12345 }Agent 拿到这个 JSON 以后可以直接确定自己的作业目录不需要再猜。这种“机器可读输出优先”的设计我在后续和多个 CLI 工具整合时体会很深。一个 CLI 如果只是给人用的那它能承担的价值有限但一旦它设计成 Agent 也能读懂、也能调用的接口它的应用半径就会立刻扩展出去。Worktrunk 从一开始就看到这层需求所以把“给 Agent 当 API 用”放在了非常靠前的位置。3. 实操安装、初始化与基本工作流3.1 安装方式与前置条件Worktrunk 的安装不复杂核心依赖就一个Git 版本建议 2.30 以上因为低版本对 Worktree 子命令的支持不够完整容易在并发操作时暴露问题。我常用的是编译安装方式。源码里直接go build生成二进制再把二进制放进PATH环境变量指定的目录就行。go build -o worktrunk ./cmd/worktrunk sudo mv worktrunk /usr/local/bin/ worktrunk --version装好之后进到项目里第一步是初始化worktrunk init这条命令做的事很简单检查当前目录是否是一个 Git 仓库确认无误后在.git/worktrunk/下创建配置文件和状态文件。如果项目还没有初始化 Git它会提醒你先执行git init或者git clone。初始化时还有两个常见参数一个是--dir用来指定所有 worktree 都集中放在哪个子目录下。比如worktrunk init --dir .worktrees另一个是--base用来设定默认的基础分支。如果你团队的主干分支叫develop而不是main可以提前设好后面每次创建 worktree 就不需要反复指定了worktrunk init --dir .worktrees --base develop3.2 创建第一个 Worktree 并启动 Agent初始化完成后最标准的工作流就是“创建 —— 绑定 —— 开工”。我用一个实际例子说明。假设我要让一个 codex Agent 去重构登录模块worktrunk create login-refactor --base main --agent codex --message 重构登录页逻辑执行完以后Worktrunk 会在.worktrees/login-refactor目录下创建一个全新的 Git worktree并把分支feature/login-refactor切好。如果你现在执行git worktree list会看到它与当前主线分支处于平行状态。接下来就是核心一步让 Agent 进入这个 worktree 工作。worktrunk attach login-refactor -- codex 重构登录页逻辑完成后提交到当前分支这条命令对 Agent 来说一箭双雕。它首先把当前 shell 的工作目录切换到了.worktrees/login-refactor然后把 worktree 的相关信息通过环境变量暴露给 Agent。你在脚本里可以直接用这些变量echo $WORKTRUNK_ROOT # 项目根目录 echo $WORKTRUNK_DIR # /path/to/project/.worktrees/login-refactor echo $WORKTRUNK_BRANCH # feature/login-refactorAgent 看到WORKTRUNK_DIR以后就明确知道自己要在这个目录下活动了哪怕它启动时自带一些“路径偏好”也会优先跟随这个环境变量。这一点很重要因为很多 Agent 默认会往当前工作目录或者默认项目目录跑你给它一个显式路径等于给它画了一条不会越界的跑道。3.3 日常循环创建、检查、收尾跑完一个 Agent 任务以后并不是直接删除工作目录就完了需要先合并分支。Worktrunk 的finish命令就是干这个的worktrunk finish login-refactor这条命令默认会执行一次“先合并后清理”的动作。具体流程是确保 worktree 的工作区是干净的把feature/login-refactor合并回基础分支合并成功后删除对应分支和 worktree。当然现实没这么美好总会遇到合并冲突的情况。这时finish不会强行继续而是停在合并冲突的状态输出一条清晰的提示告诉你哪些文件有冲突然后退出。这个设计是有意为之——我始终认为冲突必须留给真正的人去判断不能让 Agent 或者自动化脚本糊弄过去。等人工解决完冲突后再次执行finish它会从上一次中断的地方继续。赶上需要同时管理多个任务我就会用worktrunk list把全局状态看一遍。比如某天下午我跑了三个 Agent两个已经完成并合并一个因为冲突卡住了$ worktrunk list ID BRANCH BASE AGENT TASK STATUS login-refactor feature/login-refactor main codex 重构登录页逻辑 conflict api-timeout-fix feature/api-timeout-fix main claude 修复 API 超时 merged hotfix-500 feature/hotfix-500 main zcode 修复线上 500 错误 active看到这几个状态我就能快速决定下一步api-timeout-fix已经完成不占空间hotfix-500还在跑login-refactor需要我去解决冲突。整个工作流对注意力要求很低核心判断都呈现在一张小表里。4. 面向 Agent 的任务编排与状态恢复4.1 Worktree 名称应当直接等于任务 ID我见过不少团队用 Worktree 管理代码但目录命名随心所欲比如feature-1、dev、fix-abc这种。这类名字在人眼里还能忍放在并行 Agent 场景下就会立刻露馅。Agent 没有你的全局记忆它看到fix-abc只会一脸茫然不知道该做什么也不知道这条分支跟哪个需求相关。所以我强烈建议把 worktree 的 ID 跟任务 ID 直接绑定。如果你用 Jira、Linear 或者飞书表格管任务那么 worktree 的 ID 就应该是那条任务的编号。Worktrunk 在创建时也把这个考虑进去了你的create命令可以这样写worktrunk create PROJ-1234 --base main --agent codex --message 修复支付模块并发问题这样目录就是.worktrees/PROJ-1234分支是feature/PROJ-1234。任何人包括人、Agent、CI 脚本看到这个 ID就能反查任务系统的详情。这种可追溯性在多个 Agent 并行工作的时候尤其重要因为你很难从大脑里同时拎出六个任务的上下文但通过 ID 可以快速定位。4.2 让 Agent 崩溃后能快速恢复现场AI Agent 不是稳定进程经常跑着跑着就超时、断连或者因为 token 用尽直接挂掉。这是并行 Agent 工作流里最耗心神的麻烦事。如果 Agent 挂掉了它之前做的一部分改动可能还没提交但已经产生你得想尽办法找回来。Worktrunk 在恢复现场这件事上提供了很明确的支持。因为每个 Agent 绑定一个 worktree整个 Agent 的工作目录、分支、任务信息都挂在一起。Agent 挂掉后你只需要重新拉起一个同样的实例再执行一次 attach它就会再次进入同一个 worktree看到你上次留到一半的文件和未提交的改动。实操层面我经常这么干给 Agent 设置一个 wrapper 脚本让它内部反复重试直到任务完成。如果 detect 到进程非正常退出就自动重新调起。脚本大概是这个套路while true; do worktrunk attach PROJ-1234 -- codex 继续完成支付模块并发修复已完成一半从当前状态继续 if [ $? -eq 0 ]; then echo Agent completed successfully break fi echo Agent crashed with exit code $?, restarting in 10s... sleep 10 done这个 while 循环的关键好处是Agent 每次重启都能回到同一个 worktree、看到同一份代码现场而不是重新 clone 一份仓库、重新理解三遍上下文。这个“原地恢复”的能力直接决定了一大批并行 Agent 任务的完成效率。我实测下来用这种方式管理 Agent 崩溃恢复比每次重新起一个全新 Agent 省掉了至少一半的上下文消耗。4.3 合并策略先合并谁、冲突交给谁多个 Agent 并行工作意味着最后一定有多条分支需要合并回主线。合并顺序在原生 Git 流程里靠人工排队但在 Worktrunk 里我建议采用一种简单的优先级策略优先合并改动面小的分支再合并改动面大的分支。举个例子代码扫描修复这种任务通常只碰几个文件而大功能重构可能碰了几十个文件。如果先合并大分支小分支合并时很容易被大分支的改动覆盖或产生大量冲突反过来先合小分支大分支合并时只需处理一次大冲突。这个经验虽然听起来像常识但自动化场景下很容易被忽略。Worktrunk 的list会显示每个 worktree 的状态和改动量提示帮助你在跑完多个 Agent 后快速决定合并顺序。至于冲突本身我坚持一个原则让 Agent 做它们擅长的事比如生成代码、查找引用、批量重构但冲突解决这种需要全局决策的工作必须由人来拍板。Agent 在面对一个语义不明确的冲突时往往只会机械地保留一边或者两边拼接结果就是编译能过但逻辑是错的。真实的冲突解决经验是只看有冲突的文件上下文不够你需要知道这条分支到底想干嘛。所以 merge 冲突发生后我永远先回到任务描述去看原意再决定合并方向而不是直接放给 Agent 去猜。5. 常见问题与排查技巧实录5.1 报错“worktree 已检出”切不回主干分支这是我最常遇到的经典问题。你会看到 Git 报这样的错fatal: feature/PROJ-1234 is already checked out at .worktrees/PROJ-1234原因特别简单Git 不允许同一个分支被两个 worktree 同时检出。某些情况下你会误以为分支没被使用实际是某个 worktree 还挂着它。排查方式很简单直接看 Worktrunk 状态worktrunk list如果发现某个 worktree 显示active那分支被占用就是正常的。这时候要么先去那个目录把任务收尾要么执行下面的强制清理worktrunk finish PROJ-1234 --force--force会忽略未提交的改动并强制删除 worktree。这是最后的兜底手段。我的经验是不要轻易加--force。如果那个 worktree 里还有未提交的改动加了--force等于直接扔掉有一次我就是这么把 Agent 跑了半个小时的成果给抹掉的后来学乖了强制清理前一定会先看一眼worktrunk list里的状态确认分支里没有重要改动。5.2 Agent 胡思乱改总是跑到别的目录去AI Agent 的路径感知和控制是一大难题。有的 Agent 会无视当前工作目录按照自己记忆里的路径修改文件甚至跑到真实主干目录里改代码完全没注意应该在.worktrees/PROJ-1234下操作。我的解决方案比较硬核不给 Agent 任何接近主干目录的机会。在启动 Agent 的脚本里先切换到 worktree 目录再把它能访问到的环境变量里的仓库路径全部指向 worktree 路径export WORKTREE_DIR$(worktrunk attach PROJ-1234 --print-dir) cd $WORKTREE_DIR export GIT_DIR$(git rev-parse --git-dir) export GIT_WORK_TREE$PWD这样做的效果是即便 Agent 意外执行了git commit或者git add它也只能作用于当前 worktree而无法对主干产生破坏。把 run 权限控制在一个目录范围内是并行 Agent 管理里最重要的一道心理防线。5.3 分支名不匹配合并时发现目录和分支对不上这种情况多发生在手动创建过 worktree、但后来忘了删除又新建了同名的分支。Worktrunk 的 state.json 里记录的 worktree ID 与分支名可能跟实际情况不对应。解决办法倒不复杂直接重新同步一下worktrunk syncsync的作用是扫描.git/worktrees/下的实际目录把 state.json 里不再存在的 worktree 标记为缺失把实际存在但没登记的分支补回台账。本质上是让 Worktrunk 重新认识一下仓库现场。日常用的话我建议跑完每个 Agent 任务之后顺手执行一次sync防止状态发散。5.4 磁盘空间告急多个 Worktree 吃满存储每个 worktree 都包含一份完整的代码检出差量虽然 Git 对象库共享但工作目录中的文件还是要占用磁盘空间。如果项目有几个 GB六个 worktree 就是几十 GB很轻松把本地存储撑爆。尤其是你让 Agent 在 worktree 里构建产物的场景一个大前端项目构建一次 node_modules 可能就几个 GB。我的经验是尽量在.gitignore里把构建产物和依赖目录排除掉或者在create时通过--ignore参数挂载一个统一的缓存目录。Worktrunk 清理命令也要及时用所有merged状态的 worktree 都可以一波带走worktrunk cleanup --status merged这个命令会找出所有已经完成合并的 worktree并一次性地把它们从磁盘上移除。我通常会设置一个 cron 或者手动定期跑一次避免 worktree 越攒越多。6. 与主流 AI 编程 CLI 的协同实践6.1 让 codex / Claude Code / Gemini CLI 老实待在 Worktree 里现在主流的 AI 编程 CLI比如 codex、Claude Code、Gemini CLI都有各自的启动方式和上下文机制。但它们都有一个共同点默认在当前目录下工作。所以只要你把当前目录切到 worktree 里它们就会自然在这个隔离环境里工作。这一点非常关键。我在实际项目里会把启动命令包装成一个短小的脚本。比如给某个任务启动一个 codex Agentworktrunk attach PROJ-1234 -- codex --full-auto 修复支付模块并发问题worktrunk attach先切目录再执行命令。因为目录切换发生在子 shell 里命令执行完后你的当前目录还在原来的位置不会污染主 shell 状态。这个体验比cd切换干净得多。Claude Code 的启动方式类似只是交互确认习惯不同有的版本默认需要人工确认每步操作。你可以在配置文件里调为自动模式也可以保留确认模式让 Agent 停在关键动作点等人看一眼。我的建议是并行跑 Agent 时文件操作类审批可以开自动但涉及 push、merge、删除分支这类不可逆操作时保留确认拦住 Agent。6.2 用 Worktrunk 给 Agent 的上下文“续命”Agent 最大的成本是上下文消耗。如果每次重启 Agent 都从零理解项目既慢又贵。Worktrunk 配合脚本做“上下文续命”是一套很实用的组合拳。典型的做法是Agent 启动时自动注入之前的状态描述worktrunk status PROJ-1234 --format json状态里包含当前 worktree ID、分支、任务描述、上次提交的 commit sha。脚本可以把这些信息拼接成一段 prompt附加到 Agent 的启动命令后面。这样 Agent 第一次睁眼就知道自己在哪个分支、任务是什么、上次改到哪个提交不需要重新翻 commit 历史就能快速进入状态。这件事的性价比极高尤其面对那种跑了两小时、中途因为网络断开挂掉的 agent 场景用 Worktrunk 保存的这次任务台账往往能让代码在几分钟内无缝续跑。我认为这类“外部状态注入”会成为并行 AI Agent 工程化里最值得投入的方向之一。6.3 如何跟 CI 集成每个 Worktree 一个独立验证环境当你用 Worktrunk 管理多个并行 Agent 任务时一个自然的延伸需求是每个任务在合并前都能有一个独立的验证环境。Worktrunk 的 worktree 天然适合干这个因为每个 worktree 就是一条独立分支加上一份独立代码副本。我经常这样设计 CI 流程检测到 Worktrunk 状态里有新的 active worktree 时CI 就自动拉取对应分支并构建一个 preview 环境把测试结果反馈回来。这样每个 Agent 任务的产物都可以独立验证互不影响。具体的 CI 配置可以很轻量核心就是调用一次worktrunk list --format json拿到所有 worktree 的分支列表再遍历触发构建。worktrunk list --format json | jq -r .[] | select(.status active) | .branch | while read branch; do echo 触发分支 $branch 的构建 # 这里调用你的 CI 接口或本地构建脚本 done这套模式跑稳以后并行 Agent 工作流就从“只跑代码”进化成了“边跑代码边验证”每次合并都能大幅减少回归风险。我实测下来这块给团队带来的价值甚至比 worktree 本身的隔离还要高因为它让每个 Agent 的产出都能单独被验证而不是等合并主线以后才发现一堆问题。7. 一些真实的体验感受项目用了 Worktrunk 管理并行 Agent 以后我最大的变化是心态。以前同时跑三个 AI 编码代理总会忍不住每隔几分钟就去git status看一眼生怕某个 Agent 跑偏。现在创建完 worktree、绑定好 Agent我可以安心去做别的因为即使出了乱子也只是那一个 worktree 的乱子不会波及全局。最值得安利的一个细节是Worktrunk 强制给每个任务一个独立的目录和分支这件事看起来只是工程上的规范但它实际上改变了 Agent 的工作质量。Agent 在目录隔离的环境里不需要关心分支切换和冲突上下文更集中生成的代码也更稳定。我已经不止一次发现那些平时在共享目录里蠢蠢欲动的 Agent进了专用 worktree 后行为显著规矩。如果你也正在为一堆并行 AI Agent 的互相踩踏而头疼我不建议你继续硬扛。把 Git Worktree 用起来再用这类管理工具把流程标准化你会发现并行跑 Agent 这件事其实可以比跑单线程任务还省心。
返回列表