0. 引子:为什么单 agent 会撞墙
前两篇我们看到的是单个 agent 的「主循环 + tool 边界」。这套结构有一个天然天花板:单个 turn 的能力上限 = context window × tool budget。当任务变成「读 30 个文件、产出一个 PR、再跑一遍验证」这种多步骤工程任务时,单 agent 会撞到三堵墙:
- 上下文爆炸:把 30 个文件的全文都塞进主对话,下一轮 prompt cache 命中率瞬间塌陷。
- 角色漂移:让一个 agent 既写代码又审代码,最后它会站在「我刚写的」立场为自己辩护。
- 并行写冲突:两个分支同时改
package.json,谁也不让谁。
把任务派给 subagent 听起来是显然的解法,但**「让 agent 调 agent」并不是简单地多开一个会话**。它牵涉到 cache 经济、git worktree 锁、role 限权、生命周期管理。本文以 claw-code 为参照,拆解它在工程上是如何落地的——核心文件位于 src/tools/AgentTool/{AgentTool.tsx, runAgent.ts, resumeAgent.ts, forkSubagent.ts, agentMemory.ts, builtInAgents.ts},Rust runtime 侧的并行调度落在 rust/crates/runtime/src/{task_registry.rs, task_packet.rs, branch_lock.rs}。
1. 内置 Agent 是「角色」,不是 worker
引子
很多框架对 subagent 的抽象是「worker pool」:派一个任务、回收结果、生命周期到此结束。claw-code 不是这样。它的 6 个内置 agent 是有人格、有职责边界的角色,每个角色的 system prompt 都写死了「你能干什么、不能干什么、必须输出什么」。
6 个内置 agent
参见 src/tools/AgentTool/builtInAgents.ts:
| 角色 | 职责 | 工具集 | 必须输出 |
|---|---|---|---|
| General Purpose | 通用研究 / 多步任务 | 全量 | 自由格式 |
| Explore | read-only 信息收集 | Read/Grep/Glob + Bash 白名单(ls / git status / git log / git diff / find / grep / cat / head / tail) | 文件清单 + 摘要 |
| Plan | 架构师,写实现路线 | read-only | 必含「Critical Files for Implementation」段 |
| Verification | 对抗式审计 | read-only + 受控 Bash | VERDICT: PASS | FAIL | PARTIAL + 命令 + 观察输出 |
| Claude Code Guide | 文档/帮助 | 文档检索 | 引用原文 |
| Statusline Setup | 配置专用 | 配置文件读写 | diff |
Insight:Verification 的「对抗设计」
这是整个 subagent 体系最有想法的一处。Verification agent 的 system prompt 直白写着 “your job is to try to break it”——它不是「确认实现是否正确」,而是「找出实现错在哪里」。再加上强制输出格式 VERDICT: PASS|FAIL|PARTIAL + 触发命令 + 观察输出,主 agent 拿到的就不是一段含混的「看起来 OK」,而是一个可机读、可 grep、可触发后续 retry 的结构化判决。
为什么这是 standout insight?因为它解决了 LLM-as-judge 的根本病灶:自检会自我合理化。把「写」和「验」交给同一个 agent,等于让作者审稿自己——它会本能地为自己的代码找补理由。把 Verification 拆成独立角色、独立 context、独立的「敌意 prompt」,等于强制做了一次 evaluator-generator 解耦。这一招在 self-improving 工作流里几乎是必备的。
每个角色的 system prompt 都设了硬性 role cap:plan agent 不能写代码(工具集里就没 Edit/Write),verify agent 不能改实现(即便它发现 bug 也只能报告),explore agent 连 git commit 都执行不了。权限不是 runtime 检查,而是 spawn 时就编译进去的。
2. Fork 路径 vs Normal 路径:cache 经济学
引子
如果 subagent 是「新开一个 conversation」,那么每次派 subagent 的成本 = 重建一份完整 system prompt + claudeMd + git status + 工具定义 = 数千 token 的 cache miss。在 Anthropic API 里,cache write 是 1.25× input price,cache read 是 0.1×。一次失误的上下文重建 = 12.5 倍 hit 成本。claw-code 在这里给了一个非常工程化的双路径方案。
双路径
参见 src/tools/AgentTool/{forkSubagent.ts, runAgent.ts}:
| 维度 | Fork 路径 | Normal 路径 |
|---|---|---|
| 触发 | subagent_type 省略 + fork 启用 | 显式 subagent_type: "explore" 等 |
| system prompt | 继承 parent 完整 | 从 agentDefinition 重建 slim 版 |
| 工具集 | useExactTools = true,与父一致 | 按角色裁剪 |
| 历史 messages | 拷贝 parent.messages | 空 |
| API 前缀 | 字节级一致 → cache 命中 | 重建 → cache miss |
Insight:Fork 不是进程概念,是 cache 经济学
Unix 程序员看到 fork 第一反应是「进程隔离」。在这里它不是为了隔离,而是为了「让发给 API 的请求前缀和父 turn 完全一致」,从而触发 prompt cache。隔离是 normal 路径的活,fork 路径反而是主动反隔离——保留全部上下文,只是把后续的 turn 划成 sidechain,不让 sidechain 的输出回污父 transcript。
这个设计意味着:「派一个 subagent 帮我做点小事」的成本,可以低到只有几十个新 token 的 cache write。多数开源框架在这一步默认走 normal 路径,每次 subagent 调用都是几千 token 的全量重建——静默 2× 甚至 5× 的 token 成本,但用户在 dashboard 上只看到「token 花得有点多」,不会意识到根因是 cache 策略。
何时该用哪条路径?经验法则:
- Fork:任务高度依赖父上下文(“based on what we just discussed, also check X”)。
- Normal:任务有明确角色(plan/verify),需要强 role cap,或父上下文已经很大、不希望 subagent 也背着这堆历史。
3. Foreground vs Background:四种生命周期
引子
subagent 派出去之后,主 agent 该等它回来,还是该接着做别的事?claw-code 在 AgentTool.call() 里给了 4 种分支。
4 种分支
源自 src/tools/AgentTool/AgentTool.tsx:
- foreground sync:阻塞主 turn,主 agent 在 await 上等结果。适合「下一步必须依赖结果」的场景,比如先 plan 再 implement。
- background async:subagent 拿到自己的
AbortController,主 turn 立刻继续,subagent 完成时通过 notification 回调。适合「可以并行跑、结果之后再看」的探索任务。 - remote-launched:从 IDE / API 触发,主 agent 不在场。
- teammate-spawned:在 multi-agent 协作里,由另一个 subagent 派生。
Insight:Background 的 outputFile 与「不许偷看」
Background agent 的输出会落到一个 outputFile,主 agent 看得到这个路径。但是——主 prompt 里明确不鼓励主 agent 在 subagent 完成前去 cat 这个文件。
这条约束读起来像是道德说教,工程上其实是为了避免 race condition 和「半成品中毒」:subagent 还在写文件时,主 agent 偷看一眼,把残缺输出当成结论塞回主上下文,整个对话就跑偏了。把"不要看"写进 prompt 而不是写进权限系统,是因为这是一种协作礼仪而非安全边界——主 agent 完全有权限读那个文件,只是不应该。
4. runAgent() 是 runtime 构造器
引子
subagent 不是 new Agent(prompt) 这么简单。每次 spawn 都要走一遍完整的 runtime 装配流水线——这是 claw-code 把 subagent 做得「像独立 process」的关键。
装配流水线
src/tools/AgentTool/runAgent.ts 大致顺序:
init agent-specific MCP servers
→ 过滤/克隆 ToolUseContext(深拷贝,防 mutation)
→ file state cache(继承父的 read 状态,避免重读)
→ 给 read-only agent 瘦身 claudeMd / gitStatus
→ 解析 permission mode(plan / acceptEdits / etc.)
→ 计算 resolved tools(角色白名单 ∩ 启用工具)
→ 拼装 system prompt(agentDefinition + 上下文片段)
→ AbortController.create()
→ 触发 SubagentStart hooks
→ frontmatter hooks / skills 加载
→ merge MCP tools 到 toolset
→ 构造 ToolUseContext
→ query() 主循环
→ 记录 sidechain transcript
→ cleanup(MCP / hooks / perfetto / todo / bash session)
Insight:「瘦身」是上下文卫生
注意这一步:给 read-only agent 做 claudeMd 和 gitStatus 的瘦身。一个 explore agent 不需要知道完整的 contributing guide、不需要 50 行的 git status——主 agent 用得着,subagent 用不着。瘦身能直接砍掉每次 subagent 调用几百到几千 token 的固定开销。
cleanup 阶段同样关键:MCP server、hooks、perfetto trace、todo state、bash session 都要单独回收。把 subagent 写成「装了又拆」的临时 process,避免长跑后状态泄漏。
5. Worktree + Task Packet:并行写不冲突
引子
读 30 个文件可以并行——只读没有冲突。但写呢?两个 subagent 同时在 main 分支上 git apply 各自的 patch,必崩。claw-code Rust runtime 的答案是 per-task git worktree + 单 mutex 的 TaskRegistry。
Task / TaskRegistry
rust/crates/runtime/src/task_registry.rs 的核心数据结构:
| |
状态机是单向的:Created → Running/Stopped → Completed/Failed。没有 reset、没有 reopen——一个 task 失败了就让它失败,retry 是开新 task。这避免了「half-completed task 被 resume 成 zombie」的复杂边界。
整个 registry 用 Arc<Mutex<RegistryInner>> 包裹:
| |
锁粒度粗——所有 spawn / status update / list 都过同一把锁。这看起来不优雅,但spawn 频率本来就低(subagent 数量是 O(10) 量级),用细粒度锁优化收益小、复杂度高,是典型的「先把对的写出来再说」的工程取舍。
TaskPacket:意图的契约
rust/crates/runtime/src/task_packet.rs 定义的 TaskPacket 携带:
- objective:这个 task 要达成什么。
- scope:允许触碰的目录 / 文件 prefix。
- repo metadata:仓库 root、当前 HEAD、worktree base。
- branch policies:能否新建分支、命名规则。
- acceptance tests:什么命令通过算成功。
- escalation:失败/超时时升级给谁。
这是一个意图层的契约——主 agent 不是把"做这件事"扔给 subagent 就完了,而是把成功标准、可触碰的边界、升级路径全部写进 packet。subagent 跑偏了,runtime 可以根据 packet 强制中止。
Worktree 锁
rust/crates/runtime/src/branch_lock.rs + stale_branch.rs 给每个 task 分配一个 git worktree(git worktree add),worktree 之间分支独立、文件系统独立。branch_lock 防止两个 task 抢同一个分支,stale_branch 定期清理已完成 task 留下的死 worktree。
这是「并行 agent 改代码」的物理基础:进程级隔离的代价是开 git worktree,比开容器轻、比同目录互锁安全。
6. 全流程总览
flowchart TB
Main[Main agent turn] -->|AgentTool.call| D{subagent_type?}
D -->|omitted+fork| Fork[Fork: inherit ctx & tools, cache-identical prefix]
D -->|explicit| Norm[Normal: build agent-specific prompt+tools]
Fork & Norm --> RA[runAgent: MCP, hooks, skills, perm mode]
RA --> WT[(worktree / branch lock)]
RA --> Q[query loop]
Q --> R[Sidechain transcript + cleanup]
R --> Main
把图和前面五节对照起来看,整条链路的设计意图就清楚了:
- D 节点承担 cache 经济学决策。
- RA 节点做 runtime 装配 / 瘦身 / 权限解析。
- WT 节点做物理隔离。
- Q 节点是 subagent 自己的 turn loop(与文章 1 描述的主循环同构)。
- R 节点做协议化清理:sidechain transcript 入库、所有动态资源回收。
7. Demo:60 行复刻 fork/normal + 并行调度
完整代码见 code/03-subagent-plan-worktree/subagent_demo.py。核心思路:
| |
Demo 主流程派出 2 个 explore + 1 个 plan + 1 个 verify,用 ThreadPoolExecutor 并发跑,最后从 verify 的输出里 grep 出 VERDICT: 行——把第 1 节里讲的「结构化判决」真的跑给你看。
8. 收束:Workflow 三幕落幕
到这里,CLI Agent 的 Workflow 主线讲完了:
- 文章 1——主循环:单 agent 一个 turn 怎么走。
- 文章 2——tool 边界:怎么让一个 turn 安全地动手。
- 文章 3(本篇)——并行扩展:怎么让多个 turn 在隔离中协作。
三篇连起来回答了同一个问题:怎么让一个 LLM 在工程系统里像一个进程一样可控地运行。但这条主线还缺一条腿——记忆。一个跑完就忘的 agent,每次 spawn 都要重新摸索;一个能把这次的 insight 沉淀下来、下次直接复用的 agent,才是真正能「越用越聪明」的系统。
下一篇预告
文章 4《为什么需要 Memory System》将切入第二条主线,正式介绍 claw-code 的 Memory 三件套:短期 working memory(turn-local)、中期 session memory(cross-turn within session)、长期 persistent memory(cross-session, claudeMd / agent-memory / skill cache)。我们会先从「为什么不能只靠 context window」讲起,把 memory 的工程必要性建立起来,再在文章 5、6 里深入存储/检索结构与演化机制。
引用
src/tools/AgentTool/AgentTool.tsxsrc/tools/AgentTool/runAgent.tssrc/tools/AgentTool/resumeAgent.tssrc/tools/AgentTool/forkSubagent.tssrc/tools/AgentTool/agentMemory.tssrc/tools/AgentTool/builtInAgents.tsrust/crates/runtime/src/task_registry.rsrust/crates/runtime/src/task_packet.rsrust/crates/runtime/src/branch_lock.rs- claw-code internal PDF §5.4 / §6.3 / §6.5 / §6.6 / §10.2