Skip to content

第6章 工具类型系统设计

在人工智能辅助编程的领域中,模型的能力上限往往不取决于其语言理解有多强,而取决于它能调用哪些工具、如何调用这些工具。Claude Code 的工具系统是整个产品的脊梁骨——它定义了模型与外部世界交互的全部边界。每一次文件读取、每一次命令执行、每一次代码编辑,在底层都是一次工具调用。

本章将深入 Claude Code 的工具类型系统,从核心类型定义出发,逐层剖析自描述工具模式、Zod 运行时校验、工具注册表、工具渲染机制以及并发安全标记。我们将看到一个精心设计的类型系统如何在保证灵活性的同时维持严格的类型安全,如何在支持 40 多个内建工具的同时保持架构的一致性。

本章要点

  • 核心类型 Tool:理解 Claude Code 工具系统的类型基石,包含 30 多个字段和方法的完整工具契约
  • 自描述工具模式:为什么选择"工具即对象"而非类继承,以及这一决策带来的架构优势
  • Zod 运行时校验inputSchema 如何同时服务于类型推导和运行时验证,lazySchema 的延迟初始化策略
  • 工具注册表getAllBaseTools() 中条件注册的设计,特性标志如何控制 40 多个工具的可用性
  • 工具渲染体系:六种渲染方法如何协同工作,React/Ink 组件如何呈现工具的使用过程和结果
  • 并发安全标记isConcurrencySafe 如何实现工具级别的并发控制,分区编排的实现原理

6.1 Tool 类型定义:工具系统的类型基石

Claude Code 的整个工具系统建立在一个核心类型之上:Tool。这个类型定义在 src/Tool.ts 中,是一个包含三个泛型参数的复合类型,它完整地描述了一个工具从输入验证到执行、从权限检查到 UI 渲染的全部行为契约。

6.1.1 Tool 类型的泛型签名

typescript
// 源码文件:src/Tool.ts

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  // ... 30+ 个字段和方法
}

三个泛型参数各有分工:

  • Input extends AnyObject:工具输入的 Zod schema 类型。AnyObjectz.ZodType<{ [key: string]: unknown }> 的别名,确保所有工具的输入都是对象类型。
  • Output:工具执行结果的类型,默认为 unknown,由具体工具自行约束。
  • P extends ToolProgressData:进度报告的数据类型,允许不同工具上报不同格式的进度信息。

这种泛型设计的精妙之处在于:所有泛型参数都有合理的默认值。这意味着在需要处理"任意工具"的通用代码中(如工具编排器、权限系统),可以直接使用 Tool 而不必关心具体的泛型参数;而在具体工具的实现中,又能获得完整的类型推导。

6.1.2 核心标识字段

每个工具都通过一组标识字段来声明自己的身份:

typescript
// 源码文件:src/Tool.ts

export type Tool<Input, Output, P> = {
  readonly name: string
  aliases?: string[]
  searchHint?: string
  // ...
}

name 是工具的唯一标识符,用于模型调用时的匹配。它被声明为 readonly,意味着一旦创建就不可修改——这是一个重要的不变量,因为工具名称贯穿了权限系统、日志追踪和 API 交互的方方面面。

aliases 是可选的别名数组,用于工具重命名时的向后兼容。当一个工具被重命名后,旧名称作为别名保留,确保历史对话中的工具调用不会失败。查找逻辑在 toolMatchesName 函数中实现:

typescript
// 源码文件:src/Tool.ts

export function toolMatchesName(
  tool: { name: string; aliases?: string[] },
  name: string,
): boolean {
  return tool.name === name || (tool.aliases?.includes(name) ?? false)
}

searchHint 是为 ToolSearch 特性准备的关键词提示。当工具数量超过阈值时,部分工具会被"延迟加载"(deferred),模型需要通过 ToolSearch 工具来发现它们。searchHint 提供了 3-10 个关键词,帮助模型通过语义搜索找到所需工具。例如 BashTool 的 searchHint'execute shell commands',GlobTool 的是 'find files by name pattern or wildcard'

6.1.3 输入与输出 Schema

typescript
// 源码文件:src/Tool.ts

export type Tool<Input, Output, P> = {
  readonly inputSchema: Input
  readonly inputJSONSchema?: ToolInputJSONSchema
  outputSchema?: z.ZodType<unknown>
  // ...
}

inputSchema 是基于 Zod 的输入定义,承担着双重职责:它既是运行时验证的基础,也是类型推导的来源。通过 z.infer<Input>,TypeScript 可以从 schema 自动推导出输入的类型,确保 callcheckPermissionsrenderToolUseMessage 等方法的参数类型与 schema 保持一致。

inputJSONSchema 是一个可选的 JSON Schema 表示。MCP(Model Context Protocol)工具会直接提供 JSON Schema 格式的输入定义,而不需要从 Zod 转换。这种双轨设计体现了对外部工具生态的兼容考量。

