你有沒有遇過這種工作狀況。
本來以為只要交出去一件事,結果它被拆成三段:
拆的時候很快,收的時候很煩。
最常見的麻煩不是「做不出來」,而是:
第 13 天我想看的就是這件事:
一個任務拆成多段,OpenClaw 怎麼收尾
前兩天我們看了子代理怎麼被派出去、怎麼合作、也怎麼避免互相打架。
這一天要把鏡頭拉回來,看看任務拆出去之後,最後到底怎麼合回來。
sessions_yield 為什麼不是可有可無,而是收尾流程的一部分?OpenClaw 的收尾,不是單純「等子代理做完」。
它其實是三件事一起發生:
這個流程看起來像是「做完再通知」,但核心其實是「完成事件先回到系統,主代理再消化成最終答案」。
所以第 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_list 或 sessions_history 偷偷等結果這其實很像現實裡的專案節奏。
你不會叫同事去做一個任務,然後每 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
};
這段說明了完成事件不是「隨便丟一句話」。
它會帶上:
這樣做的目的,是讓完成訊息有明確 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。
它要的是事件驅動:該來就來,沒來就先做別的事。
很多人以為「收尾」就是等最後一個子代理完成。
但 OpenClaw 的思路不是這樣。
它先在派工時就把收尾設計好:
也就是說,收尾不是事後補救,而是派工時的一部分。
這很像做專案時先決定:
沒有這些設計,任務即使做完,也可能收不回來。
sessions_yield 的價值,是把等待變成正式流程如果一個主代理叫了好幾個子代理,它其實不該繼續硬做下一步。
這時候 sessions_yield 就很像「我先停在這裡,等完成事件回來」。
它的好處有三個:
簡單講,sessions_yield 不是偷懶,是把等待這件事標準化。
OpenClaw 很明確地不鼓勵:
sessions_list
sessions_history
因為這些做法都在假裝自己有在控制,其實只是 CPU 和注意力被浪費掉。
push-based completion 比較像真的團隊協作:
這種節奏更穩,也更符合長任務的現實。
子代理可能做完了,但還不代表答案真正送到了該去的地方。
最後一公里可能卡在:
OpenClaw 用 announce、wake、handoff、fallback 這些層次去收斂,就是在處理這最後一公里。
你可以把它想成外送:
這就是為什麼收尾比派工更重要。
完成事件不是純文字而已,它還會帶 provenance、idempotency key、來源 session、來源 tool。
這代表後面如果要查:
都能回頭查。
這對長流程特別重要,因為真正麻煩的 bug 常常不是「沒做」,而是「做了但不知道去哪了」。
如果換成最簡單的做法,就是子代理做完直接 print 結果。
但那樣只適合很短的任務。
只要任務一拆多段,問題就不再是「會不會做」,而是:
OpenClaw 的答案很明顯:它寧可把收尾做得複雜一點,也不要把結果弄丟。
sessions_yield 讓等待變成正式流程,而不是模型硬撐著等subagents 不是拿來輪詢進度的,而是拿來檢視控制樹內的子代理狀態第 14 天我想接著看另一個更大的問題:
流程不是直線,OpenClaw 的工作流思維是怎麼形成的?
因為一旦收尾做穩了,下一個問題就是:這些拆分與回流,怎麼組成一條真正可運作的工作流。