从聊天机器人到 Agent
你的 Agent 就像世界上最自信的实习生。让它查看 tsconfig.json,它会兴致勃勃地描述文件里大概有什么;让它找出第 42 行的错误,它也能给出一个听起来非常可信的修复方案。
但它根本没有打开文件,因为它做不到。聊天机器人没有工具,只能根据常见代码模式进行猜测,再把猜测当成分析结果。
一个工具就能改变这种情况。我们将添加 read,让这个只会自信解释的实习生真正打开文件。
学习成果
你将得到一个 ToolLoopAgent:收到提示词后,它会调用 read 工具检查指定文件并汇报发现。
快速路径
- 在
index.ts中创建ToolLoopAgent,配置instructions、model和stopWhen: stepCountIs(10) - 先不提供任何工具,运行它并观察聊天机器人如何只解释自己“会做什么”
- 添加带 Zod
inputSchema和 500 行上限的read工具,然后再次运行
动手练习 1.1
先在 index.ts 中构建最小可用的 Agent,再添加一个工具。
要求:
- 从
ai导入ToolLoopAgent、stepCountIs和tool,并从zod导入z - 创建 Agent,设置
model: "anthropic/claude-haiku-4-5"、简短指令和stopWhen: stepCountIs(10) - 添加
read工具,接受path、可选的offset和可选的limit - 将输出限制为 500 行,并在每行前加上行号
实现提示:
ToolLoopAgent接受instructions、model、tools和stopWhen。这里应使用instructions,而不是system- 使用
agent.generate({ prompt })调用 Agent,而不是agent.generate(prompt) tool()的description字段是提供给模型的提示词,不是普通 docstring;模型根据它判断何时调用工具- 基于工作目录解析路径,避免 Agent 意外读取项目外的文件
聊天机器人
从最小的 Agent 开始:没有工具,只有指令。
import { ToolLoopAgent, stepCountIs } from "ai";
const cwd = process.argv[2] || process.cwd();
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: `You are a coding agent.\nWorking directory: ${cwd}`,
tools: {},
stopWhen: stepCountIs(10),
});
const prompt = process.argv.slice(3).join(" ") || "Hello!";
const { text, steps } = await agent.generate({ prompt });
console.log(text);
console.log(`\n(${steps.length} steps)`);运行它:
bun run index.ts . "What files are in this project?"你会得到一段礼貌、热心但完全虚构的回复,例如 “我很乐意帮你探索项目文件!”,随后列出它打算查看的内容。问题是,它其实什么也看不到。这就是聊天机器人。
(1 steps)一步。没有工具调用。模型会说话,这就是它能做的全部。
**注意:AI SDK v6 命名**
请使用 instructions(不是 system)、stopWhen(不是 stopCondition)以及 agent.generate({ prompt })(不是 agent.generate(prompt))。这些名称在不同 AI SDK 版本中发生过变化。错误名称可能仍能通过编译,但 Agent 不会按预期运行。
一个工具改变一切
现在添加 read 工具。这一步会让聊天机器人真正变成 Agent:
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const cwd = resolve(process.argv[2] || process.cwd());
const read = tool({
description: `Read a file from the project. Returns numbered lines.
WHEN TO USE: viewing file contents, checking configs, reading source code.
WHEN NOT TO USE: searching across files (use grep instead).`,
inputSchema: z.object({
path: z.string().describe("File path relative to working directory"),
offset: z.number().optional().describe("Start line (1-indexed)"),
limit: z.number().optional().describe("Max lines to return"),
}),
execute: async ({ path: filePath, offset, limit }) => {
const abs = resolve(cwd, filePath);
const content = readFileSync(abs, "utf-8");
let lines = content.split("\n");
if (offset) lines = lines.slice(offset - 1);
if (limit) lines = lines.slice(0, limit);
const MAX_LINES = 500;
const truncated = lines.length > MAX_LINES;
if (truncated) lines = lines.slice(0, MAX_LINES);
const numbered = lines.map((l, i) => `${(offset || 1) + i}: ${l}`);
return truncated
? numbered.join("\n") + `\n... (truncated at ${MAX_LINES} lines)`
: numbered.join("\n");
},
});
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: `You are a coding agent.\nWorking directory: ${cwd}`,
tools: { read },
stopWhen: stepCountIs(10),
});description 字段承担的工作比看起来更多。模型会先阅读所有工具描述,再决定下一步做什么。WHEN TO USE 和 WHEN NOT TO USE 不是写给开发者看的注释,而是帮助模型在多个工具之间做选择的提示词。
为什么限制为 500 行
注意 MAX_LINES = 500。如果没有它,对一个 10000 行文件执行无界 read 会把全部内容塞进上下文窗口,而且 Agent 会在余下会话中一直携带这份结果。一次不谨慎的读取就可能消耗 10% 的可用上下文。
本课程会对每个工具贯彻这种限制。后面有专门的上下文管理模块,但良好习惯应当从工具设计本身开始。
动手试试
使用与 read 实际能力匹配的提示词运行 Agent:
bun run index.ts . "Read the tsconfig.json"你应该会看到模型先调用 read,再总结文件内容。整个过程需要两步,而不是一步:
Here's the tsconfig.json:
- target: ESNext
- moduleResolution: bundler
- strict: true
(2 steps)转变就是这么简单:一次工具调用,再生成一次回答。模型之所以选择 read,是因为工具描述明确说明了它的适用场景。
**注意:选择与工具能力匹配的提示词**
read 可以检查已知文件,但不能列出目录。在后面两课加入 grep 和 bash 之前,请使用 Read the tsconfig.json 或 Read package.json 这类提示词。
快速检查类型:
npx tsc --noEmit提交
git add index.ts
git commit -m "feat(agent): add ToolLoopAgent with read tool"完成标准
- [ ]
index.ts中的ToolLoopAgent配置了read工具 - [ ] 聊天机器人版本(无工具)在一步后返回
- [ ] Agent 版本(带
read)会调用工具并报告文件内容 - [ ]
read返回带编号的行,并支持可选偏移量和行数限制 - [ ] 输出在 500 行处截断,并附带清晰说明
- [ ]
npx tsc --noEmit
**注意:移除上限再试一次**
用 seq 1 1000 > /tmp/big.txt 创建一个 1000 行文件,然后让 Agent 读取它。移除 MAX_LINES = 500 限制后再运行一次,观察响应规模的变化。再想象一个持续 30 步的任务:先出问题的会是模型推理能力,还是 token 上限?
参考实现
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const cwd = resolve(process.argv[2] || process.cwd());
const read = tool({
description: `Read a file from the project. Returns numbered lines.
WHEN TO USE: viewing file contents, checking configs, reading source code.
WHEN NOT TO USE: searching across files (use grep instead).`,
inputSchema: z.object({
path: z.string().describe("File path relative to working directory"),
offset: z.number().optional().describe("Start line (1-indexed)"),
limit: z.number().optional().describe("Max lines to return"),
}),
execute: async ({ path: filePath, offset, limit }) => {
const abs = resolve(cwd, filePath);
const content = readFileSync(abs, "utf-8");
let lines = content.split("\n");
if (offset) lines = lines.slice(offset - 1);
if (limit) lines = lines.slice(0, limit);
const MAX_LINES = 500;
const truncated = lines.length > MAX_LINES;
if (truncated) lines = lines.slice(0, MAX_LINES);
const numbered = lines.map((l, i) => `${(offset || 1) + i}: ${l}`);
return truncated
? numbered.join("\n") + `\n... (truncated at ${MAX_LINES} lines)`
: numbered.join("\n");
},
});
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: `You are a coding agent.\nWorking directory: ${cwd}`,
tools: { read },
stopWhen: stepCountIs(10),
});
const prompt = process.argv.slice(3).join(" ") || "Hello!";
const { text, steps } = await agent.generate({ prompt });
console.log(text);
console.log(`\n(${steps.length} steps)`);