Layer 2: 工具系统 —— Agent 的能力骨架
Agent 只做两件事:思考和行动。工具系统定义了 Agent 能做什么「行动」,以及这些行动如何被定义、发现、调度和执行。
1. 架构总览
Section titled “1. 架构总览” ┌────────────────────────────────────────┐ │ Tool Registry (tools.ts) │ │ getAllBaseTools() → 50+ 工具 │ └──────────────────┬─────────────────────┘ │ ┌────────────┼────────────────┐ ▼ ▼ ▼ ┌───────────┐ ┌──────────┐ ┌────────────┐ │ Built-in │ │ Feature │ │ Dynamic │ │ Tools │ │ Gated │ │ Tools │ │ (always) │ │ (条件) │ │ (运行时) │ └───────────┘ └──────────┘ └────────────┘ BashTool REPLTool MCP Tools FileReadTool SleepTool Skill Tools GrepTool CronTools Agent Tools ... ... ... │ ┌────────────▼────────────────┐ │ StreamingToolExecutor │ │ 并发控制 + 流式执行 │ └────────────┬────────────────┘ │ ┌────────────▼────────────────┐ │ runToolUse() │ │ 权限检查 → Hook → 执行 → Hook │ └────────────────────────────┘| 文件 | 职责 |
|---|---|
Tool.ts | Tool 接口定义 + ToolUseContext |
tools.ts | 工具注册表(getAllBaseTools) |
services/tools/toolExecution.ts | 工具执行管线(runToolUse) |
services/tools/StreamingToolExecutor.ts | 流式并发执行器 |
services/tools/toolHooks.ts | Pre/Post 工具 Hook |
services/tools/toolOrchestration.ts | 工具编排(批次执行) |
2. Tool 接口设计
Section titled “2. Tool 接口设计”这是整个工具系统的基石:
// Tool.ts (概念简化版,实际接口更复杂)type Tool<Input, Output> = { // 身份 name: string // 唯一标识:"Bash", "FileRead", "mcp__server__tool" aliases?: string[] // 别名(向后兼容) isMcp?: boolean // 是否为 MCP 工具
// Schema inputSchema: z.ZodType<Input> // Zod schema(内部校验) inputJSONSchema?: ToolInputJSONSchema // JSON Schema(给 API 用)
// 描述 description(): Promise<string> // 简短描述 prompt(): Promise<string> // 完整提示词(给模型看的)
// 执行 call(input: Input, context: ToolUseContext, ...): Promise<ToolResult<Output>>
// 权限 checkPermissions(input: Input, context: ToolUseContext): Promise<PermissionResult>
// 安全性标记 isConcurrencySafe(input?: Input): boolean // 可否并行? isReadOnly(input?: Input): boolean // 只读? isDestructive(): boolean // 破坏性?
// UI renderToolResultMessage?(...): ReactNode // 自定义结果渲染
// 生命周期 isEnabled?(): boolean // 是否启用 shouldDefer?: boolean // 延迟加载(ToolSearch) interruptBehavior?(): 'cancel' | 'block' // 中断策略 maxResultSizeChars?: number // 结果持久化阈值}ToolResult 结构
Section titled “ToolResult 结构”// Tool.ts:321-336type ToolResult<T> = { data: T // 工具输出数据 newMessages?: Message[] // 可选的额外消息(注入到对话中) contextModifier?: (ctx: ToolUseContext) => ToolUseContext // 修改执行上下文 mcpMeta?: { _meta?: Record<string, unknown> } // MCP 协议元数据}★ 设计洞察:
contextModifier是一个非常精妙的设计。它允许工具在执行后修改后续工具的执行上下文。例如,cd命令可以通过 contextModifier 修改工作目录,影响后续所有 Bash 命令的执行环境。但只有isConcurrencySafe() === false的工具才能使用 contextModifier——因为并行执行的工具修改上下文会导致竞争条件。
3. 工具注册表
Section titled “3. 工具注册表”// tools.ts:193-249 — getAllBaseTools()export function getAllBaseTools(): Tools { return [ // 核心工具(始终可用) AgentTool, BashTool, FileReadTool, FileEditTool, FileWriteTool, GlobTool, GrepTool, // 可被嵌入式搜索工具替代 WebFetchTool, WebSearchTool, NotebookEditTool, TodoWriteTool, SkillTool, AskUserQuestionTool,
// 条件工具(feature gates) ...(REPLTool ? [REPLTool] : []), // ant-only ...(SleepTool ? [SleepTool] : []), // PROACTIVE | KAIROS ...cronTools, // AGENT_TRIGGERS ...(WorkflowTool ? [WorkflowTool] : []), // WORKFLOW_SCRIPTS ...(isAgentSwarmsEnabled() ? [TeamCreateTool, TeamDeleteTool] : []), ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
// MCP 资源工具 ListMcpResourcesTool, ReadMcpResourceTool,
// 工具搜索(延迟发现) ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []), ]}三种注册方式:
- 静态注册:直接 import(如 BashTool, FileReadTool)
- 条件注册:
feature()编译时或process.env运行时门控 - 懒加载注册:
require()延迟加载(如 TeamCreateTool 打破循环依赖)
★ 设计洞察:
feature('PROACTIVE')是 Bun 编译时条件编译,编译后不存在feature()调用——dead code elimination 直接移除了不需要的代码路径。这与process.env.USER_TYPE的运行时检查是不同的。前者用于区分构建产物(内部版 vs 外部版),后者用于运行时环境区分。
4. ToolUseContext —— 工具的执行世界
Section titled “4. ToolUseContext —— 工具的执行世界”每个工具调用都会收到一个 ToolUseContext,这是工具能访问的所有环境信息:
// Tool.ts:158-300 (精简)type ToolUseContext = { // 全局选项 options: { tools: Tools // 所有可用工具 commands: Command[] // 所有命令 mainLoopModel: string // 当前模型 mcpClients: MCPServerConnection[] agentDefinitions: AgentDefinitionsResult maxBudgetUsd?: number // 预算限制 refreshTools?: () => Tools // 动态刷新工具列表 }
// 状态管理 getAppState(): AppState setAppState(f: (prev: AppState) => AppState): void
// 文件系统 readFileState: FileStateCache // 文件读取缓存
// 控制流 abortController: AbortController messages: Message[] // 当前对话历史
// UI setToolJSX?: SetToolJSXFn // 工具自定义 UI addNotification?: (notif: Notification) => void
// Agent 上下文 agentId?: AgentId // 子 Agent ID agentType?: string // Agent 类型名
// 权限相关 localDenialTracking?: DenialTrackingState // 本地拒绝追踪
// 大结果管理 contentReplacementState?: ContentReplacementState}★ 设计洞察:
readFileState: FileStateCache是一个 LRU 缓存,避免同一文件被重复读取。当 Agent 需要读取一个刚刚编辑过的文件时,缓存会返回最新的内存版本而不是磁盘版本。这对提高 Agent 的「一致性感知」非常关键。
5. 流式工具执行器(StreamingToolExecutor)
Section titled “5. 流式工具执行器(StreamingToolExecutor)”这是工具系统最精妙的工程设计之一:
API 的 streaming 响应可能包含多个工具调用:
[text: "Let me check..."] → [tool_use: Grep("error")] → [tool_use: Read("file.ts")] → [end]传统做法是等 API 完全返回,再逐个执行工具。但这浪费了大量时间。
解决方案:边流式接收边执行
Section titled “解决方案:边流式接收边执行”// StreamingToolExecutor.ts:40-60class StreamingToolExecutor { private tools: TrackedTool[] = []
// API 每 stream 出一个工具调用,就加入执行队列 addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void { const isConcurrencySafe = toolDefinition.isConcurrencySafe(parsedInput) this.tools.push({ id, block, status: 'queued', isConcurrencySafe, ... }) void this.processQueue() // 立即尝试执行 }
// 并发控制核心逻辑 private canExecuteTool(isConcurrencySafe: boolean): boolean { const executingTools = this.tools.filter(t => t.status === 'executing') return ( executingTools.length === 0 || // 没有正在执行的工具 (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe)) // 当前工具和所有执行中的工具都是并发安全的 ) }}并发控制策略
Section titled “并发控制策略”Timeline:API stream: [Grep("a")] ─── [Read("b.ts")] ─── [Bash("npm test")] ─── [end] │ │ │Execution: ├── Grep 开始 ──────┤ │ │ ├── Read 并行启动 ────┤ │ (两者都是并发安全的) │ │ ├── 等待 Grep + Read 完成 │ └── Bash 独占执行(非并发安全)规则很简单:
- 并发安全工具(
isConcurrencySafe = true):可以与其他并发安全工具并行 - 非并发安全工具(
isConcurrencySafe = false):必须独占执行 - 结果保序:无论执行顺序如何,结果按接收顺序返回给 API
错误级联处理
Section titled “错误级联处理”// StreamingToolExecutor.ts:46-48// 当一个 Bash 工具出错时,取消所有兄弟工具private siblingAbortController: AbortController // 子控制器
// 错误发生时if (tool.name === BASH_TOOL_NAME && error) { this.hasErrored = true this.siblingAbortController.abort('sibling_error')}★ 设计洞察:
siblingAbortController是toolUseContext.abortController的子控制器。Bash 错误只会中止兄弟工具(通过 sibling controller),不会中止整个查询循环(通过 parent controller)。这样 Agent 可以看到错误结果并决定如何处理。但用户按 ESC 中断时,parent controller 会 abort,所有工具都会被取消。
6. 工具执行管线(runToolUse)
Section titled “6. 工具执行管线(runToolUse)”toolExecution.ts 中的 runToolUse() 是每个工具调用的完整执行路径:
runToolUse(tool, block, assistantMessage, context) │ ├── 1. 输入校验(Zod schema parse) │ └── 失败 → 返回格式化的错误消息 │ ├── 2. runPreToolUseHooks() │ ├── 遍历匹配的 PreToolUse hooks │ ├── 可能修改输入(updatedInput) │ ├── 可能阻塞执行(exit code 2) │ └── 可能注入额外上下文 │ ├── 3. 权限检查(canUseTool) │ ├── 规则检查 → Hook 检查 → 模式检查 → 分类器 │ ├── allow → 继续 │ ├── ask → 弹出权限对话框 │ └── deny → 返回拒绝消息 │ ├── 4. 工具执行(tool.call()) │ ├── 传入解析后的输入 │ ├── 传入 ToolUseContext │ ├── 传入 progress callback │ └── 返回 ToolResult<Output> │ ├── 5. 结果处理 │ ├── 处理 tool result → ToolResultBlockParam │ ├── 大结果持久化(processToolResultBlock) │ └── contextModifier 应用 │ ├── 6. runPostToolUseHooks() │ ├── 可能修改 MCP 工具输出 │ ├── 可能注入额外上下文 │ └── 可能阻止后续继续 │ └── 7. 遥测记录 ├── tool_decision(OTel) ├── tool_call_duration └── 错误分类(classifyToolError)错误分类的工程细节
Section titled “错误分类的工程细节”// toolExecution.ts:150-171function classifyToolError(error: unknown): string { // 1. TelemetrySafeError:使用其 telemetryMessage(已审计) // 2. Node.js fs 错误:记录 error code (ENOENT, EACCES) // 3. 已知错误类型:使用稳定的 .name 属性 // 4. 回退:"Error"(比 minified 的 3 字符标识符好)}★ 设计洞察:在 minified/external 构建中,
error.constructor.name会被压缩成无意义的短标识符(如 “nJT”)。所以错误分类函数有一套专门的策略来提取有意义的遥测信息。这是大型 JS 项目常见但容易被忽略的问题。
7. 工具搜索与延迟发现(ToolSearch)
Section titled “7. 工具搜索与延迟发现(ToolSearch)”当工具数量过多时,不可能把所有工具的 schema 都发送给 API。Claude Code 使用延迟发现策略:
// 某些工具标记为 shouldDefer: true// API 请求时只发送工具名称,不发送完整 schema// 当模型需要使用这些工具时,先调用 ToolSearchTool 获取 schema模型想用一个工具 → 发现 schema 未加载 → 调用 ToolSearchTool("search query") → 返回匹配工具的完整 schema → 模型现在可以正确调用该工具这个设计在工具数量达到 50+ 时非常关键——每个工具的 schema 可能有几百 token,全部发送会严重消耗上下文窗口。
8. 安全性标记系统
Section titled “8. 安全性标记系统”每个工具通过一系列布尔方法声明自己的安全特征:
| 方法 | 含义 | 影响 |
|---|---|---|
isConcurrencySafe() | 可与其他工具并行? | StreamingToolExecutor 并发调度 |
isReadOnly() | 不修改文件系统? | 权限系统快速放行 |
isDestructive() | 不可逆操作? | 权限系统强制 ask |
isSearchOrReadCommand() | 搜索/读取类? | UI 折叠显示 |
requiresUserInteraction() | 需要用户交互? | 即使 bypass 也需确认 |
interruptBehavior() | ’cancel’ or ‘block’ | 用户中断时的行为 |
这些标记共同构成了一个「工具安全性矩阵」,让框架可以在不理解工具具体逻辑的情况下做出正确的调度和权限决策。
9. Feature Gate 模式
Section titled “9. Feature Gate 模式”工具注册大量使用了两种条件加载模式:
编译时(Bun feature())
Section titled “编译时(Bun feature())”const SleepTool = feature('PROACTIVE') || feature('KAIROS') ? require('./tools/SleepTool/SleepTool.js').SleepTool : null- 编译后不存在
feature()调用,是 true/false 常量 - Dead code elimination 移除不需要的分支
- 用于区分构建产物(内部版 vs 外部版)
运行时(process.env)
Section titled “运行时(process.env)”const REPLTool = process.env.USER_TYPE === 'ant' ? require('./tools/REPLTool/REPLTool.js').REPLTool : null- 运行时检查环境变量
- 同一构建产物可以有不同行为
- 用于用户类型区分
懒加载(打破循环依赖)
Section titled “懒加载(打破循环依赖)”const getTeamCreateTool = () => require('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool- 函数包装延迟执行 require
- 解决 tools.ts ↔ TeamCreateTool 的循环依赖
- 首次调用时才加载模块
Agent 工程实践 Takeaway
Section titled “Agent 工程实践 Takeaway”统一的 Tool 接口是 Agent 框架的基石
Section titled “统一的 Tool 接口是 Agent 框架的基石”所有工具——无论内置、MCP、Skill——都实现相同的接口。这让权限系统、Hook 系统、执行器可以统一处理,不需要 if (tool.type === 'mcp') 这样的分支。
并发安全性标记 > 全局锁
Section titled “并发安全性标记 > 全局锁”不要用全局锁来保证工具执行安全。让每个工具声明自己的并发安全性(isConcurrencySafe),由执行器智能调度。读操作(Grep, Read)可以并行,写操作(Bash, Edit)独占——这是最大化吞吐量的关键。
流式执行是 Agent 延迟优化的核心
Section titled “流式执行是 Agent 延迟优化的核心”传统的「等 API 返回 → 逐个执行工具 → 发送结果 → 等 API 返回」模式有大量空闲等待。边收 API 响应边执行工具可以将延迟降低 30-50%。
大工具集需要延迟发现
Section titled “大工具集需要延迟发现”当工具数量超过 30+,把所有 schema 塞进 API 请求是不可行的。ToolSearch 模式让模型先搜索再使用,节省上下文窗口。
与其他层的交互
Section titled “与其他层的交互”- → 权限系统(L3):
checkPermissions()是 Tool 接口的一部分,每个工具实现自己的权限逻辑 - → Hook 系统(L4):PreToolUse/PostToolUse Hook 在 runToolUse 管线中执行
- → MCP 系统(L4):MCP 工具通过
...MCPTool展开包装为标准 Tool - → Agent Loop(L1):StreamingToolExecutor 被 query 循环调用,处理 API 返回的 tool_use blocks
- → Sub-agent(L6):AgentTool 本身是一个工具,子 Agent 的工具集可以被限制