如果你只讓一個 AI Agent 會講話,它就像一個很會聊天的人。
但如果你想讓它真的做事,它就不能只會講話。
它得有手、有腳、有能拿工具的能力,知道什麼時候要讀檔、什麼時候要寫檔、什麼時候要跑指令、什麼時候要把結果送回去。
所以第 5 天我想看的不是「工具有幾個」而已,而是:
前面幾天我們已經把路鋪好了:
第 5 天就要看最實際的部分:任務進來之後,Agent 到底怎麼開始動手。
createOpenClawCodingTools 到底在做什麼?exec、read、message 這類工具,為什麼要先被包裝再交給模型?我先用一句話講結論:
OpenClaw 不是把工具「列出來」而已,而是把工具「建構、過濾、包裝、正規化」之後,才交給模型使用。
這件事很重要,因為工具不是裝飾品。
它不是那種「看起來有很多能力」的名單而已。
它其實是 Agent 真的會用來動手做事的介面。
如果把這整段流程畫成白話圖,大概是這樣:
config / session / model / sender / sandbox
-> 建出候選工具
-> 套上 policy
-> 套上權限與身分邊界
-> 正規化 schema
-> 加上 hook / abort / telemetry
-> 交給模型 runtime
所以你會發現,OpenClaw 看起來在處理「工具」,實際上是在處理一整套執行邊界。
先看工具是怎麼建立的。
createOpenClawCodingTools 不是單純回傳一包 tool list,它先進到內部建構流程,根據 sandbox、conversation profile、runtime allowlist、message provider 等條件,決定這次真正要暴露哪些工具。
📄 原始碼:
src/agents/agent-tools.ts:451-505
function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions): AnyAgentTool[] {
const execToolName = "exec";
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
const isMemoryFlushRun = options?.trigger === "memory";
if (isMemoryFlushRun && !options?.memoryFlushWritePath) {
throw new Error("memoryFlushWritePath required for memory-triggered tool runs");
}
const memoryFlushWritePath = isMemoryFlushRun ? options.memoryFlushWritePath : undefined;
const cronSelfRemoveOnlyJobId =
options?.trigger === "cron" && options.jobId?.trim() ? options.jobId.trim() : undefined;
const sandboxToolPolicy = sandbox?.tools;
const capabilityProfile =
options?.conversationCapabilityProfile ??
resolveConversationCapabilityProfile({
config: options?.config,
sessionKey: options?.sessionKey,
runSessionKey: options?.runSessionKey,
sessionId: options?.sessionId,
runId: options?.runId,
agentId: options?.agentId,
agentDir: options?.agentDir,
agentAccountId: options?.agentAccountId,
messageProvider: options?.messageProvider,
messageChannel: options?.messageChannel,
chatType: options?.chatType,
messageTo: options?.messageTo,
messageThreadId: options?.messageThreadId,
currentChannelId: options?.currentChannelId,
currentMessagingTarget: options?.currentMessagingTarget,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
groupId: options?.groupId,
groupChannel: options?.groupChannel,
groupSpace: options?.groupSpace,
memberRoleIds: options?.memberRoleIds,
spawnedBy: options?.spawnedBy,
senderId: options?.senderId,
senderName: options?.senderName,
senderUsername: options?.senderUsername,
senderE164: options?.senderE164,
senderIsOwner: options?.senderIsOwner,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
modelApi: options?.modelApi,
modelContextWindowTokens: options?.modelContextWindowTokens,
modelHasVision: options?.modelHasVision,
workspaceDir: options?.workspaceDir,
cwd: options?.cwd,
spawnWorkspaceDir: options?.spawnWorkspaceDir,
skillsSnapshot: options?.skillsSnapshot,
sandboxToolPolicy,
runtimeToolAllowlist: options?.runtimeToolAllowlist,
});
這段有幾個重點:
也就是說,工具不是一開始就「全開」。
OpenClaw 先知道這次是誰、在哪裡、從哪裡來、要做什麼,再決定工具能不能出現。
接著看真正的篩選流程。
📄 原始碼:
src/agents/agent-tools.ts:852-1099
const subagentFiltered = applyToolPolicyPipeline({
tools: toolsForModelProvider,
toolMeta: (tool) => getPluginToolMeta(tool),
warn: logWarn,
steps: [
...buildDefaultToolPolicyPipelineSteps({
profilePolicy: profilePolicyWithAlsoAllow,
profile,
profileUnavailableCoreWarningAllowlist: profilePolicy?.allow,
providerProfilePolicy: providerProfilePolicyWithAlsoAllow,
providerProfile,
providerProfileUnavailableCoreWarningAllowlist: providerProfilePolicy?.allow,
globalPolicy: globalPolicyWithToolSearchControls,
globalProviderPolicy: globalProviderPolicyWithToolSearchControls,
agentPolicy: agentPolicyWithToolSearchControls,
agentProviderPolicy: agentProviderPolicyWithToolSearchControls,
groupPolicy: groupPolicyWithToolSearchControls,
senderPolicy: senderPolicyWithToolSearchControls,
agentId,
unavailableCoreToolReason,
}),
{
policy: sandboxToolPolicyWithToolSearchControls,
label: "sandbox tools.allow",
unavailableCoreToolReason,
},
{
policy: ownerOnlyCoreToolPolicy,
label: "gateway sender owner-only tools",
unavailableCoreToolReason,
},
{
policy: subagentPolicyWithToolSearchControls,
label: "subagent tools.allow",
unavailableCoreToolReason,
},
{ policy: inheritedToolPolicy, label: "inherited tools", unavailableCoreToolReason },
],
auditLogLevel: options?.toolPolicyAuditLogLevel,
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: options?.config,
workspaceDir: workspaceRoot,
toolDenylist: pluginToolDenylist,
}),
});
這段是第 5 天最核心的地方。
它告訴你 OpenClaw 的工具不是只看一個設定,而是要疊很多層:
這就像一個人進公司,不是只看「你有沒有工牌」。
還要看:
OpenClaw 的工具政策,就是把這些現實世界的邊界都翻成了程式規則。
再來看工具是怎麼被正規化與包裝的。
📄 原始碼:
src/agents/agent-tools.ts:1049-1151
const normalized = authorizedTools.map((tool) =>
normalizeToolParameters(tool, {
modelProvider: options?.modelProvider,
modelId: options?.modelId,
modelCompat: options?.modelCompat,
}),
);
options?.recordToolPrepStage?.("schema-normalization");
const hookContext = {
agentId,
...(options?.config ? { config: options.config } : {}),
cwd: codingRoot,
workspaceDir: workspaceRoot,
...(options?.skillsSnapshot ? { skillsSnapshot: options.skillsSnapshot } : {}),
...(options?.skillUsagePaths ? { skillUsagePaths: options.skillUsagePaths } : {}),
...(sandboxRoot && allowWorkspaceWrites
? { sandbox: { root: sandboxRoot, bridge: sandboxFsBridge! } }
: {}),
sessionKey: options?.sessionKey,
sessionId: options?.sessionId,
runId: options?.runId,
approvalReviewerDeviceId: options?.approvalReviewerDeviceId,
channelId: options?.hookChannelId ?? options?.currentChannelId,
...(turnSourceChannel ? { turnSourceChannel } : {}),
...(turnSourceTo ? { turnSourceTo } : {}),
...(options?.agentAccountId ? { turnSourceAccountId: options.agentAccountId } : {}),
...(options?.currentThreadTs ? { turnSourceThreadId: options.currentThreadTs } : {}),
...(options?.trace ? { trace: options.trace } : {}),
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
onToolOutcome: options?.onToolOutcome,
allocateToolOutcomeOrdinal: options?.allocateToolOutcomeOrdinal,
};
這裡很有意思。
OpenClaw 不只是在挑工具,還在幫工具「戴上工作證」。
因為工具真的要進到模型手上之前,還需要再做幾件事:
這代表工具不是裸奔進模型,而是先被整裝,再進場。
最後看最終輸出。
📄 原始碼:
src/agents/agent-tools.ts:1154-1172
const withHooks = normalized.map((tool) =>
isToolWrappedWithBeforeToolCallHook(tool)
? rewrapToolWithBeforeToolCallHook(tool, hookContext, hookOptions)
: wrapToolWithBeforeToolCallHook(tool, hookContext, hookOptions),
);
options?.recordToolPrepStage?.("tool-hooks");
const withAbort = options?.abortSignal
? withHooks.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal))
: withHooks;
options?.recordToolPrepStage?.("abort-wrappers");
const withDeferredFollowupDescriptions = applyDeferredFollowupToolDescriptions(withAbort, {
agentId,
});
options?.recordToolPrepStage?.("deferred-followup-descriptions");
return withDeferredFollowupDescriptions;
}
這裡的意思很直接:
也就是說,OpenClaw 的工具不是只有「能不能用」的問題,還有:
這種設計很像真正的工作現場,不是玩具展示箱。
再看工具可見性怎麼被反映到 inventory。
📄 原始碼:
src/agents/tools-effective-inventory.ts:309-366
const effectivePolicy = resolveEffectiveToolPolicy({
config: params.cfg,
agentId,
sessionKey: params.sessionKey,
modelProvider: params.modelProvider,
modelId: params.modelId,
});
const profile = effectivePolicy.providerProfile ?? effectivePolicy.profile ?? "full";
const entries = projectedInventory.entries;
const notices = [
...projectedInventory.notices,
...(buildToolInventoryNotices({ cfg: params.cfg, profile, entries, effectivePolicy }) ?? []),
];
這一段在做的事情,是把工具的「現況」說清楚。
如果某個工具明明設定了,卻因為 policy、profile、provider 或 plugin 被擋掉,OpenClaw 不會假裝它還存在。
它會產生 notices,告訴你為什麼看不到。
這點很重要。
因為很多系統失敗不是「功能壞掉」,而是「你以為它有,其實它根本沒被暴露」。
我把這整段想成一個人準備上工前的流程。
createOpenClawCodingTools 像是先問:
確認完之後,才開始配工具。
工具不是因為設定檔寫了就能過。
它要一層一層過:
這就像你有工牌不代表可以進所有房間。
不同 provider 對 tool schema 的吃法不一樣。
有的喜歡簡單,有的喜歡嚴格,有的會對 union schema 有意見。
所以 OpenClaw 不是把原始工具直接丟出去,而是先做 normalize。
這很像把各式各樣的轉接頭統一成同一種接法,讓模型 runtime 比較不會卡住。
工具開始跑之後,系統還要知道:
所以工具外面再包一層 hook / abort wrapper。
這讓工具不是黑箱,而是可以被追蹤的工序。
如果工具被擋掉,OpenClaw 不會只讓你「找不到」。
它會告訴你:
這讓除錯不會變成猜謎。
但這個代價是值得的。
因為工具一旦放進 Agent 的手裡,就不是展示用的 UI 元件,而是真正會動到資料、指令、記憶、送達的執行面。
如果 OpenClaw 只是單純把工具名單直接暴露給模型,短期可能比較簡單。
但長期會出現這些問題:
所以它寧願前面麻煩一點,也不要後面整個系統失控。
createOpenClawCodingTools 負責把工具從候選清單變成可執行的工作介面applyToolPolicyPipeline 把 profile、provider、agent、group、sender、sandbox、subagent、inheritance 疊成真正的 gatenormalizeToolParameters 和 hook / abort wrapper 讓工具可以被不同模型正確使用,也可以被觀察與中止如果說前面幾天是在看 OpenClaw 怎麼接任務、怎麼分派、怎麼把手伸出去,那第 5 天就是在看它怎麼真的開始做事。
第 6 天我會接著看一個很實際、也很容易出事的問題:
一次把工具叫太多,Agent 會怎麼壞掉?