Skip to content

第 6 章 · 工具定义与执行

本章目标:用 AgentTool 与 TypeBox 定义工具,理解 Agent 的工具执行循环与并行/串行两种执行模式。

6.1 工具是 Agent 的手脚

LLM 本身只会生成文本;工具让它能读文件、查数据库、调 API。pi 的工具协议分两层:

  • pi-ai 层的 Tool:纯声明(name/description/parameters),告诉模型有哪些工具;
  • pi-agent-core 层的 AgentTool:声明 + execute() 实现,Agent 循环会自动调用。

6.2 用 TypeBox 定义参数 Schema

TypeBox 提供类型安全的 JSON Schema,且可序列化为纯 JSON:

typescript
import { Type } from "typebox";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import fs from "node:fs/promises";

// 一个完整的 AgentTool 示例
const readFileTool: AgentTool = {
  name: "read_file",
  label: "Read File",                    // UI 显示名(可选)
  description: "读取指定路径文件的内容",     // 给模型看的说明,务必清晰
  parameters: Type.Object({
    // 每个字段的 description 都会成为模型理解的依据
    path: Type.String({ description: "文件的绝对或相对路径" }),
  }),
  execute: async (toolCallId, params, signal, onUpdate) => {
    const content = await fs.readFile(params.path, "utf-8");

    // 可选:向 UI 流式上报进度
    onUpdate?.({
      content: [{ type: "text", text: "读取完成" }],
      details: {},
    });

    // 返回内容块数组 + 任意 details 附加数据
    return {
      content: [{ type: "text", text: content }],
      details: { path: params.path, size: content.length },
    };
  },
};

// 挂载到 Agent 上
agent.state.tools = [readFileTool];

description 是提示工程

模型的工具选择几乎完全依赖 description 与参数描述。写得越具体(何时该用、何时不该用、单位与格式约定),调用越准确。

6.3 工具失败:抛错,不要返回错误文本

typescript
execute: async (toolCallId, params) => {
  if (!fs.existsSync(params.path)) {
    // ✅ 正确:抛出异常 —— Agent 会捕获并作为 isError:true 的结果告知模型
    throw new Error(`File not found: ${params.path}`);
  }
  // ❌ 错误做法:返回 { content: [{type:'text',text:'错误:文件不存在'}] }
  // 模型无法区分成功与失败,可能基于错误内容继续推理
}

抛出的错误由运行时统一转换为带 isError: true 的 toolResult 消息——模型能看到错误原因并自行重试或换路。

6.4 执行循环:Agent 如何决定调用

text
用户消息 → LLM 生成 assistant 消息(含 toolCall 请求)
        → 运行时校验参数 → 执行工具 → 生成 toolResult 消息
        → 回填历史 → 再次调用 LLM
        → 若仍有 toolCall 则继续循环,直到纯文本回答

你不需要写任何循环代码——Agent.prompt() 内部自动完成。想手动控制每一环可用低层 agentLoop()(第 20 章实战演示)。

6.5 并行 vs 串行执行模式

typescript
const agent = new Agent({
  // 全局默认:parallel(同一批工具并发跑)
  toolExecution: "parallel",
  initialState: { systemPrompt: "...", model },
  streamFn: models.streamSimple.bind(models),
});

// 也可以在单个工具上覆盖
const exclusiveTool: AgentTool = {
  name: "deploy",
  executionMode: "sequential",  // 强制整批按顺序执行
  // ...
};

规则要点:

  • parallel(默认):预检顺序执行、允许的工具并发执行,完成事件按完成先后发出,但落库的 toolResult 仍按模型给出的原始顺序;
  • 批次内只要有一个工具标记 sequential整批退化为串行;
  • beforeToolCall 钩子可在参数校验后拦截危险调用(第 13 章结合权限展开)。

6.6 本章小结

  • AgentTool = Tool 声明 + execute(),参数用 TypeBox schema 描述;
  • description 写清楚是提升调用准确率的第一手段;
  • 工具失败要 throw,运行时会转成 isError 结果反馈给模型;
  • 默认 parallel 并发执行,per-tool executionMode:"sequential" 可强制整批串行。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 工具执行失败时,推荐的错误处理方式是?

2. 全局 toolExecution 为 parallel 时,批次里有一个工具设了 executionMode:"sequential",结果是?

3. 工具的 parameters 字段使用什么库来定义?

4. execute 的 onUpdate 参数有什么用?

🛠️ 动手实践

  1. 实现一个 list_dir 工具(列出目录内容),description 里写清「仅接受相对路径」,观察模型是否遵守。
  2. 故意让工具对不存在路径 throw,观察终端里模型收到错误后的重试行为。
  3. 定义两个慢工具(各 sleep 2 秒),分别在 global parallel 与两工具都 sequential 下测量总耗时差异。

会定义工具了,下一章第 7 章看流式层面工具调用是如何逐步生成的。