Claude Code 詳解:SubAgent 六種執行模式

作者:精神抖擻王大鵬
日期:2026年6月7日 下午11:45
來源:WeChat 原文

整理版優先睇

速讀 5 個重點 高亮

Claude CodeSubAgent 六種執行模式,由同步到遠端,逐個拆解設計取捨

整理版摘要

呢篇文章係深入分析 Claude Code 裡面子 Agent(SubAgent)嘅六種執行模式。背景係主 Agent 因為上下文窗口限制,遇到複雜多步任務時會自動啟動一個子 Agent,等佢喺乾淨嘅上下文入面專注執行。作者從源碼入手,由 AgentTool 嘅 call() 函數開始,逐步拆解每種模式嘅觸發條件、內部實現同限制。

六種模式包括Sync(同步阻塞,快靚正)、Async(後台非阻塞,適合長時間任務)、Fork(繼承父級上下文,慳 tokens)、Coordinator(協調者模式,將任務拆解畀多個 Worker 並行)、Teammate(多 Agent 協作,互相通信)同 Remote(遠程執行,隔離環境)。文章最後對比各模式嘅差異,仲講咗子 Agent 嵌套嘅限制——預設情況下唔畀創建子 Agent,深度防止遞歸爆炸。

  • 子 Agent 嘅核心起因係上下文窗口限制,主 Agent 上下文過長時容易「逃逸」,所以需要乾淨嘅子上下文專注執行。
  • 六種模式各有適用場景Sync 最快但阻塞,Async 唔阻塞適合背景任務,Fork 慳 tokens 但唔可以再 Fork,Coordinator 並行拆大 Project,Teammate 多 Agent 協作,Remote 隔離執行。
  • Fork 模式最特別,佢復用父級上下文(包括 Prompt Cache),令子 Agent 唔使由頭建立上下文,但有限制:唔可以再 Fork
  • 預設情況下,子 Agent 唔可以再創建子 Agent(靜態過濾禁止 Agent 工具),只有內部用戶、Fork child 同 In-process teammate 等例外,而且有運行時 Guard 進一步限制。
  • 實戰選擇:簡單快速任務用 Sync,耗時獨立任務用 Async需要保留父級上下文用 Fork,大型項目用 Coordinator,多 Agent 協作用 Teammate,安全隔離用 Remote。
結構示例

內容結構

內容結構 text
// src/tools/AgentTool/prompt.tsWhen NOT to use the Agent tool:
- If you want to read a specific file path, use FileRead or Glob instead- If you are searching for a specific class definition like "class Foo", use Glob instead- If you are searching for code within 2-3 files, use FileRead instead
整理重點

點解要有 SubAgent?

Claude Code 嘅主 Agent 會根據任務複雜度自動決定係直接回應定係啟動一個子 Agent。例如你問「呢個係咩模型」,佢直接答;但如果你話「幫我重構呢個模塊再跑 Test」,佢可能會背後開一個子 Agent 處理。呢個決策唔需要你手動觸發,全由 LLM 自主判斷。

核心原因係上下文窗口限制。當對話累積大量歷史,主 Agent 嘅上下文會變得好長,帶來兩個問題:tokens 成本上升,同埋 LLM 容易「逃逸」——忽略目標直接話完成。

子 Agent 以乾淨嘅上下文啟動,只關注分配嘅具體任務,執行完交返結果,可以更專注完成長時間任務。

整理重點

六種執行模式

所有模式最終都行一個共享執行引擎 runAgent(),只係喺調用前後包裝唔同。以下逐一睇每種模式嘅分別。

  1. 1 Sync(同步模式):預設模式,當 subagent_type 存在且冇指定 background 時行呢條路。父 Agent 阻塞等子 Agent 做完,適合快速明確嘅任務。
  2. 2 Async(異步/後台模式):觸發條件係 run_in_background=true 或 agent 定義 background: true。父 Agent 唔等結果,立即繼續,適合長時間任務例如全量測試。
  3. 3 Fork 模式:省略 subagent_type 並啟用 FORK_SUBAGENT feature flag。佢複用父級上下文,透過 Prompt Cache 共享慳 tokens,但唔可以再 Fork
  4. 4 Coordinator 模式:啟動時設 CLAUDE_CODE_COORDINATOR_MODE=1,主 Agent 變身協調者,拆解任務畀多個 Worker 並行做,適合複雜多步驟項目。
  5. 5 Teammate 模式:傳入 team_name 並啟用 ENABLE_AGENT_SWARMS,多 Agent 之間可以互相通信協作,例如一個負責實現、一個負責測試。
  6. 6 Remote 模式:agent 定義設定 isolation: 'remote',子 Agent 被發送到遠程機器執行,提供物理隔離,但僅限 Anthropic 內部使用。
