内置工具全景

内置工具全景

Claude Code 的每个能力(读文件、跑命令、派子代理)都是一个 Tool<Input, Output> 实现,注册进同一张工具表。
工具多了之后要管三件事,对应三套机制:

  • 首轮 prompt 被几十份 JSONSchema 撑爆:defer + ToolSearch 懒加载。
  • 并发执行的安全性:isConcurrencySafe 分批调度。
  • 大输出冲爆上下文:maxResultSizeChars 落盘。
代码块TS · 42 行收起展开
// 架构解读,非源码转录。基于本地反混淆源 claude-code 2.1.88, src/tools/ 下 40 个工具目录。
// 每个工具都是 [01-Tool 抽象](/articles/sourcecode/claude-code/01-tool-抽象) 里 Tool<Input,Output>
// 的实现,经 buildTool 补默认值后由 src/tools.ts 的 getAllBaseTools() 汇入工具表。按职责分六类:

// ============ 一、文件读写 ============
FileReadTool      // 读文件/图片/PDF/notebook;输出被 maxTokens 自限,maxResultSizeChars=Infinity(见下)
FileEditTool      // 精确字符串替换;validateInput 查 readFileState,没 Read 过直接拒
FileWriteTool     // 覆盖写整个文件
NotebookEditTool  // 改 .ipynb 单元格;searchHint 里塞进名字里没有的 'Jupyter'

// ============ 二、搜索定位 ============
GlobTool  // 文件名模式;isSearchOrReadCommand()={isSearch:true},UI 据此折叠成一行
GrepTool  // 内容搜索,底层 utils/ripgrep.ts 起 rg 子进程(system/builtin/embedded 三种来源)
LSPTool   // 语言服务器(ENABLE_LSP_TOOL 门控);LSP 没初始化完时被临时 defer(shouldDeferLspTool)

// ============ 三、命令执行 ============
BashTool        // isConcurrencySafe 委托 isReadOnly:只读命令也能并发,写命令才串行
PowerShellTool  // Windows 下按 isPowerShellToolEnabled() 加载
REPLTool        // ant 内部构建才有;把 Read/Edit 等原语包进 VM,启用时原语从表里隐藏(REPL_ONLY_TOOLS)

// ============ 四、任务/进程(见 [05-任务与子代理](/articles/sourcecode/claude-code/05-任务与子代理)) ============
TaskCreateTool TaskGetTool TaskListTool TaskUpdateTool  // 任务清单 v2(isTodoV2Enabled 门控)
TaskOutputTool TaskStopTool  // 后台任务取输出/停止
AgentTool          // 派生子代理跑多步任务
ScheduleCronTool/  // 目录下实为 CronCreateTool/CronDeleteTool/CronListTool 三个工具(AGENT_TRIGGERS 门控)
SleepTool RemoteTriggerTool SyntheticOutputTool  // 均 feature() 门控;Synthetic 不进常规工具表

// ============ 五、协作 ============
SendMessageTool                // 跨 agent 消息
TeamCreateTool TeamDeleteTool  // agent 团队(isAgentSwarmsEnabled 门控)
AskUserQuestionTool            // 只有用户能定的决策抛回给用户
TodoWriteTool                  // 结果只更新面板不进 transcript,故整个工具没有 renderToolResultMessage

// ============ 六、扩展/外部 ============
WebFetchTool WebSearchTool
MCPTool ListMcpResourcesTool ReadMcpResourceTool McpAuthTool  // MCP 工具一律 defer(isDeferredTool)
SkillTool          // 调用打包好的技能
ToolSearchTool     // 把被 defer 的工具按名字/关键词捞出来(见下)
EnterPlanModeTool ExitPlanModeTool  // 计划模式;实际注册进表的是 ExitPlanModeV2Tool
EnterWorktreeTool ExitWorktreeTool  // git worktree 隔离(isWorktreeModeEnabled 门控)
ConfigTool BriefTool  // 配置(ant-only) / 简报
// (shared/ testing/ utils.ts 是工具间共用代码,不是工具)

