第二节:内置工具详解
核心工具与工具接口

作者:小学子 📚 | 日期:2026年4月2日 | 第二阶段 · 模块二


# 第二阶段 · 模块二 · 第二节:内置核心工具

核心问题

Read、Write、Edit、Bash 等核心工具是如何实现的?Bash 工具的安全机制是怎样的?工具参数验证的流程是什么?


◇ 本节位置


        Claude Code 全局架构
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │  入口层 → 查询引擎层 → 工具/服务层                                  │
        │                                                                      │
        │  tools.ts (40+工具注册)                                            │
        │  ├── FileReadTool  ← 本节                                          │
        │  ├── FileWriteTool                                                 │
        │  ├── FileEditTool                                                  │
        │  ├── BashTool                                                      │
        │  ├── WebSearchTool                                                 │
        │  └── ...                                                           │
        └─────────────────────────────────────────────────────────────────────┘
        


一、工具接口

1.1 Tool 接口定义

源码位置:`src/Tool.ts`


        export interface Tool {
          name: string;
          description: string;
        
          // 构建 MCP 格式的工具定义
          buildTool(): ToolRecord;
        
          // 执行工具
          execute(
            args: ToolArgs,
            context: ToolContext,
          ): Promise<ToolResult>;
        }
        

1.2 工具基类

源码位置:`src/Tool.ts`


        // 工具必须实现的接口
        interface ToolRecord {
          name: string;
          description: string;
          input_schema: z.ZodSchema;  // 参数验证
        }
        
        // 工具执行上下文
        interface ToolContext {
          toolUseId: string;
          toolUseContext: ToolUseContext;
          messages: Message[];
          // ...
        }
        


二、FileReadTool 文件读取

2.1 源码位置

源码位置:`src/tools/FileReadTool/FileReadTool.ts`

2.2 核心功能


        export class Read {
          name = 'Read';
          description = 'Read a file from the filesystem';
        
          buildTool(): ToolRecord {
            return {
              name: 'Read',
              description: 'Read a file from the filesystem',
              input_schema: z.object({
                file_path: z.string().describe('Path to the file to read'),
                offset: z.number().optional().describe('Line number to start reading from'),
                limit: z.number().optional().describe('Maximum number of lines to read'),
              }),
            };
          }
        
          async execute(args: { file_path: string; offset?: number; limit?: number }) {
            // 1. 验证路径
            const validatedPath = await validatePath(args.file_path);
        
            // 2. 读取文件
            const content = await readFileContent(validatedPath, {
              offset: args.offset,
              limit: args.limit,
            });
        
            // 3. 返回结果
            return {
              content,
              citations: extractCitations(content),
            };
          }
        }
        

2.3 五问分析

问 1:为什么需要 offset 和 limit?


        // offset: 从指定行开始读取
        // limit: 限制读取的行数
        
        // 用途:
        // 1. 避免读取过大的文件
        // 2. 支持分页读取
        // 3. 减少 token 消耗
        

问 2:路径验证的流程是什么?


        async function validatePath(path: string): Promise<string> {
          // 1. 展开路径(~、环境变量等)
          const expandedPath = expandPath(path);
        
          // 2. 解析为绝对路径
          const absolutePath = resolvePath(expandedPath);
        
          // 3. 安全检查(禁止访问 /etc, /sys 等)
          if (isForbiddenPath(absolutePath)) {
            throw new SecurityError('Cannot read forbidden path');
          }
        
          // 4. 返回规范化路径
          return absolutePath;
        }
        

问 3:如何处理大文件?


        // 如果文件超过限制,自动截断
        const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
        
        async function readFileContent(path: string, options: { offset?: number; limit?: number }) {
          const stats = await fs.stat(path);
        
          if (stats.size > MAX_FILE_SIZE) {
            // 返回部分内容 + 警告
            const content = await readPartialContent(path, options);
            return {
              content,
              truncated: true,
              warning: `File truncated at ${MAX_FILE_SIZE} bytes`,
            };
          }
        }
        

问 4:citations 是什么?


        // citations 用于追踪文件内容的来源
        // 当模型引用文件内容时,可以追踪到具体文件和行号
        
        function extractCitations(content: string): Citation[] {
          // 分析内容,提取引用信息
          return [];
        }
        

问 5:Read 工具在什么权限模式下可用?


        // Read 是低风险工具,通常在所有权限模式下可用
        // 但某些严格模式可能会限制访问特定目录
        
        const replSimple = [
          new Read({ ... }),           // 始终可用
          new Glob({ ... }),           // 始终可用
          new Grep({ ... }),           // 始终可用
          new Bash({ shell: '/bin/bash' }),  // 可能受限
        ];
        


