跳到正文

从聊天机器人到 Agent

你的 Agent 就像世界上最自信的实习生。让它查看 tsconfig.json,它会兴致勃勃地描述文件里大概有什么;让它找出第 42 行的错误,它也能给出一个听起来非常可信的修复方案。

但它根本没有打开文件,因为它做不到。聊天机器人没有工具,只能根据常见代码模式进行猜测,再把猜测当成分析结果。

一个工具就能改变这种情况。我们将添加 read,让这个只会自信解释的实习生真正打开文件。

学习成果

你将得到一个 ToolLoopAgent:收到提示词后,它会调用 read 工具检查指定文件并汇报发现。

快速路径

  1. index.ts 中创建 ToolLoopAgent,配置 instructionsmodelstopWhen: stepCountIs(10)
  2. 先不提供任何工具,运行它并观察聊天机器人如何只解释自己“会做什么”
  3. 添加带 Zod inputSchema 和 500 行上限的 read 工具,然后再次运行

动手练习 1.1

先在 index.ts 中构建最小可用的 Agent,再添加一个工具。

要求:

  1. ai 导入 ToolLoopAgentstepCountIstool,并从 zod 导入 z
  2. 创建 Agent,设置 model: "anthropic/claude-haiku-4-5"、简短指令和 stopWhen: stepCountIs(10)
  3. 添加 read 工具,接受 path、可选的 offset 和可选的 limit
  4. 将输出限制为 500 行,并在每行前加上行号

实现提示:

  • ToolLoopAgent 接受 instructionsmodeltoolsstopWhen。这里应使用 instructions,而不是 system
  • 使用 agent.generate({ prompt }) 调用 Agent,而不是 agent.generate(prompt)
  • tool()description 字段是提供给模型的提示词,不是普通 docstring;模型根据它判断何时调用工具
  • 基于工作目录解析路径,避免 Agent 意外读取项目外的文件

聊天机器人

从最小的 Agent 开始:没有工具,只有指令。

ts
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)`);

运行它:

bash
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:

ts
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:

bash
bun run index.ts . "Read the tsconfig.json"

你应该会看到模型先调用 read,再总结文件内容。整个过程需要两步,而不是一步:

Here's the tsconfig.json:
- target: ESNext
- moduleResolution: bundler
- strict: true

(2 steps)

转变就是这么简单:一次工具调用,再生成一次回答。模型之所以选择 read,是因为工具描述明确说明了它的适用场景。

**注意:选择与工具能力匹配的提示词**

read 可以检查已知文件,但不能列出目录。在后面两课加入 grepbash 之前,请使用 Read the tsconfig.jsonRead package.json 这类提示词。

快速检查类型:

bash
npx tsc --noEmit

提交

bash
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 上限?

参考实现

ts
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)`);

非官方简体中文翻译 · 原课程来自 Vercel Academy