第 06 章:工具的注册与发现

个人公众号

源码验证日期:2026-05-15,基于 commit 0d81bb6

上一章追踪了 system prompt 的构建。在 API 调用之前,还有一项关键准备工作:工具列表的组装。模型能使用哪些工具、每个工具接受什么参数、什么时候可以并行执行——这些都在注册阶段决定。


路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
graph LR
CH03["③ 准备工具箱"] --> CH04["④ 回车键之后"]
CH04 --> CH05["⑤ 消息被装进信封"]
CH05 --> CH06["⑥ 工具的注册与发现<br/>⬅ 你在这里"]
CH06 --> CH07["⑦ 信封飞向远方"]
CH07 --> CH08["⑧ 文字一个字一个字地回来"]
CH08 --> CH09["⑨ AI说要执行命令"]
CH09 --> CH10["⑩ 命令真的被执行了"]
CH10 --> CH11["⑪ 你确定吗"]
CH11 --> CH12["⑫ 结果回到AI手中"]
CH12 --> CH13["⑬ 对话越来越长"]
CH13 --> CH14["⑭ 屏幕上的每一帧"]
CH14 --> CH15["⑮ 循环的终点与起点"]
CH15 --> CH16["⑯ 你的第一次追踪"]

style CH06 fill:#4CAF50,color:#fff,stroke:#333
style CH03 fill:#e8f5e9,stroke:#333
style CH04 fill:#e8f5e9,stroke:#333
style CH05 fill:#e8f5e9,stroke:#333
style CH07 fill:#e1f5fe,stroke:#333
style CH08 fill:#e1f5fe,stroke:#333
style CH09 fill:#e1f5fe,stroke:#333
style CH10 fill:#e1f5fe,stroke:#333
style CH11 fill:#e1f5fe,stroke:#333
style CH12 fill:#e1f5fe,stroke:#333
style CH13 fill:#e1f5fe,stroke:#333
style CH14 fill:#e1f5fe,stroke:#333
style CH15 fill:#e1f5fe,stroke:#333
style CH16 fill:#e1f5fe,stroke:#333

知识补全:Zod Schema 基础

Zod 是一个 TypeScript 优先的 schema 验证库。Claude Code 用它定义工具的输入参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { z } from 'zod'

const UserSchema = z.object({
name: z.string(), // 必填字符串
age: z.number().optional(), // 可选数字
role: z.enum(['admin', 'user']), // 枚举
})

type User = z.infer<typeof UserSchema>
// 等价于: { name: string; age?: number; role: 'admin' | 'user' }

const result = UserSchema.safeParse({ name: 'Alice', role: 'admin' })
if (result.success) {
console.log(result.data) // { name: 'Alice', role: 'admin' }
}

Claude Code 使用 Zod 的三个关键能力:

  1. 类型推导z.infer<typeof schema> 自动生成 TypeScript 类型
  2. 运行时验证safeParse() 验证输入是否符合 schema
  3. JSON Schema 生成:Zod schema 可以转换为 JSON Schema,这是 Anthropic API 要求的工具参数格式

源码入口

本章追踪的调用链:

1
2
3
4
5
6
main.tsx 初始化阶段
→ src/tools.ts (getTools — 组装工具列表)
→ src/tools.ts (getAllBaseTools — 收集所有内置工具)
→ src/tools.ts (assembleToolPool — 合并内置 + MCP 工具)
→ src/Tool.ts (buildTool — 工具工厂函数)
→ src/tools/BashTool/ (BashTool — 具体工具实现示例)

逐行阅读

6.1 Tool 类型:统一的工具接口

所有工具都遵循 Tool 类型定义。它是一个泛型类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// → src/Tool.ts 的 Tool 类型(简化版,展示核心字段)
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
// === 基础属性 ===
readonly name: string // 工具名,如 "Bash"
aliases?: string[] // 别名(重命名后向后兼容)

// === 核心方法 ===
call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
description(input, options): Promise<string>
readonly inputSchema: Input // Zod schema

