iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
佛心分享-SideProject30

30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化系列 第 11

第 11 天:子代理登場,讓工作不再只有一個人扛

  • 分享至 

  • xImage
  •  

開場故事

你應該有過這種經驗。

一開始你以為某件事只是「順手做一下」,結果做著做著,突然發現它不是一件事,而是三件事:

  • 要查資料
  • 要整理狀態
  • 要確認結果

如果還硬要一個人從頭做到尾,通常不會比較快,只會比較亂。

AI Agent 也一樣。

單一 Agent 一開始看起來很萬能,什麼都能問、什麼都能做。但任務一長、分支一多、背景資料一雜,它就會開始出現很典型的疲態:

  • 一邊查資料,一邊忘記主線
  • 一邊做修正,一邊搞混舊結果
  • 一邊看對話,一邊被工具輸出塞滿
  • 一邊想回覆,一邊還要顧著下一步

第 11 天我想看的,就是這件事:

子代理登場,讓工作不再只有一個人扛

OpenClaw 不是真的想把一個任務丟給很多 AI 亂跑,它想做的是:

  • 把適合拆出去的工作拆出去
  • 讓每個角色的上下文更小、更專注
  • 讓主代理不用自己記所有細節
  • 讓背景工作可以獨立跑完,再回來匯報

這一篇我想把它寫成「OpenClaw 怎麼把一個大任務拆成幾個更好管理的小腦袋」。

今天要解的問題

  • 為什麼 OpenClaw 需要子代理?
  • 一個 agent、另一個 agent、和 binding 的差別是什麼?
  • sessions_spawn 到底是怎麼把工作切出去的?
  • 子代理為什麼要有自己的 workspace、agentDir、session store?
  • context: "fork"context: "isolated" 差在哪裡?
  • 主代理怎麼知道子代理做完了沒?

架構總覽

OpenClaw 的子代理,不是「模型分身」而已。
它其實是一套完整的隔離機制。

一個 agent 在 OpenClaw 裡,代表的是整個 persona scope:

  • 自己的 workspace
  • 自己的 AGENTS.md / SOUL.md / USER.md
  • 自己的 auth profiles
  • 自己的 session store
  • 自己的 model / config / state directory

所以當 OpenClaw 說要開子代理時,它不是只開一個新的推理執行緒,而是直接開一個新的、獨立的工作單位。

這樣的好處很明顯:

  • 子代理可以專心做一件小事
  • 主代理不用把所有細節都背在腦子裡
  • 不同任務可以同時進行
  • 不同 persona 可以各管各的

如果把它畫成流程,大概是這樣:

主代理收到任務
  -> 判斷哪些部分適合拆出去
  -> sessions_spawn 開一個獨立子代理
  -> 子代理在自己的 workspace / session 內工作
  -> 子代理回報結果
  -> 主代理整合結果並對外回覆

這其實很像一個實際工作團隊。

主管不會自己跑去寫所有文件、查所有資料、驗所有細節。
他會把可以獨立完成的工作交給專人,再回頭整合。

OpenClaw 的子代理,就是這個概念的 AI 版本。

原始碼節錄

先看什麼叫一個 agent。

An **agent** is the full per-persona scope: workspace files, auth profiles, model registry, and session store. A **binding** maps a channel account ... to one of those agents.
Each configured `agentId` is a fully isolated persona:

- Different accounts per channel
- Different personalities
- Separate auth and sessions, with no cross-talk unless explicitly enabled

這段直接把「代理」的邊界說得很清楚。

子代理不是在同一個腦袋裡偷偷多開一條想法,而是另一個完整的人格空間。
它有自己的工作目錄、自己的 session、自己的權限來源。

再看 session tools。

`sessions_spawn` creates an isolated sub-agent session for a background task. It is always non-blocking; it returns immediately with a `runId` and `childSessionKey`.

📄 文件:docs/concepts/session-tool.md:102-103

Key options:

- `runtime: "subagent"` (default) or `"acp"` for external harness agents.
- `model` and `thinking` overrides for the child session.
- `thread: true` to bind the spawn to a chat thread
- `sandbox: "require"` to enforce sandboxing on the child
- `context: "fork"` for native sub-agents when the child needs the current requester transcript; omit it or use `context: "isolated"` for a clean child

這段很關鍵,因為它顯示 OpenClaw 不是只有「叫子代理」這麼簡單,而是有三個維度可以控制:

  • 這個子代理是接近母上下文,還是乾淨重開
  • 它是要綁到 thread,還是只是背景跑
  • 它要不要承接現有 transcript

換句話說,OpenClaw 不是只會開新分身,它會先決定這個分身應該知道多少。

再看多代理 routing。

Run multiple isolated agents in one Gateway process, each with its own workspace, state directory (`agentDir`), and session store, plus multiple channel accounts.

An agent is the full per-persona scope.

這是子代理能存在的前提。

