iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

開場故事

一個系統最容易被誤解的地方,通常不是它怎麼做事,而是它怎麼「開始做事」。

你想像一個新人第一天進公司。

他不是一進門就跑去改程式、寄信、開會,而是先去櫃檯報到,確認今天有人找他做什麼,然後才知道要把事情送去哪個部門。AI Agent 也一樣。

如果你直接把需求丟給它,它不應該只是硬生生回你一句答案。真正有系統的做法,應該是先接住需求,再判斷這件事要不要拆、要不要查記憶、要不要進 session、要不要走固定流程。

所以第 3 天,我想先看最前面的那一關。

不是工具,不是記憶,不是子代理。

而是它怎麼把第一個任務接進來。

今天要解的問題

  • 任務從哪裡進到 OpenClaw?
  • agentCommandFromIngress 在做什麼?
  • 為什麼進來之前要先驗證 allowModelOverride
  • agentCommandInternal 怎麼把訊息變成 session run?
  • 為什麼「先判斷,再行動」這件事很重要?

先講結論

如果把 OpenClaw 的任務流畫成一條線,入口這一段大概長這樣:

外部入口 / CLI / Gateway
  -> agentCommandFromIngress
  -> agentCommandInternal
  -> prepareAgentCommandExecution
  -> session admission
  -> skills / model / delivery

這裡最重要的不是「它會不會做」,而是「它先確認再做」。

因為一個成熟的 Agent 系統,不會把所有事情都塞給同一個角色硬幹。它會先想清楚這件事屬於哪一類,然後再把工作往下送。

原始碼節錄

先看 ingress 的門口。agentCommandFromIngress 不會直接跳進執行,它會先檢查必要條件,再把控制權交給 internal runner。

📄 原始碼:src/agents/agent-command.ts:3004-3004

async function agentCommandFromIngress(opts, runtime = defaultRuntime, deps) {
	if (typeof opts.allowModelOverride !== "boolean") throw new Error("allowModelOverride must be explicitly set for ingress agent runs.");
	const lifecycleGeneration = opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(opts.runId ?? "");
	return await withAgentRunLifecycleGeneration(lifecycleGeneration, async () => {
		const result = await agentCommandInternal({
			...opts,
			lifecycleGeneration,
			senderIsOwner: opts.senderIsOwner === true
		}, runtime, deps);
		if (result) emitIngressModelUsageDiagnostic(result, opts);
		return result;
	});
}

這段其實很誠實。

它先做了一件很重要的事:確認 allowModelOverride 有沒有明確傳進來。

這表示 ingress 不是「隨便什麼人進來都可以改模型」,而是有明確 gate 的。

再往下看,真正的入口邏輯都落在 agentCommandInternal

📄 原始碼:src/agents/agent-command.ts:955-961

async function agentCommandInternal(initialOpts, runtime = defaultRuntime, deps) {
	const resolvedDeps = await resolveAgentCommandDeps(deps);
	const isRawModelRun = initialOpts.modelRun === true || initialOpts.promptMode === "none";
	const suppressVisibleSessionEffects = initialOpts.sessionEffects === "internal";
	const preserveUserFacingSessionModelState = initialOpts.preserveUserFacingSessionModelState === true;
	const prepared = await prepareAgentCommandExecution(initialOpts, runtime);
	const lifecycleAbortController = new AbortController();
	const opts = {
		...prepared.opts,
		abortSignal: prepared.opts.abortSignal ? AbortSignal.any([prepared.opts.abortSignal, lifecycleAbortController.signal]) : lifecycleAbortController.signal
	};
	const { body, transcriptBody, cfg, configuredThinkingCatalog, normalizedSpawned, agentCfg, thinkOverride, thinkOnce, verboseOverride, timeoutMs, runTimeoutOverrideMs, sessionId, sessionKey, sessionStore, storePath, isNewSession, persistedThinking, persistedVerbose, sessionAgentId, outboundSession, workspaceDir, cwd, agentDir, runId, isSubagentLane, acpManager, acpResolution, pluginsEnabled, manifestMetadataSnapshot, modelManifestContext } = prepared;
	...
}

這段一開始就把所有關鍵上下文拿出來:

  • body / transcriptBody
  • cfg
  • sessionId / sessionKey
  • sessionStore / storePath
  • workspaceDir / agentDir
  • agentCfg
  • runId

也就是說,OpenClaw 接到第一個任務時,不是先想答案,而是先把任務環境組完。

再往下看,session admission 會先確認工作能不能開始。

