Appearance
🐣 小a的工具箱
循环跑起来了,但 Agent 还不会干活——因为没工具可调。循环是节奏,工具是手:配上 read/write/bash 三件套,它就能读写文件、执行命令。三个够吗?对个人用够了——pi 有十几个是为了产品级完备,咱们先三个,能干活就行。
27.1 Tool 接口回顾
第 26 章定义过:
typescript
interface Tool {
name: string;
description: string; // 告诉模型这工具干嘛(第 6 章强调过:描述要写好)
schema: any; // 参数的 JSON schema
execute: (args: any) => Promise<string>;
}这一章,我们实现三个具体的 Tool。
27.2 工具一:read(读文件)
typescript
// tools/read.ts
import { readFileSync } from "fs";
export const readTool: Tool = {
name: "read",
description:
"读取指定路径的文件内容。当你需要查看文件、理解现有代码、确认配置时使用。",
schema: {
type: "object",
properties: {
path: { type: "string", description: "文件的相对或绝对路径" },
},
required: ["path"],
},
execute: async (args) => {
try {
const content = readFileSync(args.path, "utf-8");
// 截断:防止超大文件撑爆上下文(第 20 章的难点①)
if (content.length > 10000) {
return content.slice(0, 5000)
+ `\n\n... (省略 ${content.length - 10000} 字符) ...\n\n`
+ content.slice(-5000);
}
return content;
} catch (e) {
return `错误:读不到文件 ${args.path}(${e.message})`;
}
},
};🧙 两个细节:
- 描述写得好——不只是"读文件",还说明"什么时候用"。模型靠这个决定该不该调(第 6 章)。
- 截断——超大文件只留头尾。这就是第 20 章讲的"难点①:截断"。我们简化版,pi 有更精细的
truncate.ts。
27.3 工具二:write(写文件)
typescript
// tools/write.ts
import { writeFileSync, mkdirSync } from "path";
export const writeTool: Tool = {
name: "write",
description: "把内容写入指定路径的文件。用于创建新文件或覆盖现有文件。",
schema: {
type: "object",
properties: {
path: { type: "string", description: "文件路径" },
content: { type: "string", description: "要写入的内容" },
},
required: ["path", "content"],
},
execute: async (args) => {
try {
mkdirSync(dirname(args.path), { recursive: true }); // 自动建目录
writeFileSync(args.path, args.content, "utf-8");
return `已写入 ${args.path}(${args.content.length} 字符)`;
} catch (e) {
return `错误:写文件失败(${e.message})`;
}
},
};🧙 注意:write 是"危险操作"——会覆盖文件。第 30 章我们会加安全机制(危险操作确认)。
27.4 工具三:bash(执行命令)
typescript
// tools/bash.ts
import { execSync } from "child_process";
export const bashTool: Tool = {
name: "bash",
description:
"执行 shell 命令。用于跑测试、装依赖、查 git 状态等。输出限制在 5000 字符。",
schema: {
type: "object",
properties: {
command: { type: "string", description: "要执行的命令" },
},
required: ["command"],
},
execute: async (args) => {
// ⚠️ 危险命令检查(第 30 章详讲)
const dangerous = /\b(rm\s+-rf\s+\/|mkfs|dd\s+if=|>\s*\/dev\/sda)\b/;
if (dangerous.test(args.command)) {
return "错误:拒绝执行危险命令。";
}
try {
const output = execSync(args.command, {
encoding: "utf-8",
timeout: 30000, // 30 秒超时
maxBuffer: 1024 * 1024,
});
// 截断输出(难点①)
return output.length > 5000
? output.slice(0, 2500) + "\n...(省略)...\n" + output.slice(-2500)
: output;
} catch (e) {
return `命令失败(退出码 ${e.status}):\n${e.stderr || e.message}`;
}
},
};🧙 三个安全细节:
- 危险命令检查——
rm -rf /这种直接拒(第 30 章详讲)- 超时——30 秒,防止命令卡死
- 截断输出——防止几千行输出撑爆上下文
27.5 把工具组装进 Agent
typescript
// tools/index.ts
import { readTool } from "./read.ts";
import { writeTool } from "./write.ts";
import { bashTool } from "./bash.ts";
export const tools: Record<string, Tool> = {
read: readTool,
write: writeTool,
bash: bashTool,
};然后跑:
typescript
// test.ts
import { runAgent } from "./agent.ts";
import { tools } from "./tools/index.ts";
await runAgent(
{
model: "claude-sonnet-4-5",
system: "你是编程助手。可以用 read/write/bash 工具。",
tools,
},
"读一下 package.json,告诉我项目叫什么",
);跑起来——Agent 会自己调 read,读 package.json,然后回答! 你的 Agent 第一次真正"干活"了。
27.6 对照 pi:我们省了什么
🧙 我们 vs pi 的工具系统:
我们的 pi 的 工具数 3 个 十几个(read/write/edit/bash/grep/find/ls/...) 截断 简单 slice 精细的 truncate.ts 并发写保护 无 file-mutation-queue(难点②) 输出累积 无 output-accumulator(难点③) 我们省了 edit(增量改)、grep/find(检索)等工具,也省了队列和累积。 因为个人用,read+write+bash 够了。但要注意:没有 file-mutation-queue,并发写文件可能损坏(附录 E.7)——个人用串行调用问题不大,生产用得加。
本章小结
🐣 小a的第二十七课
┌──────── 工具系统 ────────┐ │ │ │ • Tool = name+desc+ │ │ schema+execute │ │ → 描述写好模型才会用 │ │ │ │ • read:截断防超长 │ │ • write:危险操作(第23章)│ │ • bash:危险检查+超时+截断│ │ │ │ • 三件套够个人用 │ │ pi 多的是产品级完备 │ │ │ │ • 对照 pi:省了 edit/ │ │ grep/find + 队列+累积 │ └──────────────────────────┘
关键认知:工具是 Agent 的"手"。三个工具(read/write/bash)就能让 Agent 干大部分编程活。复杂的工具(edit/grep)和工程保护(队列/累积)是产品级才需要的。
课后实验
- 实现三个工具:照着代码写出 read/write/bash。
- 让 Agent 干活:让它"读 package.json,告诉我用了哪些依赖"。看它自己调 read。
- 加个工具:试着自己实现一个
ls(列目录)工具。提示:readdirSync。
下一章:Agent 会干活了,但聊久了上下文会爆。加记忆管理。 → 第 28 章 · 上下文与会话