迭代 3:BuildTool 工厂与基础工具集
目标 :实现 buildTool 工厂模式和基础工具定义。这是整个工具系统的基础设施。
预计时间 :第 6-9 天
为什么需要 buildTool 工厂?
如果没有工厂模式,每个工具都需要重复实现大量样板代码——描述、权限检查、并发安全、输入验证等。buildTool 工厂通过提供智能默认值解决了这个问题:工具定义只需要关注核心差异,自动获得标准化的接口。
定义 Tool 类型和 buildTool
src/tools/Tool.ts:
copy import { z } from "zod" ;
export type ToolContext = {
cwd : string ;
};
export type ToolResult < T = unknown > = {
data : T ;
isError ?: boolean ;
};
export type ToolDef = {
name : string ;
inputSchema : z . ZodType ;
outputSchema ?: z . ZodType ;
description ?: () => Promise < string >;
prompt ?: () => Promise < string >;
call : ( input : unknown , context : ToolContext ) => Promise < ToolResult >;
isReadOnly ?: () => boolean ;
isConcurrencySafe ?: () => boolean ;
isEnabled ?: () => boolean ;
validateInput ?: ( input : unknown , ctx : ToolContext ) => ValidationResult ;
};
export type Tool = {
name : string ;
inputSchema : z . ZodType ;
outputSchema ?: z . ZodType ;
description : () => Promise < string >;
prompt : () => Promise < string >;
call : ( input : unknown , context : ToolContext ) => Promise < ToolResult >;
isReadOnly : () => boolean ;
isConcurrencySafe : () => boolean ;
isEnabled : () => boolean ;
validateInput ?: ( input : unknown , ctx : ToolContext ) => ValidationResult ;
};
type ValidationResult =
| { result : true }
| { result : false ; message : string };
const TOOL_DEFAULTS = {
description : async () => "" ,
prompt : async () => "" ,
isReadOnly : () => false ,
isConcurrencySafe : () => false ,
isEnabled : () => true ,
};
export function buildTool < D extends ToolDef >( def : D ) : Tool {
return {
... TOOL_DEFAULTS ,
... def ,
};
}
export function createToolContext ( cwd ?: string ) : ToolContext {
return { cwd: cwd ?? process . cwd () };
} 关于 TOOL_DEFAULTS 的思考
TOOL_DEFAULTS 的设计体现了"约定优于配置"的原则:
大多数工具不是只读的,所以 isReadOnly 默认为 false
大多数工具不是并发安全的(串行执行更安全),所以 isConcurrencySafe 默认为 false
描述和提示词是可选的,默认为空
工具默认启用
当这些默认值不适用时,工具定义可以覆盖它们。例如 Read 工具是只读和并发安全的,就显式覆写这两个属性。
深入理解 Zod Schema
Zod 是 TypeScript 生态中流行的 Schema 验证库。在工具系统中,每个工具的 inputSchema 使用 Zod 定义,作用包括:
运行时验证 ——当模型调用工具时,参数必须通过 Zod 验证
类型推断 ——z.infer<typeof tool.inputSchema> 可以从 Schema 中提取出 TypeScript 类型
JSON Schema 生成 ——通过 zod-to-json-schema,可以将 Zod Schema 转换为 API 需要的 JSON Schema 格式
实现 BashTool
src/tools/BashTool/BashTool.ts:
copy import { execa } from "execa" ;
import { buildTool } from "../Tool.js" ;
export const BashTool = buildTool ({
name: "Bash" ,
inputSchema: z . object ({
command: z . string (). describe ( "The shell command to execute" ),
description: z . string (). optional (). describe ( "What this command does" ),
timeout: z . number (). optional (). describe ( "Timeout in milliseconds" ),
}),
outputSchema: z . object ({
stdout: z . string (),
stderr: z . string (),
exitCode: z . number (),
}),
isConcurrencySafe : () => false ,
isReadOnly : () => false ,
async prompt () {
return `## Bash
Executes shell commands. Use this to run code, build, test, etc.
Use full paths and proper quoting. Long-running commands should use &.
Parameters:
- command (required): The shell command to execute
- description (optional): What this command does
- timeout (optional): Timeout in ms (default: 120000)` ;
},
async call ({ command , timeout }) {
const result = await execa ( "bash" , [ "-c" , command ], {
timeout: timeout ?? 120_000 ,
reject: false ,
all: true ,
});
return {
data: {
stdout: result . stdout ?? "" ,
stderr: result . stderr ?? "" ,
exitCode: result . exitCode ?? 0 ,
},
};
},
}); BashTool 的设计要点
使用 execa 而非 child_process ——execa 提供了更好的 API(Promise 返回、默认转义、更好的错误处理)
reject: false ——不因为非零退出码而抛出异常,让模型自己判断命令是否成功
all: true ——同时捕获 stdout 和 stderr
isConcurrencySafe: false ——Shell 有状态(cwd、环境变量),不能并行执行
prompt() 返回自然语言描述 ——这会被注入到系统提示词中,帮助模型理解何时使用这个工具
实现 FileReadTool
src/tools/FileReadTool/FileReadTool.ts:
copy import { readFile } from "fs/promises" ;
import { buildTool } from "../Tool.js" ;
export const FileReadTool = buildTool ({
name: "Read" ,
inputSchema: z . object ({
file_path: z . string (). describe ( "Absolute path to the file" ),
offset: z . number (). optional (). describe ( "Line number to start from (0-based)" ),
limit: z . number (). optional (). describe ( "Maximum number of lines to read" ),
}),
isReadOnly : () => true ,
isConcurrencySafe : () => true ,
async call ({ file_path , offset , limit }) {
const content = await readFile ( file_path , "utf-8" );
const lines = content . split ( " \n " );
const start = offset ?? 0 ;
const end = limit ? start + limit : lines . length ;
return {
data: {
content: lines . slice ( start , end ). join ( " \n " ),
totalLines: lines . length ,
startLine: start ,
endLine: end ,
},
};
},
}); FileReadTool 的两个关键属性:
isReadOnly: true——这告诉权限系统不需要每次都询问用户
isConcurrencySafe: true——多个文件可以同时读取
实现 FileWriteTool
src/tools/FileWriteTool/FileWriteTool.ts:
copy import { writeFile , mkdir } from "fs/promises" ;
import { dirname } from "path" ;
import { buildTool } from "../Tool.js" ;
export const FileWriteTool = buildTool ({
name: "Write" ,
inputSchema: z . object ({
file_path: z . string (). describe ( "Absolute path to the file" ),
content: z . string (). describe ( "The complete new file content" ),
}),
isReadOnly : () => false ,
isConcurrencySafe : () => false ,
async call ({ file_path , content }) {
await mkdir ( dirname ( file_path ), { recursive: true });
await writeFile ( file_path , content , "utf-8" );
return {
data: {
message: `Written ${ content . length } bytes to ${ file_path } ` ,
},
};
},
}); 实现 GlobTool
src/tools/GlobTool/GlobTool.ts:
copy import { glob } from "fs/promises" ;
import { buildTool } from "../Tool.js" ;
export const GlobTool = buildTool ({
name: "Glob" ,
inputSchema: z . object ({
pattern: z . string (). describe ( "Glob pattern to search (e.g., '**/*.ts')" ),
path: z . string (). optional (). describe ( "Directory to search in" ),
}),
isReadOnly : () => true ,
isConcurrencySafe : () => true ,
async call ({ pattern , path }) {
const cwd = path ?? process . cwd ();
const results : string [] = [];
for await ( const file of glob ( pattern , { cwd })) {
results . push ( file );
}
return { data: { files: results , count: results . length } };
},
}); 实现 GrepTool
src/tools/GrepTool/GrepTool.ts:
copy import { execa } from "execa" ;
import { buildTool } from "../Tool.js" ;
export const GrepTool = buildTool ({
name: "Grep" ,
inputSchema: z . object ({
pattern: z . string (). describe ( "Search pattern (regex supported)" ),
path: z . string (). optional (). describe ( "Directory to search in" ),
include: z . string (). optional (). describe ( "File glob pattern to include" ),
maxResults: z . number (). optional (). default ( 50 ),
}),
isReadOnly : () => true ,
isConcurrencySafe : () => true ,
async call ({ pattern , path , include , maxResults }) {
const args = [ "--color" , "never" , "--line-number" , "--with-filename" ];
if ( include ) args . push ( "--glob" , include );
args . push ( "--max-count" , String ( maxResults ?? 50 ));
args . push ( pattern );
if ( path ) args . push ( path );
const { stdout , exitCode } = await execa ( "rg" , args , {
reject: false ,
timeout: 30000 ,
});
if ( exitCode === 2 ) return { isError: true , data: { message: "rg failed" } };
const results = stdout ? stdout . split ( " \n " ). filter ( Boolean ) : [];
return { data: { results , count: results . length , truncated: results . length >= ( maxResults ?? 50 ) } };
},
}); 工具注册表
src/tools/registry.ts:
copy import { BashTool } from "./BashTool/BashTool.js" ;
import { FileReadTool } from "./FileReadTool/FileReadTool.js" ;
import { FileWriteTool } from "./FileWriteTool/FileWriteTool.js" ;
import { GlobTool } from "./GlobTool/GlobTool.js" ;
import { GrepTool } from "./GrepTool/GrepTool.js" ;
import type { Tool } from "./Tool.js" ;
export function getAllTools () : Tool [] {
return [ BashTool , FileReadTool , FileWriteTool , GlobTool , GrepTool ];
} 工具目录结构
每个工具都有自己的目录,包含核心实现、UI 组件和提示词:
copy src/tools/BashTool/
├── BashTool.ts ← 核心实现(buildTool 调用)
├── UI.tsx ← 终端渲染组件
└── index.ts ← 统一导出 这种自包含的结构使得添加新工具只需创建一个目录,然后在注册表中注册即可。
验证标准
[x] BashTool 可以执行 shell 命令并返回输出
[x] FileReadTool 可以读取文件内容
[x] FileWriteTool 可以写入文件
[x] GlobTool 可以搜索匹配的文件
[x] GrepTool 可以搜索文件内容
[x] 工具注册表返回所有已注册的工具
[x] 所有工具通过 Zod Schema 验证输入参数
测试工具
可以在 test.js 中单独测试每个工具:
copy import { BashTool } from "./src/tools/BashTool/BashTool.js" ;
const result = await BashTool . call (
{ command: "echo hello" , timeout: 5000 },
{ cwd: process . cwd () }
);
console . log ( result );