第 3 章 · 工具系统
工具系统是 LLM 智能体的"双手"——没有工具,智能体只能说话;有了工具,它才能真正做事。本章将带你深入理解工具系统的完整设计:从基础类型定义到权限模型,从注册机制到执行生命周期,再到 40+ 个工具的分类全景。
3.1 概述:为什么需要工具系统?
在 LLM 智能体架构中,工具(Tool) 是模型与外部世界交互的唯一通道。当用户要求"帮我修改这个文件"或"搜索代码中的某个模式"时,LLM 本身无法直接操作文件系统——它需要通过工具来完成这些操作。
本项目的工具系统具有以下核心特征:
- 类型安全:每个工具的输入通过 Zod Schema 严格定义和验证
- 权限优先:每个工具在执行前必须通过权限检查,确保安全性
- 自包含:每个工具是一个独立目录,包含实现、UI 渲染、提示词等
- 可扩展:通过 MCP 协议可以动态接入外部工具
- 条件加载:通过特性标志实现编译期死代码消除
工具系统的核心文件分布如下:
| 文件 | 职责 |
|---|---|
src/Tool.ts | 工具基础类型定义、buildTool 构建函数 |
src/tools.ts | 工具注册表、组装逻辑、过滤规则 |
src/tools/ | 40+ 个工具的具体实现(每个工具一个子目录) |
3.2 工具基础类型定义
Tool 接口:工具的"契约"
src/Tool.ts 定义了整个工具系统的核心类型。每个工具都必须实现 Tool 接口,这个接口可以分为几个功能域:
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
// ========== 身份标识 ==========
readonly name: string;
aliases?: string[]; // 向后兼容的别名
searchHint?: string; // ToolSearch 关键词匹配提示
// ========== Schema 定义 ==========
readonly inputSchema: Input; // Zod Schema,定义输入参数
readonly inputJSONSchema?: ToolInputJSONSchema; // MCP 工具的 JSON Schema
outputSchema?: z.ZodType<unknown>;
// ========== 核心执行 ==========
call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>;
description(input, options): Promise<string>;
prompt(options): Promise<string>;
// ========== 权限与安全 ==========
checkPermissions(input, context): Promise<PermissionResult>;
validateInput?(input, context): Promise<ValidationResult>;
isReadOnly(input): boolean;
isDestructive?(input): boolean;
isConcurrencySafe(input): boolean;
// ========== UI 渲染 ==========
renderToolUseMessage(input, options): React.ReactNode;
renderToolResultMessage?(content, progressMessages, options): React.ReactNode;
renderToolUseProgressMessage?(progressMessages, options): React.ReactNode;
renderToolUseRejectedMessage?(input, options): React.ReactNode;
renderToolUseErrorMessage?(result, options): React.ReactNode;
// ========== 元数据 ==========
maxResultSizeChars: number;
readonly shouldDefer?: boolean; // 是否延迟 加载(配合 ToolSearch)
readonly alwaysLoad?: boolean; // 是否始终加载
readonly strict?: boolean; // 是否启用严格模式
isMcp?: boolean; // 是否为 MCP 工具
// ... 更多方法
}
这个接口的设计体现了几个重要的工程决策:
- 泛型参数化:
Tool<Input, Output, P>通过三个泛型参数实现类型安全——输入类型、输出类型和进度事件类型 - 关注点分离:执行逻辑(
call)、权限检查(checkPermissions)、UI 渲染(render*)各自独立 - 渐进式复杂度:大量方法是可选的(
?),简单工具只需实现核心方法
输入 Schema:Zod 驱动的类型安全
每个工具的输入参数通过 Zod Schema 定义。这不仅提供了运行时验证,还能自动生成 JSON Schema 供 LLM 理解参数格式:
const inputSchema = lazySchema(() =>
z.strictObject({
pattern: z.string()
.describe('The regular expression pattern to search for in file contents'),
path: z.string().optional()
.describe('File or directory to search in. Defaults to current working directory.'),
glob: z.string().optional()
.describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
output_mode: z.enum(['content', 'files_with_matches', 'count']).optional()
.describe('Output mode: "content", "files_with_matches", or "count"'),
'-i': semanticBoolean(z.boolean().optional())
.describe('Case insensitive search (rg -i)'),
head_limit: semanticNumber(z.number().optional())
.describe('Limit output to first N lines/entries'),
// ... 更多参数
}),
)
注意两个精妙的设计:
lazySchema():延迟创建 Schema,避免模块加载时的循环依赖和不必要的初始化开销semanticBoolean()/semanticNumber():包装器,让 LLM 传入的"语义化"值(如字符串"true")能被正确解析为布尔值或数字
buildTool:统一的工具构建函数
为了避免每个工具都重复实现相同的默认行为,Tool.ts 提供了 buildTool 函数:
// 安全的默认值(fail-closed 原则)
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false, // 假设不安全
isReadOnly: (_input?: unknown) => false, // 假设会写入
isDestructive: (_input?: unknown) => false,
checkPermissions: (input) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?: unknown) => '',
userFacingName: (_input?: unknown) => '',
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
这个设计遵循了 fail-closed 原则:
isConcurrencySafe默认为false——假设工具不支持并发,除非显式声明isReadOnly默认为false——假设工具会写入,除非显式声明为只读- 所有 40+ 个工具都通过
buildTool构建,确保默认行为的一致性
3.3 权限模型
工具系统的权限检查是一个多层防御体系,确保 LLM 不会在未经授权的情况下执行危险操作。
权限检查流程
每个工具在执行前会经历以下 检查链:
四种权限模式
系统支持四种权限模式,通过 ToolPermissionContext 传递:
export type ToolPermissionContext = DeepImmutable<{
mode: PermissionMode; // 'default' | 'plan' | 'bypassPermissions' | 'auto'
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>;
alwaysAllowRules: ToolPermissionRulesBySource;
alwaysDenyRules: ToolPermissionRulesBySource;
alwaysAskRules: ToolPermissionRulesBySource;
isBypassPermissionsModeAvailable: boolean;
shouldAvoidPermissionPrompts?: boolean;
}>
| 模式 | 行为 | 适用场景 |
|---|---|---|
default | 危险操作需要用户确认 | 交互式使用 |
plan | 所有操作需要 Plan 审批 | 需要审查的场景 |
bypassPermissions | 跳过所有权限检查 | 受信任的自动化 |
auto | 自动分类器判断是否安全 | 半自动模式 |
工具级权限规则
除了全局权限模式,每个工具还可以定义自己的权限逻辑。以 BashTool 为例:
export const BashTool = buildTool({
// ...
async checkPermissions(input, context): Promise<PermissionResult> {
return bashToolHasPermission(input, context);
},
// 支持通配符匹配的权限规则
async preparePermissionMatcher({ command }) {
const parsed = await parseForSecurity(command);
if (parsed.kind !== 'simple') {
return () => true; // 复杂命令:安全起见,始终触发 hook
}
const subcommands = parsed.commands.map(c => c.argv.join(' '));
return pattern => {
const prefix = permissionRuleExtractPrefix(pattern);
return subcommands.some(cmd => {
if (prefix !== null) {
return cmd === prefix || cmd.startsWith(`${prefix} `);
}
return matchWildcardPattern(pattern, cmd);
});
};
},
})
preparePermissionMatcher 是一个精妙的设计:它将命令解析为子命令列表,然后返回一个闭包用于匹配权限规则。这样 ls && git push 这样的复合命令不会绕过 Bash(git *) 的安全 hook。
3.4 执行生命周期
完整的工具调用流程
从 LLM 决定调用工具到结果返回,经历以下完整生命周期:
ToolUseContext:执行上下文
每次工具调用都会收到一个 ToolUseContext 对象,它携带了工具执行所需的全部上下文信息:
export type ToolUseContext = {
options: {
commands: Command[];
tools: Tools;
mcpClients: MCPServerConnection[];
mainLoopModel: string;
thinkingConfig: ThinkingConfig;
isNonInteractiveSession: boolean;
agentDefinitions: AgentDefinitionsResult;
refreshTools?: () => Tools; // MCP 服务器中途连接时刷新工具列表
};
abortController: AbortController;
readFileState: FileStateCache;
getAppState(): AppState;
setAppState(f: (prev: AppState) => AppState): void;
messages: Message[];
// ... 更多字段
}
refreshTools 回调允许在查询过程中动态刷新工具列表。这解决了一个实际问题:MCP 服务器可能在查询开始后才完成连接,此时需要将新发现的 MCP 工具加入可用工具池。
ToolResult:执行结果
工具执行完成后返回 ToolResult,它不仅包含数据,还可以携带新消息和上下文修改器:
export type ToolResult<T> = {
data: T;
newMessages?: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[];
contextModifier?: (context: ToolUseContext) => ToolUseContext;
mcpMeta?: {
_meta?: Record<string, unknown>;
structuredContent?: Record<string, unknown>;
};
}
contextModifier 是一个强大的机制——工具可以在执行后修改后续工具调用的上下文。但注意,它只对非并发安全的工具生效,避免并发修改导致的竞态条件。
3.5 工具注册与发现机制
tools.ts:工具注册表
src/tools.ts 是整个工具系统的注册中心。它负责组装所有可用工具,并根据各种条件进行过滤。
getAllBaseTools:工具的完整清单
import { feature } from 'bun:bundle';
import { AgentTool } from './tools/AgentTool/AgentTool.js';
import { BashTool } from './tools/BashTool/BashTool.js';
import { FileEditTool } from './tools/FileEditTool/FileEditTool.js';
import { FileReadTool } from './tools/FileReadTool/FileReadTool.js';
import { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js';
// ... 更多静态导入
// 条件导入:通过特性标志实现编译期死代码消除
const SleepTool =
feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null;
const cronTools = feature('AGENT_TRIGGERS')
? [
require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
]
: [];
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
// 当内嵌搜索工具可用时,跳过独立的 Glob/Grep 工具
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
ExitPlanModeV2Tool,
FileReadTool,
FileEditTool,
FileWriteTool,
NotebookEditTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
TaskStopTool,
AskUserQuestionTool,
SkillTool,
EnterPlanModeTool,
// 条件加载的工具
...(SleepTool ? [SleepTool] : []),
...(isAgentSwarmsEnabled()
? [getTeamCreateTool(), getTeamDeleteTool()]
: []),
// ... 更多条件工具
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
];
}
这段代码展示了三种工具加载策略:
- 静态导入:核心工具(BashTool、FileEditTool 等)始终可用
- 编译期条件加载:通过
feature()标志,未启用的工具在构建时被完全移除 - 运行时条件加载:通过
isAgentSwarmsEnabled()等函数在运行时决定
getTools:权限过滤后的工具列表
getTools 函数在 getAllBaseTools 的基础上,应用权限过滤和模式过滤:
export const getTools = (permissionContext: ToolPermissionContext): Tools => {
// 简单模式:只保留 Bash、Read、Edit 三个基础工具
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool];
return filterToolsByDenyRules(simpleTools, permissionContext);
}
const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name));
let allowedTools = filterToolsByDenyRules(tools, permissionContext);
// REPL 模式下隐藏原始工具(它们在 VM 内部仍可访问)
if (isReplModeEnabled()) {
const replEnabled = allowedTools.some(tool =>
toolMatchesName(tool, REPL_TOOL_NAME),
);
if (replEnabled) {
allowedTools = allowedTools.filter(
tool => !REPL_ONLY_TOOLS.has(tool.name),
);
}
}
// 最后检查每个工具的 isEnabled() 状态
const isEnabled = allowedTools.map(_ => _.isEnabled());
return allowedTools.filter((_, i) => isEnabled[i]);
}
assembleToolPool:合并内置工具与 MCP 工具
最终呈现给 LLM 的工具列表由 assembleToolPool 函数组装:
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext);
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext);
// 排序策略:内置工具作为连续前缀,MCP 工具追加在后
// 这对 API 的 prompt cache 稳定性至关重要
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name);
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name', // 同名时内置工具优先
);
}
排序策略的设计非常讲究:内置工具和 MCP 工具分别排序后拼接,而不是混合排序。这是因为 Anthropic API 的 prompt cache 会在工具定义处设置缓存断点——如果 MCP 工具插入到内置工具之间,会导致所有下游缓存键失效。
工具发现的完整链路
3.6 工具分类全景
项目包含 40+ 个工具,可以按功能分为以下几大类:
工具分类图
分类汇总表
| 分类 | 工具数量 | 代表工具 | 说明 |
|---|---|---|---|
| 文件操作 | 4 | FileReadTool, FileEditTool, FileWriteTool, NotebookEditTool | 文件的读取、编辑、写入和 Jupyter 笔记本操作 |
| 搜索工具 | 3 | GrepTool, GlobTool, ToolSearchTool | 基于 ripgrep 的内容搜索和文件名匹配 |
| Shell 执行 | 2 | BashTool, PowerShellTool | Shell 命令执行,支持后台运行和沙箱 |
| 网络工具 | 3 | WebFetchTool, WebSearchTool, WebBrowserTool | 网页获取、搜索和浏览器自动化 |
| 智能体工具 | 5 | AgentTool, TeamCreateTool, SendMessageTool | 子智能体生成、团队管理和消息传递 |
| 任务管理 | 7 | TaskCreateTool, TodoWriteTool, TaskStopTool | 任务的创建、查询、更新和停止 |
| MCP 协议 | 3 | MCPTool, ListMcpResourcesTool, ReadMcpResourceTool | MCP 工具调用和资源管理 |
| 系统工具 | 7 | SkillTool, ConfigTool, EnterPlanModeTool | 配置、技能、Plan 模式和 LSP 集成 |
| 隔离工具 | 2 | EnterWorktreeTool, ExitWorktreeTool | Git Worktree 隔离环境 |
| 异步工具 | 5 | SleepTool, CronCreateTool, MonitorTool | 延时、定时任务和监控 |
上表中部分工具需要特定的特性标志才会被加载。例如:
SleepTool需要PROACTIVE或KAIROS标志CronCreateTool等需要AGENT_TRIGGERS标志TeamCreateTool等需要 Agent Swarms 功能启用WebBrowserTool需要WEB_BROWSER_TOOL标志MonitorTool需要MONITOR_TOOL标志
3.7 代表性工具深度分析
3.7.1 BashTool:Shell 命令执行
BashTool 是最复杂的工具之一,它不仅要执行 Shell 命令,还要处理安全性、后台任务、沙箱隔离等多个维度。
文件结构(src/tools/BashTool/):
BashTool/
├── BashTool.tsx # 主实现(1100+ 行)
├── bashPermissions.ts # 权限检查逻辑
├── bashSecurity.ts # 安全性检查
├── commandSemantics.ts # 命令语义分析
├── modeValidation.ts # 模式验证
├── pathValidation.ts # 路径验证
├── readOnlyValidation.ts # 只读约束检查
├── sedEditParser.ts # sed 编辑命令解析
├── sedValidation.ts # sed 命令验证
├── shouldUseSandbox.ts # 沙箱判断
├── prompt.ts # LLM 提示词
├── toolName.ts # 工具名称常量
├── UI.tsx # UI 渲染组件
└── utils.ts # 工具函数
核心设计特点:
- 命令语义分析:BashTool 能识别命令的语义类型(搜索、读取、列目录、静默命令),用于 UI 折叠显示:
// 搜索命令:折叠显示
const BASH_SEARCH_COMMANDS = new Set([
'find', 'grep', 'rg', 'ag', 'ack', 'locate', 'which', 'whereis'
]);
// 读取命令:折叠显示
const BASH_READ_COMMANDS = new Set([
'cat', 'head', 'tail', 'less', 'more', 'wc', 'stat', 'file',
'strings', 'jq', 'awk', 'cut', 'sort', 'uniq', 'tr'
]);
// 语义中性命令:不影响管道的搜索/读取性质
const BASH_SEMANTIC_NEUTRAL_COMMANDS = new Set([
'echo', 'printf', 'true', 'false', ':'
]);
- 后台任务支持:命令可以通过
run_in_background: true在后台运行,输出写入磁盘文件:
// 后台任务的输出路径
if (backgroundTaskId) {
const outputPath = getTaskOutputPath(backgroundTaskId);
backgroundInfo = `Command running in background with ID: ${backgroundTaskId}. ` +
`Output is being written to: ${outputPath}`;
}
-
sed 编辑预览:当用户执行
sed -i命令时,系统会先解析 sed 命令、预览修改效果,用户确认后再通过_simulatedSedEdit直接写入文件,确保"所见即所得"。 -
沙箱隔离:支持在沙箱环境中执行命令,防止危险操作影响宿主系统。