第2章:一切皆工具

1 阅读12分钟

第2章:一切皆工具

引言

在 Unix 哲学中,有一个著名的格言:"Do One Thing Well"(做好一件事)。每个工具都应该专注于单一职责,通过管道和组合来构建复杂的功能。grep 负责搜索,sed 负责替换,awk 负责文本处理——每个工具都是一个小而美的零件。

Claude Code 继承并扩展了这种哲学,将"工具"的概念提升到了新的高度。在 Claude Code 中,不仅文件操作、命令执行是工具,就连 AI 本身的推理能力也被封装成了工具。更令人惊叹的是,还存在"元工具"——用工具来创建工具。

这一章将深入剖析 Claude Code 的工具系统,理解其核心设计思想:一切皆工具。我们将从工具接口定义开始,逐步探索工具的五大核心属性、工厂方法设计,以及元工具的概念,最后对比 Unix 哲学与 Claude Code 工具哲学的异同。

工具接口:泛型设计的艺术

让我们首先看看 Claude Code 中工具的核心接口定义。从 src/Tool.ts 中,我们可以看到工具接口的精妙设计:

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  aliases?: string[]
  searchHint?: string
  call(
    args: z.infer<Input>,
    context: ToolUseContext,
    canUseTool: CanUseToolFn,
    parentMessage: AssistantMessage,
    onProgress?: ToolCallProgress<P>,
  ): Promise<ToolResult<Output>>
  description(
    input: z.infer<Input>,
    options: {
      isNonInteractiveSession: boolean
      toolPermissionContext: ToolPermissionContext
      tools: Tools
    },
  ): Promise<string>
  readonly inputSchema: Input
  readonly inputJSONSchema?: ToolInputJSONSchema
  outputSchema?: z.ZodType<unknown>
  inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
  isConcurrencySafe(input: z.infer<Input>): boolean
  isEnabled(): boolean
  isReadOnly(input: z.infer<Input>): boolean
  isDestructive?(input: z.infer<Input>): boolean
  interruptBehavior?(): 'cancel' | 'block'
  isSearchOrReadCommand?(input: z.infer<Input>): {
    isSearch: boolean
    isRead: boolean
    isList?: boolean
  }
  isOpenWorld?(input: z.infer<Input>): boolean
  requiresUserInteraction?(): boolean
  isMcp?: boolean
  isLsp?: boolean
  readonly shouldDefer?: boolean
  readonly alwaysLoad?: boolean
  mcpInfo?: { serverName: string; toolName: string }
  readonly name: string
  maxResultSizeChars: number
  readonly strict?: boolean
  backfillObservableInput?(input: Record<string, unknown>): void
  validateInput?(
    input: z.infer<Input>,
    context: ToolUseContext,
  ): Promise<ValidationResult>
  checkPermissions(
    input: z.infer<Input>,
    context: ToolUseContext,
  ): Promise<PermissionResult>
  getPath?(input: z.infer<Input>): string
  preparePermissionMatcher?(
    input: z.infer<Input>,
  ): Promise<(pattern: string) => boolean>
  prompt(options: {
    getToolPermissionContext: () => Promise<ToolPermissionContext>
    tools: Tools
    agents: AgentDefinition[]
    allowedAgentTypes?: string[]
  }): Promise<string>
  userFacingName(input: Partial<z.infer<Input>> | undefined): string
  userFacingNameBackgroundColor?(
    input: Partial<z.infer<Input>> | undefined,
  ): keyof Theme | undefined
  isTransparentWrapper?(): boolean
  getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null
  getActivityDescription?(
    input: Partial<z.infer<Input>> | undefined,
  ): string | null
  toAutoClassifierInput(input: z.infer<Input>): unknown
  mapToolResultToToolResultBlockParam(
    content: Output,
    toolUseID: string,
  ): ToolResultBlockParam
  renderToolResultMessage?(
    content: Output,
    progressMessagesForMessage: ProgressMessage<P>[],
    options: {
      style?: 'condensed'
      theme: ThemeName
      tools: Tools
      verbose: boolean
      isTranscriptMode?: boolean
      isBriefOnly?: boolean
      input?: unknown
    },
  ): React.ReactNode
  extractSearchText?(out: Output): string
}

这个接口定义展示了几个重要的设计思想:

泛型的三层抽象