三、FileWriteTool 文件写入

3.1 源码位置

源码位置:`src/tools/FileWriteTool/FileWriteTool.ts`

3.2 核心功能


        export class Write {
          name = 'Write';
          description = 'Write content to a file';
        
          buildTool(): ToolRecord {
            return {
              name: 'Write',
              description: 'Write content to a file, creating or overwriting',
              input_schema: z.object({
                file_path: z.string().describe('Path to the file to write'),
                content: z.string().describe('Content to write'),
              }),
            };
          }
        
          async execute(args: { file_path: string; content: string }) {
            // 1. 验证路径
            const validatedPath = await validateWritePath(args.file_path);
        
            // 2. 写入文件
            await writeTextContent(validatedPath, args.content);
        
            // 3. 返回结果
            return {
              success: true,
              path: validatedPath,
            };
          }
        }
        

3.3 五问分析

问 1:Write 和 Edit 的区别?

方面WriteEdit
操作覆盖整个文件只修改部分内容
风险高(可能丢失内容)低(只改一部分)
用途创建新文件修改现有文件

问 2:如何防止意外覆盖?


        // 在写入前检查文件是否存在
        async function safeWrite(path: string, content: string) {
          const exists = await pathExists(path);
        
          if (exists) {
            // 文件存在,提示用户或创建备份
            await createBackup(path);
          }
        
          await writeTextContent(path, content);
        }
        

问 3:Write 需要哪些权限?


        // Write 是中等风险操作
        // 需要在权限检查中明确允许
        
        const simpleTools = [
          // ...
          new Write({ ... }),  // 通常需要 'write' 权限
        ];
        

问 4:写入失败怎么办?


        async function execute(args) {
          try {
            await writeTextContent(args.file_path, args.content);
          } catch (error) {
            if (error.code === 'EACCES') {
              throw new ToolError('Permission denied: cannot write to this path');
            }
            if (error.code === 'ENOSPC') {
              throw new ToolError('No space left on device');
            }
            throw error;
          }
        }
        

问 5:如何处理二进制文件?


        // Write 工具主要用于文本文件
        // 二进制文件通常需要特殊处理
        
        const MAX_TEXT_SIZE = 10 * 1024 * 1024; // 10MB
        
        async function execute(args) {
          if (isBinaryContent(args.content)) {
            throw new ToolError('Binary content not supported. Use Write for text files only.');
          }
        
          if (args.content.length > MAX_TEXT_SIZE) {
            throw new ToolError(`Content too large: ${args.content.length} bytes (max: ${MAX_TEXT_SIZE})`);
          }
        }
        


四、FileEditTool 文件编辑

4.1 源码位置

源码位置:`src/tools/FileEditTool/FileEditTool.ts`

4.2 核心功能


        export class Edit {
          name = 'Edit';
          description = 'Make a small, targeted edit to a file';
        
          buildTool(): ToolRecord {
            return {
              name: 'Edit',
              description: 'Make a small, targeted edit to a file',
              input_schema: z.object({
                file_path: z.string().describe('The file to edit'),
                old_string: z.string().describe('The text to replace'),
                new_string: z.string().describe('The replacement text'),
              }),
            };
          }
        
          async execute(args: { file_path: string; old_string: string; new_string: string }) {
            // 1. 验证 old_string 存在
            const content = await readFileContent(args.file_path);
        
            if (!content.includes(args.old_string)) {
              throw new ToolError(
                `old_string not found in file.\n` +
                `File contents:\n${content.slice(0, 500)}...`
              );
            }
        
            // 2. 替换
            const newContent = content.replace(args.old_string, args.new_string);
        
            // 3. 写回文件
            await writeTextContent(args.file_path, newContent);
        
            return { success: true };
          }
        }
        

4.3 五问分析

问 1:为什么需要 old_string 和 new_string?


        // old_string 用于精确定位要修改的内容
        // 而不是简单的行号或偏移量
        
        // 这样可以避免并发编辑的冲突问题
        // 即使文件在其他地方被修改,只要 old_string 匹配就安全
        

问 2:old_string 不存在会怎样?


        // 返回详细的错误信息
        throw new ToolError(
          `old_string not found in file.\n` +
          `File contents:\n${content.slice(0, 500)}...`
        );
        
        // 帮助模型理解问题并修复
        

问 3:Edit 和 Write 的安全对比?

方面WriteEdit
覆盖风险
精确性
并发安全
适用场景创建文件修改文件

问 4:如何支持多处的替换?


        // 如果需要替换多处,使用 replaceAll
        
        const newContent = content.replaceAll(args.old_string, args.new_string);
        

