Skip to content

工具接口设计

gemini-cli 的 ServerTool 接口

gemini-cli 定义了统一的工具接口 ServerTool

源码位置packages/core/src/tools/tool.ts

typescript
interface ServerTool {
  // 工具名称
  name: string

  // 工具定义(JSON Schema)
  schema: FunctionDeclaration

  // 执行工具
  execute(
    params: Record<string, unknown>,
    signal?: AbortSignal
  ): Promise<ToolResult>

  // 是否需要用户确认(可选)
  shouldConfirmExecute?(
    params: Record<string, unknown>,
    signal?: AbortSignal
  ): Promise<ToolCallConfirmationDetails | false>
}

核心方法

execute

执行工具的核心方法。

typescript
async execute(params: { file_path: string }): Promise<ToolResult> {
  try {
    const content = await fs.readFile(params.file_path, 'utf-8')
    return {
      content,           // 返回给 LLM 的内容
      metadata: {        // 可选的元数据
        size: content.length
      }
    }
  } catch (error) {
    return {
      error: `读取失败: ${error.message}`
    }
  }
}

返回值 ToolResult

  • content:成功时返回的内容(字符串)
  • error:失败时返回的错误信息
  • metadata:可选的元数据

shouldConfirmExecute

用于危险操作的确认机制。

typescript
async shouldConfirmExecute(params: { command: string }) {
  // 只读命令不需要确认
  if (params.command.startsWith('ls ') || params.command.startsWith('cat ')) {
    return false
  }

  // 危险命令需要确认
  return {
    title: '执行 Shell 命令',
    description: `即将执行: ${params.command}`,
    risk: 'high'
  }
}

返回 false 表示不需要确认,返回确认详情对象则需要用户确认。

完整工具示例

只读工具(read_file)

typescript
export const readFileTool: ServerTool = {
  name: 'read_file',

  schema: {
    name: 'read_file',
    description: '读取指定文件的内容。用于查看代码、配置文件或文档。',
    parameters: {
      type: 'object',
      properties: {
        file_path: {
          type: 'string',
          description: '文件的绝对路径'
        },
        encoding: {
          type: 'string',
          description: '文件编码,默认 utf-8',
          enum: ['utf-8', 'ascii', 'base64']
        }
      },
      required: ['file_path']
    }
  },

  async execute(params: { file_path: string; encoding?: string }) {
    const encoding = params.encoding || 'utf-8'

    try {
      const content = await fs.readFile(params.file_path, encoding)
      return { content }
    } catch (error) {
      if (error.code === 'ENOENT') {
        return { error: `文件不存在: ${params.file_path}` }
      }
      return { error: `读取失败: ${error.message}` }
    }
  }

  // 只读操作不需要确认
  // 不实现 shouldConfirmExecute
}

写入工具(write_file)

typescript
export const writeFileTool: ServerTool = {
  name: 'write_file',

  schema: {
    name: 'write_file',
    description: '创建或覆盖文件。用于创建新文件或完全重写现有文件。',
    parameters: {
      type: 'object',
      properties: {
        file_path: {
          type: 'string',
          description: '文件的绝对路径'
        },
        content: {
          type: 'string',
          description: '要写入的内容'
        }
      },
      required: ['file_path', 'content']
    }
  },

  async execute(params: { file_path: string; content: string }) {
    try {
      await fs.writeFile(params.file_path, params.content, 'utf-8')
      return { content: `文件已写入: ${params.file_path}` }
    } catch (error) {
      return { error: `写入失败: ${error.message}` }
    }
  },

  // 写入操作需要确认
  async shouldConfirmExecute(params: { file_path: string; content: string }) {
    return {
      title: '写入文件',
      description: `将创建/覆盖文件: ${params.file_path}`,
      preview: params.content.slice(0, 500), // 预览内容
      risk: 'medium'
    }
  }
}

Shell 工具

typescript
export const shellTool: ServerTool = {
  name: 'shell',

  schema: {
    name: 'shell',
    description: '执行 shell 命令。用于运行构建、测试、git 等命令。',
    parameters: {
      type: 'object',
      properties: {
        command: {
          type: 'string',
          description: '要执行的命令'
        },
        cwd: {
          type: 'string',
          description: '工作目录(可选)'
        }
      },
      required: ['command']
    }
  },

  async execute(params: { command: string; cwd?: string }) {
    try {
      const { stdout, stderr } = await execAsync(params.command, {
        cwd: params.cwd,
        timeout: 60000
      })
      return {
        content: stdout || stderr || '命令执行完成(无输出)'
      }
    } catch (error) {
      return {
        error: `命令执行失败: ${error.message}\n${error.stderr || ''}`
      }
    }
  },

  async shouldConfirmExecute(params: { command: string }) {
    const safeCommands = ['ls', 'pwd', 'cat', 'head', 'tail', 'echo', 'which']
    const firstWord = params.command.split(' ')[0]

    if (safeCommands.includes(firstWord)) {
      return false // 安全命令不需要确认
    }

    return {
      title: '执行命令',
      description: params.command,
      risk: params.command.includes('rm ') ? 'high' : 'medium'
    }
  }
}

设计原则

1. 单一职责

每个工具只做一件事:

  • read_file 只读文件
  • write_file 只写文件
  • edit_file 只编辑文件

2. 清晰的错误处理

typescript
async execute(params) {
  // 参数校验
  if (!params.path) {
    return { error: '缺少必填参数: path' }
  }

  try {
    // 执行操作
  } catch (error) {
    // 返回有意义的错误信息
    return { error: `操作失败: ${error.message}` }
  }
}

3. 返回有用的信息

LLM 需要知道操作结果来决定下一步:

typescript
// 好
return { content: '文件已创建: /path/to/file.ts' }

// 不好
return { content: 'ok' }

4. 支持取消

通过 AbortSignal 支持取消长时间操作:

typescript
async execute(params, signal?: AbortSignal) {
  if (signal?.aborted) {
    return { error: '操作已取消' }
  }

  // 长时间操作中定期检查
  for (const item of items) {
    if (signal?.aborted) {
      return { error: '操作已取消' }
    }
    await process(item)
  }
}

工具注册

gemini-cli 在启动时注册所有工具:

typescript
// 简化的工具注册逻辑
const tools: ServerTool[] = [
  readFileTool,
  writeFileTool,
  editFileTool,
  shellTool,
  grepTool,
  globTool,
  // ...
]

// 转换为 LLM 需要的格式
const functionDeclarations = tools.map(t => t.schema)

小结

  • ServerTool 接口定义了工具的统一结构
  • execute 方法执行具体操作
  • shouldConfirmExecute 控制是否需要用户确认
  • 设计工具时注意单一职责、清晰错误、有用的返回信息

下一步

了解了工具接口设计,接下来看 gemini-cli 有哪些内置工具:内置工具分析 →

通过实际源码学习 AI Agent 开发