Claude Code 工具调用机制详解

6 阅读10分钟

目录


1. 概述

Claude Code 的工具调用系统是一个完整链路:定义工具 → 注册工具 → Schema 序列化 → 发给模型 → 模型决定调用 → 解析执行 → 结果返回

整个过程的核心思路是:

  1. 所有工具(内置 + MCP)在每次 API 请求时通过 tools 参数发给模型
  2. System Prompt 中包含工具使用的指引(何时用哪个、怎么用)
  3. tool_choice: undefined 让模型自主决定是否调用工具、调哪个工具
  4. 模型输出 tool_use block 后,客户端执行并返回 tool_result
  5. 没有自动重试——失败信息返回给模型,由模型自行决定下一步

2. 工具定义体系

2.1 Tool 接口定义

文件:src/Tool.ts:366-695

每个工具实现 Tool 接口,核心字段和方法:

type Tool<Input, Output, P> = {
  name: string                          // 工具名称,唯一标识
  aliases?: string[]                    // 向后兼容的别名
  searchHint?: string                   // 工具搜索关键字提示

  // 核心方法
  call(args, context, canUseTool, ...): Promise<ToolResult<Output>>
  prompt(options): Promise<string>      // 返回工具的完整使用说明(作为 API description)
  description(input, options): Promise<string>  // 一句话描述

  // Schema
  readonly inputSchema: Input           // Zod schema,定义参数结构
  readonly inputJSONSchema?: ToolInputJSONSchema  // MCP 工具直接提供 JSON Schema

  // 执行控制
  isConcurrencySafe(input): boolean     // 是否可并行执行
  isReadOnly(input): boolean            // 是否是只读操作
  isEnabled(): boolean                  // 是否启用

  // 安全与校验
  validateInput?(input, context): Promise<ValidationResult>
  checkPermissions(input, context): Promise<PermissionResult>

  // 结果处理
  maxResultSizeChars: number            // 结果字符数上限
  mapToolResultToToolResultBlockParam(content, toolUseID): ToolResultBlockParam
  renderToolResultMessage?(...): React.ReactNode
  renderToolUseMessage?(...): React.ReactNode

  // 延迟加载
  readonly shouldDefer?: boolean        // 是否可延迟(ToolSearch 相关)
  readonly alwaysLoad?: boolean         // 是否始终加载
  readonly strict?: boolean             // 严格模式
}

2.2 buildTool 工厂函数

文件:src/Tool.ts:757-792

使用 buildTool() 创建工具实例,它会填充默认值:

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: () => false,    // 默认认为不安全(串行执行)
  isReadOnly: () => false,
  isDestructive: () => false,
  checkPermissions: () => ({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: () => '',
  userFacingName: () => name,
}

2.3 工具的 prompt() 方法——使用说明

每个工具的 prompt() 方法返回详细的使用说明,这些说明会被放入 API 的 description 字段。模型通过阅读这些说明来了解工具的用法。

示例:BashTool 的 prompt(src/tools/BashTool/prompt.ts,包含:

  • 工具的基本能力描述
  • 何时应避免使用 Bash(如果有专用工具)
  • 命令编写规范、超时限制
  • Git 操作的安全协议
  • Sandbox 配置和覆盖规则

让模型知道:"Bash 是通用执行工具,但优先用 Read/Edit/Write 等专用工具"

BashTool 是cc中最常见的一个工具调用,可以通过 BashTool 源码分析 了解cc是怎么管理自己的工具的。


3. 工具集合的组装

3.1 所有内置工具的注册

文件:src/tools.ts:193-251

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    NotebookEditTool,
    WebFetchTool,
    TodoWriteTool,
    WebSearchTool,
    TaskStopTool,
    AskUserQuestionTool,
    SkillTool,
    EnterPlanModeTool,
    BriefTool,
    ListMcpResourcesTool,
    ReadMcpResourceTool,
    // ... 条件性添加的工具(通过 feature flag / process.env 控制)
  ]
}

