@spandan_madan: https://x.com/spandan_madan/status/2067320100911493454

X AI KOLs Timeline 工具

摘要

对Claude Code工具架构的详细技术分析,涵盖工具接口、注册表、调度管道和并发调度器,这些使AI能够执行43+种工具,如文件读取、shell命令和网络搜索。

https://t.co/iWVFS3tsHH
查看原文
查看缓存全文

缓存时间: 2026/06/18 12:14

Claude Code 的工具架构

深入解析 Claude Code 如何发现、调度和执行工具

引言

在之前的一篇文章中,我们根据 Anthropic Harness 的泄露源码对其记忆系统进行了逆向工程。结果发现,记忆系统出奇地简单——仅仅是 Markdown 文件、Frontmatter 和提示工程。而工具系统则恰恰相反。它是整个代码库中工程化程度最高的子系统:43 个以上的工具、一个流式执行管线、一个层级权限系统、一个钩子框架以及一个并发调度器——所有这些组件被连接在一起,将一个无状态的语言模型转变为能够读取文件、运行 Shell 命令、搜索网络并生成子代理的实体。

本文将从工具的生命周期出发,逐步讲解:工具如何定义、模型的工具调用如何被分发、以及结果如何流回对话。整体上,该系统由四个层级组成:一个每个工具都必须实现的工具接口,一个汇总工具池的注册表,一个负责验证、权限检查和执行每次调用的调度管线,以及一个决定哪些任务并行运行的并发调度器

整体架构速览

工具接口

Claude Code 中的每个工具都实现了相同的接口,定义在 Tool.ts 中。该类型是泛型的,包含三个参数:Input(Zod 模式)、Output(结果类型)和 P(进度数据)。实际上,一个工具是一个包含大约 30 个方法的对象,但只有少数几个对理解系统至关重要。核心结构如下:

type Tool = {
  name: string
  inputSchema: ZodType          // 用于输入验证的 Zod 模式
  call(input, context, canUseTool, parentMessage, onProgress): Promise

  // 行为声明
  isConcurrencySafe(input): boolean  // 能否并行运行?
  isReadOnly(input): boolean         // 是否只读操作?
  isDestructive(input): boolean      // 是否破坏性操作?

  // 权限与验证
  checkPermissions(input, context): Promise
  validateInput(input, context): Promise

  // API 集成
  description(input, options): Promise
  prompt(options): Promise           // 该工具的系统提示文本
  mapToolResultToToolResultBlockParam(result, toolUseId): ToolResultBlockParam

  // UI 渲染(React)
  renderToolUseMessage(input, options): ReactNode
  renderToolResultMessage(content, ...): ReactNode
}

没有工具是从头开始实现上述所有方法的。一个名为 buildTool() 的工厂函数会填充安全的默认值:
默认值刻意保守。一个忘记声明并发安全性的工具作者,会得到串行执行的结果;一个忘记实现权限检查的工具作者,会使用默认的权限流程。系统默认保持封闭安全。

ToolResult 类型值得注意:

type ToolResult = {
  data: T                           // 实际输出
  newMessages?: Message[]           // 可选的后续消息
  contextModifier?: (ctx) => ToolUseContext  // 为下一个工具修改上下文
  mcpMeta?: { ... }                 // MCP 协议元数据
}

contextModifier 很重要——它允许一个工具更改同一轮次中后续工具的执行上下文。这就是像 EnterWorktree 这样的工具如何改变后续所有工具的工作目录。关键在于,上下文修改器只允许用于非并发安全的工具。如果一个工具并行运行,它不能修改共享状态。

工具注册表

所有工具都在一个单独的函数 getAllBaseTools()(位于 tools.ts)中注册。该函数返回一个扁平数组。有些工具始终存在,而另一些则受功能标志、环境变量或平台检查的限制。

始终可用的工具(16 个)

(列表内容省略,原文为列表)

