2026年8月9日星期日

给 coding agent 建个 brain 目录:工作区与代码分离

题图:Alexa Williams via Unsplash

让 coding agent 同时管好几个微服务后端加一个前端,最麻烦的是上下文散在各仓库里。摸索了几个月,我现在的做法是给 agent 单独建一个工作目录,再配合 worktree 做任务隔离。写完发现别人也有类似思路,一起整理进来。

一开始的问题

最早我是直接在项目仓库根目录启动 agent 的。仓库里塞了 CLAUDE.md、skills、各种记忆文件,一开始挺顺,但项目一多就乱套:

  • 每个仓库都要单独配一遍 skill,换个项目就要重复劳动
  • agent 的知识和记忆跟着单个仓库走,跨仓库的上下文完全接不上
  • 改一个项目的时候,agent 读到的”全局约定”和这个项目的”本地约定”混在一起,分不清

一个典型的项目往往是好几个微服务后端加上一个前端,仓库本来就是拆开的。让 agent 陷在某个子仓库里,等于把它的视野锁死在局部。

现在的结构:把工作区从代码里拆出来

做法很简单,给整个项目单独建一个 agent 的工作目录,叫 brain,和代码仓库平级:

1
2
3
4
5
6
7
8
9
projectA/
├── brain/ # agent 的家
│ ├── .claude/ # 原样兼容 Claude Code
│ │ └── skills/ # 所有 skill 放这
│ └── CLAUDE.md # 项目级约定
├── repo1/ # 微服务后端
├── repo2/
├── repo3/
└── frontend/ # 前端

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
2
3
4
5
6
7
projectA/
├── brain/
├── repo1/
├── worktrees/ # 任务隔离区
│ └── task1/
│ ├── repo1
│ └── frontend

每个任务一个 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、状态(pendingin_progresscompleted),还有绑定的 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 的上下文管理,跟人的项目管理一样,讲究的也是职责分离和清晰的边界。

参考

没有评论: