iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

開場故事

你可以把一個長任務想成一間很忙的小公司。

主代理是專案負責人,手上同時有需求、排程、查驗、收尾,腦袋裡一直在切換畫面。
如果它硬要自己把所有事情做完,通常不是做不完,而是做得很亂。

所以它會找子代理幫忙。

一個子代理去查資料,一個子代理去整理結果,一個子代理去做驗證。
表面上看起來像是大家一起合作,實際上常常也會出現很像「吵架」的狀況:

  • 兩個子代理都想回答同一件事
  • 一個子代理帶著舊上下文亂入
  • 子代理做太深,開始自己再生子代理
  • 任務還沒收斂,結果先回來一堆半成品

第 12 天要看的就是這件事:

子代理怎麼合作,又怎麼吵架?

我想把它拆成兩個角度來看。

一個是「OpenClaw 怎麼讓子代理乖乖合作」;另一個是「OpenClaw 怎麼避免子代理自己打架」。

今天要解的問題

  • 子代理到底是怎麼被正式派出去的?
  • sessions_spawn 在派工前做了哪些檢查?
  • 子代理是怎麼接住主代理的上下文,又怎麼保持隔離?
  • 為什麼有些子代理可以再管別的子代理,有些不行?
  • subagents 這個工具看到的是什麼?
  • 所謂「吵架」,在系統設計上其實是哪幾種衝突?

架構總覽

OpenClaw 的子代理不是「多開一個模型回答問題」這麼簡單。

它比較像是把一個工作單正式拆出去,然後交給另一個獨立工作單位處理。
這個單位有自己的 session、有自己的 session key、有自己的工作邊界,也有自己的限制。

從系統角度看,子代理合作大概分成四步:

  1. 主代理判斷這件事值得拆出去
  2. sessions_spawn 驗證權限、深度、數量、sandbox、上下文模式
  3. 子代理在自己的 session 裡工作,必要時再往下派工
  4. 完成後把結果回報給主代理,再由主代理整合成最終答案

真正有趣的地方在於:

  • 合作不是靠大家自由發揮
  • 而是靠很細的邊界控制
  • 邊界控制得好,子代理就像分工清楚的團隊
  • 邊界控制不好,子代理就會像一群人同時在改同一份文件

OpenClaw 很明顯是後者先想清楚,才讓前者成立。

原始碼節錄

先看 sessions_spawn 在真正開子代理之前做了什麼。

📄 原始碼:src/agents/openclaw-tools.ts(本機版本行號已變動,僅標到檔案)

const maxSpawnDepth = cfg.agents?.defaults?.subagents?.maxSpawnDepth ?? 1;
if (callerDepth >= maxSpawnDepth) return {
	status: "forbidden",
	error: `sessions_spawn is not allowed at this depth (current depth: ${callerDepth}, max: ${maxSpawnDepth})`
};

const maxChildren = cfg.agents?.defaults?.subagents?.maxChildrenPerAgent ?? 5;
const activeChildren = countActiveRunsForSession(requesterInternalKey);
if (activeChildren >= maxChildren) return {
	status: "forbidden",
	error: `sessions_spawn has reached max active children for this session (${activeChildren}/${maxChildren})`
};

這裡先擋兩件事:

  • 不能無限往下生子代理
  • 不能同一個主代理一次塞太多活出去

再看允不允許指定別的 agent id。

📄 原始碼:src/agents/openclaw-tools.ts(本機版本行號已變動,僅標到檔案)

if ((resolveAgentConfig(cfg, requesterAgentId)?.subagents?.requireAgentId ?? cfg.agents?.defaults?.subagents?.requireAgentId ?? false) && !requestedAgentId?.trim()) return {
	status: "forbidden",
	error: "sessions_spawn requires explicit agentId when requireAgentId is configured. Use agents_list to see allowed agent ids."
};

const targetPolicy = resolveSubagentTargetPolicy({
	requesterAgentId,
	targetAgentId,
	requestedAgentId,
	allowAgents: resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? cfg?.agents?.defaults?.subagents?.allowAgents,
	configuredAgentIds: resolveConfiguredAgentIds(cfg)
});
if (!targetPolicy.ok) return {
	status: "forbidden",
	error: targetPolicy.error
};

這段是在管「你能不能叫這個人來幫忙」。

不是所有 agent 都能互相叫來叫去,必須先過 allowlist 和配置檢查。
換句話說,OpenClaw 不讓子代理合作得太隨便,因為太隨便通常就會變成失控。

再往下看 sandbox 和工作目錄的衝突。

📄 原始碼:src/agents/openclaw-tools.ts(本機版本行號已變動,僅標到檔案)

