跳到主要内容

MiniOpenCode 02 - 工具系统详解

配合 src/services/toolService.ts 代码讲解

一句话总结

工具是 Agent 感知和改变世界的双手


核心代码:toolService.ts 完整解析

// src/services/toolService.ts
/**
* 工具服务 - 提供内置工具
*/

import { jsonSchema } from 'ai';
import type { ToolSet } from 'ai';
import fs from 'node:fs/promises';
import path from 'node:path';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';

interface ToolResult {
output: string;
title: string;
metadata: Record<string, unknown>;
}

第一步:理解工具定义结构

每个工具包含 4 个核心属性:

{
id: 'tool_name', // 1. 唯一标识符
description: '工具描述', // 2. 功能描述(LLM 会看到)
inputSchema: jsonSchema({ // 3. 参数 schema
type: 'object',
properties: {
paramName: { type: 'string', description: '参数描述' }
},
required: ['paramName']
}),
async execute(args, options) { // 4. 执行函数
// 工具逻辑
return {
output: '结果', // 用户可见输出
title: '标题', // 结果标题
metadata: {} // 附加信息
};
}
}

工具结构图解

┌─────────────────────────────────────┐
│ Tool 对象 │
├─────────────────────────────────────┤
│ id: 'read' │ ← 唯一标识
│ description: '读取文件内容' │ ← LLM 看到的描述
│ inputSchema: { │ ← 参数定义
│ type: 'object', │
│ properties: { path: {...} }, │
│ required: ['path'] │
│ } │
│ execute: async (args) => { ... } │ ← 执行逻辑
└─────────────────────────────────────┘

第二步:read 工具实现

const readTool = {
id: 'read',
description: '读取文件内容',
inputSchema: jsonSchema({
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' }
},
required: ['path']
}),
async execute({ path }: { path: string }): Promise<ToolResult> {
try {
// 使用 fs.promises 读取文件
const content = await fs.readFile(path, 'utf-8');

// 返回成功结果
return {
output: content, // 文件内容
title: `文件: ${path}`, // 标题
metadata: { path } // 元数据
};
} catch (error) {
// 返回错误结果
return {
output: `读取失败: ${(error as Error).message}`,
title: '错误',
metadata: { error: (error as Error).message }
};
}
}
};

执行流程

LLM: "我需要读取 /tmp/test.txt"


tool-call: { toolName: 'read', args: { path: '/tmp/test.txt' } }


execute({ path: '/tmp/test.txt' })

├── fs.readFile('/tmp/test.txt', 'utf-8')


return { output: '文件内容...', title: '文件: /tmp/test.txt', metadata: {...} }


tool-result: { toolName: 'read', output: {...} }

第三步:write 工具实现

const writeTool = {
id: 'write',
description: '写入内容到文件',
inputSchema: jsonSchema({
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
content: { type: 'string', description: '文件内容' }
},
required: ['path', 'content'] // 两个都是必需参数
}),
async execute({ path, content }: { path: string; content: string }): Promise<ToolResult> {
try {
// 写入文件
await fs.writeFile(path, content, 'utf-8');

return {
output: `已写入文件: ${path}`,
title: '写入成功',
metadata: {
path,
bytes: Buffer.byteLength(content, 'utf-8') // 写入字节数
}
};
} catch (error) {
return {
output: `写入失败: ${(error as Error).message}`,
title: '错误',
metadata: { error: (error as Error).message }
};
}
}
};

关键点

细节说明
Buffer.byteLength(content, 'utf-8')计算 UTF-8 编码后的字节数
required: ['path', 'content']两个参数都是必需的
错误处理try-catch 捕获所有异常

第四步:edit 工具实现(高级)

const editTool = {
id: 'edit',
description: '编辑文件,通过替换字符串',
inputSchema: jsonSchema({
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
oldString: { type: 'string', description: '要替换的字符串' },
newString: { type: 'string', description: '新字符串' }
},
required: ['path', 'oldString', 'newString']
}),
async execute({ path, oldString, newString }: {
path: string;
oldString: string;
newString: string
}): Promise<ToolResult> {
try {
// 1. 读取原文件
const content = await fs.readFile(path, 'utf-8');

// 2. 检查 oldString 是否存在
if (!content.includes(oldString)) {
return {
output: '未找到要替换的字符串',
title: '错误',
metadata: { error: 'oldString not found in file' }
};
}

// 3. 执行替换
const newContent = content.replace(oldString, newString);

// 4. 写回文件
await fs.writeFile(path, newContent, 'utf-8');

return {
output: `已编辑文件: ${path}`,
title: '编辑成功',
metadata: { path }
};
} catch (error) {
return {
output: `编辑失败: ${(error as Error).message}`,
title: '错误',
metadata: { error: (error as Error).message }
};
}
}
};

edit 工具的独特性

// 替换逻辑使用 String.replace()
// 注意:这只会替换第一个匹配项
const newContent = content.replace(oldString, newString);

// 如果需要替换所有匹配项,使用:
const newContent = content.split(oldString).join(newString);

// 或者使用正则表达式(需转义特殊字符)
const newContent = content.replace(new RegExp(escapeRegExp(oldString), 'g'), newString);

第五步:grep 工具实现(复杂)