工具接口上决定调度行为的几个成员(商业闭源,只录签名级片段):

代码块TS · 19 行收起展开
// 基于本地反混淆源 claude-code 2.1.88, src/Tool.ts
export type Tool<Input, Output> = {
  isConcurrencySafe(input: z.infer<Input>): boolean   // 按"这次输入"判定,不是工具级常量
  isReadOnly(input: z.infer<Input>): boolean
  maxResultSizeChars: number                          // 结果落盘阈值,超了只回预览+文件路径

  readonly shouldDefer?: boolean   // true→首轮不发 schema,要模型先用 ToolSearch 捞
  readonly alwaysLoad?: boolean    // 强制首轮带全 schema;MCP 工具经 _meta['anthropic/alwaysLoad'] 设
  searchHint?: string              // 3-10 词能力短语,给 ToolSearch 关键词打分用
  // ...
}

// 同文件 TOOL_DEFAULTS——buildTool 补的默认值,fail-closed:
const TOOL_DEFAULTS = {
  isConcurrencySafe: (_input?: unknown) => false,   // 忘实现→当不可并发,只慢不危险
  isReadOnly: (_input?: unknown) => false,          // 忘实现→当会写,按最严的审
  isDestructive: (_input?: unknown) => false,
  // ...
}

执行链路上的四个关键点:

代码块TS · 25 行收起展开
// 基于本地反混淆源 claude-code 2.1.88(签名级片段)
// src/services/tools/toolOrchestration.ts——相邻且都并发安全的调用合并成一批
type Batch = { isConcurrencySafe: boolean; blocks: ToolUseBlock[] }
parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10  // 并发上限,默认 10

// src/tools/BashTool/BashTool.tsx——Bash 的并发安全不是写死 false
const BashTool = buildTool({
  isConcurrencySafe(input) {
    return this.isReadOnly?.(input) ?? false;   // 静态解析命令:纯读命令可与其他读并发
  },
  // ...
})

// src/tools/FileReadTool/FileReadTool.ts——唯一设 Infinity 的落盘阈值,注释为原文
// Output is bounded by maxTokens (validateContentTokens). Persisting to a
// file the model reads back with Read is circular — never persist.
maxResultSizeChars: Infinity,

// src/tools/ToolSearchTool/prompt.ts——isDeferredTool 的判定顺序(中间省略若干特例)
if (tool.alwaysLoad === true) return false   // 显式豁免最优先
if (tool.isMcp === true) return true         // MCP 工具一律 defer
return tool.shouldDefer === true             // 内置工具看自己声明

// src/utils/toolResultStorage.ts——落盘后回给模型的预览大小
export const PREVIEW_SIZE_BYTES = 2000

原理串讲

一次典型调用:模型在一条 assistant 消息里连发三个 tool_use——两个 Read 加一个 Edit。
query.ts(主循环,见 03-agent 主循环)把这批 block 交给 src/services/tools/toolOrchestration.tsrunTools
第一步 partitionToolCalls:

  1. 逐个用 inputSchema.safeParse 解析输入。
  2. 再调 tool.isConcurrencySafe(parsedInput.data),相邻且都安全的合并进同一个 Batch

于是切成 [Read, Read][Edit] 两批。
前一批走 runToolsConcurrently,用 all() 把多个异步生成器合流,并发上限默认 10;后一批走 runToolsSerially
为什么只合并”相邻的”而不做全局重排?因为 tool_use 的先后是模型表达的执行语义:排在 Edit 之后的 Read 期望看到改完的文件,全局重排会悄悄破坏这种依赖——保序分批是并行收益和语义安全之间的折中。
另一处防御:isConcurrencySafe 本身抛异常(比如 Bash 的 shell-quote 解析失败)时,partitionToolCalls 按不安全处理,判定失败一律往串行一侧倒。

