工具架构
30+ 属性的 Tool 接口
编译期死代码消除(bun:bundle DCE)实现零运行时开销的特性门控
学习
概念讲解与核心设计分析
工具架构概述
Claude Code 的核心能力来自于其工具系统。每一个"工具"(Tool)都是一个独立的能力单元——文件读写、代码搜索、Bash 执行、Web 搜索等。工具架构定义了统一的接口约束、注册发现机制、上下文传递模式和特性门控策略,使得 42+ 种工具可以在同一个框架下协同工作。
Tool<Input, Output, P> 接口:30+ 属性的"超级接口"
Tool.ts 定义了工具系统最核心的泛型接口 Tool<Input, Output, P>。每个工具实现都必须满足这个接口的约束。三个泛型参数分别是:
- Input — 工具的输入类型,由 Zod schema 定义(如 BashTool 的
{ command: string, timeout?: number }) - Output — 工具的输出类型,通常是
string或结构化数据 - P — 权限类型参数,用于类型安全的权限检查
该接口包含 30+ 个属性和方法,按职责分为以下几组:
标识与发现
name— 工具的唯一标识符(如"Bash"、"Read")aliases— 别名列表,用于兼容旧名称searchHint— 搜索提示词,供 ToolSearchTool 做关键词匹配userFacingName()— 返回面向用户显示的名称
Schema 与校验
inputSchema— Zod schema 定义,用于运行时校验输入inputJSONSchema— 转换后的 JSON Schema,发送给 APIvalidateInput()— 自定义输入校验逻辑(超出 schema 的业务规则)
执行与结果
call(input, context)— 核心执行方法,接收输入和 ToolUseContextmaxResultSizeChars— 结果最大字符数,超出则持久化到磁盘mapToolResultToToolResultBlockParam()— 将结果转换为 API 兼容格式
权限与安全
checkPermissions()— 检查当前上下文是否有权执行该工具preparePermissionMatcher()— 准备权限匹配器(如 Bash 的命令分类)isReadOnly()— 是否为只读工具(读文件、搜索等)isDestructive()— 是否为破坏性工具(删除文件、强制推送等)
并发与中断
isConcurrencySafe()— 是否支持并发执行(如 GlobTool 可以并发,BashTool 不行)interruptBehavior()— 中断时的行为:'abort'(立即停止)或'finish'(完成当前操作)
提示与渲染
description()— 发送给 Claude 的工具描述(影响模型选择哪个工具)prompt()— 额外的提示信息,注入到系统提示词中renderToolUseMessage()— 在 UI 中渲染工具调用的消息toAutoClassifierInput()— 转换为自动分类器(权限判断)的输入
ToolUseContext:"上帝上下文"
ToolUseContext 是传递给每个工具的 call() 方法的上下文对象,它携带了工具执行所需的一切信息:
options— 全局选项(工作目录、模型、权限模式等)abortController— 用于中断当前工具执行的 AbortControllerreadFileState— 文件读取状态缓存(避免重复读取)getAppState()/setAppState()— 全局应用状态的读写接口messages— 当前对话的完整消息历史agentId— 当前 Agent 的标识符(主 Agent 或子 Agent)queryTracking— 查询追踪信息(用于遥测)contentReplacementState— 内容替换状态(用于大结果的磁盘持久化)
ToolResult<T>:结构化返回值
工具的返回值不只是简单的字符串,而是一个结构化的 ToolResult<T>:
data— 实际的输出数据newMessages— 工具执行过程中生成的新消息(如 AskUserQuestionTool 收到的用户回复)contextModifier— 上下文修改器(如 SkillTool 加载 skill 后修改系统提示词)mcpMeta— MCP 元数据(如果是 MCP 工具调用)
工具注册与发现
tools.ts 是工具的注册中心,提供三个关键函数:
getAllBaseTools()— 返回所有 42+ 个内置工具实例的数组getTools()— 在getAllBaseTools()基础上,根据 deny 规则和isEnabled()过滤不可用的工具assembleToolPool()— 合并内置工具和 MCP 工具,使用缓存稳定排序确保工具顺序一致
特性门控与 DCE
Claude Code 使用 bun:bundle 的编译时死代码消除(DCE, Dead Code Elimination)实现特性门控:
- 不同编译目标(如 OSS vs 内部版本)会定义不同的特性标志
- 编译器在打包时将关闭的特性对应的代码完全移除
- 这意味着某些工具在特定版本中根本不存在于最终产物中,而不是运行时判断
循环依赖破解
由于工具系统与其他模块(如查询循环、权限系统)存在循环依赖,Claude Code 使用了延迟 require() 模式:
- 在文件顶层不导入有循环依赖的模块
- 在函数体内使用
require()延迟加载 - 这确保模块初始化顺序正确,避免导入时获得
undefined
架构
模块关系与设计决策
工具架构图
核心类型层次
| 类型 | 职责 | 关键字段/方法 |
|---|---|---|
| Tool<I,O,P> | 工具接口(30+ 属性) | name, call(), checkPermissions(), inputSchema |
| ToolUseContext | 执行上下文("上帝对象") | options, abortController, messages, readFileState |
| ToolPermissionContext | 权限上下文 | mode, alwaysAllowRules, alwaysDenyRules |
| ToolResult<T> | 结构化返回值 | data, newMessages, contextModifier, mcpMeta |
工具发现流程
- getAllBaseTools() — 实例化 42+ 个内置工具
- getTools(deny) — 过滤被禁用的工具(deny rules + isEnabled())
- assembleToolPool(built-in, MCP) — 合并内置 + MCP 工具,缓存稳定排序
- 发送给 API — 将工具池序列化为 JSON Schema 格式的工具定义
Tool 接口属性分组
- 标识组 (4):name, aliases, searchHint, userFacingName()
- Schema 组 (3):inputSchema, inputJSONSchema, validateInput()
- 执行组 (3):call(), maxResultSizeChars, mapToolResultToToolResultBlockParam()
- 权限组 (4):checkPermissions(), preparePermissionMatcher(), isReadOnly(), isDestructive()
- 并发组 (2):isConcurrencySafe(), interruptBehavior()
- 提示组 (4):description(), prompt(), renderToolUseMessage(), toAutoClassifierInput()
- 门控组 (1):isEnabled()
特性门控机制
编译时 DCE(Dead Code Elimination)流程:
- 定义特性标志常量(如
FEATURE_MCP = true) - 代码中使用
if (FEATURE_MCP) { ... }条件分支 - Bun bundler 在编译时求值常量条件
- 不可达分支被完全移除,最终产物中不包含相关代码
设计决策
为什么 ToolUseContext 是"上帝对象"?
在工具系统中,每个工具可能需要访问截然不同的上下文信息。BashTool 需要 abortController 来中断进程,FileReadTool 需要 readFileState 做缓存,SkillTool 需要 messages 来理解对话历史。与其让每个工具单独获取这些依赖,不如把所有可能需要的上下文打包成一个对象传入。这虽然违反了"最小知识原则",但在实践中大幅简化了工具的编写和注册流程。
源码
共 3 个关键代码示例
// Tool.ts — 简化版核心接口
import { z } from 'zod'
export interface Tool<
Input = unknown,
Output = unknown,
P = unknown
> {
// === 标识与发现 ===
name: string
aliases?: string[]
searchHint?: string
userFacingName(): string
// === Schema ===
inputSchema: z.ZodType<Input>
inputJSONSchema: Record<string, unknown>
validateInput?(input: Input): Promise<string | null>
// === 执行 ===
call(input: Input, context: ToolUseContext): Promise<ToolResult<Output>>
maxResultSizeChars?: number
// === 权限 ===
checkPermissions(
input: Input,
context: ToolPermissionContext
): Promise<PermissionCheckResult>
isReadOnly(): boolean
isDestructive(): boolean
// === 并发与中断 ===
isConcurrencySafe(): boolean
interruptBehavior(): 'abort' | 'finish'
// === 提示与渲染 ===
description(): string
prompt?(): string
isEnabled?(): boolean
renderToolUseMessage?(input: Input): ToolUseMessage
}
// ToolUseContext — 工具执行的"上帝上下文"
export interface ToolUseContext {
options: GlobalOptions
abortController: AbortController
readFileState: ReadFileState
getAppState: () => AppState
setAppState: (updater: (state: AppState) => AppState) => void
messages: Message[]
agentId: string
queryTracking: QueryTracking
contentReplacementState: ContentReplacementState
}
// ToolResult<T> — 结构化返回值
export interface ToolResult<T = string> {
data: T
newMessages?: Message[]
contextModifier?: (context: SystemPromptContext) => SystemPromptContext
mcpMeta?: McpMeta
}// tools.ts — 工具注册中心(简化版)
// 1. 获取所有内置工具
export function getAllBaseTools(): Tool[] {
return [
new BashTool(),
new FileReadTool(),
new FileEditTool(),
new FileWriteTool(),
new GlobTool(),
new GrepTool(),
new LSPTool(),
new WebFetchTool(),
new WebSearchTool(),
new NotebookEditTool(),
new SkillTool(),
new AskUserQuestionTool(),
new ToolSearchTool(),
new TodoWriteTool(),
new EnterPlanModeTool(),
new ExitPlanModeV2Tool(),
// ... 42+ 工具实例
]
}
// 2. 按规则过滤不可用的工具
export function getTools(
denyRules: DenyRule[] = []
): Tool[] {
return getAllBaseTools().filter(tool => {
// 特性门控:编译时 DCE 可能已移除某些工具
if (tool.isEnabled && !tool.isEnabled()) return false
// 运行时禁用规则
if (denyRules.some(rule => matchTool(rule, tool.name))) {
return false
}
return true
})
}
// 3. 合并内置 + MCP 工具
export function assembleToolPool(
builtInTools: Tool[],
mcpTools: Tool[]
): Tool[] {
const allTools = [...builtInTools, ...mcpTools]
// 缓存稳定排序:确保相同的工具集合
// 总是产生相同的排序顺序
// 这对 API 缓存命中很重要
return stableSortTools(allTools)
}
function stableSortTools(tools: Tool[]): Tool[] {
// 内置工具保持注册顺序
// MCP 工具按名称字母序排列
// 合并时内置工具在前,MCP 在后
const builtIn = tools.filter(t => !t.isMcpTool)
const mcp = tools
.filter(t => t.isMcpTool)
.sort((a, b) => a.name.localeCompare(b.name))
return [...builtIn, ...mcp]
}// === 特性门控:编译时 DCE ===
// features.ts — 特性标志(编译时常量)
// Bun bundler 会在编译时将这些替换为字面量
export const FEATURE_MCP = true // MCP 工具支持
export const FEATURE_POWER_SHELL = false // PowerShell (仅 Windows)
export const FEATURE_WORKTREE = true // Git worktree 支持
// 在工具注册中使用
import { FEATURE_MCP, FEATURE_POWER_SHELL } from './features'
export function getAllBaseTools(): Tool[] {
const tools: Tool[] = [
new BashTool(),
new FileReadTool(),
// ... 核心工具
]
// 编译时 DCE:如果 FEATURE_POWER_SHELL = false,
// 整个分支(包括 PowerShellTool 的 import)被移除
if (FEATURE_POWER_SHELL) {
tools.push(new PowerShellTool())
}
// 编译时 DCE:MCP 相关代码仅在启用时保留
if (FEATURE_MCP) {
// MCP 工具在 assembleToolPool 中动态添加
}
return tools
}
// === 循环依赖破解:延迟 require() ===
// 问题:Tool → QueryLoop → Tool(循环)
// 解决方案:在函数体内延迟加载
export class SkillTool implements Tool {
async call(input: SkillInput, context: ToolUseContext) {
// 延迟 require,避免循环依赖
// 如果在文件顶层 import,模块初始化时
// QueryLoop 可能还未完成导出
const { runSubConversation } = require('../queryLoop')
return runSubConversation({
skill: input.skill,
messages: context.messages,
// ...
})
}
}
// 另一个常见模式:类型导入不会引起循环依赖
import type { QueryLoop } from '../queryLoop' // OK: 仅类型
// import { QueryLoop } from '../queryLoop' // 危险: 运行时导入互动
步进式流程演示
工具架构互动解析
第 1 步:理解 30+ 属性接口的设计意图
为什么一个工具接口需要 30+ 个属性?让我们从一个工具的"生命周期"来理解:
- 发现阶段 — Claude 需要知道有哪些工具可用(name, description, searchHint)
- 选择阶段 — Claude 决定使用哪个工具(inputJSONSchema 告诉 Claude 参数格式)
- 权限阶段 — 系统检查是否允许执行(checkPermissions, isReadOnly, isDestructive)
- 执行阶段 — 实际运行工具逻辑(call, validateInput, maxResultSizeChars)
- 渲染阶段 — 在 UI 中展示结果(renderToolUseMessage, userFacingName)
- 编排阶段 — 多工具并发调度(isConcurrencySafe, interruptBehavior)
每个阶段都需要工具提供不同的信息,这就是为什么接口如此"丰富"。
第 2 步:ToolUseContext 的"上帝对象"模式
ToolUseContext 包含了工具可能需要的所有上下文。这是一种务实的设计选择:
- 优点:新增工具时不需要修改调用方代码,只需要从 context 中取需要的数据
- 优点:工具签名统一,所有工具的 call 方法参数一致
- 缺点:工具可以访问它不需要的数据(如 BashTool 可以访问 readFileState)
- 权衡:在 42+ 工具的规模下,统一接口的生产力收益远大于松耦合的理论优势
第 3 步:assembleToolPool 的缓存稳定排序
为什么工具排序需要"缓存稳定"?这与 Anthropic API 的提示缓存有关:
- API 请求中的
tools数组是请求的一部分 - 如果工具顺序变化,整个请求的哈希变化,缓存失效
- 通过保证相同工具集合始终以相同顺序排列,缓存命中率大幅提高
- 这是一个典型的"看不见的优化"——用户无感知但节省大量 token 成本
第 4 步:编译时 DCE vs 运行时判断
传统做法是运行时 if (config.enableFeature),但 Claude Code 选择了编译时 DCE:
- 运行时判断:代码仍在最终产物中,只是不执行。增加包大小和攻击面
- 编译时 DCE:代码从最终产物中完全移除。包更小,不可能意外启用
- 代价:需要为不同特性集合构建不同的二进制文件
- 适用场景:OSS 版本 vs 内部版本等需要完全隔离的场景
第 5 步:循环依赖是如何产生的?
工具系统中循环依赖的典型案例:
- SkillTool 需要调用 QueryLoop 来运行子对话
- QueryLoop 需要 getTools() 来获取工具列表
- getTools() 实例化所有工具,包括 SkillTool
- 形成闭环:SkillTool → QueryLoop → getTools() → SkillTool
延迟 require() 打破了这个闭环:SkillTool 在定义时不导入 QueryLoop,只在 call() 被实际调用时才加载。此时所有模块都已完成初始化,不会得到 undefined。
关键设计洞察
- 接口驱动:30+ 属性的接口看似笨重,但确保了工具系统的统一性和可预测性
- 务实 > 纯洁:ToolUseContext 的"上帝对象"模式牺牲了耦合度,换取了开发效率
- 缓存意识:工具排序的稳定性直接影响 API token 成本
- 编译时安全:DCE 确保不同版本之间的特性隔离在物理层面不可逆