好的,这是您要求的英文内容到繁體中文的翻譯。
深入探討 Claude Code 如何發現、分派與執行工具
簡介
在前一篇文章中,我們從 Anthropic Harness 外洩的原始碼中逆向工程了其記憶系統。結果發現記憶出奇地簡單——就是 Markdown 檔案、frontmatter 以及提示詞工程。而工具系統則恰恰相反。它是整個程式碼庫中工程化程度最高的子系統:超過 43 個工具、一個串流執行管道、一個分層的權限系統、一個鉤子(hook)框架,以及一個並發排程器——所有這些組件被整合在一起,將一個無狀態的語言模型轉變為能夠讀取檔案、執行 Shell 命令、搜尋網路以及產生子 Agent 的實體。
這篇文章將帶領你了解一個工具從定義、模型工具呼叫的分派,到結果如何回流至對話中的完整生命週期。廣義來說,整個系統由四個層級組成:一個所有工具都實作的 工具介面(Tool Interface)、一個組裝工具池的 註冊中心(Registry)、一個負責驗證、權限檢查與執行每個呼叫的 分派管線(Dispatch Pipeline),以及一個決定哪些任務可並行執行的 並發排程器(Concurrency Scheduler)。
整體架構概覽

工具介面
Claude Code 中的每個工具都實作相同的介面,定義在 Tool.ts 中。該型別對於三個參數是泛型的:Input(一個 Zod 模式)、Output(結果型別)、以及 P(進度資料)。實際上,一個工具是一個擁有約 30 個方法的物件,但對於理解系統而言,只有少數幾個是重要的。
核心結構如下:
1type Tool<Input, Output, P> = {2 name: string3 inputSchema: ZodType // 用於輸入驗證的 Zod 模式4 call(input, context, canUseTool,5 parentMessage, onProgress): Promise<ToolResult>67 // 行為宣告8 isConcurrencySafe(input): boolean // 能否並行執行?9 isReadOnly(input): boolean // 是否為唯讀操作?10 isDestructive(input): boolean // 是否為破壞性操作?1112 // 權限與驗證13 checkPermissions(input, context): Promise<PermissionResult>14 validateInput(input, context): Promise<ValidationResult>1516 // API 整合17 description(input, options): Promise<string>18 prompt(options): Promise<string> // 此工具的系統提示文字19 mapToolResultToToolResultBlockParam(result, toolUseId): ToolResultBlockParam2021 // UI 渲染 (React)22 renderToolUseMessage(input, options): ReactNode23 renderToolResultMessage(content, ...): ReactNode24}
沒有一個工具是從頭開始實作這個介面的。一個名為 buildTool() 的工廠函數會填入安全預設值:

這些預設值刻意設計得很保守。忘記宣告並行安全性的工具作者,會得到序列執行。忘記實作權限檢查的工具作者,會得到預設的權限流程。系統預設是「失敗關閉」(fail closed)。
值得注意的 ToolResult 型別:
1type ToolResult<T> = {2 data: T // 實際輸出3 newMessages?: Message[] // 可選的後續訊息4 contextModifier?: (ctx) => ToolUseContext // 為下一個工具變更上下文5 mcpMeta?: { ... } // MCP 協議元資料6}
contextModifier 很重要——它允許一個工具為同一輪中後續的工具變更執行上下文。這就是像 EnterWorktree 這類工具變更後續所有操作的工作目錄的方式。關鍵在於,上下文修飾器僅允許用於非並行安全的工具。如果一個工具是並行執行的,它就無法修改共享狀態。
工具註冊中心
所有工具都在一個名為 getAllBaseTools() 的函數中註冊,位於 tools.ts。它回傳一個扁平陣列。有些工具始終存在;其他的則受到功能開關、環境變數或平台檢查的限制。
始終可用的工具(16 個)

受功能開關限制的工具(約 27 個)
其餘的工具則是有條件地包含。有些受到環境變數限制(USER_TYPE=ant 用於 Anthropic 內部工具,如 config 和 tungsten)。有些受到透過 Statsig 設置的功能開關限制(web_browser、sleep、monitor)。有些則是平台專用的(Windows 上的 powershell)。有些受到複合條件限制——repl 工具同時需要 USER_TYPE=ant 和一個 REPL 功能開關。
受功能開關限制的工具完整列表
僅限 Ant:config、tungsten、suggest_background_pr、repl(還需要 REPL 開關)
功能開關:web_browser、web_search、sleep、monitor、overflow_test、ctx_inspect、terminal_capture、list_peers、workflow、snip
Agent 觸發器:cron_create、cron_delete、cron_list、remote_trigger
Kairos(主動式 Agent):sleep、send_user_file、push_notification、subscribe_pr
多 Agent 群組:team_create、team_delete、send_message
Todo v2:task_create、task_get、task_update、task_list
環境:lsp(ENABLE_LSP_TOOL)、enter_worktree / exit_worktree(worktree 模式)、powershell(Windows)
工具探索:tool_search(當工具池很大時)
僅測試用:testing_permission(NODE_ENV=test)
MCP 工具
除了內建工具之外,Claude Code 還支援 模型上下文協議(Model Context Protocol,MCP) 伺服器——這些外部程序透過標準化協議暴露自己的工具。MCP 工具在運行時從已連接的伺服器動態註冊,並封裝在相同的 Tool 介面中。從分派管線的角度來看,MCP 工具與內建工具是無法區分的。
每個 MCP 工具都帶有其來源伺服器的元資料(mcpInfo: { serverName, toolName }),用於權限規則、錯誤處理和身份驗證。當 MCP 工具因身份驗證錯誤而失敗時,系統會自動將伺服器狀態更新為 needs-auth,並將問題顯示給使用者。
工具池組裝
三個函數組裝最終的工具集合:
- getAllBaseTools() — 回傳應用功能開關後的 43 個以上內建工具的原始列表
- getTools(permissionContext) — 根據拒絕規則和 isEnabled() 進行過濾
- assembleToolPool(permissionContext, mcpTools) — 合併內建工具與 MCP 工具
assembleToolPool() 中的合併策略是經過深思熟慮的:
內建工具優先,因此在名稱衝突時,內建工具會勝出。每個分割區內按字母順序排序,可確保順序在會話之間保持穩定,這對於提示詞快取非常重要——工具陣列是 API 請求的一部分,重新排序會使快取失效。
assembleToolPool() 中的合併策略是經過深思熟慮的:
1// 對每個分割區按字母順序排序,連接,去重2const byName = (a, b) => a.name.localeCompare(b.name)3return uniqBy(4 [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),5 'name',6)
API 序列化
在工具到達 Claude API 之前,toolToAPISchema() 會將每個工具的 Zod 模式轉換為 Anthropic API 的 JSON Schema 格式。
分派管線
當 Claude 回應時,其訊息可能包含 tool_use 區塊——呼叫工具的結構化請求。分派管線會透過七個階段處理這些區塊。每個工具呼叫都會依序經歷所有階段。
階段 1:提取
在主查詢循環(query.ts)中,tool_use 區塊會從助手訊息中過濾出來:
1const msgToolUseBlocks = message.message.content.filter(2 content => content.type === 'tool_use',3) as ToolUseBlock[]
每個區塊都有一個 name、一個 input 物件和一個唯一的 id。這個 id 至關重要——工具結果在送回 API 時必須引用相同的 id,否則對話會中斷。
階段 2:輸入驗證
工具的 Zod 模式使用 safeParse() 驗證原始輸入——這是一個非拋出異常的變體,會回傳有效資料或結構化錯誤。如果驗證失敗,模型會收到一條包含模式提示的格式化錯誤訊息,並且該工具的執行會停止。無效輸入不會執行任何程式碼。
1const parsedInput = tool.inputSchema.safeParse(input)2if (!parsedInput.success) {3 let errorContent = formatZodValidationError(tool.name, parsedInput.error)4 // 向模型回傳錯誤,跳過執行5}
Zod 驗證之後,某些工具會執行第二個 validateInput() 檢查,用於模式無法表達的語義驗證——例如,確認檔案路徑是絕對路徑而非相對路徑。
階段 3:工具前鉤子
在權限檢查之前,會執行使用者設定的鉤子。這些是在工具呼叫時觸發的外部 Shell 命令或腳本。一個工具前鉤子可以:
- 允許 工具呼叫,跳過互動式權限提示
- 拒絕 工具呼叫
- 修改輸入 再執行
- 阻止執行 並回傳錯誤訊息
- 向使用者提供額外上下文
一個關鍵的不變量:鉤子的 allow 並 不會 繞過來自設定的拒絕規則。原始碼中有一個明確的註解提到:「鉤子的 'allow' 並不會繞過 settings.json 的 deny/ask 規則。」其意圖是鉤子可以打開門,但不能覆蓋鎖。
階段 4:權限檢查
權限系統是管線中最複雜的部分。它按順序透過多個層級進行解析:
- 拒絕規則 — 首先檢查。如果任何拒絕規則匹配,執行會立即停止。拒絕規則是最終決定,不能被任何其他層級覆蓋。
- 詢問規則 — 如果匹配,則會提示使用者批准(除非 Bash 適用沙箱自動允許)。
- 工具特定權限 — 工具自身的 checkPermissions() 方法會執行。例如,BashTool 會解析命令以檢查子命令層級的規則。
- 安全性檢查 — 針對敏感路徑(.git/、.claude/、Shell 設定檔)的硬編碼保護。這些是無法繞過的——即使在完全繞過模式下,也需要互動式批准。
- 權限模式 — 使用者設定的模式決定預設行為。
- 允許規則 — 最後檢查。如果允許規則匹配且未觸發拒絕/詢問規則,則工具繼續執行。
權限模式
default — 始終就「詢問」決策提示使用者。
acceptEdits — 自動允許安全的檔案操作(讀取、編輯),其他操作則提示。
bypassPermissions — 除了拒絕規則和安全性檢查外,自動允許所有操作。
plan — 先批准一個計劃,然後遵循先前的模式執行。
auto — 使用 AI 分類器來決定允許或提示。
dontAsk — 將所有「詢問」決策轉換為「拒絕」——從不提示,只拒絕。
權限規則來自多個來源,按優先順序解析:policySettings、localSettings、projectSettings、userSettings、flagSettings、cliArg、command、session。這使得組織政策可以覆蓋使用者偏好,而 CLI 參數可以覆蓋這兩者。
階段 5:執行
如果權限獲得批准,則會呼叫工具的 call() 方法:
1const result = await tool.call(2 callInput,3 { ...toolUseContext, toolUseId: toolUseID },4 canUseTool,5 assistantMessage,6 progress => onToolProgress({ toolUseID: progress.toolUseID, data: progress.data })7)
五個參數: 驗證後的輸入、一個執行上下文(工作目錄、中止控制器、應用程式狀態)、一個權限回呼(用於需要在執行中請求額外權限的工具)、父助手訊息,以及一個用於即時更新的進度回呼。持續時間會全域追蹤。
一個微妙的細節: 傳遞給 call() 的輸入是模型的 原始輸入,而不是鉤子和權限所看到的已補齊版本。這保持了對話紀錄的一致性——對話中記錄的工具呼叫與模型產生的內容完全匹配。
階段 6:工具後鉤子
執行後,會觸發工具後鉤子。這些鉤子可以修改 MCP 工具輸出、提供額外上下文,或阻止對話繼續。還有一個單獨的 PostToolUseFailure 鉤子,僅在發生錯誤時觸發,讓外部系統有機會記錄失敗或建議補救措施。
階段 7:結果映射
每個工具都實作 mapToolResultToToolResultBlockParam(),將其輸出轉換為 Anthropic API 的 ToolResultBlockParam 格式——一個包含 tool_use_id 引用以及字串或結構化內容的 tool_result 區塊。
如果結果超過大小閾值,它會被持久化到磁碟上的 sessionDir/tool-results/{toolUseId}.txt,並將帶有檔案引用的預覽發送給 API。這可以防止大型輸出(例如讀取 10,000 行的檔案、冗長的命令輸出)使對話上下文膨脹。
並發排程器
當模型在一則訊息中發出多個工具呼叫時,它們不會全部同時執行。排程器會根據並行安全性將它們分割成批次。
演算法很簡單。依序遍歷工具呼叫。對於每個呼叫,檢查 isConcurrencySafe(input)。如果是安全的,且前一個批次也是安全的,則將其加入該批次。否則,開始一個新批次。
1// 從 toolOrchestration.ts 簡化而來2for (const toolUse of toolUseMessages) {3 const isSafe = tool.isConcurrencySafe(parsedInput)4 if (isSafe && lastBatch.isConcurrencySafe) {5 lastBatch.blocks.push(toolUse) // 合併到並行批次中6 } else {7 batches.push({ isConcurrencySafe: isSafe, blocks: [toolUse] })8 }9}
安全的批次會並行執行(上限為 10 個,可透過 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 設定)。不安全的批次則序列執行,一次一個工具。上下文修飾器僅在批次之間應用,批次內不應用。
實際上,這意味著像「讀取這 5 個檔案」這樣的訊息會產生一個並行批次,而像「讀取這個檔案,然後編輯它」則會產生兩個序列批次。模型甚至可以在單一回合中觸發這兩種模式——連續的唯讀呼叫會被批次處理,而第一個寫入操作則會打破該批次。
串流執行器
還有第二個執行路徑:StreamingToolExecutor。啟用串流時,工具會在 模型仍在產生回應時 就開始執行。當串流中的每個 tool_use 區塊完成時,它會立即被排入執行佇列,而不是等待完整回應。
串流執行器使用相同的並發規則,但增加了一個行為:Bash 錯誤級聯。如果一個 Bash 命令在兄弟工具並行執行時失敗,執行器會中止所有兄弟工具。其理由是,一個失敗的 Bash 命令很可能會使其他工具正在操作的上下文失效——繼續執行它們只會浪費時間,並可能導致混淆的錯誤。
1if (isErrorResult && tool.block.name === BASH_TOOL_NAME) {2 this.hasErrored = true3 this.siblingAbortController.abort('sibling_error')4}
一個實際範例
為了更具體,讓我們追蹤一下當模型決定讀取一個檔案時會發生什麼。模型發出:
1{2 "type": "tool_use",3 "id": "toolu_01XYZ",4 "name": "read",5 "input": { "file_path": "/src/index.ts" }6}
提取:query.ts 從助手訊息內容中過濾出此內容。
- 工具查找:findToolByName(tools, "read") 找到 FileReadTool。
- 輸入驗證:Zod 將 { file_path: "/src/index.ts" } 與 z.object({ file_path: z.string(), offset: z.number().optional(), limit: z.number().optional(), pages: z.string().optional() }) 進行比對。驗證通過。
- 工具前鉤子:任何使用者設定的鉤子都會觸發。沒有鉤子修改輸入。
- 權限檢查:FileReadTool 的 checkPermissions() 會呼叫 checkReadPermissionForTool()。在大多數權限模式下,讀取工具通常是被允許的。
- 執行:[FileReadTool.call](//FileReadTool.call)() 讀取檔案,應用行號(cat -n** 格式),並將 PDF/圖片/筆記本作為特殊情況處理。
- 結果映射:檔案內容成為一個引用 "toolu_01XYZ" 的 tool_result 區塊。
- 回傳:結果作為使用者訊息附加到對話中,並在下一次 API 呼叫中發送。
由於 FileReadTool 宣告了 isConcurrencySafe: () => true 和 isReadOnly: () => true,如果模型在同一則訊息中發出了五個讀取呼叫,這五個呼叫將並行執行。
總結
工具系統是 Claude Code 的執行骨幹。它接收模型的意圖——以結構化的 tool_use 區塊表達——並將其轉化為你機器上的實際操作,每個步驟都配有驗證、權限和並發控制。
其設計是分層的:一個保守的 buildTool() 工廠確保了安全預設值,一個受功能開關限制的註冊中心控制著哪些工具可用,一個七階段的分配管線驗證和權限檢查每個呼叫,而一個並發調度器則在保持正確性的同時最大化並行性。串流執行器在此基礎上增加了一項效能最佳化——工具在模型完成思考之前就開始執行。
與記憶體系統(5 個路徑、一個 Markdown 檔案目錄和提示詞工程)相比,工具系統是一個真正的運行時。這就像檔案櫃與作業系統之間的差別。
有趣之處
模型即排程器
並發排程器是反應式的——它對模型發出的任何內容進行批次處理。但模型本身才是真正的排程器。系統提示詞告訴它「將所有獨立的工具呼叫並行進行」,以及「使用單一的 Bash 呼叫,並用 && 連接依賴的命令」。運行時信任這一點。如果模型發出了五次讀取操作然後一次寫入操作,排程器會並行化讀取操作並序列化寫入操作。但順序是模型決定的。排程器是在執行模型的計劃,而不是制定自己的計劃。
預設為「失敗關閉」
最一致的設計原則:一切預設為失敗關閉。未知的工具?錯誤。無效的輸入?錯誤。沒有並行宣告?序列執行。沒有權限宣告?詢問使用者。沒有功能開關?工具不存在。對於一個主要使用者是可能憑空想像工具名稱或錯誤格式化輸入的 AI 模型的系統來說,這很不尋常。該系統旨在包含模型的錯誤,而不是容納它們。
鉤子作為擴展點
鉤子系統——工具前、工具後和失敗後——是主要的擴展點。它是組織執行政策(在鉤子前的拒絕規則)、日誌系統捕獲工具使用情況(鉤子後)以及 CI/CD 管線整合(失敗鉤子)的方式。重要的是,鉤子只能收緊限制,而不能放寬。一個鉤子可以拒絕設定允許的工具,但它不能允許設定拒絕的工具。
43 個工具,1 個介面
也許最引人注目的是其統一性。一個 bash 命令、一個 web_fetch、一個子 Agent 生成、一個 cron 任務建立和一個推送通知都實作了相同的 30 個方法介面,經歷相同的七個階段管線,並遵守相同的權限系統。排程器中沒有特殊情況。複雜性在於各個工具的實作和權限規則中,而不是在路由中。





