iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

開場故事

你有沒有遇過這種工作狀況。

本來以為只要交出去一件事,結果它被拆成三段:

  • 第一段先查資料
  • 第二段整理成可讀的答案
  • 第三段再補驗證、補細節、補格式

拆的時候很快,收的時候很煩。

最常見的麻煩不是「做不出來」,而是:

  • 第一段做完了,第二段不知道接哪裡
  • 第二段做完了,第三段又重新查一次
  • 明明已經有結果,最後卻沒人把它合成一份完整回覆
  • 任務都做了一半,主代理卻已經開始回答別的事情

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

一個任務拆成多段,OpenClaw 怎麼收尾

前兩天我們看了子代理怎麼被派出去、怎麼合作、也怎麼避免互相打架。
這一天要把鏡頭拉回來,看看任務拆出去之後,最後到底怎麼合回來。

今天要解的問題

  • 任務被拆成多段之後,誰負責收?
  • sessions_yield 為什麼不是可有可無,而是收尾流程的一部分?
  • 子代理完成後,結果怎麼回到 requester?
  • 為什麼 OpenClaw 不鼓勵輪詢,而是鼓勵 push-based completion?
  • 什麼時候會走 announce,什麼時候會直接 fallback?
  • 為什麼收尾這件事,本質上比派工還重要?

架構總覽

OpenClaw 的收尾,不是單純「等子代理做完」。

它其實是三件事一起發生:

  1. 子代理在自己的 session 內把工作做完
  2. 子代理完成時把結果 announce 回 requester
  3. 主代理把這些完成事件收進來,再決定最後要怎麼回給人類

這個流程看起來像是「做完再通知」,但核心其實是「完成事件先回到系統,主代理再消化成最終答案」。

所以第 13 天的重點不是任務拆分本身,而是:

  • 拆出去之後,系統怎麼知道它真的結束了
  • 結束之後,資訊怎麼回流
  • 回流之後,誰負責把答案說完整

如果只會派工,卻不會收尾,系統就會變成一個很忙但沒有結果的工廠。

原始碼節錄

先看子代理被建立時,系統 prompt 直接告訴它:結果要 auto-announce,而且不要 busy-poll。

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

function buildSubagentInitialUserMessage(params) {
	const lines = [`[Subagent Context] You are running as a subagent (depth ${params.childDepth}/${params.maxSpawnDepth}). Results auto-announce to your requester; do not busy-poll for status.`];
	if (params.persistentSession) lines.push("[Subagent Context] This subagent session is persistent and remains available for thread follow-up messages.");
	const taskBody = params.task?.trim();
	if (taskBody) lines.push("[Subagent Task]", taskBody, "Begin. Execute the assigned task to completion.");
	else lines.push("Begin. Execute the assigned task to completion.");
	return lines.join("\n\n");
}

這段是收尾思維的起點。

子代理不是被叫出去後自己猜流程,而是一開始就被明講:

  • 你的結果會自動回報
  • 不要自己一直查狀態
  • 你要把任務做完,不是做到一半就丟回來

再看更直接的提示。

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

const SUBAGENT_SPAWN_ACCEPTED_NOTE = "Auto-announce is push-based. After spawning children, do NOT call sessions_list, sessions_history, exec sleep, or any polling tool. Track expected child session keys. Continue any independent work. If your final answer depends on child output, wait for runtime completion events to arrive as user messages and only answer after completion events for ALL required children arrive. If a child completion event arrives AFTER your final answer, reply ONLY with NO_REPLY.";

這一段幾乎就是收尾規則的總結。

它在說:

  • 不要輪詢
  • 不要用 sessions_listsessions_history 偷偷等結果
  • 要記住預期中的 child session key
  • 如果答案依賴子代理輸出,就等完成事件
  • 如果完成事件太晚才來,主代理就不要硬補發

這其實很像現實裡的專案節奏。

你不會叫同事去做一個任務,然後每 10 秒問一次「做完沒」。
你會希望他做完就回來告訴你,而你把注意力放在下一件還能獨立做的事。

接著看 sessions_yield

📄 原始碼:src/agents/tools/sessions-yield-tool.ts:24-34

function createSessionsYieldTool(opts) {
	return {
		name: "sessions_yield",
		description: "End current turn. Use after spawning subagents; results arrive as next message.",
		execute: async (_toolCallId, args) => {
			const message = readStringParam(args, "message") || "Turn yielded.";
			if (!opts?.sessionId) return jsonResult({
				status: "error",
				error: "No session context"
			});
			if (!opts?.onYield) return jsonResult({
				status: "error",
				error: "Yield not supported in this context"
			});
			await opts.onYield(message);
			return jsonResult({
				status: "yielded",
				message
			});
		}
	};
}