📄 原始碼:src/agents/agent-command.ts:1042-1074

const sessionWorkAdmission = await beginSessionWorkAdmission({
	scope: storePath ?? `agent:${sessionAgentId}`,
	identities: [sessionKey, sessionId],
	signal: opts.abortSignal,
	onInterrupt: () => lifecycleAbortController.abort(createAgentRunRestartAbortError()),
	assertAllowed: () => {
		const currentEntry = sessionStoreRuntime && storePath && sessionKey ? sessionStoreRuntime.loadSessionEntry({
			storePath,
			sessionKey,
			readConsistency: "latest"
		}) : sessionEntry;
		if (!currentEntry && preparedSessionId) throw new Error(`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`);
		const matchesIntentionalRollover = isNewSession && currentEntry?.sessionId === preparedSessionId;
		if (currentEntry && currentEntry.sessionId !== sessionId && !matchesIntentionalRollover) throw new Error(`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`);
		const archivedSessionError = resolveSessionWorkStartError(sessionKey ?? sessionId, currentEntry);
		if (archivedSessionError) throw new Error(archivedSessionError);
		sessionEntry = currentEntry;
		if (sessionStore && sessionKey) if (currentEntry) sessionStore[sessionKey] = currentEntry;
		else delete sessionStore[sessionKey];
	}
});

這一段就是入口層的真功夫。

它不是在說「任務來了」,而是在說:

  • 這個 session 還在嗎?
  • 這個 session 還是我現在要接的那個嗎?
  • 這個 session 有沒有被封存?
  • 這個 session 有沒有在切換過程中變掉?

如果答案不對,OpenClaw 直接要求重試。

這就是為什麼入口不能亂做,因為你接錯的不是一個字串,而是一整個工作狀態。

白話拆解

我把這個過程想成一間公司的櫃檯加門禁。

1. agentCommandFromIngress 像前台

前台不負責做完工作,但它負責確認:

  • 這個單有沒有必要先驗證
  • 這次是不是允許 override
  • 這筆請求要交給哪個內部流程

這層的重點是先守規矩,再把案子往下送。

2. agentCommandInternal 像作業中心

進到內部後,就不是「收到一句話」而已了。

它會把整個 run 的背景補齊:

  • 哪個 session
  • 哪個 workspace
  • 哪個 agent
  • 哪些 config
  • 哪些 skills
  • 哪些 model 視圖

這樣後面的模型、工具、記錄,才知道自己到底在處理哪一個工作。

3. session admission 像門禁驗票

這段最有感。

系統不是把 session key 有看到就算,而是會看它現在是不是有效、是不是變動中的那一個、是不是已經封存。

如果你把這想成機場登機口,就很好懂:

  • 不是有票就能上
  • 票還要對班次
  • 時間過了也可能不能上

OpenClaw 的 session admission 就是這種感覺。

4. 為什麼先判斷再行動

因為 AI 系統最可怕的事情,不是慢,而是「跑錯上下文」。

如果一開始就直接做,後面你會很難知道:

  • 是入口收錯任務
  • 還是 session 不對
  • 還是技能 snapshot 過期
  • 還是模型權限不符

先判斷再行動,就是在幫後面的除錯留路。

設計取捨

好處

  • 入口有門禁,流程比較穩
  • session 變動能被提早擋下來
  • allowModelOverride 這種高風險能力有明確檢查
  • 後面的 delivery、skills、model selection 才有穩定基礎

代價

  • 進場前要先過幾關
  • 看 code 會覺得比較厚
  • 不能用「一個 function 解決全部」的直覺看它

但這正是我喜歡 OpenClaw 的地方。

它不假裝自己很簡單,而是老老實實把每一層責任都攤開。

今天的結論

  • 任務不是一進來就直接做,而是先進 ingress
  • agentCommandFromIngress 會先檢查必要權限與 lifecycle
  • agentCommandInternal 先把執行環境、session、workspace 都準備好
  • beginSessionWorkAdmission 負責確認這個 session 現在真的能開始工作
  • 第 3 天最值得記住的,是 OpenClaw 的入口不是接一句話,而是接一個有狀態的工作

下一步

如果說第 2 天是在看腦袋長什麼樣,那第 3 天就是在看它怎麼把第一個任務真的接住。

接下來第 4 天,我會繼續往下看它怎麼分派工作,而不是只把任務停在入口。


上一篇
第 2 天:OpenClaw 的腦袋長什麼樣,先看整體架構
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言