0. 引子:为什么单 agent 会撞墙

前两篇我们看到的是单个 agent 的「主循环 + tool 边界」。这套结构有一个天然天花板:单个 turn 的能力上限 = context window × tool budget。当任务变成「读 30 个文件、产出一个 PR、再跑一遍验证」这种多步骤工程任务时,单 agent 会撞到三堵墙:

  1. 上下文爆炸:把 30 个文件的全文都塞进主对话,下一轮 prompt cache 命中率瞬间塌陷。
  2. 角色漂移:让一个 agent 既写代码又审代码,最后它会站在「我刚写的」立场为自己辩护。
  3. 并行写冲突:两个分支同时改 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通用研究 / 多步任务全量自由格式
Exploreread-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 + 受控 BashVERDICT: 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

  1. foreground sync:阻塞主 turn,主 agent 在 await 上等结果。适合「下一步必须依赖结果」的场景,比如先 plan 再 implement。
  2. background async:subagent 拿到自己的 AbortController,主 turn 立刻继续,subagent 完成时通过 notification 回调。适合「可以并行跑、结果之后再看」的探索任务。
  3. remote-launched:从 IDE / API 触发,主 agent 不在场。
  4. 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 的核心数据结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pub struct Task {
    pub task_id: String,           // "task_{ts}_{counter}"
    pub prompt: String,
    pub task_packet: TaskPacket,
    pub status: TaskStatus,
    pub messages: Vec<Message>,
    pub output: Option<String>,
    pub team_id: Option<String>,
}

pub enum TaskStatus {
    Created, Running, Stopped, Completed, Failed,
}

状态机是单向的Created → Running/Stopped → Completed/Failed。没有 reset、没有 reopen——一个 task 失败了就让它失败,retry 是开新 task。这避免了「half-completed task 被 resume 成 zombie」的复杂边界。

整个 registry 用 Arc<Mutex<RegistryInner>> 包裹:

1
2
3
pub struct TaskRegistry {
    inner: 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。核心思路:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def spawn_subagent(parent: "MainAgent", prompt: str, role: str, fork: bool = False) -> Task:
    if fork:
        # cache-friendly: 继承父 messages + 全工具集
        messages = list(parent.messages)
        tools = parent.tools
        sys_prompt = parent.system_prompt
    else:
        # role-capped: 重建 slim prompt + 角色工具白名单
        messages = []
        tools = ROLE_TOOLS[role]
        sys_prompt = ROLE_PROMPTS[role]
    task = registry.create(prompt=prompt, role=role, system=sys_prompt, tools=tools)
    return task

Demo 主流程派出 2 个 explore + 1 个 plan + 1 个 verify,用 ThreadPoolExecutor 并发跑,最后从 verify 的输出里 grepVERDICT: 行——把第 1 节里讲的「结构化判决」真的跑给你看。


8. 收束:Workflow 三幕落幕

到这里,CLI Agent 的 Workflow 主线讲完了:

  1. 文章 1——主循环:单 agent 一个 turn 怎么走。
  2. 文章 2——tool 边界:怎么让一个 turn 安全地动手。
  3. 文章 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.tsx
  • src/tools/AgentTool/runAgent.ts
  • src/tools/AgentTool/resumeAgent.ts
  • src/tools/AgentTool/forkSubagent.ts
  • src/tools/AgentTool/agentMemory.ts
  • src/tools/AgentTool/builtInAgents.ts
  • rust/crates/runtime/src/task_registry.rs
  • rust/crates/runtime/src/task_packet.rs
  • rust/crates/runtime/src/branch_lock.rs
  • claw-code internal PDF §5.4 / §6.3 / §6.5 / §6.6 / §10.2