// === 行为标记 ===
isReadOnly(input): boolean // 是否只读(影响并发策略)
isConcurrencySafe(input): boolean // 是否并发安全
isDestructive?(input): boolean // 是否破坏性操作
isEnabled(): boolean // 当前是否启用

// === 中断行为 ===
interruptBehavior?(): 'cancel' | 'block' // 用户发新消息时:取消 or 等待

// === 其他 ===
maxResultSizeChars?: number // 结果持久化阈值
}

关键设计决策

方法默认值含义
isReadOnlyfalse默认假设是写操作——安全第一
isConcurrencySafefalse默认不能并行——避免竞态条件
isDestructivefalse默认非破坏性
isEnabledtrue默认启用

设计一瞥:为什么 isReadOnly 默认是 false?因为”默认不安全”比”默认安全”更安全——如果开发者忘了标记,工具会被当作写操作处理(串行执行),而不是意外并行执行导致竞态条件。

6.2 buildTool:工具工厂函数

buildTool 是创建工具的标准方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// → src/Tool.ts 的 buildTool() 函数(简化版)
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false,
isReadOnly: (_input?: unknown) => false,
isDestructive: (_input?: unknown) => false,
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS, // 先铺上默认值
userFacingName: () => def.name,
...def, // 然后用具体实现覆盖
} as BuiltTool<D>
}

这个模式让每个工具只需要定义自己特殊的部分。比如 ReadTool 只需要 isReadOnly: () => true,其他都用默认值。

6.3 BashTool:一个完整的工具实现

BashTool 是最复杂的工具之一:

1
2
3
4
5
6
7
8
// → src/tools/BashTool/BashTool.tsx 的 fullInputSchema(简化版)
const fullInputSchema = lazySchema(() => z.strictObject({
command: z.string().describe('The command to execute'),
timeout: z.number().optional().describe('Optional timeout in milliseconds'),
description: z.string().optional().describe('Clear, concise description...'),
run_in_background: z.boolean().optional(),
dangerouslyDisableSandbox: z.boolean().optional(),
}))

有趣的设计:

1. lazySchema():延迟求值。schema 在第一次使用时才创建,避免循环依赖。

2. _simulatedSedEdit 被从模型 schema 中移除

1
2
3
4
5
6
// → src/tools/BashTool/BashTool.tsx
// Always omit _simulatedSedEdit from the model-facing schema. It is an internal-only
// field set by SedEditPermissionRequest after the user approves a sed edit preview.
const inputSchema = lazySchema(() => fullInputSchema().omit({
_simulatedSedEdit: true
}))

如果暴露给模型,模型就能绕过权限检查直接写入任意文件。

3. buildTool() 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const BashTool = buildTool({
name: 'Bash',
searchHint: 'execute shell commands',
maxResultSizeChars: 30_000, // 超过 30K 字符时结果存到文件
isReadOnly(input) {
const result = checkReadOnlyConstraints(input)
return result.behavior === 'allow'
},
isConcurrencySafe(input) {
return this.isReadOnly?.(input) ?? false // 只读命令才能并发
},
async call(args, context, canUseTool, parentMessage) {
// ...执行 bash 命令的完整逻辑
},
})

6.4 getAllBaseTools:收集所有内置工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// → src/tools.ts 的 getAllBaseTools() 函数(简化版)
export function getAllBaseTools(): Tools {
return [
// === 核心工具(始终存在)===
AgentTool, // 子代理
BashTool, // Shell 命令
FileReadTool, // 读文件
FileEditTool, // 编辑文件
FileWriteTool, // 写文件
NotebookEditTool, // Jupyter notebook

// === 条件工具(feature flag 控制)===
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
...(isAgentSwarmsEnabled() ? [TeamCreateTool, TeamDeleteTool] : []),
...(SleepTool ? [SleepTool] : []),
...(ToolSearchTool ? [ToolSearchTool] : []),
// ... 其余 20+ 个工具
]
}