if (!childRuntime.sandboxed && (requesterRuntime.sandboxed || sandboxMode === "require")) {
	if (requesterRuntime.sandboxed) return {
		status: "forbidden",
		error: "Sandboxed sessions cannot spawn unsandboxed subagents. Set a sandboxed target agent or use the same agent runtime."
	};
	return {
		status: "forbidden",
		error: "sessions_spawn sandbox=\"require\" needs a sandboxed target runtime. Pick a sandboxed agentId or use sandbox=\"inherit\"."
	};
}

if (childRuntime.sandboxed && spawnedCwd && spawnedCwd !== spawnedWorkspaceCwd) return {
	status: "forbidden",
	error: "cwd override is not supported for sandboxed subagent runs; omit cwd or use the target agent workspace as cwd"
};

這裡就很像現實工作:

  • 如果你的工作環境是封閉的,就不能隨便把任務丟給一個不受控的外部人
  • 如果子代理被要求在 sandbox 裡跑,就不能再亂指定別的 cwd

這不是小氣,是避免工作環境互相踩踏。

接著看子代理拿到什麼上下文。

📄 原始碼:src/auto-reply/reply/commands-system-prompt.ts:285-285

const childSystemPrompt = buildSubagentSystemPrompt({
	requesterSessionKey,
	requesterOrigin: childSessionOrigin,
	childSessionKey,
	label: label || void 0,
	task,
	acpEnabled: isAcpRuntimeSpawnAvailable({
		config: cfg,
		sandboxed: childRuntime.sandboxed
	}),
	nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance({ surface: "subagent" }),
	childDepth,
	maxSpawnDepth
});

子代理不是空白開場,它會收到:

  • 誰派它來的
  • 自己的 session key
  • 這次任務是什麼
  • 現在的深度是多少
  • 還能不能再往下派工

這些資訊就是它的工作身份證。

最後看子代理怎麼被正式登記。

📄 原始碼:src/agents/subagent-spawn.ts:1629-1646

registerSubagentRun({
	runId: childRunId,
	childSessionKey,
	controllerSessionKey: ownership.controllerSessionKey,
	requesterSessionKey: ownership.completionRequesterSessionKey,
	requesterOrigin,
	requesterDisplayKey: ownership.completionRequesterDisplayKey,
	task,
	taskName,
	agentId: targetAgentId,
	requesterAgentId,
	cleanup,
	label: label || void 0,
	model: resolvedModel,
	agentDir: targetAgentDir,
	workspaceDir: spawnedMetadata.workspaceDir,
	runTimeoutSeconds,
	expectsCompletionMessage: shouldAnnounceCompletion,
	spawnMode,
	attachmentsDir: attachmentAbsDir,
	attachmentsRootDir: attachmentRootDir,
	retainAttachmentsOnKeep: retainOnSessionKeep
});

這一段很像把工單寫進系統:

  • 誰開的單
  • 誰是控制者
  • 誰是實際接單的人
  • 這單叫什麼
  • 在哪個 workspace 跑
  • 用什麼模型
  • 有沒有附件
  • 完成後要不要保留

OpenClaw 不是把子代理看成一個模糊的「背景任務」,而是把它當成一個可追蹤的工作節點。

最後再看 subagents 工具,它到底列的是什麼。

📄 原始碼:src/agents/tools/subagents-tool.ts:35-52

