工具是 Agent 从“会说话”变成“能做事”的关键。
Pi 的工具设计有三个核心点:
toolResult 消息,再交给模型继续推理。一个工具最少包含:
| 字段 | 作用 |
|---|---|
name |
模型调用时使用的稳定名称 |
description |
告诉模型什么时候应该调用 |
parameters |
参数 schema,用于模型理解和运行时校验 |
execute |
本地真正执行工具的函数 |
教学版中我们用 Zod/手写类型都可以。Pi 源码使用 TypeBox schema,并在执行前调用 validateToolArguments()。
interface AgentTool<TArgs, TResult> {
name: string;
description: string;
parameters: unknown;
execute(args: TArgs, signal?: AbortSignal): Promise<TResult>;
}
sequenceDiagram participant AgentLoop as Agent Loop participant Model as LLM participant Hook as Hooks participant Tool as Tool participant Ctx as Context AgentLoop->>Model: messages + tools Model-->>AgentLoop: assistant content: toolCall AgentLoop->>AgentLoop: find tool by name AgentLoop->>AgentLoop: validate args AgentLoop->>Hook: beforeToolCall Hook-->>AgentLoop: allow/block AgentLoop->>Tool: execute(args) Tool-->>AgentLoop: result AgentLoop->>Hook: afterToolCall Hook-->>AgentLoop: optional override AgentLoop->>Ctx: append toolResult AgentLoop->>Model: next request
工具很强,也很危险。让模型直接执行 shell 或写文件,必须有拦截点。
| hook | 常见用途 |
|---|---|
beforeToolCall |
权限审批、路径保护、命令黑名单、参数改写 |
afterToolCall |
脱敏输出、截断超长结果、记录审计日志、把结果改成结构化摘要 |
Pi 的扩展系统就通过这些 hook 实现很多能力,比如阻止危险命令、修改工具结果、定制压缩。
Pi 支持同一条助手消息里多个工具调用。默认可以并行,但某些工具需要串行。
| 模式 | 适合场景 | 风险 |
|---|---|---|
| parallel | 多个只读操作,例如 grep、read、find | 输出顺序与完成顺序不同,需要回写时保持原始调用顺序 |
| sequential | 写文件、执行依赖前一步结果的命令 | 慢一些,但更容易保证副作用顺序 |
Pi 的做法是:可以并发执行允许并发的工具,但生成 toolResult 消息时仍按助手原始 tool call 顺序写回,避免模型看到错乱上下文。
工具有两种“说明书”:
| 说明书 | 给谁看 | 例子 |
|---|---|---|
description + parameters |
给模型和参数校验器 | read_file(path: string) |
| system prompt 中的工具准则 | 给模型的行为策略 | “读取文件前先用 grep/ls 缩小范围” |
Pi 的内置工具会把一行 promptSnippet 和若干 promptGuidelines 合入系统提示词。这样模型不仅知道“有 read 工具”,还知道“什么时候该用 read、什么时候先搜索”。教学版没有实现完整 prompt guideline,但你应该知道生产级 Agent 需要这层。
工具不存在、参数校验失败、被 hook 阻止、执行抛错,都不应该让整个进程直接炸掉。更好的做法是返回一个 isError: true 的 toolResult,让模型有机会调整计划。
{
role: "toolResult",
toolCallId: "call_1",
toolName: "read_file",
isError: true,
content: [{ type: "text", text: "File not found: README.md" }]
}
| 错误 | 后果 | 修正 |
|---|---|---|
| 工具描述太短 | 模型不知道何时调用 | 写清楚用途、边界和参数含义 |
| 参数不校验 | 模型传错参数时产生不可控行为 | 执行前做 schema 校验 |
| 工具直接返回复杂对象给 UI | 模型看不到结果 | 结果必须转成 toolResult 消息 |
| 长输出不截断 | 上下文窗口被工具结果撑爆 | 工具层或 after hook 做截断和摘要 |
| 工具权限太宽 | 模型一次错误调用可能造成真实副作用 | 把路径限制、命令审批放在工具或 before hook |
| 工具结果只给前端 | 模型下一轮不知道发生了什么 | 同时生成 toolResult 消息进入上下文 |
在 examples/demos/02-tools.ts 新增一个 uppercase 工具,让模型先读取文本,再调用 uppercase 处理文本。你会马上遇到一个问题:第二个工具的参数来自第一个工具结果,模型必须在下一轮才能知道它。