Tool<Input, Output, Progress> 使用了三个泛型参数,分别代表:

  1. Input:工具的输入类型,必须是 Zod schema 的子类型
  2. Output:工具的输出类型,可以是任意类型
  3. Progress:工具执行过程中的进度数据类型

这种三层抽象使得工具接口既灵活又类型安全。输入通过 Zod schema 进行验证,输出可以是任意结构,进度数据提供了执行过程的可见性。

可选方法的策略模式

接口中大量使用了可选方法(?),这体现了策略模式的思想。不同的工具可以根据需要实现不同的方法组合。例如:

  • isReadOnly()isDestructive() 用于权限控制
  • isSearchOrReadCommand() 用于 UI 优化
  • renderToolResultMessage() 用于自定义渲染

这种设计使得工具系统具有极强的扩展性,新工具可以只实现需要的方法,而不必处理所有可能的场景。

上下文感知的工具行为

工具的行为不是固定的,而是依赖于上下文。ToolUseContext 类型提供了丰富的上下文信息:

export type ToolUseContext = {
  options: {
    commands: Command[]
    debug: boolean
    mainLoopModel: string
    tools: Tools
    verbose: boolean
    thinkingConfig: ThinkingConfig
    mcpClients: MCPServerConnection[]
    mcpResources: Record<string, ServerResource[]>
    isNonInteractiveSession: boolean
    agentDefinitions: AgentDefinitionsResult
    maxBudgetUsd?: number
    customSystemPrompt?: string
    appendSystemPrompt?: string
    querySource?: QuerySource
    refreshTools?: () => Tools
  }
  abortController: AbortController
  readFileState: FileStateCache
  getAppState(): AppState
  setAppState(f: (prev: AppState) => AppState): void
  setAppStateForTasks?: (f: (prev: AppState) => AppState) => void
  handleElicitation?: (
    serverName: string,
    params: ElicitRequestURLParams,
    signal: AbortSignal,
  ) => Promise<ElicitResult>
  setToolJSX?: SetToolJSXFn
  addNotification?: (notif: Notification) => void
  // ... 更多上下文信息
}

这种上下文感知设计使得工具可以根据运行时环境调整行为,提供了极大的灵活性。

工具的五大核心属性

虽然工具接口定义了数十个方法,但其中有五个核心属性构成了工具的本质:

1. inputSchema:输入的契约

inputSchema 定义了工具的输入契约,使用 Zod schema 进行类型验证。这不仅提供了类型安全,还使得 AI 能够理解工具的输入要求。

让我们看看 BashTool 的输入 schema 定义:

import { z } from 'zod/v4';

// 虽然完整的 BashTool schema 没有在读取的片段中显示,
//但从导入可以看出它使用 Zod schema 定义输入结构

Zod schema 的优势在于:

  • 类型推导z.infer<Input> 可以自动推导出 TypeScript 类型
  • 运行时验证:在执行前验证输入,防止错误
  • 描述性元数据:通过 .describe() 方法添加人类可读的描述

2. call():工具的核心逻辑

call() 方法是工具的核心执行逻辑,它接收输入参数和上下文,返回工具结果:

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

这个方法的签名揭示了几个重要设计决策:

  • 异步执行:所有工具调用都是异步的,支持长时间运行的操作
  • 权限检查:通过 canUseTool 回调进行权限验证
  • 进度反馈:通过 onProgress 回调提供执行进度
  • 上下文传递:接收完整的 ToolUseContext,可以访问全局状态

3. checkPermissions():安全的守门人

checkPermissions() 方法实现了工具级别的权限控制:

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

这个方法在工具执行前被调用,决定了是否允许工具执行。它接收输入和上下文,返回权限检查结果。

权限系统的设计体现了"安全第一"的原则。在 AI 编程助手中,工具可能会执行危险操作(如删除文件、发送网络请求),因此必须有严格的权限控制。

4. isReadOnly():行为的分类

isReadOnly() 方法用于判断工具是否是只读操作:

isReadOnly(input: z.infer<Input>): boolean

这个方法有几个重要用途:

  • 权限控制:只读操作可能不需要用户确认
  • 并发控制:只读操作可以并发执行
  • UI 优化:只读操作可以以不同的方式展示

与之配套的是 isDestructive() 方法,用于标记具有破坏性的操作:

isDestructive?(input: z.infer<Input>): boolean

这种分类设计使得系统可以根据操作的性质采取不同的策略。