单个调用进 runToolUsecheckPermissionsAndCallTool(src/services/tools/toolExecution.ts),四道闸门依次过:

  1. zod 结构校验 inputSchema.safeParse——源码注释原话是 “surprisingly, the model is not great at generating valid input”。
  2. tool.validateInput,工具自定义的语义校验,FileEditTool 在这里查 readFileState,没 Read 过的文件直接回 “File has not been read yet”。
  3. runPreToolUseHooks 跑用户配置的 hook。
  4. resolveHookPermissionDecision 汇总 hook 结论并走 canUseTool 进权限系统(规则匹配/弹窗/auto 模式的分类器)。

全过了才执行 tool.call()

为什么 checkPermissions 的默认值反而是 allow?因为它只是工具的”补充意见”,真正把关的是通用权限系统(permissions.ts),工具级默认 allow 表示”我没有额外要求”;
isConcurrencySafe/isReadOnly 默认 false 是 fail-closed——两类默认值方向相反,取决于它是最后一道闸还是其中一道。

结果回程:tool.call 的输出经 mapToolResultToToolResultBlockParam 序列化成 tool_result,再过 processToolResultBlock
超过 getPersistenceThreshold(tool.name, tool.maxResultSizeChars) 就由 persistToolResult 写进结果目录。
模型只拿到 buildLargeToolResultMessage 拼的前 2000 字节预览(PREVIEW_SIZE_BYTES)加文件路径,要全量就自己再 Read。
Read 自己设 Infinity 豁免:它的输出已被 maxTokens 截断过,再落盘会形成 “Read → 超限落盘 → Read 那个文件 → 又超限落盘” 的循环。

懒加载这条线:isDeferredTool(src/tools/ToolSearchTool/prompt.ts)决定谁被 defer——

  • alwaysLoad 豁免最优先。
  • MCP 工具一律 defer。
  • ToolSearchTool 自己永不 defer(否则没人能捞别人)。

被 defer 的工具在 services/api/claude.ts 组请求时带 defer_loading: true(需要 tool-search beta 头),API 把它们的 schema 从 prompt 里剥掉。
模型只在 system-reminder(旧路径是 <available-deferred-tools> 块)里看到名字。
模型要用时调 ToolSearchTool:select:名字 直接选,或关键词搜——searchToolsWithKeywords 的打分是:

  • 工具名分词精确命中:内置 +10,MCP +12。
  • searchHint 命中:+4。
  • 描述命中:+2。

命中结果不是文本:mapToolResultToToolResultBlockParam 返回 tool_reference block,由服务端展开成完整 schema。
为什么绕这么一圈?几十个工具(MCP 一挂一大串)的完整 JSONSchema 能吃掉上万 token 的首轮 prompt,且 MCP 工具因人而异,会打碎跨用户的全局 prompt 缓存;名字占位 + 按需展开让 prompt 里只留真会被用的 schema。
同样为了缓存,assembleToolPool(src/tools.ts)把内置工具排序后放在 MCP 工具前面构成连续前缀——服务端在最后一个内置工具后打全局缓存断点,MCP 工具怎么变都冲不掉前缀缓存。

设计取舍

  • isConcurrencySafe 是按输入的函数,不是工具级布尔:Bash 委托给 isReadOnly,git status 能并发,git commit 串行。
    “Bash 一律串行”是误区。
  • 并行只合并相邻批:一个 Edit 夹在中间,前后的 Read 就被切成三批。想吃满并发,读操作要连着发。
  • 落盘阈值每工具一档:getPersistenceThreshold 按工具名可覆盖,Read 用 Infinity 整体逃逸该机制。
  • defer 不是白开的:tst-auto 模式下 isToolSearchEnabled 按被 defer 工具的 token/字符量过阈值才启用——工具少时懒加载反而多付一次 ToolSearch round-trip。
  • 工具表随构建与环境变:feature()/USER_TYPE/env 层层门控,两台机器的 getAllBaseTools() 可以不同;且该函数注释要求列表与服务端缓存配置保持同步,否则跨用户 system prompt 缓存失效。

架构总览见 Claude Code 索引,工具接口本身见 01-Tool 抽象

延伸阅读