整理重點

共享執行引擎 runAgent()

runAgent() 係一個 AsyncGenerator,每產生一個消息就 yield 畀調用方,邊執行邊輸出。進入執行前有六個準備階段,包括生成系統提示詞、組合工具列表等。

核心係一個 Query Loop:調 LLMLLM 返回消息(可能含 tool_use)→ 執行工具 → 結果反饋畀 LLM → 重複,直到 LLM 停止或達到最大循環次數。

成本優化方面:普通子 Agent 關閉 thinking(因為具體任務唔需要深度推理),但 Fork 例外,需要繼承父級 thinking 配置嚟保持 Prompt Cache 命中。

整理重點

子 Agent 嵌套限制

子 Agent 可唔可以再創建子 Agent?答案係預設唔得。Claude Code 有兩層保護:靜態過濾同運行時 Guard

靜態過濾:所有子 Agent 預設禁止池入麪包含 Agent 工具,佢哋根本睇唔到呢個工具,自然無法嵌套。

例外情況包括Anthropic 內部用戶、Fork child(因為要繼承完整工具定義)、In-process Teammate(swarms 模式下代碼特判)。但即使有工具,運行時仲有 Guard 攔截,例如 Teammate 唔可以再開 Teammate,Fork child 唔可以再 Fork。

靜態過濾決定邊啲場景可以拎到 Agent 工具

  • 普通子 Agent(外部用戶):❌ 被過濾,根本冇 Agent 工具。
  • 子 Agent(Anthropic 內部):✅ 保留,因為禁止列表對 ant 用戶為空。
  • Async/background 子 Agent:❌ 被過濾,行白名單模式。
  • In-process Teammate:✅ 特殊放行。
  • Fork child:✅ 保留,因為繼承父級完整工具定義(Cache 一致性)。
整理重點

模式對比同總結

決定用邊種模式,要睇任務特性:是否阻塞、是否需要共享上下文、是否需要並行、是否需要多 Agent 協作。

總括嚟講,Sync 適合簡單快速任務,Async 適合耗時獨立任務,Fork 適合需要保持上下文但唔想佔主線程嘅場景,Coordinator 適合大型項目,Teammate 適合多 Agent 協作,Remote 適合需要隔離環境。

每一種模式都係取捨Sync 快但阻塞,Fork 慳 tokens 但限制嵌套,Coordinator 並行度高但管理複雜,Teammate 協作能力強但需要 feature flag。

 

用緊Claude Code嗰陣,你可能會留意到,有啲任務佢會自己搞掂,但有啲任務佢就會「派個agent去做」。例如你問「呢個係咩模型」,佢就直接答你;但如果你話「幫我重構呢個module再跑通個測試」,佢就可能會開一個子智能體喺後台處理。

呢個決定唔需要用戶主動觸發。Claude Code嘅主agent手上有一個叫Agent嘅工具,佢嘅描述係:

Launch a new agent to handle complex, multi-step tasks autonomously.

LLM會根據任務複雜程度自行決定要唔要呼叫呢個工具,喺源碼入面,都清楚話畀LLM知幾時不該用它:

// src/tools/AgentTool/prompt.ts
When NOT to use the Agent tool:
If you want to read a specific file path, use FileRead or Glob instead
If you are searching for a specific class definition like "class Foo", use Glob instead
If you are searching for code within 2-3 files, use FileRead instead

咁點解要引入子智能體,而唔係俾主agent自己做曬所有嘢?核心原因係上下文窗口嘅限制。當對話累積咗好多歷史訊息之後,主agent嘅上下文會變得好長,呢個會帶嚟兩個問題:第一係tokens成本上升,第二係LLM喺太長嘅上下文入面容易「逃逸」——即係忽略目標直接話完成。子智能體用乾淨嘅上下文啟動,淨係專注喺分配咗嘅具體任務,做完之後將結果交返嚟,咁就可以更專注咁完成長時間嘅任務。

下面由源碼入口AgentTool.tsx嘅call()函數開始,睇嚇子智能體由誕生到結束嘅完整鏈路。

入口:AgentTool.tsx — call()

先睇輸入,AgentTool接受啲咩參數?