条件性添加的工具示例:

  • ConfigTool — 仅 USER_TYPE === 'ant'
  • EnterWorktreeTool / ExitWorktreeTool — worktree 模式启用时
  • TaskCreateTool / TaskGetTool / TaskUpdateTool / TaskListTool — todo v2 启用时
  • ToolSearchTool — ToolSearch 功能启用时

3.2 工具过滤与最终集合

文件:src/tools.ts:271-327src/tools.ts:345-367

getTools(permissionContext)
  → 根据 mode(simple/normal)筛选
  → 根据 deny rules 过滤
  → 根据 isEnabled() 过滤
  → 返回最终的内置工具列表

assembleToolPool(permissionContext, mcpTools)
  → getTools() + MCP 工具
  → uniqBy name(内置优先)
  → 排序(按名字保证 prompt cache 稳定)
  → 返回完整的工具池

3.3 工具的禁用列表

文件:src/constants/tools.ts

不同类型的 agent 可用的工具集不同:

ALL_AGENT_DISALLOWED_TOOLS      // 普通 sub-agent 不能用的工具
ASYNC_AGENT_ALLOWED_TOOLS       // 异步 agent 能用的工具
COORDINATOR_MODE_ALLOWED_TOOLS  // 协调者模式能用的工具

4. 工具 Schema 转换与 API 请求

4.1 toolToAPISchema——转为 API 格式

文件:src/utils/api.ts:119-266

async function toolToAPISchema(tool, options): Promise<BetaToolUnion> {
  // 1. 获取 JSON Schema(Zod → JSON Schema 或 直接使用 inputJSONSchema)
  let input_schema = tool.inputJSONSchema || zodToJsonSchema(tool.inputSchema)

  // 2. 过滤掉 swarm 相关字段(非 EAP 用户不可见)
  if (!isAgentSwarmsEnabled()) {
    input_schema = filterSwarmFieldsFromSchema(tool.name, input_schema)
  }

  // 3. 构建基础 schema
  const schema = {
    name: tool.name,
    description: await tool.prompt({...}),  // ← 工具使用说明就在这里
    input_schema,
    strict: tool.strict ? true : undefined,
    eager_input_streaming: true,  // 细粒度 tool streaming
  }

  // 4. 可选:defer_loading(ToolSearch 功能)
  if (options.deferLoading) {
    schema.defer_loading = true  // 模型调用前需要先发现此工具
  }

  // 5. 可选:cache_control(prompt caching)
  if (options.cacheControl) {
    schema.cache_control = options.cacheControl
  }

  return schema
}

关键的 Schema 转换路径:

Zod Schema(z.object({...}))
  → zodToJsonSchema()
  → JSON Schema({ type: "object", properties: {...} })
  → Anthropic API 的 BetaToolUnion

4.2 API 请求的构建

文件:src/services/api/claude.ts:1100-1396

工具过滤阶段(ToolSearch):

// 判断是否启用 ToolSearch
let useToolSearch = await isToolSearchEnabled(...)

// 若启用,只发送非延迟工具 + 已发现的延迟工具
if (useToolSearch) {
  const discoveredToolNames = extractDiscoveredToolNames(messages)
  filteredTools = tools.filter(tool => {
    if (!deferredToolNames.has(tool.name)) return true
    if (toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME)) return true
    return discoveredToolNames.has(tool.name)
  })
} else {
  // 不启用 ToolSearch,直接过滤掉 ToolSearchTool
  filteredTools = tools.filter(t => !toolMatchesName(t, TOOL_SEARCH_TOOL_NAME))
}

构建所有工具 Schema:

const toolSchemas = await Promise.all(
  filteredTools.map(tool =>
    toolToAPISchema(tool, {
      getToolPermissionContext: ...,
      tools,
      agents: ...,
      model: options.model,
      deferLoading: willDefer(tool),  // 是否延迟加载
    }),
  ),
)

