之前的文章介绍过主控Agent以及上下文实现的细节,除了主控Agent和上下文管理外,工具实现也是Agentic的一个重要环节。
核心架构设计
1. 工具接口(Tool Interface)
所有工具都必须实现Tool
接口,这是整个系统的基础:
javascript
export interface Tool<TParams = unknown, TResult extends ToolResult = ToolResult> {
name: string; // 工具的唯一标识符
displayName: string; // 用户友好的显示名称
description: string; // 工具功能描述
icon: Icon; // 显示图标
schema: FunctionDeclaration; // JSON Schema 参数定义
// 核心方法
validateToolParams(params: TParams): string | null;
getDescription(params: TParams): string;
shouldConfirmExecute(params: TParams, abortSignal: AbortSignal): Promise<ToolCallConfirmationDetails | false>;
execute(params: TParams, signal: AbortSignal, updateOutput?: (output: string) => void): Promise<TResult>;
}
这个接口设计的巧妙之处在于:
● 类型安全:通过泛型确保参数和返回值的类型正确性
● 参数验证:内置验证机制,确保输入数据的有效性
● 用户确认:对于危险操作,可以要求用户确认
● 可中断性:支持通过 AbortSignal 取消长时间运行的操作
2. 基础工具类(BaseTool)
为了减少重复代码,系统提供了BaseTool
抽象类:
javascript
export abstract class BaseTool<TParams = unknown, TResult extends ToolResult = ToolResult>
implements Tool<TParams, TResult> {
constructor(
readonly name: string,
readonly displayName: string,
readonly description: string,
readonly icon: Icon,
readonly parameterSchema: Schema,
readonly isOutputMarkdown: boolean = true,
readonly canUpdateOutput: boolean = false,
) {}
// 自动生成 schema
get schema(): FunctionDeclaration {
return {
name: this.name,
description: this.description,
parameters: this.parameterSchema,
};
}
// 子类必须实现的抽象方法
abstract execute(params: TParams, signal: AbortSignal, updateOutput?: (output: string) => void): Promise<TResult>;
}
3. 工具结果(ToolResult)
每个工具执行后都返回标准化的结果:
javascript
export interface ToolResult {
summary?: string; // 操作摘要
llmContent: PartListUnion; // 发送给 LLM 的内容
returnDisplay: ToolResultDisplay; // 显示给用户的内容
}
这种设计分离了"AI 需要知道的"和"用户需要看到的",让系统更加灵活。
工具注册与发现机制
工具注册表(ToolRegistry)
ToolRegistry
是整个工具系统的中央管理器:
javascript
export class ToolRegistry {
private tools: Map<string, Tool> = new Map();
// 注册内置工具
registerTool(tool: Tool): void {
this.tools.set(tool.name, tool);
}
// 动态发现工具(外部工具)
async discoverAllTools(): Promise<void> {
await this.discoverAndRegisterToolsFromCommand();
await discoverMcpTools(/* ... */);
}
}
动态工具发现
系统支持两种动态工具发现机制:
1. 命令行发现
通过配置toolDiscoveryCommand
,系统可以执行命令来发现自定义工具:
javascript
class DiscoveredTool extends BaseTool<ToolParams, ToolResult> {
async execute(params: ToolParams): Promise<ToolResult> {
const callCommand = this.config.getToolCallCommand()!;
const child = spawn(callCommand, [this.name]);
child.stdin.write(JSON.stringify(params));
// ... 处理执行结果
}
}
2. MCP 服务器发现
支持 Model Context Protocol (MCP) 服务器,实现更复杂的工具集成:
javascript
class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
constructor(
private mcpTool: CallableTool,
public readonly serverName: string,
private serverToolName: string,
// ...
) {
// 工具名称会加上服务器前缀,如:serverAlias__actualToolName
super(`${serverName}__${serverToolName}`, /* ... */);
}
}
内置工具详解
让我们看看几个核心的内置工具是如何实现的:
1. 文件读取工具
javascript
export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> {
static readonly Name: string = 'read_file';
constructor(private config: Config) {
super(
ReadFileTool.Name,
'ReadFile',
'Reads and returns the content of a specified file from the local filesystem...',
Icon.FileSearch,
{
properties: {
absolute_path: {
description: "The absolute path to the file to read...",
type: Type.STRING,
},
offset: { /* 起始行号 */ },
limit: { /* 读取行数 */ },
},
required: ['absolute_path'],
type: Type.OBJECT,
},
);
}
validateToolParams(params: ReadFileToolParams): string | null {
// 验证路径是否为绝对路径
if (!path.isAbsolute(params.absolute_path)) {
return `File path must be absolute, but was relative: ${params.absolute_path}`;
}
// 验证路径是否在允许的根目录内
if (!isWithinRoot(params.absolute_path, this.config.getTargetDir())) {
return `File path must be within the root directory`;
}
// 检查是否被 .geminiignore 忽略
if (this.config.getFileService().shouldGeminiIgnoreFile(params.absolute_path)) {
return `File path is ignored by .geminiignore pattern(s)`;
}
return null;
}
async execute(params: ReadFileToolParams, _signal: AbortSignal): Promise<ToolResult> {
const validationError = this.validateToolParams(params);
if (validationError) {
return {
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
returnDisplay: validationError,
};
}
const result = await processSingleFileContent(
params.absolute_path,
this.config.getTargetDir(),
params.offset,
params.limit,
);
return {
llmContent: result.llmContent,
returnDisplay: result.returnDisplay,
};
}
}
这个实现展示了几个重要的安全特性:
● 路径验证:确保只能访问允许的目录
● 忽略文件检查:尊重 .geminiignore 配置
● 分页支持:支持大文件的分页读取
2. 内存工具
内存工具的核心作用是让AI助手能够"记住"用户提供的重要信息 ,这些信息会被保存到本地文件中,供后续对话使用。
javascript
export class MemoryTool extends BaseTool<SaveMemoryParams, ToolResult> {
static readonly Name: string = 'save_memory';
async execute(params: SaveMemoryParams, _signal: AbortSignal): Promise<ToolResult> {
const { fact } = params;
if (!fact || typeof fact !== 'string' || fact.trim() === '') {
const errorMessage = 'Parameter "fact" must be a non-empty string.';
return {
llmContent: JSON.stringify({ success: false, error: errorMessage }),
returnDisplay: `Error: ${errorMessage}`,
};
}
try {
await MemoryTool.performAddMemoryEntry(fact, getGlobalMemoryFilePath(), {
readFile: fs.readFile,
writeFile: fs.writeFile,
mkdir: fs.mkdir,
});
const successMessage = `Okay, I've remembered that: "${fact}"`;
return {
llmContent: JSON.stringify({ success: true, message: successMessage }),
returnDisplay: successMessage,
};
} catch (error) {
// 错误处理...
}
}
static async performAddMemoryEntry(text: string, memoryFilePath: string, fsAdapter: FsAdapter): Promise<void> {
let processedText = text.trim();
// 移除可能被误解为 markdown 列表项的前导连字符
processedText = processedText.replace(/^(-+\s*)+/, '').trim();
const newMemoryItem = `- ${processedText}`;
// 读取现有内容
let content = '';
try {
content = await fsAdapter.readFile(memoryFilePath, 'utf-8');
} catch (_e) {
// 文件不存在,将创建新文件
}
const headerIndex = content.indexOf(MEMORY_SECTION_HEADER);
if (headerIndex === -1) {
// 没有找到记忆区块,添加新的区块
const separator = ensureNewlineSeparation(content);
content += `${separator}${MEMORY_SECTION_HEADER}\n${newMemoryItem}\n`;
} else {
// 找到记忆区块,在其中添加新条目
// ... 复杂的文本处理逻辑
}
await fsAdapter.writeFile(memoryFilePath, content, 'utf-8');
}
}
内存工具的设计亮点:
● 结构化存储:使用 Markdown 格式组织记忆内容
● 智能文本处理:自动处理格式化问题
● 依赖注入:通过 fsAdapter 参数便于测试
3. Shell工具
javascript
export class ShellTool extends BaseTool<ShellToolParams, ToolResult> {
async shouldConfirmExecute(
params: ShellToolParams,
abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
// Shell 命令通常需要用户确认
return {
type: 'exec',
title: 'Execute Shell Command',
command: params.command,
rootCommand: params.command.split(' ')[0],
onConfirm: async (outcome: ToolConfirmationOutcome) => {
// 处理用户确认结果
},
};
}
async execute(
params: ShellToolParams,
signal: AbortSignal,
updateOutput?: (output: string) => void,
): Promise<ToolResult> {
// 在沙箱环境中执行命令
const child = spawn(command, args, {
cwd: params.directory || this.config.getTargetDir(),
env: { ...process.env, ...sandboxEnv },
});
// 实时输出更新
if (updateOutput) {
const updateInterval = setInterval(() => {
updateOutput(currentOutput);
}, OUTPUT_UPDATE_INTERVAL_MS);
}
// 处理命令执行结果...
}
}
Shell工具展示了系统的安全机制:
● 用户确认:危险操作需要明确确认
● 沙箱执行:在受控环境中运行命令
● 实时反馈:支持流式输出更新
工具执行流程
整个工具执行流程设计得非常优雅:
这个流程的关键特点:
-
参数验证:在执行前确保参数有效性
-
用户确认:危险操作需要用户明确同意
-
结果分离:AI 和用户看到不同格式的结果
-
错误处理:每个环节都有完善的错误处理机制
扩展性设计
自定义工具开发
开发自定义工具非常简单,只需继承BaseTool
:
javascript
class MyCustomTool extends BaseTool<MyParams, ToolResult> {
constructor() {
super(
'my_custom_tool',
'My Custom Tool',
'Description of what this tool does',
Icon.Hammer,
{
properties: {
param1: { type: Type.STRING, description: '...' },
param2: { type: Type.NUMBER, description: '...' },
},
required: ['param1'],
type: Type.OBJECT,
},
);
}
async execute(params: MyParams, signal: AbortSignal): Promise<ToolResult> {
// 实现具体逻辑
return {
llmContent: 'Result for AI',
returnDisplay: 'Result for user',
};
}
}
配置驱动的工具发现
通过配置文件可以轻松集成外部工具:
javascript
{
"toolDiscoveryCommand": "./scripts/discover-tools.sh",
"toolCallCommand": "./scripts/call-tool.sh",
"mcpServers": {
"myServer": {
"command": "node",
"args": ["./my-mcp-server.js"]
}
}
}
安全性考虑
工具系统在设计时充分考虑了安全性:
1. 路径安全
● 所有文件操作都限制在指定的根目录内
● 支持.geminiignore
文件排除敏感文件
● 绝对路径验证防止路径遍历攻击
2. 用户确认机制
javascript
async shouldConfirmExecute(params: TParams): Promise<ToolCallConfirmationDetails | false> {
// 根据操作危险程度决定是否需要确认
if (isDangerousOperation(params)) {
return {
type: 'exec',
title: 'Dangerous Operation',
onConfirm: async (outcome) => {
// 处理用户决定
},
};
}
return false;
}
3. 沙箱执行
● Shell 命令在受限环境中执行
● 环境变量过滤
● 资源限制(内存、CPU、时间)
4. 输入验证
● JSON Schema验证确保参数格式正确
● 自定义验证逻辑处理业务规则
● 类型安全的TypeScript接口
性能优化
1. 懒加载
工具只在需要时才被实例化和注册,减少启动时间。
2. 流式输出
javascript
async execute(
params: TParams,
signal: AbortSignal,
updateOutput?: (output: string) => void,
): Promise<TResult> {
// 支持实时输出更新
if (updateOutput) {
setInterval(() => {
updateOutput(getCurrentOutput());
}, 1000);
}
}
3. 可中断操作
通过 AbortSignal 支持长时间运行操作的取消。
4. 结果缓存
对于幂等操作,可以实现结果缓存减少重复计算。
总结
Gemini CLI的工具系统通过标准化的接口、灵活的扩展机制、完善的安全措施和优雅的执行流程,为构建实用的AI助手提供了坚实的基础,其中不乏设计亮点:
-
类型安全的接口设计:确保编译时和运行时的正确性
-
灵活的扩展机制:支持内置工具、命令行发现和 MCP 服务器
-
完善的安全机制:多层次的安全验证和用户确认
-
优雅的执行流程:清晰的职责分离和错误处理
对于AI 开发者以及所有对AI工具集成感兴趣的人来说,如何让复杂的系统保持简洁、安全和可扩展,Gemini CLI的工具系统都值得深入学习和借鉴。