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+ 其他字段
}

基于 VitePress 构建