内置工具全景
内置工具全景
Claude Code 的每个能力(读文件、跑命令、派子代理)都是一个 Tool<Input, Output> 实现,注册进同一张工具表。
工具多了之后要管三件事,对应三套机制:
- 首轮 prompt 被几十份 JSONSchema 撑爆:defer + ToolSearch 懒加载。
- 并发执行的安全性:isConcurrencySafe 分批调度。
- 大输出冲爆上下文:maxResultSizeChars 落盘。
代码块收起展开
// 架构解读,非源码转录。基于本地反混淆源 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 是工具间共用代码,不是工具)工具接口上决定调度行为的几个成员(商业闭源,只录签名级片段):
代码块收起展开
// 基于本地反混淆源 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,
// ...
}执行链路上的四个关键点:
代码块收起展开
// 基于本地反混淆源 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.ts 的 runTools。
第一步 partitionToolCalls:
- 逐个用
inputSchema.safeParse解析输入。 - 再调
tool.isConcurrencySafe(parsedInput.data),相邻且都安全的合并进同一个Batch。
于是切成 [Read, Read] 和 [Edit] 两批。
前一批走 runToolsConcurrently,用 all() 把多个异步生成器合流,并发上限默认 10;后一批走 runToolsSerially。
为什么只合并”相邻的”而不做全局重排?因为 tool_use 的先后是模型表达的执行语义:排在 Edit 之后的 Read 期望看到改完的文件,全局重排会悄悄破坏这种依赖——保序分批是并行收益和语义安全之间的折中。
另一处防御:isConcurrencySafe 本身抛异常(比如 Bash 的 shell-quote 解析失败)时,partitionToolCalls 按不安全处理,判定失败一律往串行一侧倒。
单个调用进 runToolUse → checkPermissionsAndCallTool(src/services/tools/toolExecution.ts),四道闸门依次过:
- zod 结构校验
inputSchema.safeParse——源码注释原话是 “surprisingly, the model is not great at generating valid input”。 tool.validateInput,工具自定义的语义校验,FileEditTool 在这里查readFileState,没 Read 过的文件直接回 “File has not been read yet”。runPreToolUseHooks跑用户配置的 hook。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 抽象。