Claude Code 的工具架構

@spandan_madan
英語4 週前 · 2026年6月17日
552K
416
115
86
479

TL;DR

這篇技術深度解析探討了 Claude Code 的工具架構,詳細說明其七階段調度管線、並發排程器,以及實現複雜 AI Agent 動作的故障關閉(fail-closed)權限系統。

好的,这是您要求的英文内容到繁體中文的翻譯。


深入探討 Claude Code 如何發現、分派與執行工具

簡介

前一篇文章中,我們從 Anthropic Harness 外洩的原始碼中逆向工程了其記憶系統。結果發現記憶出奇地簡單——就是 Markdown 檔案、frontmatter 以及提示詞工程。而工具系統則恰恰相反。它是整個程式碼庫中工程化程度最高的子系統:超過 43 個工具、一個串流執行管道、一個分層的權限系統、一個鉤子(hook)框架,以及一個並發排程器——所有這些組件被整合在一起,將一個無狀態的語言模型轉變為能夠讀取檔案、執行 Shell 命令、搜尋網路以及產生子 Agent 的實體。

這篇文章將帶領你了解一個工具從定義、模型工具呼叫的分派,到結果如何回流至對話中的完整生命週期。廣義來說,整個系統由四個層級組成:一個所有工具都實作的 工具介面(Tool Interface)、一個組裝工具池的 註冊中心(Registry)、一個負責驗證、權限檢查與執行每個呼叫的 分派管線(Dispatch Pipeline),以及一個決定哪些任務可並行執行的 並發排程器(Concurrency Scheduler)

整體架構概覽

Spandan Madan - inline image

工具介面

Claude Code 中的每個工具都實作相同的介面,定義在 Tool.ts 中。該型別對於三個參數是泛型的:Input(一個 Zod 模式)、Output(結果型別)、以及 P(進度資料)。實際上,一個工具是一個擁有約 30 個方法的物件,但對於理解系統而言,只有少數幾個是重要的。

核心結構如下:

text
1type Tool<Input, Output, P> = {
2 name: string
3 inputSchema: ZodType // 用於輸入驗證的 Zod 模式
4 call(input, context, canUseTool,
5 parentMessage, onProgress): Promise<ToolResult>
6
7 // 行為宣告
8 isConcurrencySafe(input): boolean // 能否並行執行?
9 isReadOnly(input): boolean // 是否為唯讀操作?
10 isDestructive(input): boolean // 是否為破壞性操作?
11
12 // 權限與驗證
13 checkPermissions(input, context): Promise<PermissionResult>
14 validateInput(input, context): Promise<ValidationResult>
15
16 // API 整合
17 description(input, options): Promise<string>
18 prompt(options): Promise<string> // 此工具的系統提示文字
19 mapToolResultToToolResultBlockParam(result, toolUseId): ToolResultBlockParam
20
21 // UI 渲染 (React)
22 renderToolUseMessage(input, options): ReactNode
23 renderToolResultMessage(content, ...): ReactNode
24}

沒有一個工具是從頭開始實作這個介面的。一個名為 buildTool() 的工廠函數會填入安全預設值:

Spandan Madan - inline image

這些預設值刻意設計得很保守。忘記宣告並行安全性的工具作者,會得到序列執行。忘記實作權限檢查的工具作者,會得到預設的權限流程。系統預設是「失敗關閉」(fail closed)。

值得注意的 ToolResult 型別:

text
1type ToolResult<T> = {
2 data: T // 實際輸出
3 newMessages?: Message[] // 可選的後續訊息
4 contextModifier?: (ctx) => ToolUseContext // 為下一個工具變更上下文
5 mcpMeta?: { ... } // MCP 協議元資料
6}

contextModifier 很重要——它允許一個工具為同一輪中後續的工具變更執行上下文。這就是像 EnterWorktree 這類工具變更後續所有操作的工作目錄的方式。關鍵在於,上下文修飾器僅允許用於非並行安全的工具。如果一個工具是並行執行的,它就無法修改共享狀態。

工具註冊中心

所有工具都在一個名為 getAllBaseTools() 的函數中註冊,位於 tools.ts。它回傳一個扁平陣列。有些工具始終存在;其他的則受到功能開關、環境變數或平台檢查的限制。

始終可用的工具(16 個)

Spandan Madan - inline image

受功能開關限制的工具(約 27 個)