const allTools = [...toolSchemas, ...extraToolSchemas]

最终 API 请求参数(claude.ts:1699-1728):

return {
  model: normalizeModelStringForAPI(options.model),
  messages: addCacheBreakpoints(messagesForAPI, ...),
  system,                          // System Prompt
  tools: allTools,                 // ← 所有工具的定义数组
  tool_choice: options.toolChoice, // ← undefined → auto(模型自主决定)
  betas: betasParams,
  max_tokens: maxOutputTokens,
  thinking,
  temperature,
}

4.3 传给模型的参数说明

模型看到的工具信息由三部分组成:

参数来源作用
tools[].nametool.name工具的唯一名称,模型通过此名称调用
tools[].descriptiontool.prompt() 的输出工具的使用说明,包含详细的用法、注意事项、示例
tools[].input_schematool.inputSchema 转换的 JSON Schema定义参数结构、类型、必填项
tools[].defer_loadingToolSearch 功能标记为 true 时,模型先要通过 ToolSearch 发现才能调用
tool_choiceundefined让模型自主决定是否调用工具(auto 模式)
system prompt 中"Using your tools"部分prompts.ts引导模型何时使用什么工具

5. 模型如何使用工具的引导机制

5.1 System Prompt 中的工具使用指引

文件:src/constants/prompts.ts:269-314

getUsingYourToolsSection() 生成的内容告诉模型:

# Using your tools
- 当有专用工具时不要用 Bash
- 读文件用 FileReadTool 而不是 cat/head/tail
- 编辑文件用 FileEditTool 而不是 sed/awk
- 创建文件用 FileWriteTool
- 搜索文件用 GlobTool 而不是 find/ls
- 内容搜索用 GrepTool 而不是 grep/rg
- 可以用 TaskCreate 管理任务
- 可以并行调用无依赖的工具

5.2 工具被拒绝后的行为指引

- 如果用户拒绝工具调用,不要重试完全相同的调用
- 思考为什么被拒绝并调整方法
- 可用 AskUserQuestionTool 询问用户原因

5.3 每个工具的独立指引

每个工具的 prompt() 方法提供针对性的使用说明。例如 BashTool 的说明中包含:

  • 执行的命令优先使用专用工具
  • Git 操作的安全规范
  • 如何管理超时
  • Sandbox 的限制和绕过方式

模型综合阅读 system prompt 中的通用指引 + 每个工具的私有关键规则字段,来判断什么场景用什么工具。

5.4 ToolSearch:延迟加载机制

文件:src/services/api/claude.ts:1120-1172src/tools/ToolSearchTool/prompt.ts

当工具数量很多时,部分工具设定 shouldDefer: true,其 Schema 不会随每次请求发送。模型需要通过 ToolSearch 工具来发现这些延迟工具:

  1. 初始请求中只包含非延迟工具 + ToolSearchTool
  2. 模型调用 ToolSearch(query: "select:toolName") 发现某个延迟工具
  3. 接下来的请求会包含该工具的完整 Schema
  4. 模型此时才能调用该工具

6. 工具调用的执行流程

6.1 主循环

文件:src/query.ts

用户消息 → 构建 API 请求(含 tools 参数)
  → 流式接收响应
    → 收集 tool_use blocks(query.ts:829-835)
      → 触发工具执行(query.ts:1382)
        → 工具结果作为 tool_result 返回
          → 继续下一轮循环(或结束)

关键代码(query.ts:829-835):

const msgToolUseBlocks = message.message.content.filter(
  content => content.type === 'tool_use',
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
  toolUseBlocks.push(...msgToolUseBlocks)
  needsFollowUp = true
}

6.2 工具编排

文件:src/services/tools/toolOrchestration.ts:19-82

