零、从第一性原理思考:工具系统该怎么设计?
本节是基于源码的设计推导,不是源码本身的直接描述。每个问题先推理"应该怎样",再用源码印证"实际怎样"。
0.1 起点:LLM 光靠嘴说不够
LLM 能推理、能写代码,但它没有手——不能读文件、不能跑命令、不能搜索代码库。要让它真正干活,必须给它一套"工具箱",每个工具对应一个能力:读文件、写文件、执行 Shell、搜索……
那该怎么设计这个工具箱?沿着一个工具从定义到执行的生命周期,自然会遇到四个问题:
0.2 问题链:定义 → 注册 → 执行 → 编排
1. 一个工具长什么样?(定义)
工具需要一个统一的接口:名字叫什么、接受什么输入、输出什么、能不能并发执行。
源码印证:Tool.ts定义了通用的Tool<Input, Output, P>泛型接口,所有工具通过buildTool()工厂函数构建,自动填充默认值。
2. 有哪些工具、怎么加载?(注册)
不是所有工具都应该始终可用。有些是所有人都能用的(读文件),有些只在特定条件下开放(PowerShell 只在 Windows),有些是第三方扩展(MCP 工具)。
源码印证:tools.ts的getAllBaseTools()用require()+ feature flag 实现条件加载,assembleToolPool()把内置工具和 MCP 工具合并成统一工具池。
3. 从调用到返回经历了什么?(执行)
工具不是"直接调用就完事"——调用前要验证输入、检查权限、跑前置钩子;调用后要跑后置钩子、序列化结果、处理错误。
源码印证:services/tools/toolExecution.ts的runToolUse()实现了完整的五步链路:验证 → 权限 → 钩子 → call → 结果。
4. 多个工具怎么同时跑?(编排)
模型一次可能调用多个工具(比如同时读 3 个文件)。哪些能并发、哪些必须串行、执行到一半出错了怎么办?
源码印证:services/tools/toolOrchestration.ts按isConcurrencySafe()分批,安全的并发跑、不安全的串行跑。StreamingToolExecutor.ts实现了边收边跑的流式执行。
接下来四章(一~四),按这四个阶段逐一展开源码实现。第五章"分类"是贯穿前四章的横切维度,从安全属性、Agent 权限、延迟加载、权限模式四个角度对工具进行分类。
一、定义:一个工具长什么样?
1.1 Tool 接口
所有工具的统一接口定义在 Tool.ts:362,核心方法和属性:
| 方法/属性 | 类型 | 说明 |
|---|---|---|
name | string | 工具名称(如 'Bash'、'Read') |
call() | async | 执行工具,返回 ToolResult<Output> |
description() | async | 动态生成工具描述(注入系统提示) |
inputSchema | Zod Schema | 输入参数验证(Zod 类型安全) |
isEnabled() | boolean | 工具是否启用 |
isConcurrencySafe() | boolean | 是否可以和其他工具并发执行 |
isReadOnly() | boolean | 是否只读(不修改文件系统) |
isDestructive() | boolean | 是否破坏性(删除、覆盖、发送等) |
checkPermissions() | async | 权限检查 |
validateInput() | async | 自定义输入验证(Zod 之外的业务逻辑) |
maxResultSizeChars | number | 结果最大字符数 |
shouldDefer | boolean | 是否延迟加载(工具数量多时按需加载) |
searchHint | string | ToolSearch 关键词匹配提示 |
渲染方法(控制终端 UI 展示):renderToolUseMessage()、renderToolResultMessage()、renderToolUseProgressMessage()、renderToolUseRejectedMessage() 等。
1.2 ToolResult:工具返回什么
// Tool.ts:321 type ToolResult<T> = { data: T // 实际输出数据 newMessages?: Message[] // 工具可选生成新消息(如附加上下文) contextModifier?: (ctx: ToolUseContext) => ToolUseContext // 修改上下文(非并发安全工具用) mcpMeta?: { ... } // MCP 协议元数据 }
关键设计:contextModifier 只有非并发安全的工具才能用——因为并发执行时多个 modifier 同时改上下文会冲突,编排层(第四章)会在批次结束后统一应用。
1.3 buildTool:工厂函数和默认值
所有工具通过 buildTool(def) 构建(Tool.ts:757),它的核心作用是填充安全默认值:
| 方法 | 默认值 | 设计意图 |
|---|---|---|
isEnabled | true | 默认启用 |
isConcurrencySafe | false | 默认不安全——保守策略,必须显式声明安全 |
isReadOnly | false | 默认当作写操作 |
isDestructive | false | 默认非破坏性 |
checkPermissions | 返回 allow | 交给通用权限系统处理 |
这是一个 fail-closed 设计:忘了声明 isConcurrencySafe 的工具会被当作不安全的串行执行,不会意外并发出问题。
1.4 ToolUseContext:执行上下文
工具执行时收到的上下文(Tool.ts:158),包含它需要知道的一切:
- options:命令行参数、模型配置、已加载的工具列表、MCP 客户端
- abortController:取消控制(用户按 ESC)
- messages:当前对话历史
- readFileState:文件状态缓存(避免重复读取)
- getAppState / setAppState:全局应用状态的读写
二、注册:有哪些工具、怎么加载?
2.1 工具注册表:getAllBaseTools()
tools.ts:193 是所有工具的注册源头。工具按加载方式分为三类:
始终可用的工具(18 个)
直接 import,无条件加载。完整清单及只读/可并发属性见 2.5 工具清单与属性总览。
条件加载的工具
通过 feature() flag、环境变量或函数判断:
| 条件 | 工具 | 说明 |
|---|---|---|
isTodoV2Enabled() | TaskCreate/Get/Update/ListTool | Todo V2 系统 |
isWorktreeModeEnabled() | EnterWorktreeTool、ExitWorktreeTool | Git worktree 隔离 |
isToolSearchEnabledOptimistic() | ToolSearchTool | 工具多时按需搜索 |
isPowerShellToolEnabled() | PowerShellTool | Windows PowerShell |
isEnvTruthy('ENABLE_LSP_TOOL') | LSPTool | 语言服务器协议 |
USER_TYPE === 'ant' | ConfigTool、TungstenTool、REPLTool | Anthropic 内部专用 |
Feature Flag 控制的工具🚧 全部未启用
以下工具因feature()返回false而不会加载。
| Feature Flag | 工具 |
|---|---|
WEB_BROWSER_TOOL | WebBrowserTool |
PROACTIVE / KAIROS | SleepTool |
AGENT_TRIGGERS | CronCreate/Delete/ListTool |
AGENT_TRIGGERS_REMOTE | RemoteTriggerTool |
MONITOR_TOOL | MonitorTool |
KAIROS | SendUserFileTool |
KAIROS / KAIROS_PUSH_NOTIFICATION | PushNotificationTool |
KAIROS_GITHUB_WEBHOOKS | SubscribePRTool |
WORKFLOW_SCRIPTS | WorkflowTool |
UDS_INBOX | ListPeersTool |
HISTORY_SNIP | SnipTool |
CONTEXT_COLLAPSE | CtxInspectTool |
TERMINAL_PANEL | TerminalCaptureTool |
OVERFLOW_TEST_TOOL | OverflowTestTool |
2.2 条件加载的实现:动态 require()
工具注册使用 require() 而非 import,这是有意的设计(tools.ts 文件头部):
// feature flag 控制的工具用 require(),flag 为 false 时整个模块不会被加载 const SleepTool = feature('PROACTIVE') || feature('KAIROS') ? require('./tools/SleepTool/SleepTool.js').default : null
好处:flag 关闭时不加载模块代码,实现死代码消除。如果用 import,即使 flag 为 false 模块也会被打包进去。
2.3 工具池合并:assembleToolPool()
内置工具和 MCP 工具最终通过 assembleToolPool()(tools.ts:345)合并为统一的工具池:
getTools(permissionContext) → 内置工具(过滤后)
+
filterToolsByDenyRules(mcpTools) → MCP 工具(过滤后)
│
▼
分别排序,然后连接(内置在前,MCP 在后)
uniqBy 去重(内置优先级高于同名 MCP 工具)
│
▼
最终工具池(供系统提示注入和工具调用使用)
排序是为了 prompt cache 稳定性——工具列表顺序不变,系统提示的工具描述不变,API 请求前缀字节一致,缓存命中率更高。内置和 MCP 分开排序再连接,保证内置工具的连续前缀不被 MCP 工具打断。
2.4 工具过滤:getTools()
getTools()(tools.ts:271)在 getAllBaseTools() 基础上做三层过滤:
getAllBaseTools()
│
├─ 1. filterToolsByDenyRules() — 按拒绝规则(deny rules)过滤
│ 支持 MCP 前缀规则(如 mcp__server 会过滤该服务器的所有工具)
│
├─ 2. REPL 模式过滤 — 隐藏原始工具(通过 REPL 虚拟机间接访问)
│
└─ 3. isEnabled() 过滤 — 每个工具的自检(动态禁用)
还有一个特殊模式:CLAUDE_CODE_SIMPLE=true 时只保留 [BashTool, FileReadTool, FileEditTool],极简模式。
2.5 工具清单与属性总览
以下按功能域整理 getAllBaseTools() 中始终加载的工具:
| 功能域 | 工具 | 作用 | 只读 | 可并发 |
|---|---|---|---|---|
| Shell 执行 | Bash | 执行 Shell 命令 | 条件¹ | 条件¹ |
| 文件操作 | FileRead | 读取文件/图片/PDF/笔记本 | ✅ | ✅ |
| FileEdit | 精确字符串替换编辑文件 | ❌ | 条件 | |
| FileWrite | 创建或覆盖写入文件 | ❌ | 条件 | |
| NotebookEdit | 编辑 Jupyter Notebook 单元格 | ❌ | 条件 | |
| 搜索 | Glob | 按 glob 模式匹配文件路径 | ✅ | ✅ |
| Grep | 基于 ripgrep 的正则内容搜索 | ✅ | ✅ | |
| 网络 | WebFetch | 抓取 URL 页面内容 | ✅ | ❌ |
| WebSearch | 搜索引擎查询 | ✅ | ❌ | |
| Agent | Agent | 派出子 Agent 执行任务 | 条件 | ❌ |
| SendMessage | Agent 间邮箱通信 | 条件 | ❌ | |
| 任务管理 | TaskCreate/Update | 创建/更新任务 | ❌ | ✅ |
| TaskGet/List | 查询/列出任务 | ✅ | ✅ | |
| TaskOutput | 读取后台任务输出 | ✅ | ❌ | |
| TaskStop | 停止正在运行的后台任务 | ❌ | ✅ | |
| TodoWrite | Todo V1 待办管理 | 条件 | ❌ | |
| 计划模式 | EnterPlanMode | 进入计划模式(只思考不执行) | ✅ | ✅ |
| ExitPlanModeV2 | 退出计划模式恢复执行 | 条件 | ❌ | |
| 交互 | AskUserQuestion | 向用户提问(多选) | 条件 | ✅ |
| Skill | 调用 Skill(/commit 等) | 条件 | ❌ | |
| Brief | 发送带附件的消息 | 条件 | ❌ | |
| MCP | ListMcpResources | 列出 MCP 服务器资源 | ✅ | ✅ |
| ReadMcpResource | 按 URI 读取 MCP 资源 | ✅ | ✅ | |
| 工具发现 | ToolSearch | 搜索延迟加载的工具 | ✅ | ✅ |
只读:工具是否不修改文件系统。✅ = 纯读取操作,不需要用户确认权限;❌ = 有写入/修改行为,默认权限模式下需用户批准。
可并发:模型一次调用多个工具时,该工具能否与其他工具同时执行。✅ = 可以并行(如同时读 3 个文件);❌ = 必须串行排队。编排层按此属性将工具调用分批:连续的可并发工具合为一批同时跑,遇到不可并发的就断开新建一批。
条件:取决于输入参数。如 BashTool 会分析命令内容——
ls判定为只读可并发,rm则非只读须串行。¹ BashTool 通过
checkReadOnlyConstraints()动态分析命令内容判定安全性。
条件加载和 Feature Flag 控制的工具详见 2.1 工具注册表:getAllBaseTools()。
三、执行:从调用到返回的完整链路
3.1 五步执行链路
一个工具从模型调用到结果返回,在 toolExecution.ts 中经历五步(runToolUse()):
模型输出 tool_use 块(名称 + 输入参数)
│
┌─── 1. 输入验证 ─────────────────────────────────┐
│ inputSchema.safeParse(input) — Zod 类型校验 │
│ validateInput(input) — 自定义业务校验 │
│ 失败 → 返回 InputValidationError │
└─────────────────────────────────────────────────────┘
│
┌─── 2. 权限检查 ─────────────────────────────────┐
│ PreToolUse 钩子 → 规则系统 → 用户交互 │
│ 拒绝 → 返回权限拒绝消息 │
│ (权限系统详解见后续独立文档) │
└─────────────────────────────────────────────────────┘
│
┌─── 3. 工具调用 ─────────────────────────────────┐
│ tool.call(input, context, canUseTool, ...) │
│ 支持进度回调(onToolProgress) │
└─────────────────────────────────────────────────────┘
│
┌─── 4. 后置钩子 ─────────────────────────────────┐
│ PostToolUse 钩子 │
│ 可修改输出(仅 MCP 工具)、阻止继续执行 │
└─────────────────────────────────────────────────────┘
│
┌─── 5. 结果序列化 ──────────────────────────────┐
│ mapToolResultToToolResultBlockParam() │
│ 截断超长结果(DEFAULT_MAX_RESULT_SIZE_CHARS=50000)│
└─────────────────────────────────────────────────────┘
3.2 钩子系统:前置和后置
钩子(Hooks)是用户可配置的 Shell 命令,在工具执行前后自动运行(toolHooks.ts)。
PreToolUse 钩子能做 5 件事
| 能力 | 说明 |
|---|---|
| 修改输入 | 返回 updatedInput,替换原始输入 |
| 做权限决策 | 返回 allow / deny / ask,影响权限流程 |
| 阻止执行 | 设置 preventContinuation,工具不执行 |
| 提供上下文 | 返回 additionalContexts,附加到对话中 |
| 取消自身 | 钩子执行超时或被中止 |
PostToolUse 钩子能做 3 件事
| 能力 | 说明 |
|---|---|
| 修改输出 | 仅 MCP 工具允许(updatedMCPToolOutput) |
| 阻止继续 | 设置 preventContinuation,模型不再生成后续消息 |
| 提供上下文 | 返回 additionalContexts |
钩子与权限的优先级(resolveHookPermissionDecision())
钩子说 allow + 规则说 deny → 规则胜(规则不可被钩子绕过) 钩子说 deny → 立即拒绝(规则不再检查) 钩子说 ask → 弹出用户交互对话 无钩子决策 → 走正常权限流程
3.3 错误分类
classifyToolError()(toolExecution.ts:150)按优先级分类错误,用于遥测:
| 优先级 | 错误类型 | 分类方式 |
|---|---|---|
| 1 | TelemetrySafeError | 取错误的 telemetryMessage(已验证安全) |
| 2 | 文件系统错误 | 取 errno 代码(ENOENT、EACCES 等) |
| 3 | 命名错误 | 取 error.name(如 ShellError) |
| 4 | 兜底 | 'Error' 或 'UnknownError' |
3.4 结果大小限制
工具结果有三层截断保护(constants/toolLimits.ts):
| 限制 | 值 | 说明 |
|---|---|---|
DEFAULT_MAX_RESULT_SIZE_CHARS | 50,000 | 单个工具结果最大字符数 |
MAX_TOOL_RESULT_TOKENS | 100,000 | 单个结果的 token 限制 |
MAX_TOOL_RESULTS_PER_MESSAGE_CHARS | 200,000 | 一条消息中所有工具结果的聚合限制 |
四、编排:多工具怎么并发?
4.1 分批策略:partitionToolCalls()
模型一次可能调用多个工具。toolOrchestration.ts:91 的 partitionToolCalls() 把工具调用序列分成批次,每批要么全并发、要么全串行:
工具1(Read—安全) ─┐ 工具2(Grep—安全) ─┤→ 批 A:并发执行 工具3(Read—安全) ─┘ 工具4(Bash—不安全) ──→ 批 B:串行执行 工具5(Read—安全) ──→ 批 C:并发执行(不能和批 A 合并,因为中间断了)
规则很简单:连续的并发安全工具合并为一批,遇到不安全的就断开新建一批。
判断依据是每个工具的 isConcurrencySafe(input) 方法——注意它接收 input 参数,所以同一个工具可以根据不同输入返回不同的安全性判断(比如 Bash 读取命令安全、写入命令不安全)。
4.2 执行模式:并发 vs 串行
runTools()(toolOrchestration.ts:19)按批次依次执行:
并发批次
- 批内所有工具同时启动
contextModifier暂存,批次结束后统一应用(避免并发冲突)- 并发上限 10 个(
getMaxToolUseConcurrency())
串行批次
- 逐个执行,每个工具的
contextModifier立即应用 - 后面的工具能看到前面工具对上下文的修改
4.3 流式执行:StreamingToolExecutor
StreamingToolExecutor.ts 是更激进的优化——不等模型输出完所有 tool_use 块,边收到边开始执行:
模型输出流:[tool_use_1] [tool_use_2 ...还在生成...] [tool_use_3]
│ │
▼ ▼
立即开始执行 收到后立即开始
核心判断逻辑(canExecuteTool()):
// 只要所有正在执行的工具都是并发安全的,新工具就能立即开始
executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
错误级联机制:如果 Bash 工具执行出错,会通过 siblingAbortController 中止同批次其他工具——避免命令链失败后继续浪费资源。
结果有序输出:虽然执行是乱序的,但输出严格按工具调用顺序。每个工具有状态机 queued → executing → completed → yielded,getCompletedResults() 按序遍历,遇到未完成的就停下等。
五、分类:工具的四种分类维度
源码中没有 toolCategory 或 ToolGroup 字段——不存在一个显式的"分类枚举"。但存在四种隐式分类维度:
5.1 维度一:安全属性分类(Tool.ts 接口属性)
每个工具通过 4 个布尔属性声明自己的安全特征:
isReadOnly?
╱ ╲
是 否
(纯读取) (有写入)
│ │
isConcurrencySafe? isDestructive?
╱ ╲ ╱ ╲
是 否 是 否
(可并发) (须串行) (不可逆: (可逆:
删除/覆盖) 普通写入)
这四个属性均采用 fail-closed 默认值(默认值和设计意图详见 1.3 buildTool:工厂函数和默认值)——忘了声明 isConcurrencySafe 的工具会被当作不安全的串行执行,宁可慢,不能并发出错。
5.2 维度二:Agent 使用权限分类(constants/tools.ts)
这是最接近"等级分类"的设计——按工具在不同 Agent 角色中的可用性分组:
┌─────────────────────────────────────────────────────────┐ │ 所有工具池(getAllBaseTools) │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 主 Claude 可用:全部工具 │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ 子 Agent 可用(排除 DISALLOWED) │ │ │ │ │ │ 排除:AgentTool*、AskUserQuestion、 │ │ │ │ │ │ TaskOutput、TaskStop、 │ │ │ │ │ │ EnterPlanMode、ExitPlanMode │ │ │ │ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ │ │ 异步 Agent 允许(ALLOWED 白名单) │ │ │ │ │ │ │ │ 仅 15 个:Read/Write/Edit/Glob/ │ │ │ │ │ │ │ │ Grep/Bash/WebSearch/WebFetch/ │ │ │ │ │ │ │ │ Notebook/Skill/Todo/ToolSearch/ │ │ │ │ │ │ │ │ Worktree │ │ │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ │ │ 协调器模式 🚧 未启用 │ │ │ │ │ │ │ │ (COORDINATOR 白名单) │ │ │ │ │ │ │ │ 仅 4 个:Agent/TaskStop/ │ │ │ │ │ │ │ │ SendMessage/SyntheticOutput │ │ │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ * AgentTool 仅 ant 用户的子 Agent 可嵌套使用
设计逻辑:越深层的角色,可用工具越少——防止递归、保护主线程状态、避免资源冲突。
对应源码常量(constants/tools.ts):
| 常量 | 成员数 | 用途 |
|---|---|---|
ALL_AGENT_DISALLOWED_TOOLS | 7-8 | 所有子 Agent 禁用的工具(黑名单) |
CUSTOM_AGENT_DISALLOWED_TOOLS | 同上 | 自定义 Agent 禁用(当前与上相同) |
ASYNC_AGENT_ALLOWED_TOOLS | 15 | 异步 Agent 允许的工具(白名单) |
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS | 5-8 | 进程内队友额外允许的工具(Task 系列 + SendMessage) |
COORDINATOR_MODE_ALLOWED_TOOLS | 4 | 协调器模式仅允许的工具🚧 未启用 |
REPL_ONLY_TOOLS | 8 | REPL 模式下被隐藏的原始工具(通过 REPL 虚拟机间接访问) |
5.3 维度三:延迟加载分类(shouldDefer + alwaysLoad)
当工具总数超过阈值时,ToolSearch 机制启用,工具分为两类:
| 分类 | 判断逻辑 | 对模型的影响 |
|---|---|---|
| 立即加载 | shouldDefer=false 或 alwaysLoad=true | 完整 schema 出现在初始提示中,模型直接可调用 |
| 延迟加载 | shouldDefer=true 且非特殊工具 | 仅名称出现在提示中,需先用 ToolSearch 获取 schema 才能调用 |
立即加载的核心工具:Bash、FileRead、FileEdit、FileWrite、Glob、Grep、Agent、ToolSearch 本身。
延迟加载的工具(约 24 个):WebFetch、WebSearch、TodoWrite、TaskCreate/Get/Update/List、SendMessage、EnterWorktree、EnterPlanMode、AskUserQuestion、NotebookEdit、ListMcpResources、ReadMcpResource 等。
判断优先级(ToolSearchTool/prompt.ts:62 的 isDeferredTool()):
alwaysLoad=true → 永不延迟 MCP 工具 → 总是延迟 特殊工具 → 永不延迟(ToolSearch、Agent、Brief 等) shouldDefer → 按标记延迟
5.4 维度四:权限模式分类(types/permissions.ts)
这不是工具本身的分类,而是运行环境的分类——不同模式下工具的权限检查行为不同:
| 模式 | 说明 |
|---|---|
default | 默认模式,写操作需要用户确认 |
plan | 计划模式,只允许只读工具,写工具被阻止 |
acceptEdits | 自动接受文件编辑,其他写操作仍需确认 |
bypassPermissions | 跳过所有权限检查(--dangerously-skip-permissions) |
dontAsk | 不询问用户,被拒绝的操作直接跳过 |
auto | 🚧 未启用(需要 TRANSCRIPT_CLASSIFIER) |
六、关键源码速查
| 阶段 | 文件 | 核心职责 |
|---|---|---|
| 定义 | Tool.ts | 工具接口、buildTool 工厂函数、ToolUseContext |
| 定义 | constants/toolLimits.ts | 结果大小限制常数 |
| 注册 | tools.ts | 工具注册表、条件加载、工具池合并 |
| 注册 | constants/tools.ts | 各场景工具白名单/黑名单 |
| 执行 | services/tools/toolExecution.ts | 单工具五步执行链路 |
| 执行 | services/tools/toolHooks.ts | 前置/后置钩子执行 |
| 编排 | services/tools/toolOrchestration.ts | 多工具分批、并发/串行编排 |
| 编排 | services/tools/StreamingToolExecutor.ts | 流式边收边执行 |
| 分类 | types/permissions.ts | 权限模式定义(default/plan/acceptEdits 等) |
| 分类 | tools/ToolSearchTool/prompt.ts | 延迟加载判断逻辑(isDeferredTool()) |