如果整個 Gateway 只能有一個工作空間,那所有子代理最後都會撞在一起。
OpenClaw 之所以能拆,靠的是它本來就支援多 agent 隔離。

再往實作層看,OpenClaw 不是只把子代理當成「背景任務」記一下而已,而是把它綁進 run context。

📄 原始碼:src/infra/agent-events.ts:222-329

function claimAgentRunContext(runId, context, options = {}) {
	if (!runId) return;
	const state = getAgentEventState();
	const lifecycleGeneration = context.lifecycleGeneration ?? state.lifecycleGeneration;
	const existing = state.runContextById.get(runId);
	const ownersById = getAgentRunContextOwners(state);
	const existingOwners = ownersById.get(runId);
	let ownerToken;
	if (options.trackOwner) {
		ownerToken = randomUUID();
		if (existingOwners?.lifecycleGeneration === lifecycleGeneration) {
			existingOwners.ownerTokens.add(ownerToken);
			if (options.ownsContext) existingOwners.preserveAfterRelease = false;
		} else ownersById.set(runId, {
			lifecycleGeneration,
			ownerTokens: new Set([ownerToken]),
			preserveAfterRelease: options.ownsContext !== true && existing?.lifecycleGeneration === lifecycleGeneration,
			clearRequested: false
		});
	}
	state.runContextById.set(runId, {
		...context,
		lifecycleGeneration,
		registeredAt: context.registeredAt ?? Date.now()
	});
}

這段透露的意思是:

  • 一個 run 不只是 runId
  • 它還要帶 sessionKeysessionIdagentId 這些狀態
  • 如果 lifecycle 已經換代,舊的 owner 不能繼續污染新的 run

再看真正註冊子代理工作的地方。這裡可以直接看到 childSessionKeyrequesterSessionKeycontrollerSessionKey 都被保存進去了。

📄 原始碼:src/agents/subagent-registry-run-manager.ts:730-804

const registerSubagentRun = (registerParams) => {
	const runId = registerParams.runId.trim();
	const childSessionKey = registerParams.childSessionKey.trim();
	const requesterSessionKey = registerParams.requesterSessionKey.trim();
	const controllerSessionKey = registerParams.controllerSessionKey?.trim() || requesterSessionKey;
	if (!runId || !childSessionKey || !requesterSessionKey) return;
	const now = Date.now();
	const generation = nextSubagentRunGeneration(params.runs.values(), childSessionKey);
	const spawnMode = registerParams.spawnMode === "session" ? "session" : "run";
	const entry = normalizeSubagentRunState({
		runId,
		taskRunId: runId,
		childSessionKey,
		controllerSessionKey,
		requesterSessionKey,
		spawnMode,
		execution: { status: "running", startedAt: now },
		createdAt: now,
		startedAt: now
	});
	params.runs.set(runId, entry);
	createRunningTaskRun({
		runtime: "subagent",
		sourceId: runId,
		ownerKey: requesterSessionKey,
		scopeKind: "session",
		childSessionKey,
		runId,
		startedAt: now,
		lastEventAt: now
	});
};

這很像真的在工單系統裡開了一張新單:

  • runId 是追蹤碼
  • childSessionKey 是子代理工作的房間
  • requesterSessionKey 是誰派工出去的
  • controllerSessionKey 是誰在控制這件事

所以子代理不是模糊地「被叫出去幫忙」,而是被正式登記成一個可追蹤的工作單位。

最後補一段,讓你看見子代理還會被列出、去重,然後跟 binding 對起來。

📄 原始碼:src/auto-reply/reply/commands-subagents/action-agents.ts:28-59

function handleSubagentsAgentsAction(ctx) {
	const { params, requesterKey, runs } = ctx;
	const runsSnapshot = getSubagentRunsSnapshotForRead(subagentRuns);
	const channel = resolveCommandSurfaceChannel(params);
	const accountId = resolveChannelAccountId(params);
	const bindingService = getSessionBindingService();
	const bindingsBySession = new Map();
	const resolveSessionBindings = (sessionKey) => {
		const cached = bindingsBySession.get(sessionKey);
		if (cached) return cached;
		const resolved = bindingService.listBySession(sessionKey).filter((entry) => entry.status === "active" && entry.conversation.channel === channel && entry.conversation.accountId === accountId);
		bindingsBySession.set(sessionKey, resolved);
		return resolved;
	};
	const dedupedRuns = [];
	const seenChildSessionKeys = new Set();
	for (const entry of sortSubagentRuns(runs)) {
		if (seenChildSessionKeys.has(entry.childSessionKey)) continue;
		seenChildSessionKeys.add(entry.childSessionKey);
		dedupedRuns.push(entry);
	}
}

這段的味道就很明確了:

  • 子代理不是一次性黑盒
  • 它是可列出、可去重、可追蹤的
  • 而且會和 session binding 連動