其餘的工具則是有條件地包含。有些受到環境變數限制(USER_TYPE=ant 用於 Anthropic 內部工具,如 configtungsten)。有些受到透過 Statsig 設置的功能開關限制(web_browsersleepmonitor)。有些則是平台專用的(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,並將問題顯示給使用者。

工具池組裝

三個函數組裝最終的工具集合:

  1. getAllBaseTools() — 回傳應用功能開關後的 43 個以上內建工具的原始列表
  2. getTools(permissionContext) — 根據拒絕規則和 isEnabled() 進行過濾
  3. assembleToolPool(permissionContext, mcpTools) — 合併內建工具與 MCP 工具

assembleToolPool() 中的合併策略是經過深思熟慮的:

內建工具優先,因此在名稱衝突時,內建工具會勝出。每個分割區內按字母順序排序,可確保順序在會話之間保持穩定,這對於提示詞快取非常重要——工具陣列是 API 請求的一部分,重新排序會使快取失效。

assembleToolPool() 中的合併策略是經過深思熟慮的:

text
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 區塊會從助手訊息中過濾出來:

text
1const msgToolUseBlocks = message.message.content.filter(
2 content => content.type === 'tool_use',
3) as ToolUseBlock[]

每個區塊都有一個 name、一個 input 物件和一個唯一的 id。這個 id 至關重要——工具結果在送回 API 時必須引用相同的 id,否則對話會中斷。

階段 2:輸入驗證

工具的 Zod 模式使用 safeParse() 驗證原始輸入——這是一個非拋出異常的變體,會回傳有效資料或結構化錯誤。如果驗證失敗,模型會收到一條包含模式提示的格式化錯誤訊息,並且該工具的執行會停止。無效輸入不會執行任何程式碼。

text
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:權限檢查

權限系統是管線中最複雜的部分。它按順序透過多個層級進行解析:

  1. 拒絕規則 — 首先檢查。如果任何拒絕規則匹配,執行會立即停止。拒絕規則是最終決定,不能被任何其他層級覆蓋。
  2. 詢問規則 — 如果匹配,則會提示使用者批准(除非 Bash 適用沙箱自動允許)。
  3. 工具特定權限 — 工具自身的 checkPermissions() 方法會執行。例如,BashTool 會解析命令以檢查子命令層級的規則。
  4. 安全性檢查 — 針對敏感路徑(.git/.claude/、Shell 設定檔)的硬編碼保護。這些是無法繞過的——即使在完全繞過模式下,也需要互動式批准。
  5. 權限模式 — 使用者設定的模式決定預設行為。
  6. 允許規則 — 最後檢查。如果允許規則匹配且未觸發拒絕/詢問規則,則工具繼續執行。

權限模式

default — 始終就「詢問」決策提示使用者。

acceptEdits — 自動允許安全的檔案操作(讀取、編輯),其他操作則提示。

bypassPermissions — 除了拒絕規則和安全性檢查外,自動允許所有操作。

plan — 先批准一個計劃,然後遵循先前的模式執行。

auto — 使用 AI 分類器來決定允許或提示。

dontAsk — 將所有「詢問」決策轉換為「拒絕」——從不提示,只拒絕。

權限規則來自多個來源,按優先順序解析:policySettingslocalSettingsprojectSettingsuserSettingsflagSettingscliArgcommandsession。這使得組織政策可以覆蓋使用者偏好,而 CLI 參數可以覆蓋這兩者。

階段 5:執行

如果權限獲得批准,則會呼叫工具的 call() 方法:

text
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)。如果是安全的,且前一個批次也是安全的,則將其加入該批次。否則,開始一個新批次。

text
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 命令很可能會使其他工具正在操作的上下文失效——繼續執行它們只會浪費時間,並可能導致混淆的錯誤。

text
1if (isErrorResult && tool.block.name === BASH_TOOL_NAME) {
2 this.hasErrored = true
3 this.siblingAbortController.abort('sibling_error')
4}

一個實際範例

為了更具體,讓我們追蹤一下當模型決定讀取一個檔案時會發生什麼。模型發出:

text
1{
2 "type": "tool_use",
3 "id": "toolu_01XYZ",
4 "name": "read",
5 "input": { "file_path": "/src/index.ts" }
6}