這段很短,但很關鍵。

它的意思不是「休息一下」,而是:

  • 先結束這一輪
  • 把後續完成事件留給下一個訊息
  • 讓系統把等待狀態變成正規流程,而不是模型自己硬撐著等

再看完成事件到底怎麼送回來。

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

if (params.expectsCompletionMessage && requesterActivity.sessionId && requesterActivity.isActive) {
	const wakeOptions = {
		deliveryTimeoutMs: announceTimeoutMs,
		steeringMode: "all",
		...completionSourceReplyDeliveryMode ? { sourceReplyDeliveryMode: completionSourceReplyDeliveryMode } : {},
		...requesterQueueSettings.debounceMs !== void 0 ? { debounceMs: requesterQueueSettings.debounceMs } : {},
		waitForTranscriptCommit: true
	};
	const wakeOutcome = await resolveActiveWakeWithRetries(requesterActivity.sessionId, params.triggerMessage, wakeOptions, params.signal);
	if (wakeOutcome.queued) return {
		delivered: true,
		deliveredAt: wakeOutcome.deliveredAtMs,
		enqueuedAt: wakeOutcome.enqueuedAtMs,
		path: "steered"
	};
	activeRequesterWakeFailed = true;
}

這裡的邏輯很像「先嘗試把醒著的人叫回來」。

如果 requester session 還活著,OpenClaw 會盡量直接把完成訊息導回去。
如果叫得動,就走 steering。
如果叫不動,就進下一層 fallback。

這種設計的重點不是炫技,而是降低丟訊息的機率。

再看 fallback 到哪裡。

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

const directAgentParams = {
	sessionKey: canonicalRequesterSessionKey,
	message: params.triggerMessage,
	deliver: shouldDeliverAgentFinal,
	bestEffortDeliver: params.bestEffortDeliver,
	internalEvents: params.internalEvents,
	channel: shouldDeliverAgentFinal ? deliveryTarget.channel : sessionOnlyOriginChannel,
	accountId: shouldDeliverAgentFinal ? deliveryTarget.accountId : sessionOnlyOriginChannel ? sessionOnlyOrigin?.accountId : void 0,
	to: shouldDeliverAgentFinal ? deliveryTarget.to : sessionOnlyOriginChannel ? sessionOnlyOrigin?.to : void 0,
	threadId: directAgentThreadId,
	inputProvenance: {
		kind: "inter_session",
		sourceSessionKey: params.sourceSessionKey,
		sourceChannel: params.sourceChannel ?? "webchat",
		sourceTool: params.sourceTool ?? "subagent_announce"
	},
	...completionSourceReplyDeliveryMode ? { sourceReplyDeliveryMode: completionSourceReplyDeliveryMode } : {},
	idempotencyKey: params.directIdempotencyKey
};

這段說明了完成事件不是「隨便丟一句話」。

它會帶上:

  • 來源 session
  • 來源 channel
  • 來源 tool
  • idempotency key
  • 要不要真的直接 deliver

這樣做的目的,是讓完成訊息有明確 provenance。
也就是說,主代理收到的不是漂浮的內容,而是一筆有來源、有路徑、有身份的完成事件。

最後看 announce 本身怎麼做降級處理。

📄 原始碼:src/agents/subagent-announce-delivery.ts:1593-1593

if (params.expectsCompletionMessage && isCronRunSessionKey(canonicalRequesterSessionKey) && !resolveRequesterSessionActivity(canonicalRequesterSessionKey).isActive && !agentMediatedCompletion) {
	const generatedMediaDelivery = await tryGeneratedMediaDirectDelivery();
	if (generatedMediaDelivery) return generatedMediaDelivery;
	if (!agentMediatedCompletion) return {
		delivered: true,
		path: "none"
	};
}

這裡很像現場救火。

如果 requester 已經不在現場,OpenClaw 不會傻等它醒來。
它會先看能不能直接投遞,不能的話就保底處理,避免整個完成流程卡死。

再看 subagents 工具的語氣。

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

description: "List active and recent subagents for the requester session. If sessions_yield exists, use it for completion; do not poll wait loops."

這句話其實就是在強調:

你要管理子代理,但不要把管理變成等待輪詢。

這是 OpenClaw 收尾設計很核心的一點。

它不要你自己用 while-loop 盯著結果,也不要你一直刷 session history。
它要的是事件驅動:該來就來,沒來就先做別的事。

白話拆解

1. 收尾不是等結果,而是設計結果怎麼回來

很多人以為「收尾」就是等最後一個子代理完成。