问 5:Edit 支持 sed 语法吗?

源码位置:`src/tools/BashTool/sedEditParser.ts`


        // Claude Code 可能支持类似 sed 的语法
        // 但主要是通过 Edit 工具的 old_string/new_string 机制
        
        // sed 示例:s/foo/bar/g
        // Edit 等价:
        edit({
          file_path: 'example.txt',
          old_string: 'foo',
          new_string: 'bar',
        });
        


五、BashTool 命令执行

5.1 源码位置

源码位置:`src/tools/BashTool/BashTool.tsx`(1143行)

5.2 核心功能


        export class BashTool {
          name = 'Bash';
          description = 'Execute a bash command';
        
          buildTool(): ToolRecord {
            return {
              name: 'Bash',
              description: 'Execute a bash command in the terminal',
              input_schema: z.object({
                command: z.string().describe('The bash command to execute'),
                timeout_ms: z.number().optional().describe('Command timeout in milliseconds'),
              }),
            };
          }
        
          async execute(args: { command: string; timeout_ms?: number }) {
            // 1. 解析命令
            const parsedCommand = parseCommand(args.command);
        
            // 2. 安全检查
            const securityResult = await securityCheck(parsedCommand);
            if (!securityResult.allowed) {
              throw new ToolError(`Security check failed: ${securityResult.reason}`);
            }
        
            // 3. 执行命令
            const result = await exec(parsedCommand.command, {
              timeout: args.timeout_ms,
              cwd: this.cwd,
            });
        
            return formatResult(result);
          }
        }
        

5.3 Bash 工具的安全机制

源码位置:`src/tools/BashTool/bashSecurity.ts`


        // 安全检查流程
        
        interface SecurityCheckResult {
          allowed: boolean;
          reason?: string;
          riskLevel?: 'low' | 'medium' | 'high';
        }
        
        async function securityCheck(command: ParsedCommand): Promise<SecurityCheckResult> {
          // 1. 检查危险命令模式
          if (matchesDangerousPattern(command.raw)) {
            return { allowed: false, reason: 'Command matches dangerous pattern', riskLevel: 'high' };
          }
        
          // 2. 检查只读约束
          if (violatesReadOnlyConstraints(command.raw)) {
            return { allowed: false, reason: 'Command violates read-only constraints', riskLevel: 'high' };
          }
        
          // 3. 检查路径遍历
          if (containsPathTraversal(command.raw)) {
            return { allowed: false, reason: 'Path traversal detected', riskLevel: 'high' };
          }
        
          // 4. 检查网络操作
          if (containsNetworkOperation(command.raw)) {
            return { allowed: false, reason: 'Network operations not allowed', riskLevel: 'medium' };
          }
        
          return { allowed: true, riskLevel: assessRisk(command) };
        }
        

5.4 五问分析

问 1:有哪些危险命令被禁止?


        // 危险命令模式
        const DANGEROUS_PATTERNS = [
          /^rm\s+-rf\s+/,                    // rm -rf /
          /^dd\s+/,                         // dd 命令
          /^mkfs/,                          // 格式化
          /^mount\s+.*bind/,                // bind mount
          />/dev\/s/,                      // 直接写入设备
          /:\(\)\{.*:\|:&};:/,              // fork bomb
        ];
        

问 2:什么是只读约束?


        // 只读模式下只能执行特定命令
        const READ_ONLY_CONSTRAINTS = {
          allowed: [
            'ls', 'cat', 'head', 'tail', 'grep', 'find', 'which',
            'pwd', 'cd', 'echo', 'true', 'false',
          ],
          denied: [
            'rm', 'mv', 'cp', 'chmod', 'chown', 'dd',
          ],
        };
        

问 3:Bash 如何处理超时?


        // 默认超时
        const DEFAULT_TIMEOUT_MS = 60_000; // 60秒
        
        // 可以通过参数覆盖
        const result = await exec(command, {
          timeout: args.timeout_ms ?? DEFAULT_TIMEOUT_MS,
        });
        
        // 超时处理
        if (result.timedOut) {
          throw new ToolError(`Command timed out after ${timeout}ms`);
        }
        

问 4:什么是 sandbox 模式?

源码位置:`src/tools/BashTool/shouldUseSandbox.ts`


        // Sandbox 模式限制命令的权限
        const shouldUseSandbox = (command: string): boolean => {
          // 危险命令使用 sandbox
          if (matchesDangerousPattern(command)) {
            return true;
          }
        
          // 实验性功能:AI 决定是否使用 sandbox
          if (feature('AI_SANDBOX_SELECTION')) {
            return await aiRecommendsSandbox(command);
          }
        
          return false;
        };
        