outputSchema 定义工具输出的结构。源码注释标注这个字段是 "Optional because TungstenTool doesn't define this",并计划在未来将其改为必选。

6.1.4 核心方法集

Tool 类型定义了一组覆盖工具全生命周期的方法。按功能可划分为以下几个层次:

执行层:

typescript
// 源码文件:src/Tool.ts

call(
  args: z.infer<Input>,
  context: ToolUseContext,
  canUseTool: CanUseToolFn,
  parentMessage: AssistantMessage,
  onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>

call 方法是工具执行的入口。它接收经过 Zod 验证的输入 args、执行上下文 context、权限校验函数 canUseTool、触发工具调用的助手消息 parentMessage,以及可选的进度回调 onProgress。返回值 ToolResult<Output> 不仅包含执行结果数据,还可以携带新消息和上下文修改器:

typescript
// 源码文件:src/Tool.ts

export type ToolResult<T> = {
  data: T
  newMessages?: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[]
  contextModifier?: (context: ToolUseContext) => ToolUseContext
  mcpMeta?: {
    _meta?: Record<string, unknown>
    structuredContent?: Record<string, unknown>
  }
}

contextModifier 允许工具在执行完成后修改后续工具的执行上下文。这个设计非常关键——源码注释明确指出 "contextModifier is only honored for tools that aren't concurrency safe",这意味着只有串行执行的工具才能修改上下文,避免并发修改导致的竞态条件。

描述层:

typescript
// 源码文件:src/Tool.ts

description(
  input: z.infer<Input>,
  options: {
    isNonInteractiveSession: boolean
    toolPermissionContext: ToolPermissionContext
    tools: Tools
  },
): Promise<string>

prompt(options: {
  getToolPermissionContext: () => Promise<ToolPermissionContext>
  tools: Tools
  agents: AgentDefinition[]
  allowedAgentTypes?: string[]
}): Promise<string>

descriptionprompt 方法共同构成了工具的自描述能力。prompt 方法生成发送给 Claude API 的工具描述文本,模型根据这些描述来决定何时以及如何调用工具。description 方法则提供面向上下文的简短描述。两者都是异步方法,因为描述内容可能依赖于运行时状态(如当前权限配置、可用工具列表等)。

验证层:

typescript
// 源码文件:src/Tool.ts

validateInput?(
  input: z.infer<Input>,
  context: ToolUseContext,
): Promise<ValidationResult>

checkPermissions(
  input: z.infer<Input>,
  context: ToolUseContext,
): Promise<PermissionResult>

validateInput 是可选的业务逻辑验证,在 Zod schema 验证通过之后执行。例如 BashTool 会在此检测被阻止的 sleep 模式。checkPermissions 是必选的权限检查方法,决定工具是否需要用户确认。

属性层:

typescript
// 源码文件:src/Tool.ts

isConcurrencySafe(input: z.infer<Input>): boolean
isEnabled(): boolean
isReadOnly(input: z.infer<Input>): boolean
isDestructive?(input: z.infer<Input>): boolean
interruptBehavior?(): 'cancel' | 'block'

这些布尔方法(或返回枚举的方法)描述了工具的静态属性。注意它们大多接收 input 参数——这意味着同一个工具在不同输入下可能有不同的属性。例如 BashTool 执行 ls 命令时是只读且并发安全的,而执行 rm 命令时则不是。这种输入敏感的属性系统是 Claude Code 工具编排的基础。

6.1.5 ToolUseContext:执行上下文

ToolUseContext 是传递给每个工具 call 方法的执行上下文,它是整个系统状态的一个横切面:

typescript
// 源码文件:src/Tool.ts(简化)

export type ToolUseContext = {
  options: {
    commands: Command[]
    tools: Tools
    mcpClients: MCPServerConnection[]
    thinkingConfig: ThinkingConfig
    // ...
  }
  abortController: AbortController
  readFileState: FileStateCache
  getAppState(): AppState
  setAppState(f: (prev: AppState) => AppState): void
  messages: Message[]
  setToolJSX?: SetToolJSXFn
  updateFileHistoryState: (updater: (prev: FileHistoryState) => FileHistoryState) => void
  updateAttributionState: (updater: (prev: AttributionState) => AttributionState) => void
  // ... 40+ 其他字段
}

ToolUseContext 之所以如此庞大,是因为它承担了"依赖注入容器"的角色。不同的工具需要访问不同的系统服务:BashTool 需要 abortController 来支持命令中断,FileEditTool 需要 readFileState 来检测文件修改冲突,AgentTool 需要完整的 options 来配置子代理。将这些依赖统一在上下文对象中,避免了各工具自行管理依赖的复杂性。

下图展示了 Tool 类型核心字段与方法的完整架构,三大泛型参数贯穿整个工具契约:

6.2 自描述工具模式

6.2.1 "工具即对象"的设计抉择

Claude Code 的工具实现采用了一种独特的模式:每个工具不是一个类的实例,而是一个满足 Tool 类型的普通对象字面量,通过 buildTool 工厂函数构建。这个决策与许多框架中常见的类继承模式形成了鲜明对比。

typescript
// 源码文件:src/tools/GlobTool/GlobTool.ts

export const GlobTool = buildTool({
  name: GLOB_TOOL_NAME,
  searchHint: 'find files by name pattern or wildcard',
  maxResultSizeChars: 100_000,
  async description() {
    return DESCRIPTION
  },
  get inputSchema(): InputSchema {
    return inputSchema()
  },
  isConcurrencySafe() {
    return true
  },
  isReadOnly() {
    return true
  },
  renderToolUseMessage,
  renderToolResultMessage,
  // ...
})

这种模式有几个深层的设计考量:

可组合性优于继承性。 不同工具之间的共性并非层次化的——GlobTool 和 GrepTool 共享渲染逻辑(GlobTool 直接复用了 GrepTool 的 renderToolResultMessage),但它们的执行逻辑完全不同。如果使用类继承,要么需要多重继承(TypeScript 不支持),要么需要复杂的 mixin 体系。对象字面量的方式允许自由地组合:你可以从其他模块导入任意方法并直接赋值。

树摇友好。 对象字面量比类实例更容易被打包工具优化。未使用的工具可以被 dead code elimination 完全移除。源码中大量使用的 feature()process.env 条件导入正是依赖这一特性。

类型推导更自然。 TypeScript 对对象字面量有"excess property checking"和更精确的类型收窄。buildTool 函数能够从传入的对象字面量中推导出具体的工具类型,而类继承模式下的类型推导往往需要更多样板代码。

6.2.2 buildTool 工厂函数

buildTool 是工具系统中的一个关键抽象层。它接收一个 ToolDef(部分定义),补充默认值,返回一个完整的 Tool

typescript
// 源码文件:src/Tool.ts

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false,
  isReadOnly: (_input?: unknown) => false,
  isDestructive: (_input?: unknown) => false,
  checkPermissions: (
    input: { [key: string]: unknown },
    _ctx?: ToolUseContext,
  ): Promise<PermissionResult> =>
    Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: (_input?: unknown) => '',
  userFacingName: (_input?: unknown) => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

默认值的设计遵循"安全关闭"(fail-closed)原则:

  • isConcurrencySafe 默认为 false——假设工具不是并发安全的,需要串行执行
  • isReadOnly 默认为 false——假设工具会产生写操作
  • isDestructive 默认为 false——这是少数默认为"宽松"的字段
  • checkPermissions 默认为 allow——将权限决策委托给通用权限系统

BuiltTool<D> 类型通过条件类型精确地反映了运行时的展开行为:如果定义 D 提供了某个方法,使用 D 的类型;否则使用默认值的类型。这确保了 60 多个工具定义都能通过 0 错误的类型检查,源码注释中特别提到:"The type semantics are proven by the 0-error typecheck across all 60+ tools."

6.2.3 自描述如何服务于模型

工具的自描述能力不仅是代码组织的需要,更直接影响着模型的行为。当 Claude Code 向 API 发送请求时,每个工具会被转换为 API 所需的工具定义格式:

typescript
// 源码文件:src/utils/api.ts(简化)

async function buildToolSchema(tool: Tool, options): Promise<BetaToolUnion> {
  let input_schema = (
    'inputJSONSchema' in tool && tool.inputJSONSchema
      ? tool.inputJSONSchema
      : zodToJsonSchema(tool.inputSchema)
  ) as Anthropic.Tool.InputSchema

  base = {
    name: tool.name,
    description: await tool.prompt({
      getToolPermissionContext: options.getToolPermissionContext,
      tools: options.tools,
      agents: options.agents,
    }),
    input_schema,
  }
  // ...
}

这里有一个值得关注的细节:工具的 prompt 方法(而非 description 方法)负责生成发送给 API 的描述文本。prompt 方法可以根据当前可用的工具列表、权限配置和代理定义来动态调整描述内容。例如 AgentTool 的 prompt 方法会根据可用的 MCP 服务器列表来调整其描述,告知模型可以创建哪些类型的子代理。

inputSchema 通过 zodToJsonSchema 函数转换为 JSON Schema 格式。Zod 的 .describe() 方法为每个字段添加的描述文本会被保留在 JSON Schema 中,成为模型理解工具参数的重要线索。例如 BashTool 的 command 参数描述为:

The command to execute. Must be a valid shell command.

description 参数则提供了更丰富的指导:

Clear, concise description of what this command does in active voice...
For simple commands, keep it brief (5-10 words).
For commands that are harder to parse at a glance, add enough context.

基于 VitePress 构建