iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

開場故事

如果你只讓一個 AI Agent 會講話,它就像一個很會聊天的人。

但如果你想讓它真的做事,它就不能只會講話。
它得有手、有腳、有能拿工具的能力,知道什麼時候要讀檔、什麼時候要寫檔、什麼時候要跑指令、什麼時候要把結果送回去。

所以第 5 天我想看的不是「工具有幾個」而已,而是:

  • 工具是怎麼被組裝出來的?
  • 為什麼不是所有工具都直接塞給模型?
  • 為什麼同一個工具,到了不同情境會長得不一樣?
  • 工具的權限、包裝、格式,最後是怎麼一起決定行為的?

前面幾天我們已經把路鋪好了:

  • 第 1 天看整體架構
  • 第 2 天看 agent 的腦袋
  • 第 3 天看入口怎麼接任務
  • 第 4 天看任務怎麼分派

第 5 天就要看最實際的部分:任務進來之後,Agent 到底怎麼開始動手。

今天要解的問題

  • OpenClaw 的工具清單是怎麼組出來的?
  • createOpenClawCodingTools 到底在做什麼?
  • 為什麼工具不是一層設定就結束,而是要經過一串 policy?
  • execreadmessage 這類工具,為什麼要先被包裝再交給模型?
  • 為什麼同樣叫工具,到了不同 agent、不同 session、不同 provider,結果會不一樣?

架構總覽

我先用一句話講結論:

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,
    });

這段有幾個重點:

  • 它先看這是不是 memory flush 或 cron 之類的特殊 run
  • 它先確認 sandbox 和工具 policy
  • 它先把 conversation capability profile 算出來

也就是說,工具不是一開始就「全開」。
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 的工具不是只看一個設定,而是要疊很多層:

  • profile policy
  • provider policy
  • global policy
  • agent policy
  • group policy
  • sender policy
  • sandbox policy
  • owner-only policy
  • subagent policy
  • inherited policy

這就像一個人進公司,不是只看「你有沒有工牌」。
還要看:

  • 你是哪個部門
  • 你今天是不是外包
  • 你是不是主管
  • 你是不是只被允許看某些資料
  • 你是不是從別的 session 繼承了某些能力

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 不只是在挑工具,還在幫工具「戴上工作證」。

因為工具真的要進到模型手上之前,還需要再做幾件事:

  • schema 要先正規化,不然不同 provider 會不吃
  • hookContext 要把 agent、workspace、session、sandbox、trace 補齊
  • loop detection 要先準備好,避免工具互相呼叫到失控

這代表工具不是裸奔進模型,而是先被整裝,再進場。

最後看最終輸出。

📄 原始碼: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;
}

這裡的意思很直接:

  • 先加 hook,讓每個 tool call 都能被觀察
  • 再加 abort wrapper,讓跑到一半可以停
  • 最後再補 deferred follow-up 的描述

也就是說,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,告訴你為什麼看不到。

這點很重要。

因為很多系統失敗不是「功能壞掉」,而是「你以為它有,其實它根本沒被暴露」。

白話拆解

我把這整段想成一個人準備上工前的流程。

1. 不是先發工具箱,而是先看今天去哪裡上班

createOpenClawCodingTools 像是先問:

  • 你今天是一般 agent、subagent,還是 memory flush / cron run?
  • 你在哪個 workspace?
  • 你從哪個 channel 來?
  • 你能不能用 sandbox?
  • 你是不是能碰 message tool?

確認完之後,才開始配工具。

2. policy pipeline 像門口保全一路驗證

工具不是因為設定檔寫了就能過。

它要一層一層過:

  • profile 看你是什麼級別
  • provider 看模型支不支援
  • agent 看這個角色允不允許
  • group 看群組上下文能不能用
  • sender 看這個人是不是 owner
  • sandbox 看這個環境能不能碰
  • inherited 看上層有沒有繼承限制

這就像你有工牌不代表可以進所有房間。

3. normalize schema 像把工具接口統一格式

不同 provider 對 tool schema 的吃法不一樣。
有的喜歡簡單,有的喜歡嚴格,有的會對 union schema 有意見。

所以 OpenClaw 不是把原始工具直接丟出去,而是先做 normalize。

這很像把各式各樣的轉接頭統一成同一種接法,讓模型 runtime 比較不會卡住。

4. hooks 和 abort 像安全繩與監視器

工具開始跑之後,系統還要知道:

  • 誰叫的
  • 叫了什麼
  • 跑了多久
  • 中途能不能停
  • 結果怎麼記錄

所以工具外面再包一層 hook / abort wrapper。

這讓工具不是黑箱,而是可以被追蹤的工序。

5. inventory notices 像現場告示牌

如果工具被擋掉,OpenClaw 不會只讓你「找不到」。
它會告訴你:

  • 是 policy 擋的
  • 是 profile 擋的
  • 是 plugin allowlist 擋的

這讓除錯不會變成猜謎。

設計取捨

好處

  • 工具權限非常清楚,不會亂開
  • 不同模型、provider、session 可以有不同工具面
  • 可以在 schema、hook、abort、telemetry 上一起做控制
  • 出問題時比較知道是 policy、provider,還是 runtime 本身有事

代價

  • 工具準備流程變長
  • 看 code 會覺得比「直接丟一包 tools」複雜很多
  • 需要先理解 policy pipeline,才看得懂最後到底有哪些工具

但這個代價是值得的。

因為工具一旦放進 Agent 的手裡,就不是展示用的 UI 元件,而是真正會動到資料、指令、記憶、送達的執行面。

如果換另一種做法會怎樣

如果 OpenClaw 只是單純把工具名單直接暴露給模型,短期可能比較簡單。

但長期會出現這些問題:

  • 不同模型吃不了同一種 schema
  • 不同 session 的權限會混掉
  • subagent 可能碰到不該碰的工具
  • sandbox、群組、sender 的限制會失效
  • 工具結果難以追蹤,也難以除錯

所以它寧願前面麻煩一點,也不要後面整個系統失控。

今天的結論

  • OpenClaw 的工具不是直接暴露,而是經過建構、篩選、包裝、正規化才出場
  • createOpenClawCodingTools 負責把工具從候選清單變成可執行的工作介面
  • applyToolPolicyPipeline 把 profile、provider、agent、group、sender、sandbox、subagent、inheritance 疊成真正的 gate
  • normalizeToolParameters 和 hook / abort wrapper 讓工具可以被不同模型正確使用,也可以被觀察與中止
  • 第 5 天最重要的感覺是:工具不是裝飾品,它就是 OpenClaw 的手腳

下一步

如果說前面幾天是在看 OpenClaw 怎麼接任務、怎麼分派、怎麼把手伸出去,那第 5 天就是在看它怎麼真的開始做事。

第 6 天我會接著看一個很實際、也很容易出事的問題:

一次把工具叫太多,Agent 會怎麼壞掉?


上一篇
第 4 天:任務不是直接做,先學會怎麼分派
下一篇
第 6 天:一次把工具叫太多,Agent 會怎麼壞掉
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言