题图:Alexa Williams via Unsplash
让 coding agent 同时管好几个微服务后端加一个前端,最麻烦的是上下文散在各仓库里。摸索了几个月,我现在的做法是给 agent 单独建一个工作目录,再配合 worktree 做任务隔离。写完发现别人也有类似思路,一起整理进来。
一开始的问题
最早我是直接在项目仓库根目录启动 agent 的。仓库里塞了 CLAUDE.md、skills、各种记忆文件,一开始挺顺,但项目一多就乱套:
- 每个仓库都要单独配一遍 skill,换个项目就要重复劳动
- agent 的知识和记忆跟着单个仓库走,跨仓库的上下文完全接不上
- 改一个项目的时候,agent 读到的”全局约定”和这个项目的”本地约定”混在一起,分不清
一个典型的项目往往是好几个微服务后端加上一个前端,仓库本来就是拆开的。让 agent 陷在某个子仓库里,等于把它的视野锁死在局部。
现在的结构:把工作区从代码里拆出来
做法很简单,给整个项目单独建一个 agent 的工作目录,叫 brain,和代码仓库平级:
1 | projectA/ |
agent 启动的时候 cd 到 brain/,而不是任何一个仓库里。
brain 目录里放什么
我遵循一个原则:能直接用 Claude Code 原生的,就不自己发明格式。所以 brain/ 里第一个东西就是 .claude/ 目录,里面按 CC 的规范放 skills。这样 agent 一启动就自动识别这些 skill,不需要任何适配层。
CLAUDE.md 放在 brain/ 的根下,写的是这个项目所有仓库共用的约定——目录结构、工作流、注意事项。单独的项目有自己的规矩,就写在它自己仓库的 CLAUDE.md 里。职责分两层:
brain/CLAUDE.md:跨仓库的全局约定- 各仓库
CLAUDE.md:仓库局部的约定
这样做的好处是 agent 启动后先读全局约定,建立起整个项目的地图,再按任务进到具体仓库。
后来我在网上看到,Karun Japhet 的《Structuring Claude Code for Multi-Repo Workspaces》也做了类似的分离。他的做法是把 workspace 根目录本身当做一个不装代码的 bootstrap repo,里面放 repo manifest 和分层 CLAUDE.md(组织级、团队级、仓库级),其他仓库被 gitignore 掉、由 repo manager 克隆进去。Claude Code 会沿着目录树向上找 CLAUDE.md,所以从任何一个子仓库启动,都能叠加读到所有层级的约定。
跟我的 brain/ 有个区别:他的 bootstrap repo 是父目录,所有仓库都装在里面;我的 brain/ 是平级目录,和仓库并排放。父目录方案适合整个公司/团队共享一套上下文的场景,平级方案更轻,一个项目一套,互不干扰。
用 worktrees 做任务隔离
工作区分离解决的是”上下文”问题,任务隔离解决的是”互相污染”问题。同一个仓库,agent 做任务 A 时改了文件,任务 B 接着做,两边就打架了。
我的做法是用 git worktrees 给每个任务开一个独立工作区:
1 | projectA/ |
每个任务一个 worktrees/task1/ 目录,里面按需要 checkout 相关的仓库。任务做完了整体合并、丢弃都干净,不会污染主工作区。
这块我写了一个 skill 来自动维护,不用每次手动敲 git 命令。skill 负责建 worktree、把对应仓库 checkout 过去、记录任务和 worktree 的对应关系。agent 接到任务时,先问 skill 要个新 worktree,任务结束后再让它收尾。人只需要描述任务,不用管 worktree 具体怎么建。
查资料的时候发现,Claude Code 现在其实内置了 worktree 支持:claude --worktree <name> 会在 .claude/worktrees/ 下建一个隔离的工作区,还可以用 .worktreeinclude 把 .env 这类被 gitignore 的文件自动带进新 worktree,subagent 也能通过 isolation: worktree 单独隔离。
但我没直接用内置的这套,因为默认目录在仓库里。.claude/worktrees/ 建在仓库内部,有几个问题:
- 它跟着单个仓库走,一个任务要同时动多个仓库(比如改一个后端加前端),内置方案管不过来
- worktree 的状态文件落在仓库里,会污染仓库本身,还得靠
.gitignore兜着 - 任务隔离区的语义不清晰,
.claude/worktrees/里混着各任务的 worktree,时间一长分不清哪个是哪个
我写 skill 的目的,就是解决这个默认目录的问题:把 worktree 统一挪到项目顶层,按任务归组,不放进任何单个仓库里。
learn-claude-code 项目里有一节专门讲”任务和 worktree 绑定”,我看了它的源码,机制和我的 skill 是一个思路。它把状态拆成两个面:
- 控制面(task):管目标。一个 task 有 id、状态(
pending→in_progress→completed),还有绑定的 worktree 名 - 执行面(worktree):管目录。一个 worktree 有名字、路径、分支(
wt/<name>)、归属的 task_id
关键在绑定这一步:建 worktree 时传 task_id,系统会做三件事——git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD 建目录、往 worktree 索引里写一条带 task_id 的记录、把 task 状态从 pending 推进到 in_progress。收尾时 worktree_remove(name, complete_task=True),删掉 worktree 的同时把绑定的 task 标记为 completed 并解绑,一步搞定清理和完结。
它把事件流也记下来了:每次 create/remove/keep 都往 events.jsonl 里追加一条,崩溃之后可以从 .tasks/ 和 worktree 索引重建状态,不用靠对话记忆。这个设计比我 skill 里的纯对应关系记录更完整,值得抄。
不过有个差异:它是单仓库的方案,worktree 建在仓库自己的 .worktrees/ 下;我这边 worktree 是项目顶层、按任务归组、跨仓库的,所以只能自己维护,不能直接复用它的代码。机制可以参考,目录布局得按自己的场景来。
brain 会自己进化
这个结构跑通之后,最意外的收获是 brain/ 变成了一个会生长的东西。日常使用中,agent 在任务里发现的规律、踩过的坑、验证过的做法,会沉淀下来:
- 一个坑踩了两次,就把它写成一个 skill,下次直接复用
- 某类任务的固定步骤,整理成工作流文档放进
CLAUDE.md - 跨仓库共用的知识,从某个仓库的局部经验升级到
brain/
也就是说,brain/ 不只是配置文件,它是我和 agent 一起维护的知识库。agent 在任务里产生知识,人负责整理和把关,把散的经验固化成可复用的能力。这个目录越用越厚,agent 也越用越顺手。
维护时我给自己定了一条规矩:每条规则都来自一次真实的踩坑,没真实出现过的问题不写。这样 brain 里没有想当然的假设,每一条都是验证过的。
一些实际的体会
- 先问清楚再动工。 任务开始前让 agent 先读
brain/CLAUDE.md,把项目结构说一遍,确认理解对了再干活,比直接让它改代码稳。 - worktree 的生命周期要盯。 skill 建了 worktree,但任务结束没人合并的话,会越积越多,隔段时间清理一下。
- 别什么都往 brain 塞。 只放跨仓库通用的东西,单仓库的琐碎约定放回它自己的
CLAUDE.md,否则 brain 很快会变成一锅粥。 - 别开太多并行任务。 网上普遍的结论是两三个并行的会话是甜点区,开十个的话,光是审合并就够你忙的。
这套东西没什么新发明,就是把”工作区和代码分开”这种工程上常用的思路,搬到了 agent 身上。agent 的上下文管理,跟人的项目管理一样,讲究的也是职责分离和清晰的边界。
没有评论:
发表评论