function createSubagentsTool(opts) {
	return {
		name: "subagents",
		description: "List active and recent subagents for the requester session. If sessions_yield exists, use it for completion; do not poll wait loops.",
		execute: async (_toolCallId, args) => {
			const controller = resolveSubagentController({
				cfg,
				agentSessionKey: opts?.agentSessionKey
			});
			const runs = listControlledSubagentRuns(controller.controllerSessionKey);
			const list = buildSubagentList({
				cfg,
				runs,
				recentMinutes
			});

這個工具不是拿來「催進度」的,而是拿來「看樹狀範圍內有哪些子代理正在跑、剛跑完、或還在最近的活動窗內」。

它會按照控制者 session tree 去列,不是全世界亂撈。

白話拆解

1. 合作不是共享一切,而是共享該共享的部分

OpenClaw 的子代理合作方式很像「有邊界的協作」。

主代理把任務拆出去,不代表子代理能看到全部,也不代表它能碰所有工具。
它只能看到該看見的上下文,拿到該拿到的權限,然後在自己的 session 裡完成自己的工作。

這樣做的好處很直接:

  • 每個子代理的上下文比較小
  • 每個子代理比較專心
  • 主代理比較不會被細節淹沒

但代價也很明顯:

  • 一旦邊界切錯,子代理就會缺資訊
  • 一旦上下文 fork 得太少,它就很容易誤判
  • 一旦 fork 得太多,又會把主對話一起拖肥

所以合作不是「越多越好」,而是「切得剛剛好」。

2. 吵架的第一種形式,是搶資源

第一種衝突很像現實裡的工時衝突。

OpenClaw 用 maxChildrenPerAgent 擋掉同時太多子代理,避免主代理把自己撐爆。
也用 maxSpawnDepth 擋掉無限遞迴,避免子代理一直生子代理。

這兩個限制看起來很像保守,但其實很務實。

如果沒有這些限制,系統會變成:

  • 主代理一直派活
  • 子代理又一直派活
  • 結果真正的答案沒人收

所以所謂「吵架」,有時候不是內容在吵,而是資源先打架。

3. 吵架的第二種形式,是上下文錯位

context: "fork"context: "isolated" 這兩種模式很有意思。

  • isolated 比較像新開一個乾淨辦公桌
  • fork 比較像把上一輪聊天紀錄複製一部分過去

如果你要的是純研究、純查資料、純執行,isolated 比較乾淨。
如果你要的是接著前文往下做,fork 才能避免子代理什麼都不知道。

但 fork 太多也會出問題。

因為子代理一旦帶著太厚的前文,可能會:

  • 把不該繼承的假設一起帶過去
  • 把已經過時的方向當成真命題
  • 把主代理只是「參考」的東西,誤以為是硬指令

這就是另一種吵架:不是人跟人吵,是新上下文跟舊上下文互相打架。

4. 吵架的第三種形式,是權限邊界不一致

子代理最麻煩的不是會不會做事,而是能不能在同一個邊界內做事。

OpenClaw 很明確地檢查:

  • sandboxed 的 session 不能 spawn 出不 sandboxed 的 child
  • sandbox="require" 不能找一個不 sandboxed 的目標
  • sandboxed child 不能亂換 cwd

這些規則看起來很碎,但其實是一條原則:

你不能把安全邊界當成可有可無的裝飾。

如果每個子代理都能隨便突破邊界,那合作就會變成混亂。
所以 OpenClaw 選擇先嚴格,後面才有機會穩定擴張。

5. 子代理不只是做事,也要會回報

真正好的合作,不是把事情做完就消失。

OpenClaw 的子代理是會回來 announce 的。
它不是默默跑完就算了,而是要把結果帶回 requester session。

這很像團隊工作裡的回報節奏:

  • 做完先告訴主管
  • 主管再決定要不要公開、要不要補刀、要不要追問

這樣主代理才不會一邊忙一邊猜子代理到底做完了沒。

而且 sessions_yield 的設計也很聰明,它不是叫你一直輪詢等待,而是先結束當前 turn,讓完成事件變成下一個可見訊息。
這樣比較像正常工作節奏,而不是 CPU 在那邊空轉等人回信。

6. subagents 不是用來偷看所有人,而是用來看自己的小隊

subagents 這個工具只列出自己控制範圍內的 sub-agent runs。

這很重要。

因為如果它能隨便看所有子代理,狀態就會太雜;
如果它什麼都看不到,主代理就不知道自己派出去的人在哪。

所以 OpenClaw 做的是折衷:

  • 看得到自己 tree 內的狀態
  • 看不到不屬於自己的東西
  • 夠用,但不外溢

這就是所謂的「協作,但不共享全部世界」。

設計取捨

  • 好處是子代理彼此隔離,任務比較不容易互相污染
  • 好處是主代理可以把複雜工作拆成小塊
  • 好處是完成結果可以推回 requester,不需要人工去撿
  • 好處是深度、數量、sandbox 都有明確護欄
  • 代價是配置變多,理解成本也比較高
  • 代價是上下文切錯時,子代理會直接少看或多看
  • 代價是回報鏈條越長,除錯就越需要看 session / run / ownership

如果換成「完全自由協作」的設計,短期看起來會比較順手。
但長期通常會變成:

  • 誰都能叫誰
  • 誰都能改誰的狀態
  • 誰都不知道這單最後算誰的

OpenClaw 顯然不想要那種松散模型。
它想要的是可控、可追蹤、能收斂的合作。

今天的結論

  • 子代理的合作是被設計出來的,不是自然長出來的
  • sessions_spawn 先管深度、數量、allowlist、sandbox,再管真正派工
  • forkisolated 決定子代理是接續前文,還是乾淨重開
  • subagents 看到的是控制樹內的子代理狀態,不是全域亂撈
  • 所謂「吵架」,本質上多半是資源、上下文、權限三種衝突
  • OpenClaw 透過明確邊界,讓多代理協作變成可管理的團隊,而不是一群自由亂跑的分身

下一步

第 13 天我想接著看一件更實際的事:

一個任務拆成多段之後,OpenClaw 到底怎麼收尾?

因為拆出去不難,難的是最後怎麼把它們重新拼回一個完整答案。


上一篇
第 11 天:子代理登場,讓工作不再只有一個人扛
下一篇
第 13 天:一個任務拆成多段,OpenClaw 怎麼收尾
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言