但 OpenClaw 的思路不是這樣。

它先在派工時就把收尾設計好:

  • 哪個 child 做完要 announce
  • 哪個 requester 要接 completion event
  • 哪些完成訊息可以直接送,哪些需要經過 handoff
  • 哪些情況要 fallback

也就是說,收尾不是事後補救,而是派工時的一部分。

這很像做專案時先決定:

  • 交付格式是什麼
  • 驗收人是誰
  • 失聯時要通知誰
  • 文件不到位時要怎麼回補

沒有這些設計,任務即使做完,也可能收不回來。

2. sessions_yield 的價值,是把等待變成正式流程

如果一個主代理叫了好幾個子代理,它其實不該繼續硬做下一步。
這時候 sessions_yield 就很像「我先停在這裡,等完成事件回來」。

它的好處有三個:

  • 避免主代理在同一輪裡一直空轉
  • 讓 completion event 成為下一個明確訊號
  • 讓子代理結果有機會自然回到模型可見的流程裡

簡單講,sessions_yield 不是偷懶,是把等待這件事標準化。

3. push-based completion 比輪詢更像真正的協作

OpenClaw 很明確地不鼓勵:

  • 一直查 sessions_list
  • 一直查 sessions_history
  • 一直 sleep 等子代理

因為這些做法都在假裝自己有在控制,其實只是 CPU 和注意力被浪費掉。

push-based completion 比較像真的團隊協作:

  • 你派工作
  • 你繼續做別件事
  • 對方做完再回來
  • 你收到事件後再整合

這種節奏更穩,也更符合長任務的現實。

4. 收尾最難的地方,是最後一公里

子代理可能做完了,但還不代表答案真正送到了該去的地方。

最後一公里可能卡在:

  • requester 已經結束
  • 通知路徑不在
  • thread 綁定不存在
  • 需要 fallback 到 requester-agent handoff
  • 完成事件太晚,主答案已經先送出去了

OpenClaw 用 announce、wake、handoff、fallback 這些層次去收斂,就是在處理這最後一公里。

你可以把它想成外送:

  • 廚房做完不算完成
  • 送到門口才算完成
  • 找不到收件人時還要想辦法聯絡
  • 真聯絡不到,也要有保底處理

這就是為什麼收尾比派工更重要。

5. 好的收尾會留下可追蹤的痕跡

完成事件不是純文字而已,它還會帶 provenance、idempotency key、來源 session、來源 tool。

這代表後面如果要查:

  • 是哪個子代理回的
  • 是從哪裡來的
  • 是否重送過
  • 有沒有被成功 deliver

都能回頭查。

這對長流程特別重要,因為真正麻煩的 bug 常常不是「沒做」,而是「做了但不知道去哪了」。

設計取捨

  • 好處是完成事件是推送式的,子代理做完就能自然回流
  • 好處是主代理不用被迫輪詢,節奏更像真正的協作
  • 好處是 announce / handoff / fallback 分層清楚,較不容易漏訊息
  • 好處是 completion 帶 provenance,追查問題比較有依據
  • 代價是系統複雜度提高,光看表面很難理解整條收尾鏈
  • 代價是如果 requester session 已經變動,完成事件路徑會更繞
  • 代價是事件驅動做得越完整,除錯就越需要理解 session、route、run 的互動

如果換成最簡單的做法,就是子代理做完直接 print 結果。
但那樣只適合很短的任務。

只要任務一拆多段,問題就不再是「會不會做」,而是:

  • 做完誰收
  • 收到哪裡
  • 能不能證明是這個 child 做的
  • 出問題時能不能回頭查

OpenClaw 的答案很明顯:它寧可把收尾做得複雜一點,也不要把結果弄丟。

今天的結論

  • 任務拆成多段後,收尾本身就是系統設計的一部分
  • sessions_yield 讓等待變成正式流程,而不是模型硬撐著等
  • 子代理完成後會走 push-based completion,盡量把結果回送 requester
  • announce / wake / handoff / fallback 是一條完整的收尾鏈
  • subagents 不是拿來輪詢進度的,而是拿來檢視控制樹內的子代理狀態
  • OpenClaw 真正在乎的不是「有沒有做完」,而是「做完之後有沒有穩穩回來」

下一步

第 14 天我想接著看另一個更大的問題:

流程不是直線,OpenClaw 的工作流思維是怎麼形成的?

因為一旦收尾做穩了,下一個問題就是:這些拆分與回流,怎麼組成一條真正可運作的工作流。


上一篇
第 12 天:子代理怎麼合作,又怎麼吵架
下一篇
第 14 天:流程不是直線,OpenClaw 的工作流思維
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言