Layer 2: 工具系统 —— Agent 的能力扩展
如何设计一个既标准化又可扩展的工具体系,让 50+ 工具能够安全地并发执行,同时支持动态加载(MCP)和延迟发现(Deferred Tools)?
工具系统是 Agent 的「手」——Agent Loop 决定做什么,Tool System 决定怎么做。
1. Tool 接口:一切工具的契约
Section titled “1. Tool 接口:一切工具的契约”1.1 核心接口
Section titled “1.1 核心接口”type Tool<Input, Output, P = unknown> = { // ──── 身份 ──── name: string // 唯一标识符 aliases?: string[] // 向后兼容别名 searchHint?: string // ToolSearch 关键词(3-10 词)
// ──── 输入/输出 ──── inputSchema: Input // Zod schema(类型安全) inputJSONSchema?: ToolInputJSONSchema // 原始 JSON Schema(给 MCP 工具) outputSchema?: z.ZodType // 可选输出 schema
// ──── 核心执行 ──── call( args: z.infer<Input>, context: ToolUseContext, canUseTool: CanUseToolFn, parentMessage: AssistantMessage, onProgress?: ToolCallProgress<P> ): Promise<ToolResult<Output>>
// ──── 安全标记 ──── isConcurrencySafe(input): boolean // 能否与其他工具并行? isReadOnly(input): boolean // 是否只读? isDestructive(input): boolean // 是否不可逆? checkPermissions(input, ctx): PermissionResult // 权限检查
// ──── 描述与渲染 ──── description(input, options): string prompt(options): string // 完整工具说明(每会话调用一次) renderToolUseMessage(input): ReactElement renderToolResultMessage(output, ...): ReactElement
// ──── 高级特性 ──── shouldDefer?: boolean // 延迟加载(需要 ToolSearch 激活) alwaysLoad?: boolean // 永远不延迟 maxResultSizeChars: number // 超过此值 → 持久化到磁盘 isEnabled(): boolean // 当前环境是否可用 validateInput(input, ctx): ValidationResult // 预执行验证}1.2 ToolResult:工具执行的返回值
Section titled “1.2 ToolResult:工具执行的返回值”type ToolResult<T> = { data: T // 实际结果 newMessages?: Message[] // 追加到对话的额外消息 contextModifier?: (ctx: ToolUseContext) => ToolUseContext // 上下文修改器 mcpMeta?: { _meta?, structuredContent? } // MCP 协议元数据}★ 设计洞察:
contextModifier是一个巧妙的设计。工具不直接修改全局状态,而是返回一个纯函数来修改上下文。这意味着:
- 只有非并发安全的工具才能返回 contextModifier(并发安全的工具不应修改共享状态)
- 修改在工具执行完成后才应用,保证了执行过程中的状态一致性
- 测试时容易 assert contextModifier 的效果
1.3 buildTool():Fail-Closed 工厂
Section titled “1.3 buildTool():Fail-Closed 工厂”function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D>所有工具通过 buildTool() 工厂创建,它提供安全默认值:
| 属性 | 默认值 | 设计意图 |
|---|---|---|
isEnabled | true | 工具默认可用 |
isConcurrencySafe | false | 默认不能并发——安全第一 |
isReadOnly | false | 默认非只读——宁可多问,不可误写 |
isDestructive | false | 需要显式声明 |
checkPermissions | { behavior: 'allow' } | 默认放行(交给外层权限系统) |
★ 设计洞察:这就是Fail-Closed 哲学——当工具作者忘记声明某个属性时,系统会选择更安全的行为。忘记声明
isConcurrencySafe?那就串行执行。忘记声明isReadOnly?那就当写操作处理。
2. 工具注册与发现
Section titled “2. 工具注册与发现”2.1 三层注册体系
Section titled “2.1 三层注册体系”getAllBaseTools() ← 所有工具(含 feature-gated 的) ↓ 过滤 deny 规则 + isEnabledgetTools(permCtx) ← 当前权限下可用的内置工具 ↓ 合并 MCP 工具 + 去重 + 排序assembleToolPool(permCtx, mcp) ← 最终工具池(发送给 API)2.2 getAllBaseTools():单一真理来源
Section titled “2.2 getAllBaseTools():单一真理来源”function getAllBaseTools(): Tools { return [ // ── 始终可用 ── AgentTool, BashTool, FileReadTool, FileEditTool, FileWriteTool, GlobTool, GrepTool, WebFetchTool, WebSearchTool, TaskCreateTool, TaskUpdateTool, // ...30+ 始终可用的工具
// ── Feature-Gated ── ...(feature('KAIROS') ? [SleepTool, PushNotificationTool] : []), ...(feature('AGENT_TRIGGERS') ? [CronCreateTool, CronDeleteTool] : []), ...(feature('COORDINATOR_MODE') ? [CoordinatorModeTool] : []), ...(USER_TYPE === 'ant' ? [ConfigTool, REPLTool] : []), // ...更多条件注册 ]}2.3 assembleToolPool():最终组装
Section titled “2.3 assembleToolPool():最终组装”function assembleToolPool(permissionContext, mcpTools): Tools { // 1. 获取内置工具(已过滤 deny 规则和 isEnabled) const builtins = getTools(permissionContext)
// 2. 过滤 MCP 工具的 deny 规则 const filteredMcp = filterToolsByDenyRules(mcpTools, permissionContext)
// 3. 去重(内置优先) const deduped = deduplicateByName(builtins, filteredMcp)
// 4. 排序(prompt cache 稳定性) // 内置工具连续排列,MCP 工具追加在后 return sortForPromptCacheStability(deduped)}★ 设计洞察:排序是为了 Prompt Cache 稳定性。API 的 prompt cache 是按消息前缀匹配的——如果工具列表的顺序每次变化,cache 就会失效。通过固定排序(内置在前、MCP 在后、同类按名字排),最大化 cache 命中率。
2.4 Deferred Tools:延迟加载
Section titled “2.4 Deferred Tools:延迟加载”问题:50+ 工具的描述占用大量 token(系统提示词膨胀)解决:不常用的工具标记 shouldDefer = true,只发送名字和 searchHint
激活流程:1. 模型需要某个 deferred 工具2. 模型调用 ToolSearch(query)3. ToolSearch 返回完整的工具 schema4. 模型现在可以调用这个工具了3. 工具执行管线
Section titled “3. 工具执行管线”3.1 完整执行流程
Section titled “3.1 完整执行流程”API 返回 tool_use block ↓findToolByName(name) ← 查找工具(含别名回退) ↓Zod 输入校验 ← inputSchema.parse(input) ↓tool.validateInput() ← 工具特定验证 ↓backfillObservableInput() ← 补充衍生字段(幂等) ↓runPreToolUseHooks() ← Pre-Hook(可修改输入、可阻止执行) ↓resolvePermission() ← Hook 权限 → canUseTool() → 用户对话框 ↓tool.call() ← 实际执行 ↓runPostToolUseHooks() ← Post-Hook(可处理结果) ↓yield ToolResult ← 返回结果给 Agent Loop3.2 runToolUse():单工具执行
Section titled “3.2 runToolUse():单工具执行”async function* runToolUse( toolUse: ToolUseBlock, assistantMessage: AssistantMessage, canUseTool: CanUseToolFn, toolUseContext: ToolUseContext,): AsyncGenerator<MessageUpdateLazy, void>关键特性:
- Async generator:支持流式 yield 进度消息和最终结果
- Deferred 工具提示:如果 Zod 校验失败且工具是 deferred 的,提示模型先用 ToolSearch
- 投机性 Bash 分类器:BashTool 会在权限检查之前提前启动 ML 分类器(节省等待时间)
3.3 StreamingToolExecutor:边流式边执行
Section titled “3.3 StreamingToolExecutor:边流式边执行”class StreamingToolExecutor { // 工具在 API 流式返回过程中就入队执行 addTool(block: ToolUseBlock, message: AssistantMessage): void
// 取回已完成的结果(不阻塞,有多少返回多少) getCompletedResults(): ToolResult[]
// 流式结束后,等待剩余工具完成 async *getRemainingResults(): AsyncGenerator<ToolResult>}并发语义:
┌──────────────────────────────────────────────┐│ StreamingToolExecutor ││ ││ Queue: [Read₁, Read₂, Edit₃, Grep₄] ││ ││ 执行策略: ││ 1. Read₁ + Read₂ 并行(都是 concurrencySafe) ││ 2. Edit₃ 独占执行(非 concurrencySafe) ││ 3. Grep₄ 等 Edit₃ 完成后开始 ││ ││ canExecuteTool(tool) 检查: ││ - 当前无正在执行的工具 → 可以执行 ││ - 所有正在执行的 + 新工具 都是 concurrencySafe ││ → 可以并行执行 ││ - 否则 → 等待 ││ ││ 特殊规则: ││ - Bash error → 取消同批次所有 sibling ││ (因为 Bash 命令间常有隐式依赖) ││ - 用户中断 → 按 interruptBehavior 决定 ││ 'cancel' → 取消 'block' → 阻塞等待 │└──────────────────────────────────────────────┘3.4 toolOrchestration:批次分区
Section titled “3.4 toolOrchestration:批次分区”async function* runTools( toolUseBlocks: ToolUseBlock[], assistantMessages: AssistantMessage[], canUseTool: CanUseToolFn, toolUseContext: ToolUseContext,): AsyncGenerator<MessageUpdate, void>分区算法(partitionToolCalls):
输入: [Grep, Read, FileEdit, Read, Glob]
分区结果: Batch 1 (concurrent): [Grep, Read] ← 连续的只读工具 Batch 2 (serial): [FileEdit] ← 非只读,独占 Batch 3 (concurrent): [Read, Glob] ← 又是连续的只读
执行: Batch 1: 并行执行,最大并发 10 Batch 2: 串行执行,apply contextModifier Batch 3: 并行执行最大并发数通过环境变量控制:
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY = 10 (默认值)4. 具体工具剖析:BashTool
Section titled “4. 具体工具剖析:BashTool”BashTool 是最复杂的内置工具之一,值得深入研究。
4.1 输入 Schema
Section titled “4.1 输入 Schema”z.strictObject({ command: z.string(), // Shell 命令 timeout: z.number().optional(), // 超时(ms, 最大 600000) description: z.string().optional(), // 活动描述(spinner 文本) run_in_background: z.boolean().optional(), // 后台执行 dangerouslyDisableSandbox: z.boolean().optional(), // 绕过沙箱})4.2 安全分类
Section titled “4.2 安全分类”// BashTool 自报告的安全属性isConcurrencySafe(input): 解析命令 → 检查是否有 cd、环境变量修改 只读命令 → safe 有 cd → not safe(改变工作目录)
isReadOnly(input): 使用 checkReadOnlyConstraints() + 语义分析 无文件写入、无副作用 → true
isSearchOrReadCommand(input): 搜索: find, grep, rg, ag, ack, locate, which, whereis 读取: cat, head, tail, less, more, jq, awk, cut, sort, uniq, wc, stat, file 列表: ls, tree, du4.3 执行流程
Section titled “4.3 执行流程”命令解析(shell-quote) ↓沙箱检查(sandbox bypass flag) ↓执行 shell 命令 ↓每 2 秒 yield 进度消息(onProgress callback) ↓检测特殊情况: - 代码索引完成? - git 操作? - 输出包含图片? ↓大输出?→ 持久化到磁盘 ↓后台任务?→ 创建 BackgroundTask ↓返回 ToolResult5. MCP 动态工具
Section titled “5. MCP 动态工具”5.1 MCP 工具命名
Section titled “5.1 MCP 工具命名”mcp__[server-name]__[tool-name]
示例: mcp__github__create_issue mcp__slack__send_message mcp__figma__get_design_context5.2 MCPTool 包装器
Section titled “5.2 MCPTool 包装器”MCPTool 不是一个工具,而是一个工具工厂。每个 MCP 服务器注册时,为其每个工具创建一个 MCPTool 实例。
MCPTool 实例 = { name: `mcp__${serverName}__${toolName}` inputJSONSchema: 从 MCP 服务器获取(JSON Schema,非 Zod) call(): 验证输入 → 调用 MCP tool(100s 超时) → 处理 elicitation → 截断/返回结果}5.3 Elicitation:工具执行中的用户交互
Section titled “5.3 Elicitation:工具执行中的用户交互”MCP 工具执行中 → 服务器发送 elicitation 请求(JSON-RPC -32042) ↓触发 Elicitation Hook ↓用户在 UI 中输入响应 ↓ElicitationResult Hook 触发 ↓工具恢复执行6. 设计洞察总结
Section titled “6. 设计洞察总结”Tool 接口的「Fail-Closed」哲学
Section titled “Tool 接口的「Fail-Closed」哲学”安全关键系统的核心设计原则:默认选择更安全的行为。
忘记声明 系统行为 ───────── ───────── isConcurrencySafe → 串行执行(安全) isReadOnly → 当写操作处理(保守) checkPermissions → 放行(交给外层) maxResultSizeChars → 有限制(不会爆内存)流式工具执行的延迟隐藏
Section titled “流式工具执行的延迟隐藏”传统模式: API stream ████████████ (5s) Tool run ████ (3s) 总计: 8s
流式模式: API stream ████████████ (5s) Tool run ████ (3s, 从第 2s 开始) 总计: 5s (节省 37.5%)contextModifier:副作用的函数式封装
Section titled “contextModifier:副作用的函数式封装”工具不直接修改全局状态,而是返回一个修改函数。好处:
- 可组合:多个 modifier 可以链式应用
- 可测试:assert
modifier(oldCtx)的输出 - 可控制:只有非并发工具才能返回 modifier,避免竞态
Deferred Tools 的 Token 经济学
Section titled “Deferred Tools 的 Token 经济学”50+ 工具 × ~200 tokens/工具描述 = ~10,000 tokens 系统提示词Deferred 后:20 常用工具 + 30 个名字 ≈ ~5,000 tokens节省:~50% 系统提示词 token与其他层的交互
Section titled “与其他层的交互”| 交互方向 | 说明 |
|---|---|
| ← L1 (Agent Loop) | queryLoop Phase 3 调用 runTools() / StreamingToolExecutor |
| → L3 (权限系统) | 每次工具执行前通过 canUseTool() → hasPermissionsToUseTool() |
| → L4 (Hook 系统) | Pre/Post ToolUse Hooks 在执行管线中触发 |
| → L5 (上下文管理) | maxResultSizeChars 触发大结果外置到磁盘 |
| → L6 (子 Agent) | AgentTool 是最复杂的工具,会递归创建新的 Agent Loop |
| ← MCP 系统 | assembleToolPool() 合并 MCP 动态工具到工具池 |