问 5:Bash 工具的权限级别?

权限级别可用命令风险
minimalls, cat, grep, cd
low+ find, which, echo
medium+ git, npm, node
high+ rm, mv, chmod
all所有命令极高


六、工具参数验证

6.1 Zod Schema 验证

源码位置:`src/Tool.ts`


        import { z } from 'zod/v4';
        
        // 每个工具通过 Zod schema 定义输入参数
        const WriteSchema = z.object({
          file_path: z.string().describe('Path to the file to write'),
          content: z.string().describe('Content to write'),
        });
        
        interface ValidationResult {
          success: boolean;
          error?: string;
          parsed?: z.infer<typeof WriteSchema>;
        }
        
        function validateInput(schema: z.ZodSchema, input: unknown): ValidationResult {
          const result = schema.safeParse(input);
        
          if (!result.success) {
            return {
              success: false,
              error: result.error.message,
            };
          }
        
          return {
            success: true,
            parsed: result.data,
          };
        }
        

6.2 五问分析

问 1:为什么使用 Zod?


        // Zod 提供运行时验证 + 类型推断
        
        // 1. 类型安全
        const parsed = WriteSchema.parse(input);
        // parsed 类型是 { file_path: string; content: string }
        
        // 2. 验证消息
        const result = WriteSchema.safeParse(input);
        if (!result.success) {
          console.log(result.error.message); // 人类可读的验证错误
        }
        

问 2:参数验证的流程?


        async function execute(tool: Tool, rawInput: unknown) {
          // Step 1: Schema 验证
          const validation = validateInput(tool.input_schema, rawInput);
          if (!validation.success) {
            throw new ValidationError(validation.error);
          }
        
          // Step 2: 业务逻辑验证
          if (!await pathExists(validation.parsed.file_path)) {
            throw new ToolError('File does not exist');
          }
        
          // Step 3: 执行
          return await tool.execute(validation.parsed);
        }
        

问 3:如何自定义验证错误消息?


        const WriteSchema = z.object({
          file_path: z.string().min(1, 'Path cannot be empty'),
          content: z.string().refine(
            content => !content.includes('\x00'),  // 不能包含 null 字节
            { message: 'Content cannot contain null bytes' }
          ),
        });
        


七、思考题

思考题 1:能否自定义工具的错误处理?

问题:如果需要自定义工具的错误处理逻辑(如重试、降级),如何实现?

答案


        class RetryableTool implements Tool {
          constructor(
            private inner: Tool,
            private maxRetries: number = 3,
          ) {}
        
          async execute(args: ToolArgs): Promise<ToolResult> {
            for (let i = 0; i < this.maxRetries; i++) {
              try {
                return await this.inner.execute(args);
              } catch (error) {
                if (i === this.maxRetries - 1) throw error;
        
                // 指数退避
                await sleep(Math.pow(2, i) * 1000);
              }
            }
            throw new Error('Unreachable');
          }
        }
        


思考题 2:工具能否返回流式输出?

问题:对于长时间运行的命令(如 `tail -f`),能否支持流式输出,而不是等待完成?

答案


        // 可以通过 AsyncGenerator 实现流式输出
        
        async *execute(args: { command: string }): AsyncGenerator<string> {
          const process = spawn('bash', ['-c', args.command]);
        
          for await (const line of process.stdout) {
            yield line;  // 边读边 yield
          }
        }
        
        // 使用方
        for await (const line of tool.execute({ command: 'tail -f log.txt' })) {
          console.log(line);
        }
        


思考题 3:工具之间能否共享状态?

问题:如果需要在多个工具调用之间共享状态(如缓存、临时文件),如何实现?

答案


        // 通过 ToolContext 传递共享状态
        
        interface SharedState {
          cache: Map<string, unknown>;
          tempFiles: string[];
        }
        
        interface ToolContext {
          toolUseId: string;
          sharedState: SharedState;  // 工具之间共享
        }
        
        // 示例:WriteTool 创建临时文件,ReadTool 读取
        async function execute(args, context) {
          if (args.is_temp) {
            context.sharedState.tempFiles.push(args.file_path);
          }
        }
        


八、延伸阅读

文件行数核心内容
`src/tools/FileReadTool/FileReadTool.ts`?文件读取
`src/tools/FileWriteTool/FileWriteTool.ts`?文件写入
`src/tools/FileEditTool/FileEditTool.ts`?文件编辑
`src/tools/BashTool/BashTool.tsx`1143Bash 命令执行


九、下节预告

下一节我们将深入 工具执行机制

- StreamingToolExecutor 的并行执行

- 工具结果如何返回给模型

- 工具执行失败的处理


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*