async function* runTools(toolUseMessages, assistantMessages, canUseTool, toolUseContext) {
  // 1. 按并发安全性分区
  for (const { isConcurrencySafe, blocks } of partitionToolCalls(toolUseMessages)) {
    if (isConcurrencySafe) {
      // 读操作 → 并行执行(最多 MAX_TOOL_USE_CONCURRENCY=10 个)
      yield* runToolsConcurrently(blocks, ...)
    } else {
      // 写操作 → 串行执行
      yield* runToolsSerially(blocks, ...)
    }
  }
}

6.3 单个工具执行

文件:src/services/tools/toolExecution.ts:337-490

runToolUse(toolUse, assistantMessage, canUseTool, toolUseContext)
  │
  ├─ 1. findToolByName() 查找工具实例
  │     └─ 未找到 → 返回 "No such tool available" 错误
  │
  ├─ 2. 检查是否已取消(abortController)
  │     └─ 已取消 → 返回取消消息
  │
  └─ 3. checkPermissionsAndCallTool()
        ├─ a. parsedInput = tool.inputSchema.safeParse(input)
        │     └─ 校验失败 → 返回 InputValidationError
        │
        ├─ b. tool.validateInput(input, context)
        │     └─ 校验失败 → 返回校验错误
        │
        ├─ c. runPreToolUseHooks() 运行预执行钩子
        │
        ├─ d. checkPermissions() → 权限检查
        │     └─ 被拒绝 → 返回权限错误
        │
        ├─ e. tool.call(input, context, ...) 真正执行工具
        │     └─ 成功 → mapToolResultToToolResultBlockParam()
        │
        ├─ f. runPostToolUseHooks() 运行后置钩子
        │
        └─ g. 返回 tool_result + 可能的额外消息

6.4 调用时机

模型调用的"时机"由以下三要素决定:

  1. API 参数 tool_choice: undefined:让模型自主决定何时调用工具
  2. 模型的训练和推理:模型根据用户请求和对话上下文,判断是否需要调用工具
  3. System Prompt 中的指引:告诉模型在特定场景下优先使用特定工具

从架构层面看,调用时机分为两个层面:

  • 业务层面:模型决定调用工具(在 API 响应中输出 tool_use block)
  • 执行层面:客户端收到 tool_use block 后立即执行(query.ts 流式处理)

7. 工具调用失败的兜底逻辑

7.1 核心哲学:不自动重试

Claude Code 对工具调用失败 不做客户端自动重试。失败信息通过 is_error: truetool_result 返回给模型,由模型自行决定如何处理。

7.2 各场景的失败处理

场景 1:模型调用了不存在的工具

文件:src/services/tools/toolExecution.ts:368-410

if (!tool) {
  yield {
    message: createUserMessage({
      content: [{
        type: 'tool_result',
        content: `<tool_use_error>Error: No such tool available: ${toolName}</tool_use_error>`,
        is_error: true,
        tool_use_id: toolUse.id,
      }],
    }),
  }
  return  // 返回给模型,模型决定下一步
}

场景 2:参数校验失败(Zod)

文件:src/services/tools/toolExecution.ts:615-679

const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
  let errorContent = formatZodValidationError(tool.name, parsedInput.error)
  // 如果是延迟工具且不在已发现集合中,附加提示让模型先用 ToolSearch 加载
  const schemaHint = buildSchemaNotSentHint(tool, messages, tools)
  if (schemaHint) errorContent += schemaHint
  // 返回 InputValidationError
}

场景 3:自定义校验失败

文件:src/services/tools/toolExecution.ts:682-732

const isValidCall = await tool.validateInput?.(parsedInput.data, toolUseContext)
if (isValidCall?.result === false) {
  // 返回工具自定义的校验错误信息
}

场景 4:执行时抛异常

文件:src/services/tools/toolExecution.ts:469-489

catch (error) {
  const errorMessage = error instanceof Error ? error.message : String(error)
  yield {
    message: createUserMessage({
      content: [{
        type: 'tool_result',
        content: `<tool_use_error>Error calling tool (${tool.name}): ${errorMessage}</tool_use_error>`,
        is_error: true,
        tool_use_id: toolUse.id,
      }],
    }),
  }
}