受功能标志控制的工具(约 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 还支持 模型上下文协议(MCP) 服务器——这些外部进程通过标准化协议暴露自己的工具。MCP 工具在运行时从连接的服务器动态注册,并包装在相同的 Tool 接口中。从调度管线的角度来看,MCP 工具与内置工具没有区别。每个 MCP 工具都携带关于其原始服务器的元数据(mcpInfo: { serverName, toolName }),用于权限规则、错误处理和身份验证。当 MCP 工具发生身份验证错误时,系统会自动将服务器状态更新为 needs-auth,并向用户提示问题。

工具池组装

三个函数组装最终的工具集:

  • getAllBaseTools()——返回 43 个以上内置工具的原始列表,已应用功能标志过滤
  • getTools(permissionContext)——根据拒绝规则和 isEnabled() 进行过滤
  • assembleToolPool(permissionContext, mcpTools)——合并内置工具和 MCP 工具

assembleToolPool() 中的合并策略是精密的:内置工具优先,因此命名冲突时内置工具胜出。每个分区内按字母排序,保持顺序跨会话稳定,这对提示缓存很重要——工具数组是 API 请求的一部分,重新排序会破坏缓存。

// 按字母排序每个分区,连接,去重
const byName = (a, b) => a.name.localeCompare(b.name)
return uniqBy(
  [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
  'name',
)

API 序列化

在工具到达 Claude API 之前,toolToAPISchema() 会将每个工具的 Zod 模式转换为 Anthropic API 的 JSON Schema 格式。

调度管线

当 Claude 响应时,其消息可能包含 tool_use 块——对调用工具的结构化请求。调度管线通过七个阶段处理这些块。每个工具调用都会按顺序经过每个阶段。

第一阶段:提取

在主查询循环(query.ts)中,tool_use 块从助手的消息中被过滤出来:

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

每个块包含 nameinput 对象和唯一的 id。这个 id 至关重要——工具结果在返回给 API 时必须引用相同的 id,否则对话会中断。

第二阶段:输入验证

工具的 Zod 模式使用 safeParse() 验证原始输入——这是一种不抛出异常的方法,返回有效数据或结构化错误。如果验证失败,模型会收到一个格式化的错误消息(包含模式提示),并且该工具的执行会停止。无效输入不会运行任何代码。

const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
  let errorContent = formatZodValidationError(tool.name, parsedInput.error)
  // 向模型返回错误,跳过执行
}

Zod 验证之后,一些工具会运行第二层 validateInput() 检查,用于无法在模式中表达的语义验证——例如,验证文件路径是绝对路径而非相对路径。

第三阶段:预工具钩子

在权限检查之前,用户配置的钩子开始执行。这些是外部 Shell 命令或脚本,在工具调用时触发。预工具钩子可以:

  • 允许工具调用,绕过交互式权限提示
  • 拒绝工具调用,直接停止
  • 修改输入后再执行
  • 阻止执行并附带错误消息
  • 提供额外上下文给用户

一个关键的不变量:钩子的 allow绕过来自设置文件的拒绝规则。源码中有明确的注释说明:“钩子 ‘allow’ 并不会绕过 settings.json 中的 deny/ask 规则。”其意图是钩子可以开门,但不能覆盖锁。

第四阶段:权限检查

权限系统是管线中最复杂的部分。它按顺序通过多个层次解析:

  • 拒绝规则——首先检查。如果任何拒绝规则匹配,执行立即停止。拒绝规则是最终的,不能被任何其他层覆盖。
  • 询问规则——如果匹配,会提示用户审批(除非沙箱自动允许适用于 Bash)。
  • 工具特定权限——工具自身的 checkPermissions() 方法运行。例如,BashTool 会解析命令以检查子命令级别的规则。
  • 安全检查——针对敏感路径(.git/.claude/、Shell 配置)的硬编码保护。这些是无法绕过的——即使在完全绕过模式下,也需要交互式审批。
  • 权限模式——用户配置的模式决定了默认行为。
  • 允许规则——最后检查。如果允许规则匹配且没有触发拒绝/询问规则,则工具继续执行。

权限模式

  • default——始终对“ask”决策提示用户。
  • acceptEdits——自动允许安全的文件操作(读取、编辑),其他操作则提示。
  • bypassPermissions——除拒绝规则和安全检查外,自动允许所有操作。
  • plan——先批准一个计划,然后按之前的模式执行。
  • auto——使用 AI 分类器决定是允许还是提示。
  • dontAsk——将所有“ask”决策转换为“deny”——从不提示,直接拒绝。

权限规则来自多个来源,按优先级顺序解析:policySettingslocalSettingsprojectSettingsuserSettingsflagSettingscliArgcommandsession。这使得组织策略可以覆盖用户偏好,CLI 参数可以覆盖两者。

第五阶段:执行

如果权限授予,工具会调用 call() 方法:

const result = await tool.call(
  callInput,
  { ...toolUseContext, toolUseId: toolUseID },
  canUseTool,
  assistantMessage,
  progress => onToolProgress({ toolUseID: progress.toolUseID, data: progress.data })
)

五个参数:验证后的输入、执行上下文(工作目录、中止控制器、应用状态)、权限回调(用于工具在运行过程中需要额外权限的情况)、父助手消息,以及用于实时更新的进度回调。执行时长会被全局跟踪。

一个细微之处:传递给 call() 的输入是模型的原始输入,而不是经过钩子和权限回填后的版本。这样可以保持对话记录的一致性——对话中记录的工具调用与模型生成的内容完全匹配。

第六阶段:后工具钩子

执行完成后,后工具钩子触发。它们可以修改 MCP 工具的输出、提供额外上下文,或阻止对话继续。还有一个独立的 PostToolUseFailure 钩子,仅在出错时触发,允许外部系统记录失败或建议补救措施。

第七阶段:结果映射

每个工具实现 mapToolResultToToolResultBlockParam(),将其输出转换为 Anthropic API 的 ToolResultBlockParam 格式——一个包含 tool_use_id 引用以及字符串或结构化内容的 tool_result 块。
如果结果超过大小阈值,它会被持久化到磁盘 sessionDir/tool-results/{toolUseId}.txt,并向 API 发送一个预览加文件引用,而不是直接发送完整内容。这样可以防止大型输出(如读取一万行文件、命令输出冗长)膨胀对话上下文。

并发调度器

当模型在一次消息中发出多个工具调用时,它们不会同时全部运行。调度器根据并发安全性将它们划分为批次。算法很简单:按顺序遍历工具调用,对每一个调用检查 isConcurrencySafe(input)。如果安全且前一批次也安全,则将其加入当前批次;否则,开始一个新批次。

// 简化自 toolOrchestration.ts
for (const toolUse of toolUseMessages) {
  const isSafe = tool.isConcurrencySafe(parsedInput)
  if (isSafe && lastBatch.isConcurrencySafe) {
    lastBatch.blocks.push(toolUse) // 合并到并发批次
  } else {
    batches.push({ isConcurrencySafe: isSafe, blocks: [toolUse] })
  }
}

安全的批次并发运行(上限为 10,可通过 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 配置)。不安全的批次串行运行,一次一个工具。上下文修改器仅在批次之间应用,而非批次内部。
在实际使用中,这意味着像“读取这 5 个文件”这样的消息会生成一个并发批次,而“读取这个文件,然后编辑它”会生成两个串行批次。模型甚至可以在一次轮次中同时触发两种模式——连续的只读调用会被批量处理,而第一个写操作会中断批次。

流式执行器

还有第二条执行路径:StreamingToolExecutor。当启用流式传输时,工具会在模型仍在生成响应时就开始执行。每当流中完成一个 tool_use 块,它立即被加入执行队列,而无需等待完整响应。
流式执行器使用相同的并发规则,但增加了一个行为:Bash 错误级联。如果 Bash 命令失败而同级工具仍在并行运行,执行器会中止所有同级工具。其理由是一个失败的 Bash 命令很可能使其他工具正在操作的上下文失效——继续运行会浪费时间并可能导致令人困惑的错误。

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

一个实际示例

为了让这个过程更具体,让我们追踪当模型决定读取一个文件时会发生什么。

模型发出:

{
  "type": "tool_use",
  "id": "toolu_01XYZ",
  "name": "read",
  "input": {
    "file_path": "/src/index.ts"
  }
}
  • 提取query.ts 从助手消息内容中过滤出该块。
  • 工具查找findToolByName(tools, “read”) 找到 FileReadTool。
  • 输入验证:Zod 根据 z.object({ file_path: z.string(), offset: z.number().optional(), limit: z.number().optional(), pages: z.string().optional() }) 解析 { file_path: “/src/index.ts” }。通过。
  • 预工具钩子:任何用户配置的钩子触发。没有修改输入。
  • 权限检查:FileReadTool 的 checkPermissions() 调用 checkReadPermissionForTool()。在大多数权限模式下,读取工具通常是允许的。
  • 执行FileReadTool.call() 读取文件,应用行号(cat -n 格式),并特殊处理 PDF/图像/笔记本。
  • 结果映射:文件内容变成一个引用 “toolu_01XYZ”tool_result 块。
  • 返回:结果作为用户消息附加到对话中,并在下一次 API 调用时发送。

由于 FileReadTool 声明了 isConcurrencySafe: () => trueisReadOnly: () => true,如果模型在同一个消息中发出了五个读取调用,这五个调用会并行执行。

总结

工具系统是 Claude Code 的执行骨干。它将模型的意图——以结构化的 tool_use 块表示——转化为你机器上的真实操作,在每一步都进行验证、权限控制和并发管理。其设计是分层的:一个保守的 buildTool() 工厂确保安全默认值,一个受功能标志控制的注册表控制可用性,一个七阶段的调度管线对每次调用进行验证和权限检查。

相似文章