工具分类一览

类别工具特点
文件操作Read, Edit, Write, NotebookEditRead 是只读,其他是写操作
搜索Glob, Grep只读,可并行
执行Bash根据 isReadOnly() 动态判断
网络WebFetch, WebSearch只读
子代理Agent发起子任务
交互AskUserQuestion, TodoWrite需要用户交互
MCPMCPTool(动态)外部工具

6.5 getTools:过滤和组装

1
2
3
4
5
6
7
8
9
10
11
// → src/tools.ts 的 getTools() 函数(简化版)
export const getTools = (permissionContext: ToolPermissionContext): Tools => {
// --bare 模式:只有 Bash + Read + Edit
if (process.env.CLAUDE_CODE_SIMPLE) {
return [BashTool, FileReadTool, FileEditTool]
}

const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
let allowedTools = filterToolsByDenyRules(tools, permissionContext)
return allowedTools.filter((_, i) => allowedTools[i].isEnabled())
}

6.6 assembleToolPool:合并内置 + MCP 工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// → src/tools.ts 的 assembleToolPool() 函数(简化版)
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tool[],
): Tools {
const builtinTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

const seen = new Set(builtinTools.map(t => t.name))
const dedupedMcp = allowedMcpTools.filter(t => !seen.has(t.name))

// 各自排序后合并(排序保证 prompt cache 稳定性)
return [...builtinTools.sort(byName), ...dedupedMcp.sort(byName)]
}

排序的原因:工具列表的顺序影响 prompt cache。按名称排序确保缓存稳定。

设计一瞥:为什么内置工具优先于 MCP 工具?因为 MCP 工具来自外部服务器,可能不稳定。如果 MCP 服务器定义了一个叫 “Bash” 的工具,它不应该覆盖内置的 BashTool。


常见错误与检查方法

常见错误检查方法
工具没注册getTools() return 前打印 tools.map(t => t.name)
工具被 deny 规则过滤filterToolsByDenyRules 前后对比工具列表
MCP 工具没加载检查 assembleToolPool 是否收到 mcpTools
Zod 验证失败检查 inputSchema.safeParse() 的 error 信息
缓存命中率低检查工具列表排序是否稳定

试试看

修改 1:打印所有已注册工具

src/tools.tsgetTools() 函数 return 之前加:

1
2
console.log('[DEBUG] Active tools:', result.map(t => t.name).join(', '))
console.log('[DEBUG] Tool count:', result.length)

然后启动 Claude Code,观察输出了哪些工具。

修改 2:检查工具属性

1
2
3
4
5
6
for (const tool of result) {
try {
const ro = tool.isReadOnly({})
console.log(`[DEBUG] ${tool.name}: isReadOnly=${ro}`)
} catch { /* 有些工具需要具体输入 */ }
}

修改 3:查看 BashTool 的 schema

1
console.log('[DEBUG] BashTool inputSchema:', JSON.stringify(inputSchema(), null, 2))

检查点

你现在已经理解了:

  • Tool 接口:统一的方法签名(calldescriptioninputSchema)和行为标记(isReadOnlyisConcurrencySafeisDestructive
  • buildTool 工厂:填充默认值(默认不安全),具体工具只需定义特殊部分
  • Zod schemalazySchema() 延迟求值,.describe() 生成模型可见的描述,.omit() 隐藏内部字段
  • 工具注册流程getAllBaseTools()getTools()assembleToolPool()
  • 条件包含feature() 编译时消除、process.env.USER_TYPE 运行时检查、isEnabled() 动态检查
  • 工具过滤:deny 规则、模式过滤、排序保证 prompt cache 稳定性
  • 内置 vs MCP:内置工具优先,MCP 工具按名称去重,各自排序后合并

下一站预告:第 7 章将追踪 API 调用——query() 的 AsyncGenerator 模式、流式响应、Prompt Cache 机制。


← 上一章:消息被装进信封 | 下一章:信封飞向远方 →