场景 5:权限被拒绝

文件:src/services/tools/toolExecution.ts:995-1103

permissionDecision.behavior !== 'allow' 时,返回权限拒绝错误。特定情况(auto mode 下的 classifier 拒绝)会运行 PermissionDenied hooks,如果 hook 返回 retry: true,会在结果中附带提示让模型重试。

7.3 system prompt 中对工具失败的指引

文件:src/constants/prompts.ts

- If the user denies a tool call, do not re-attempt the exact same tool call.
  Instead, think about why the user has denied the tool call and adjust your approach.
- If you do not understand why the user has denied a tool call, use AskUserQuestionTool to ask them.

7.4 关于重试的总结

失败类型客户端自动重试模型收到后行为
工具不存在模型自行决定(换工具或报告错误)
参数校验失败模型自行决定(修正参数重试)
执行异常模型自行决定(换方法或报告错误)
权限被拒绝模型不应重试相同调用,需调整方法
API 网络错误✅(withRetry)claude.ts 中有 API 级别的重试机制,与工具调用无关
流错误(413等)✅(reactive compact)进行上下文压缩后重试,也是 API 级别而非工具级别

8. 完整链路图示

┌─────────────────────────────────────────────────────────────────┐
│ 1. 工具定义(Tool.ts)                                          │
│    每个工具实现 Tool 接口:name, prompt(), inputSchema, call()   │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. 工具注册(tools.ts)                                          │
│    getAllBaseTools() + MCP 工具 → assembleToolPool()             │
│    按 deny rules / isEnabled() / mode 过滤                       │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Schema 转换(api.ts)                                         │
│    Zod → JSON Schema                                           │
│    tool.prompt() → description 字段                             │
│    添加 defer_loading(可选)                                    │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. API 请求(claude.ts:1699)                                    │
│    POST /v1/messages                                            │
│    { tools: allTools, tool_choice: undefined, ... }             │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. 模型处理                                                    │
│    读取 tools[].description → 理解工具用途                       │
│    读取 tools[].input_schema → 理解参数结构                      │
│    读取 system prompt 中的使用指引                               │
│    自主决定是否调用、调用哪个工具                                  │
│    输出 tool_use block                                          │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 6. 流式接收(query.ts)                                          │
│    收集 tool_use blocks                                         │
│    stream → assistantMessages                                    │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 7. 工具编排(toolOrchestration.ts)                              │
│    partitionToolCalls() → 读并行 / 写串行                        │
│    runToolsConcurrently / runToolsSerially                       │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 8. 工具执行(toolExecution.ts)                                  │
│    findToolByName → 校验输入 → 权限检查 → 执行 → hooks           │
│    失败时返回 is_error: true 的 tool_result                      │
└──────────────────────────┬──────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────────┐
│ 9. 结果返回                                                     │
│    tool_result → 作为 user message 加入消息历史                   │
│    API 再次请求(含新的消息)→ 模型处理结果 → 继续或结束            │
└─────────────────────────────────────────────────────────────────┘

关键文件索引

文件功能
src/Tool.tsTool 接口定义、buildTool 工厂函数、findToolByName
src/tools.ts工具组装、过滤、getTools / assembleToolPool
src/constants/tools.ts各类 agent 的工具黑/白名单
src/constants/prompts.tsSystem Prompt 中工具使用指引的生成
src/utils/api.tstoolToAPISchema、Schema 转换
src/services/api/claude.tsAPI 请求构建、工具过滤(ToolSearch)
src/services/tools/toolOrchestration.ts工具编排(并行/串行执行)
src/services/tools/toolExecution.ts工具执行核心逻辑、错误处理
src/query.ts主查询循环、流式响应处理
src/tools/BashTool/prompt.tsBashTool 使用说明(prompt 示例)