5. renderToolResultMessage():结果的呈现

renderToolResultMessage() 方法负责将工具结果渲染为用户界面:

renderToolResultMessage?(
  content: Output,
  progressMessagesForMessage: ProgressMessage<P>[],
  options: {
    style?: 'condensed'
    theme: ThemeName
    tools: Tools
    verbose: boolean
    isTranscriptMode?: boolean
    isBriefOnly?: boolean
    input?: unknown
  },
): React.ReactNode

这个方法体现了几个重要设计思想:

  • 可定制性:工具可以自定义结果呈现方式
  • 主题支持:支持不同的主题和样式
  • 上下文感知:根据运行时环境调整呈现方式
  • 可选性:不是所有工具都需要自定义渲染

工具注册中心:集中管理的设计

src/tools.ts 展示了工具的集中管理模式:

import { toolMatchesName, type Tool, type Tools } from './Tool.js'
import { AgentTool } from './tools/AgentTool/AgentTool.js'
import { SkillTool } from './tools/SkillTool/SkillTool.js'
import { BashTool } from './tools/BashTool/BashTool.js'
import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
import { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
import { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
import { GlobTool } from './tools/GlobTool/GlobTool.js'
import { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool.js'
import { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
import { TaskStopTool } from './tools/TaskStopTool/TaskStopTool.js'
import { BriefTool } from './tools/BriefTool/BriefTool.js'
// ... 更多工具导入

这种集中管理的设计有几个优势:

1. 统一的发现机制

所有工具都在一个地方注册,使得工具的发现和查找变得简单。getAllBaseTools() 函数返回所有可用的工具:

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    // ... 更多工具
  ]
}

2. 条件导入和死代码消除

通过条件导入,可以实现死代码消除,减少最终包的大小:

/* eslint-disable @typescript-eslint/no-require-imports */
const REPLTool =
  process.env.USER_TYPE === 'ant'
    ? require('./tools/REPLTool/REPLTool.js').REPLTool
    : null
const SuggestBackgroundPRTool =
  process.env.USER_TYPE === 'ant'
    ? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
        .SuggestBackgroundPRTool
    : null
/* eslint-enable @typescript-eslint/no-require-imports */

3. 工具预设

通过工具预设,可以方便地管理不同场景下的工具集合:

export const TOOL_PRESETS = ['default'] as const

export type ToolPreset = (typeof TOOL_PRESETS)[number]

export function parseToolPreset(preset: string): ToolPreset | null {
  const presetString = preset.toLowerCase()
  if (!TOOL_PRESETS.includes(presetString as ToolPreset)) {
    return null
  }
  return presetString as ToolPreset
}

export function getToolsForDefaultPreset(): string[] {
  const tools = getAllBaseTools()
  const isEnabled = tools.map(tool => tool.isEnabled())
  return tools.filter((_, i) => isEnabled[i]).map(tool => tool.name)
}

元工具:用工具创建工具

Claude Code 最令人惊叹的设计之一是"元工具"的概念——用工具来创建工具。AgentTool 就是这样一个元工具。

让我们看看 AgentTool 的输入定义:

const baseInputSchema = lazySchema(() => z.object({
  description: z.string().describe('A short (3-5 word) description of the task'),
  prompt: z.string().describe('The task for the agent to perform'),
  subagent_type: z.string().optional().describe('The type of specialized agent to use for this task'),
  model: z.enum(['sonnet', 'opus', 'haiku']).optional().describe("Optional model override for this agent. Takes precedence over the agent definition's model frontmatter. If omitted, uses the agent definition's model, or inherits from the parent."),
  run_in_background: z.boolean().optional().describe('Set to true to run this agent in the background. You will be notified when it completes.')
}));

这个定义展示了元工具的几个关键特征:

1. 任务描述而非参数列表

元工具的输入不是具体的参数,而是对任务的自然语言描述。这使得 AI 可以根据语义理解动态创建工具。

2. 灵活的配置选项

通过 subagent_typemodel 等选项,可以灵活配置子代理的行为。这种设计使得元工具具有极强的适应性。

3. 后台执行支持

run_in_background 选项支持后台执行,使得元工具可以处理长时间运行的任务。

元工具的概念体现了几个重要的设计思想:

递归的工具定义

工具可以创建工具,工具可以调用工具,形成了一个递归的工具系统。这种递归性使得系统具有极强的扩展性。

动态的工具组合

不同于传统工具的静态组合,元工具可以根据任务动态创建和组合工具。这种动态性使得系统能够应对各种复杂的场景。

语义驱动的工具选择

元工具根据任务的语义描述选择合适的子工具,而不是根据固定的规则。这种语义驱动的方式更加灵活和智能。

Unix 哲学 vs Claude Code 工具哲学

Unix 哲学强调"Do One Thing Well",每个工具专注于单一职责。Claude Code 的工具哲学继承了这个思想,同时又有自己的创新。

相似之处

1. 单一职责原则

Claude Code 的工具也遵循单一职责原则。BashTool 负责执行命令,FileReadTool 负责读取文件,AgentTool 负责创建子代理——每个工具都有明确的职责。

2. 可组合性

Claude Code 的工具也可以组合使用,形成复杂的操作流。虽然不像 Unix 管道那样通过文本流组合,但通过 AI 的推理能力,工具可以智能地组合和调用。

3. 文本为中心

Unix 工具以文本为中心,Claude Code 的工具也大量处理文本(代码、文档、日志)。这种文本为中心的设计使得工具具有通用性。

不同之处

1. 从确定性到概率性

Unix 工具的行为是确定性的,Claude Code 的工具具有概率性。同样的输入,Unix 工具总是产生相同的输出,而 Claude Code 的工具可能会根据上下文和推理产生不同的结果。

2. 从参数到语义

Unix 工具通过参数定义接口,Claude Code 的工具通过语义描述定义接口。这种从参数到语义的转变,使得工具更加灵活和易用。

3. 从静态到动态

Unix 工具的行为是静态的,Claude Code 的工具行为是动态的。工具可以根据上下文调整行为,提供更智能的响应。

4. 从工具到元工具

Unix 没有元工具的概念,Claude Code 引入了元工具,可以用工具创建工具。这种递归的工具定义极大地扩展了系统的能力。

设计启示:工具系统的核心原则

通过对 Claude Code 工具系统的分析,我们可以总结出几个重要的设计原则:

1. 类型安全与灵活性的平衡

通过 Zod schema 和 TypeScript 泛型,Claude Code 实现了类型安全与灵活性的平衡。类型安全保证了代码的可靠性,灵活性使得系统具有适应性。

2. 可扩展性优于完备性

工具接口定义了大量可选方法,使得新工具可以根据需要实现不同的功能组合。这种设计优先考虑可扩展性,而不是试图预先定义所有可能的功能。

3. 上下文感知的设计

工具的行为依赖于上下文,而不是固定的规则。这种上下文感知的设计使得系统能够适应各种复杂的场景。

4. 安全第一的原则

通过权限检查、只读标记、破坏性操作标记等机制,Claude Code 实现了多层次的安全保障。这种安全第一的设计在 AI 编程助手中尤为重要。

5. 渐进式的复杂度

从简单的工具(如文件读取)到复杂的工具(如 AgentTool),Claude Code 提供了渐进式的复杂度。开发者可以从简单的工具开始,逐步构建复杂的功能。

思考题

  1. 在你的项目中,哪些功能可以抽象为工具?如何设计工具的输入和输出?

  2. 元工具的概念对你的项目有什么启发?你能否用工具创建工具?

  3. 如何平衡工具的灵活性和安全性?在什么情况下应该限制工具的能力?

  4. Unix 哲学"Do One Thing Well"在现代软件设计中还有意义吗?如何与 AI 能力结合?

  5. 工具的上下文感知设计对你的项目有什么启发?如何设计上下文传递机制?

小结

"一切皆工具"不仅是一个口号,更是 Claude Code 工具系统的核心设计哲学。从文件操作到命令执行,从 AI 推理到工具创建,一切都是工具。

通过泛型接口、可选方法、上下文感知等设计模式,Claude Code 构建了一个既灵活又强大的工具系统。这个系统继承了 Unix 哲学的精髓,同时融入了 AI 的智能推理能力,创造了一种全新的工具范式。

元工具的概念尤其令人深思:用工具创建工具,用代码生成代码,用智能构建智能。这种递归的设计不仅扩展了系统的能力,更体现了编程的本质——通过抽象和组合,构建无限的可能。

在下一章中,我们将探讨 Claude Code 的查询引擎,理解 AI 如何通过工具系统理解用户意图并执行复杂任务。