// 递归搜索文件
async function searchFiles(
dir: string,
pattern: RegExp,
include?: string,
results: string[] = [],
depth = 0
): Promise<string[]> {
// 防止无限递归,限制深度为 10
if (depth > 10) return results;

try {
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
// 递归处理子目录(排除 . 开头的隐藏目录和 node_modules)
await searchFiles(fullPath, pattern, include, results, depth + 1);
} else if (entry.isFile()) {
// 检查文件类型过滤
if (include && !entry.name.match(new RegExp(include.replace(/\*/g, '.*')))) {
continue;
}

try {
const content = await fs.readFile(fullPath, 'utf-8');
const lines = content.split('\n');

// 逐行匹配,收集结果
lines.forEach((line, index) => {
if (pattern.test(line)) {
results.push(`${fullPath}:${index + 1}: ${line}`);
}
});
} catch {} // 忽略无法读取的文件
}
}
} catch {} // 忽略无法访问的目录

return results;
}

const grepTool = {
id: 'grep',
description: '在文件中搜索内容',
inputSchema: jsonSchema({
type: 'object',
properties: {
pattern: { type: 'string', description: '搜索模式(正则表达式)' },
path: { type: 'string', description: '搜索目录路径' },
include: { type: 'string', description: '文件类型过滤,如 *.ts' }
},
required: ['pattern']
}),
async execute({ pattern, path: searchPath, include }: {
pattern: string;
path?: string;
include?: string
}): Promise<ToolResult> {
try {
// 编译正则表达式
const regex = new RegExp(pattern, 'g');

// 默认搜索当前目录
const cwd = searchPath || process.cwd();

// 执行搜索
const results = await searchFiles(cwd, regex, include);

if (results.length === 0) {
return { output: '未找到匹配结果', title: '搜索结果', metadata: { pattern, path: cwd } };
}

return {
output: results.join('\n'),
title: `找到 ${results.length} 个匹配`,
metadata: { pattern, path: cwd, count: results.length }
};
} catch (error) {
return {
output: `搜索失败: ${(error as Error).message}`,
title: '错误',
metadata: { error: (error as Error).message }
};
}
}
};

grep 搜索结果格式

/path/to/file.ts:10: const foo = 'bar';
/path/to/file.ts:25: console.log(foo);
/path/to/another.ts:5: const foo = require('./bar');

格式:文件路径:行号: 匹配内容


第六步:bash 工具实现

const execAsync = promisify(exec);  // 将 callback 风格转为 Promise

const bashTool = {
id: 'bash',
description: '执行 bash 命令',
inputSchema: jsonSchema({
type: 'object',
properties: {
command: { type: 'string', description: 'bash 命令' },
cwd: { type: 'string', description: '工作目录' }
},
required: ['command']
}),
async execute({ command, cwd }: { command: string; cwd?: string }): Promise<ToolResult> {
try {
// 执行命令
const { stdout, stderr } = await execAsync(command, { cwd });

// stdout 和 stderr 至少有一个有值
const output = stderr || stdout;

return {
output: output || '(无输出)',
title: '命令执行结果',
metadata: { command, cwd, stdout, stderr }
};
} catch (error) {
return {
output: `命令执行失败: ${(error as Error).message}`,
title: '错误',
metadata: { error: (error as Error).message }
};
}
}
};

execAsync 详解

import { exec } from 'node:child_process';
import { promisify } from 'node:util';

// promisify 将 callback 风格转为 Promise
const execAsync = promisify(exec);

// 使用方式
const { stdout, stderr } = await execAsync('ls -la', { cwd: '/tmp' });

第七步:工具注册与执行

// 导出所有内置工具
export const TOOLS: ToolSet = {
read: readTool,
write: writeTool,
edit: editTool,
grep: grepTool,
bash: bashTool
} as ToolSet;

// 动态执行工具
export async function executeTool(
name: string,
args: Record<string, unknown>
): Promise<ToolResult | { error: string }> {
const tool = TOOLS[name];

if (!tool) {
return { error: `Unknown tool: ${name}` };
}

// 调用工具的 execute 方法
return (tool as { execute: (args: Record<string, unknown>) => Promise<ToolResult> }).execute(args);
}

执行流程

Agent 决定调用工具


executeTool('read', { path: '/tmp/test.txt' })

├── TOOLS['read'] 找到对应工具

├── read.execute({ path: '/tmp/test.txt' })

├── 返回 ToolResult


返回 { output, title, metadata }

工具全景图

┌─────────────────────────────────────────────────────────────┐
│ TOOLS 工具集 │
├───────────┬───────────┬───────────┬───────────┬────────────┤
│ read │ write │ edit │ grep │ bash │
├───────────┼───────────┼───────────┼───────────┼────────────┤
│ 读取文件 │ 写入文件 │ 替换字符串 │ 搜索内容 │ 执行命令 │
│ │ │ │ │ │
│ fs.read │ fs.write │ replace │ search │ execAsync │
│ File │ File │ 字符串 │ Files │ Command │
└───────────┴───────────┴───────────┴───────────┴────────────┘

检查点

  • 能画出一个工具的 4 个组成部分
  • 理解 read/write/edit/grep/bash 各自用途
  • 掌握 execute 返回值的三要素
  • 能添加一个自定义工具

本课小结

工具功能核心 API
read读取文件fs.readFile
write写入文件fs.writeFile
edit替换字符串String.replace
grep递归搜索fs.readdir + 正则
bash执行命令execAsync

下课预告

MiniOpenCode 03 - 会话与提示词管理

  • 配合 session.ts + prompts.ts 讲解
  • 会话存储与消息管理
  • 提示词模板系统