// src/tools/AgentTool/AgentTool.tsx
inputSchema = z.object({
  prompt: z.string(),           // 給子 agent 的任務描述
  subagent_type: z.string(),    // agent 類型名稱(可選,fork 時省略)
  run_in_background: z.boolean(), // 是否後台執行(可選)
})

其中subagent_type係由一個註冊表入面揀一個角色。呢個註冊表喺啟動嗰陣由loadAgentsDir.ts由多個來源收集:代碼內置嘅agent(general-purpose、Explore、Plan、Verification等)、用戶自定義嘅~/.claude/agents/*.md、項目級嘅.claude/agents/*.md。每個agent定義包括:可以用咩工具、系統提示詞、最大循環次數、模型選擇。

call()攞到參數之後,核心工作係路由分發——根據參數同環境條件,決定行邊條執行路徑:

圖片

下面逐個詳細講每種執行模式嘅內部實現。

執行模式詳解

喺詳細講各模式之前,先睇佢哋共享嘅執行引擎——runAgent()。無論係 Sync、Async 定係 Fork,最終都係行呢個函數。

runAgent():共享執行引擎

runAgent() 係一個 AsyncGenerator——每產生一條訊息就 yield 畀調用方,邊執行邊輸出:

// src/tools/AgentTool/runAgent.ts
async functionrunAgent({
  agentDefinition,      // Agent 配置(工具、提示詞、模型等)
  promptMessages,       // 初始消息(用戶的 prompt)
  toolUseContext,       // 工具執行上下文
  isAsync,             // 是否後台
  useExactTools,       // fork 路徑標記
  maxTurns,            // 最大循環次數
  ...
}): AsyncGenerator<StreamEvent>

進入執行之前,先經過6個準備階段:

圖片

然後進入核心嘅 Query Loop:

// src/tools/AgentTool/runAgent.ts
for await (const msg of query(...)) {
  yield msg                        // 流式輸出給調用方
  recordSidechainTranscript()      // 持久化到磁盤(崩潰可恢復)
}

循環邏輯:call LLM → LLM 返返訊息(可能包含 tool_use)→ 執行工具 → 結果反饋俾 LLM → 重複,直至 LLM 停止或者達到 maxTurns。

成本優化點:普通子 agent 嘅 thinking 被關閉(thinkingConfig: { type: 'disabled' }),因為執行具體任務唔需要深度推理,可以慳 tokens。但 Fork 路徑例外——佢需要繼承父級嘅 thinking 配置嚟保證 Prompt Cache 命中。

理解了 runAgent() 之後,各模式嘅分別就清楚曬:佢哋只係喺呼叫 runAgent() 前後做咗唔同嘅包裝。


Sync(同步模式)

Sync 係默認模式——當 subagent_type 存在而且冇指定後台執行嗰陣就行呢條路。用戶話「幫我揾嚇呢個函數嘅定義」、「檢查呢個文件有冇語法錯誤」呢類明確、快速嘅任務嗰陣,LLM 通常會揀同步執行。

父 agent 呼叫 runAgent() 之後阻塞消費成個 generator,直到子 agent 行完。完成之後透過 finalizeAgentTool() 提取結果:

// src/tools/AgentTool/agentToolUtils.ts
function finalizeAgentTool(agentMessages, agentId, metadata): AgentToolResult {
  // 取最後一條 assistant 消息的文本內容
  const content = lastAssistantMessage.message.content.filter(_ => _.type === 'text')
  // 統計 token 消耗和工具調用次數
  const totalTokens = getTokenCountFromUsage(...)
  const totalToolUseCount = countToolUses(agentMessages)
  // 發送 cache 驅逐信號——子 agent 的 cache 鏈可以釋放了
  logEvent('tengu_cache_eviction_hint', { scope'subagent_end' })
  return { agentId, agentType, content, totalDurationMs, totalTokens, totalToolUseCount }
}

返回的 AgentToolResult 包含子 agent 嘅文本輸出、耗時、token 消耗等資訊,父 agent 攞到之後繼續自己嘅邏輯。

Async(異步/後台模式)

觸發條件:run_in_background=true,或者 agent 定義入面 background: true,或者 Coordinator 模式。用戶話「幫我喺後台行一次全量測試」、「後台分析一下呢個日誌文件」嗰陣就行呢條路。

同 Sync 嘅分別在於外面包咗一層異步註冊:

// src/tools/AgentTool/AgentTool.tsx
registerAsyncAgent(agentId, ...)  // 註冊到後台任務框架
// 立即返回 { status: 'async_launched', agentId }

// 子 agent 在後台獨立執行 runAgent()
// 完成後:
completeAgentTask(agentId)
enqueueAgentNotification(...)    // 通知父級"我做完了"

父 agent 唔等結果,即刻繼續工作。子 agent 完成之後透過通知機制通知。

Async 模式底下嘅隔離更徹底:

  • • isNonInteractiveSession = true(唔可以互動)
  • • shouldAvoidPermissionPrompts = true(自動拒絕權限彈窗)
  • • 上下文完全隔離(唔共享父級嘅 appState)

Fork 模式

觸發條件:省略 subagent_type + FORK_SUBAGENT feature flag 開咗。Claude Code 嘅 prompt 入面話畀 LLM 知:「當中間工具輸出唔值得保留喺你嘅上下文嘅時候,fork 自己」——例如開放式嘅調研問題、需要多步編輯嘅實現任務。

Fork 解決嘅問題:當父級上下文已經好長嗰陣,建立一個全新子 agent 需要由零開始構建上下文,浪費 tokens。Fork 令子 agent 重用父級已經有嘅上下文——類似 Unix 嘅 fork() 系統呼叫。

// src/tools/AgentTool/forkSubagent.ts
const FORK_AGENT = {
  agentType'fork',
  tools: ['*'],              // 繼承所有工具
  maxTurns200,
  model'inherit',          // 繼承父模型
  getSystemPrompt() => ''// 不用自己的——直接用父級的
}

Fork 嘅核心優化係Prompt Cache 共享。Claude API 對請求前綴做咗緩存,前綴字節級相同就可以命中。Fork 點樣做到嘅:

  1. 1. useExactTools = true → 工具定義同父級完全一致
  2. 2. override.systemPrompt = parent.renderedSystemPrompt → 系統 prompt 字節相同
  3. 3. 所有歷史 tool_result 替換成統一 placeholder → 消除內容差異
  4. 4. thinking 配置繼承父級 → 保持請求結構一致

遞歸防護

// src/tools/AgentTool/forkSubagent.ts:78-89
function isInForkChild(messages): boolean {
  return messages.some(m => m.content.includes('<fork_boilerplate>'))
}

每個 fork 子 agent 嘅初始訊息會注入一個 <fork_boilerplate> 標籤。再次 fork 嗰陣掃描歷史發現呢個標籤 → 拒絕。

限制條件:同 Coordinator 互斥,非互動模式唔支援。


Coordinator 模式

觸發條件:環境變數 CLAUDE_CODE_COORDINATOR_MODE=1。用戶用 --coordinator 啟動 Claude Code 嗰陣進入呢個模式。

本質上 Coordinator 係 Async 模式嘅特化應用——所有 Worker 都行 Async 路徑,但加咗專屬嘅系統提示詞同工具限制。啟用之後 Claude Code 唔再自己執行任務,而係拆解任務之後派俾多個 Worker 同時執行:

// src/coordinator/coordinatorMode.ts
export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

Coordinator 模式底下嘅行為變化:

  • • 所有子 agent 強制異步AgentTool.tsx:567)——派完嘢即刻回覆用戶
  • • 唔指定模型參數(AgentTool.tsx:252)——Worker 用默認模型
  • • 有專屬系統提示詞,指示「你係協調者,將任務分配俾 Worker」
  • • Worker 只能用 Bash/Read/Edit 等有限工具,唔可以再創建 agent
  • • 同 Fork 互斥

Coordinator 嘅系統提示詞入面定義咗完整嘅工作流程:

研究階段(Worker 並行調研)→ 綜合階段(Coordinator 自己理解)
→ 實施階段(Worker 執行修改)→ 驗證階段(Worker 測試)

Worker 完成之後用 <task-notification> XML 格式匯報結果。Coordinator 消化結果之後向用戶匯報,或者透過 SendMessage 繼續指揮同一個 Worker。


Teammate 模式

觸發條件:傳入 team_name 參數 + ENABLE_AGENT_SWARMS feature flag 開咗。當 LLM 需要多個 agent 之間互相協作(例如一個負責實現、一個負責測試、互相通信反饋結果)嗰陣用。

Teammate 同普通子 agent 嘅分別:有名字、喺團隊花名冊入面註冊、可以透過 SendMessage 同其他 Teammate 互相通信。

兩種後端實現:

後端
機制
特點
Pane (tmux)
獨立 tmux pane
獨立進程,CLI 參數繼承
In-Process
同進程
共享內存,零 IPC 開銷

Pane 後端嘅啟動流程:

1. ensureSession() — 確保 tmux session 存在
2. buildInheritedCliFlags() — 繼承權限模式/模型/插件
3. buildInheritedEnvVars() — 繼承環境變量
4. createTeammatePaneInSwarmView() — 創建新 pane
5. sendCommandToPane() — 在 pane 中啓動 teammate

權限繼承嘅安全設計:plan 模式下不繼承 bypassPermissions。就算父級跳過咗權限檢查,創建嘅 teammate 都唔會自動獲得呢個特權。

Teammate 名稱保證唯一——衝突嗰陣追加數字後綴(tester → tester-2)。


Remote 模式

觸發條件:agent 定義入面設定 isolation: 'remote',而且滿足遠程執行嘅前提條件(僅 Anthropic 內部可用)。

子 agent 透過 teleportToRemote() 被發送到遠程機器執行,任務、工具配置、權限一齊打包發出。執行完成之後結果回傳。


橫向對比

模式
是否阻塞父級
上下文來源
隔離程度
典型場景
Sync
阻塞
全新構建
共享 appState
快速查找、簡單操作
Async
不阻塞
全新構建
完全隔離
耗時長的獨立任務
Fork
不阻塞
繼承父級
共享 cache
需要上下文但不想佔主線程
Coordinator
不阻塞
全新構建
完全隔離
複雜多步驟項目
Teammate
不阻塞
全新構建
獨立進程/線程
多 agent 協作通信
Remote
不阻塞
全新構建
物理隔離
需要隔離環境

子Agent嵌套:子 Agent 可唔可以再創建子 Agent

子 agent 可唔可以再創建子 agent,本質取決於兩個條件:工具池入面有冇保留 Agent 工具 + 運行時 guard 有冇放行

靜態過濾:邊個可以攞到 Agent 工具

Claude Code 設計咗分層嘅工具過濾機制。對於子 agent 嘅工具池:

// src/constants/tools.ts:41
const ALL_AGENT_DISALLOWED_TOOLS = new Set([
  AGENT_TOOL_NAME,              // Agent 工具本身——防遞歸
  ASK_USER_QUESTION_TOOL_NAME,  // 向用戶提問
  TASK_STOP_TOOL_NAME,          // 停止任務
  // ...
])

默認情況下,Agent 工具被放進所有子 agent 嘅禁止列表——子 agent 根本睇唔到呢個工具,自然冇得嵌套。

例外:

場景
Agent 工具係咪保留
原因
普通子 agent(外部用戶)
❌ 被過濾
禁止列表包含 AGENT_TOOL_NAME
子 agent(Anthropic 內部用戶)
✅ 保留
禁止列表對 ant 用戶為空
異步/background 子 agent
❌ 被過濾
白名單模式,白名單入面冇 Agent
In-process teammate
✅ 特殊放行
swarms 模式下代碼特判
Fork child
✅ 保留
繼承父級完整工具定義(cache 一致性)

運行時 Guard:就算有工具都會攔截

通過靜態過濾之後,call() 入面仲有運行時檢查:

// src/tools/AgentTool/AgentTool.tsx

// teammate 不能再 spawn teammate(第 272 行)
if (isTeammate() && teamName && name) {
  throw new Error('Teammates cannot spawn other teammates — the team roster is flat.');
}

// in-process teammate 不能創建 background agent(第 278 行)
if (isInProcessTeammate() && run_in_background === true) {
  throw new Error('In-process teammates cannot spawn background agents.');
}

// fork child 不能再 fork(第 332-333 行)
if (querySource === 'agent:builtin:fork' || isInForkChild(messages)) {
  throw new Error('Fork is not available inside a forked worker.');
}
圖片

設計邏輯入面,用靜態過濾管「廣度」,對大部分場景直接砍咗 Agent 工具,防止子Agent遞歸。運行時 guard 管「深度」,就算開咗子agent創建子agent,都會限制特定行為,防止遞歸爆炸。

 


 

在使用Claude Code時,你可能注意到,有些任務它會自己處理,而有些任務它會"派一個agent去做"。比如你問"這是什麼模型",它直接回答;但你說"幫我把這個模塊重構一下並跑通測試",它可能會啓動一個子智能體在後台處理。

這個決策不需要用戶顯式觸發。Claude Code的主agent手裏有一個名為Agent的工具,它的描述是:

Launch a new agent to handle complex, multi-step tasks autonomously.

LLM根據任務複雜度自主決定是否調用這個工具,在源碼中,也明確告訴LLM什麼時候不該用它:

// src/tools/AgentTool/prompt.ts
When NOT to use the Agent tool:
If you want to read a specific file path, use FileRead or Glob instead
If you are searching for a specific class definition like "class Foo", use Glob instead
If you are searching for code within 2-3 files, use FileRead instead

那為什麼要引入子智能體,而不是讓主agent自己做完所有事?核心原因是上下文窗口的限制。當對話積累了大量歷史消息後,主agent的上下文會變得很長,這會帶來兩個問題:一是tokens成本升高,二是LLM在過長上下文中容易"逃逸"——忽略目標直接宣佈完成。子智能體以乾淨的上下文啓動,只關注被分配的具體任務,執行完把結果交回來,會能更專注的完成長時任務。

下面從源碼入口AgentTool.tsx的call()函數開始,來看子智能體從誕生到結束的完整鏈路。

入口:AgentTool.tsx — call()

先看輸入,AgentTool接受什麼參數?

// src/tools/AgentTool/AgentTool.tsx
inputSchema = z.object({
  prompt: z.string(),           // 給子 agent 的任務描述
  subagent_type: z.string(),    // agent 類型名稱(可選,fork 時省略)
  run_in_background: z.boolean(), // 是否後台執行(可選)
})

其中subagent_type是從一個註冊表裏選一個角色。這個註冊表在啓動時由loadAgentsDir.ts從多個來源收集:代碼內置的agent(general-purpose、Explore、Plan、Verification等)、用戶自定義的~/.claude/agents/*.md、項目級的.claude/agents/*.md。每個agent定義包含:能用什麼工具、系統提示詞、最大循環次數、模型選擇。

call()拿到參數後,核心工作是路由分發——根據參數和環境條件,決定走哪條執行路徑:

圖片

下面逐一展開每種執行模式的內部實現。

執行模式詳解

在展開各模式之前,先看它們共享的執行引擎——runAgent()。無論 Sync、Async 還是 Fork,最終都走這個函數。

runAgent():共享執行引擎

runAgent() 是一個 AsyncGenerator——每產生一條消息就 yield 給調用方,邊執行邊輸出:

// src/tools/AgentTool/runAgent.ts
async functionrunAgent({
  agentDefinition,      // Agent 配置(工具、提示詞、模型等)
  promptMessages,       // 初始消息(用戶的 prompt)
  toolUseContext,       // 工具執行上下文
  isAsync,             // 是否後台
  useExactTools,       // fork 路徑標記
  maxTurns,            // 最大循環次數
  ...
}): AsyncGenerator<StreamEvent>

進入執行前,先經過 6 個準備階段:

圖片

然後進入核心的 Query Loop:

// src/tools/AgentTool/runAgent.ts
for await (const msg of query(...)) {
  yield msg                        // 流式輸出給調用方
  recordSidechainTranscript()      // 持久化到磁盤(崩潰可恢復)
}

循環邏輯:調 LLM → LLM 返回消息(可能含 tool_use)→ 執行工具 → 結果反饋給 LLM → 重複,直到 LLM 停止或達到 maxTurns。

成本優化點:普通子 agent 的 thinking 被關閉(thinkingConfig: { type: 'disabled' }),因為執行具體任務不需要深度推理,省 tokens。但 Fork 路徑例外——它需要繼承父級的 thinking 配置來保證 Prompt Cache 命中。

理解了 runAgent() 後,各模式的區別就清楚了:它們只是在調用 runAgent() 前後做了不同的包裝。


Sync(同步模式)

Sync 是默認模式——當 subagent_type 存在且沒有指定後台執行時走這條路。用戶說"幫我搜一下這個函數的定義"、"檢查這個文件有沒有語法錯誤"這類明確、快速的任務時,LLM 通常選擇同步執行。

父 agent 調用 runAgent() 後阻塞消費整個 generator,直到子 agent 跑完。完成後通過 finalizeAgentTool() 提取結果:

// src/tools/AgentTool/agentToolUtils.ts
function finalizeAgentTool(agentMessages, agentId, metadata): AgentToolResult {
  // 取最後一條 assistant 消息的文本內容
  const content = lastAssistantMessage.message.content.filter(_ => _.type === 'text')
  // 統計 token 消耗和工具調用次數
  const totalTokens = getTokenCountFromUsage(...)
  const totalToolUseCount = countToolUses(agentMessages)
  // 發送 cache 驅逐信號——子 agent 的 cache 鏈可以釋放了
  logEvent('tengu_cache_eviction_hint', { scope'subagent_end' })
  return { agentId, agentType, content, totalDurationMs, totalTokens, totalToolUseCount }
}

返回的 AgentToolResult 包含子 agent 的文本輸出、耗時、token 消耗等信息,父 agent 拿到後繼續自己的邏輯。

Async(異步/後台模式)

觸發條件:run_in_background=true,或 agent 定義中 background: true,或 Coordinator 模式。用戶說"幫我在後台跑一下全量測試"、"後台分析一下這個日誌文件"時走這條路。

和 Sync 的區別在於外面包了一層異步註冊:

// src/tools/AgentTool/AgentTool.tsx
registerAsyncAgent(agentId, ...)  // 註冊到後台任務框架
// 立即返回 { status: 'async_launched', agentId }

// 子 agent 在後台獨立執行 runAgent()
// 完成後:
completeAgentTask(agentId)
enqueueAgentNotification(...)    // 通知父級"我做完了"

父 agent 不等待結果,立即繼續工作。子 agent 完成後通過通知機制告知。

Async 模式下的隔離更徹底:

  • • isNonInteractiveSession = true(不可交互)
  • • shouldAvoidPermissionPrompts = true(自動拒絕權限彈窗)
  • • 上下文完全隔離(不共享父級的 appState)

Fork 模式

觸發條件:省略 subagent_type + FORK_SUBAGENT feature flag 開啓。Claude Code 的 prompt 中告訴 LLM:"當中間工具輸出不值得保留在你的上下文中時,fork 自己"——比如開放式的調研問題、需要多步編輯的實現任務。

Fork 解決的問題:當父級上下文已經很長時,創建一個全新子 agent 需要從零構建上下文,浪費 tokens。Fork 讓子 agent 複用父級已有的上下文——類似 Unix 的 fork() 系統調用。

// src/tools/AgentTool/forkSubagent.ts
const FORK_AGENT = {
  agentType'fork',
  tools: ['*'],              // 繼承所有工具
  maxTurns200,
  model'inherit',          // 繼承父模型
  getSystemPrompt() => ''// 不用自己的——直接用父級的
}

Fork 的核心優化是Prompt Cache 共享。Claude API 對請求前綴做了緩存,前綴字節級相同就能命中。Fork 怎麼做到的:

  1. 1. useExactTools = true → 工具定義和父級完全一致
  2. 2. override.systemPrompt = parent.renderedSystemPrompt → 系統 prompt 字節相同
  3. 3. 所有歷史 tool_result 替換為統一 placeholder → 消除內容差異
  4. 4. thinking 配置繼承父級 → 保持請求結構一致

遞歸防護

// src/tools/AgentTool/forkSubagent.ts:78-89
function isInForkChild(messages): boolean {
  return messages.some(m => m.content.includes('<fork_boilerplate>'))
}

每個 fork 子 agent 的初始消息會注入一個 <fork_boilerplate> 標籤。再次 fork 時掃描歷史發現這個標籤 → 拒絕。

限制條件:與 Coordinator 互斥,非交互模式不支持。


Coordinator 模式

觸發條件:環境變量 CLAUDE_CODE_COORDINATOR_MODE=1。用戶以 --coordinator 啓動 Claude Code 時進入此模式。

本質上 Coordinator 是 Async 模式的特化應用——所有 Worker 都走 Async 路徑,但加了專屬的系統提示詞和工具限制。啓用後 Claude Code 不再自己執行任務,而是拆解任務後派給多個 Worker 並行執行:

// src/coordinator/coordinatorMode.ts
export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

Coordinator 模式下的行為變化:

  • • 所有子 agent 強制異步AgentTool.tsx:567)——派完活立即回覆用戶
  • • 不指定模型參數(AgentTool.tsx:252)——Worker 用默認模型
  • • 有專屬系統提示詞,指示"你是協調者,把任務分配給 Worker"
  • • Worker 只能用 Bash/Read/Edit 等有限工具,不能再創建 agent
  • • 與 Fork 互斥

Coordinator 的系統提示詞裏定義了完整的工作流程:

研究階段(Worker 並行調研)→ 綜合階段(Coordinator 自己理解)
→ 實施階段(Worker 執行修改)→ 驗證階段(Worker 測試)

Worker 完成後以 <task-notification> XML 格式回報結果。Coordinator 消化結果後向用戶彙報,或通過 SendMessage 繼續指揮同一個 Worker。


Teammate 模式

觸發條件:傳入 team_name 參數 + ENABLE_AGENT_SWARMS feature flag 開啓。當 LLM 需要多個 agent 之間互相協作(比如一個負責實現、一個負責測試、互相通信反饋結果)時使用。

Teammate 和普通子 agent 的區別:有名字、在團隊花名冊中註冊、可以通過 SendMessage 與其他 Teammate 互相通信。

兩種後端實現:

後端
機制
特點
Pane (tmux)
獨立 tmux pane
獨立進程,CLI 參數繼承
In-Process
同進程
共享內存,零 IPC 開銷

Pane 後端的啓動流程:

1. ensureSession() — 確保 tmux session 存在
2. buildInheritedCliFlags() — 繼承權限模式/模型/插件
3. buildInheritedEnvVars() — 繼承環境變量
4. createTeammatePaneInSwarmView() — 創建新 pane
5. sendCommandToPane() — 在 pane 中啓動 teammate

權限繼承的安全設計:plan 模式下不繼承 bypassPermissions。即使父級跳過了權限檢查,創建的 teammate 也不會自動獲得這個特權。

Teammate 名稱保證唯一——衝突時追加數字後綴(tester → tester-2)。


Remote 模式

觸發條件:agent 定義中設置 isolation: 'remote',且滿足遠程執行的前提條件(僅 Anthropic 內部可用)。

子 agent 通過 teleportToRemote() 被髮送到遠程機器執行,任務、工具配置、權限一起打包發出。執行完成後結果回傳。


橫向對比

模式
是否阻塞父級
上下文來源
隔離程度
典型場景
Sync
阻塞
全新構建
共享 appState
快速查找、簡單操作
Async
不阻塞
全新構建
完全隔離
耗時長的獨立任務
Fork
不阻塞
繼承父級
共享 cache
需要上下文但不想佔主線程
Coordinator
不阻塞
全新構建
完全隔離
複雜多步驟項目
Teammate
不阻塞
全新構建
獨立進程/線程
多 agent 協作通信
Remote
不阻塞
全新構建
物理隔離
需要隔離環境

子Agent嵌套:子 Agent 能否再創建子 Agent

子 agent 能否再創建子 agent,本質取決於兩個條件:工具池裏是否保留了 Agent 工具 + 運行時 guard 是否放行

靜態過濾:誰能拿到 Agent 工具

Claude Code 設計了分層的工具過濾機制。對於子 agent 的工具池:

// src/constants/tools.ts:41
const ALL_AGENT_DISALLOWED_TOOLS = new Set([
  AGENT_TOOL_NAME,              // Agent 工具本身——防遞歸
  ASK_USER_QUESTION_TOOL_NAME,  // 向用戶提問
  TASK_STOP_TOOL_NAME,          // 停止任務
  // ...
])

默認情況下,Agent 工具被放進所有子 agent 的禁止列表——子 agent 根本看不到這個工具,自然無法嵌套。

例外:

場景
Agent 工具是否保留
原因
普通子 agent(外部用戶)
❌ 被過濾
禁止列表包含 AGENT_TOOL_NAME
子 agent(Anthropic 內部用戶)
✅ 保留
禁止列表對 ant 用戶為空
異步/background 子 agent
❌ 被過濾
白名單模式,白名單裏沒有 Agent
In-process teammate
✅ 特殊放行
swarms 模式下代碼特判
Fork child
✅ 保留
繼承父級完整工具定義(cache 一致性)

運行時 Guard:即使有工具也攔截

通過靜態過濾後,call() 裏還有運行時檢查:

// src/tools/AgentTool/AgentTool.tsx

// teammate 不能再 spawn teammate(第 272 行)
if (isTeammate() && teamName && name) {
  throw new Error('Teammates cannot spawn other teammates — the team roster is flat.');
}

// in-process teammate 不能創建 background agent(第 278 行)
if (isInProcessTeammate() && run_in_background === true) {
  throw new Error('In-process teammates cannot spawn background agents.');
}

// fork child 不能再 fork(第 332-333 行)
if (querySource === 'agent:builtin:fork' || isInForkChild(messages)) {
  throw new Error('Fork is not available inside a forked worker.');
}
圖片

設計邏輯中,用靜態過濾管"廣度",對大部分場景直接砍掉 Agent 工具,防止子Agent遞歸。運行時 guard 管"深度",即使開了子agent創建子agent,也會限制特定行為,防止遞歸爆炸。