如果沒有這層管理,主代理就很容易變成一個只會派工、但不會記帳的人。

白話拆解

1. 子代理不是「更多模型」,而是「更小的任務單位」

很多人第一次聽到子代理,直覺會想成:

「喔,就是再叫一個 AI 幫忙想。」

這樣講只對一半。

更精準的說法是:

子代理不是為了讓 AI 變多,而是為了讓任務變小。

因為大任務最怕的不是算力不夠,而是上下文太雜。
你讓主代理一口氣處理所有事情,它就會一直在這些事情之間切來切去。

但如果你把它拆成:

  • 一個負責查資料
  • 一個負責整理結論
  • 一個負責驗證結果

那每個角色看到的東西都更少、更準,出錯機率也比較低。

2. 隔離是子代理最值錢的地方

OpenClaw 很強調 isolation,不是因為它愛分裂,而是因為沒有隔離就沒有信任邊界。

如果子代理跟主代理共用所有狀態,那它做的事很容易把主上下文弄亂。

但現在它有自己的:

  • workspace
  • session store
  • auth profile
  • model registry

這表示它可以:

  • 自己做事
  • 自己留紀錄
  • 自己失敗
  • 自己回報

而不會把主代理拖進一個混亂的腦內空間。

3. context: "fork"context: "isolated" 是兩種很不同的合作方式

這兩個選項我覺得很像「要不要把前情提要一起帶去」。

  • fork:沿用母 transcript 的脈絡,像把前情提要一起帶進去
  • isolated:乾淨開局,像把任務交給一個只知道目標、但不被舊細節干擾的人

哪個比較好,取決於任務類型。

如果子代理要處理的是一段延續中的問題,fork 很有用。
如果子代理要做的是獨立分析、清理、整理,isolated 往往更乾淨。

4. non-blocking 代表主代理不需要在那裡乾等

sessions_spawn 會立即回傳 runIdchildSessionKey

這很像你把工作交出去,不是一直站在旁邊盯著人家做。

主代理可以先去做別的事情,等結果回來再整合。
這種模式才真的有「分工」的味道,不然只是把同一個腦袋換個名字繼續跑。

5. 子代理讓主代理不用記所有細節

這是最實際的好處。

單一 Agent 最容易死在這種事:

  • 它自己做了很多事
  • 它自己又要記很多事
  • 它自己還要回答使用者

結果就是:

  • 主線模糊
  • 細節爆炸
  • 回覆開始失焦

子代理的價值,就是把一部分細節關到另一個 session 裡。
主代理只要知道:

  • 我有派誰去
  • 對方在處理什麼
  • 結果回來了沒有
  • 最後要怎麼整合

這樣主代理就不用把所有工作過程都背成短期記憶。

6. 不同 persona 是真的不同腦袋,不是同一個人換表情

Multi-agent routing 的概念也很重要。

OpenClaw 不是只有一個 agent 加很多工具,而是可以同時有多個 agent,每個 agent 都有自己的 workspace 和 session。

這代表:

  • coding agent 可以跟 social agent 分開
  • 不同 bot / 不同 channel 可以綁到不同 agent
  • 不同 agent 的工具、權限、記憶都能分流

這種設計很像真實團隊,不是每個人都拿同一份待辦。

設計取捨

  • 好處是長任務可以拆成比較好管理的小塊
  • 好處是每個子代理的 context 更乾淨,不容易互相污染
  • 好處是主代理不用一肩扛完全部細節
  • 好處是多 persona / 多 channel 能真的隔離
  • 代價是協調成本上升,派工和整合要更小心
  • 代價是如果 fork / isolated 選錯,可能不是太吵就是資訊不足
  • 代價是子代理太多時,主代理也可能變成「只會派工的人」

所以子代理不是越多越好。
它比較像工具箱裡的可拆式模組,該用才用,不是整天狂開。

今天的結論

  • 子代理的價值,不是「更多 AI」,而是「更小的任務單位」
  • OpenClaw 的 agent 是完整 persona scope,包含 workspace、auth、session store
  • sessions_spawn 會建立一個獨立的背景子代理,而且是 non-blocking
  • context: "fork" 適合要承接前情,context: "isolated" 適合乾淨開局
  • multi-agent routing 讓多個 agent 在同一個 Gateway 內保持隔離
  • 主代理的工作,從「自己做全部」變成「拆分、派工、整合」

下一步

第 10 天我們看的是一個 Agent 為什麼會忘記自己在做什麼。

第 11 天則往前推一步,答案就更像職場了:

有些事不是你一個人做不了,而是你不該一個人做。

子代理出場之後,下一個問題自然會來:

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

因為只要有分工,就一定有交接、衝突和協調成本。這正是第 12 天要看的地方。


上一篇
第 10 天:Agent 為什麼會忘記自己在做什麼
下一篇
第 12 天:子代理怎麼合作,又怎麼吵架
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言