提取query.ts 從助手訊息內容中過濾出此內容。

  1. 工具查找findToolByName(tools, "read") 找到 FileReadTool。
  2. 輸入驗證:Zod 將 { file_path: "/src/index.ts" }z.object({ file_path: z.string(), offset: z.number().optional(), limit: z.number().optional(), pages: z.string().optional() }) 進行比對。驗證通過。
  3. 工具前鉤子:任何使用者設定的鉤子都會觸發。沒有鉤子修改輸入。
  4. 權限檢查:FileReadTool 的 checkPermissions() 會呼叫 checkReadPermissionForTool()。在大多數權限模式下,讀取工具通常是被允許的。
  5. 執行[FileReadTool.call](//FileReadTool.call)() 讀取檔案,應用行號(cat -n** 格式),並將 PDF/圖片/筆記本作為特殊情況處理。
  6. 結果映射:檔案內容成為一個引用 "toolu_01XYZ"tool_result 區塊。
  7. 回傳:結果作為使用者訊息附加到對話中,並在下一次 API 呼叫中發送。

由於 FileReadTool 宣告了 isConcurrencySafe: () => trueisReadOnly: () => true,如果模型在同一則訊息中發出了五個讀取呼叫,這五個呼叫將並行執行。

總結

工具系統是 Claude Code 的執行骨幹。它接收模型的意圖——以結構化的 tool_use 區塊表達——並將其轉化為你機器上的實際操作,每個步驟都配有驗證、權限和並發控制。

其設計是分層的:一個保守的 buildTool() 工廠確保了安全預設值,一個受功能開關限制的註冊中心控制著哪些工具可用,一個七階段的分配管線驗證和權限檢查每個呼叫,而一個並發調度器則在保持正確性的同時最大化並行性。串流執行器在此基礎上增加了一項效能最佳化——工具在模型完成思考之前就開始執行。

與記憶體系統(5 個路徑、一個 Markdown 檔案目錄和提示詞工程)相比,工具系統是一個真正的運行時。這就像檔案櫃與作業系統之間的差別。

有趣之處

模型即排程器

並發排程器是反應式的——它對模型發出的任何內容進行批次處理。但模型本身才是真正的排程器。系統提示詞告訴它「將所有獨立的工具呼叫並行進行」,以及「使用單一的 Bash 呼叫,並用 && 連接依賴的命令」。運行時信任這一點。如果模型發出了五次讀取操作然後一次寫入操作,排程器會並行化讀取操作並序列化寫入操作。但順序是模型決定的。排程器是在執行模型的計劃,而不是制定自己的計劃。

預設為「失敗關閉」

最一致的設計原則:一切預設為失敗關閉。未知的工具?錯誤。無效的輸入?錯誤。沒有並行宣告?序列執行。沒有權限宣告?詢問使用者。沒有功能開關?工具不存在。對於一個主要使用者是可能憑空想像工具名稱或錯誤格式化輸入的 AI 模型的系統來說,這很不尋常。該系統旨在包含模型的錯誤,而不是容納它們。

鉤子作為擴展點

鉤子系統——工具前、工具後和失敗後——是主要的擴展點。它是組織執行政策(在鉤子前的拒絕規則)、日誌系統捕獲工具使用情況(鉤子後)以及 CI/CD 管線整合(失敗鉤子)的方式。重要的是,鉤子只能收緊限制,而不能放寬。一個鉤子可以拒絕設定允許的工具,但它不能允許設定拒絕的工具。

43 個工具,1 個介面

也許最引人注目的是其統一性。一個 bash 命令、一個 web_fetch、一個子 Agent 生成、一個 cron 任務建立和一個推送通知都實作了相同的 30 個方法介面,經歷相同的七個階段管線,並遵守相同的權限系統。排程器中沒有特殊情況。複雜性在於各個工具的實作和權限規則中,而不是在路由中。

一鍵儲存

使用 YouMind AI 深度閱讀爆款文章

保存原文、追問細節、總結觀點,並在一個 AI 工作空間裡把爆款文章沉澱成可複用筆記。

了解 YouMind
寫給創作者

把你的 Markdown 變成乾淨的 𝕏 文章

圖片上傳、表格、程式碼區塊,往 𝕏 上手動重排太痛苦。YouMind 把整篇 Markdown 一鍵轉成乾淨、可直接發佈的 𝕏 文章草稿。

試試 Markdown 轉 𝕏

更多可拆解樣本

近期爆款文章

探索更多爆款文章