M05. 工具:从 schema 到执行结果#
模型说“请调用 read”,并不等于程序可以直接执行一个函数。模型输出是不可信的结构化数据,工具又可能读写文件、运行命令或访问网络。Pi 把工具调用拆成多个阶段,目的不是增加仪式感,而是让每个风险点都有位置可检查。
1. 三层工具对象#
可以把工具理解成逐层增厚的契约:
Tool:名称、描述和参数 schema,模型据此决定是否调用。AgentTool:再加上真正的异步execute能力。ToolDefinition:产品层的标签、渲染、提示指导和额外行为。
底层类型不应该知道终端如何显示“正在读取文件”,产品层也不应该绕过 schema 直接调用 execute。
2. 五段执行管道#
一次调用可以画成:
模型 tool call
→ prepareArguments
→ validateToolArguments
→ beforeToolCall
→ tool.execute
→ afterToolCall
→ ToolResultMessageprepareArguments 可以处理兼容旧格式的输入,但不应拿它替代验证;schema 验证负责结构正确;前置钩子负责策略判断;execute 负责动作;后置钩子负责审计、补充细节或统一结果。最后必须形成模型能理解的 tool result。
3. 为什么错误要变成结果#
工具常见错误包括:参数非法、资源不存在、权限拒绝、执行超时、进程退出非零和宿主异常。它们可以共享一种消息形态,但必须保留错误标记和可行动描述。
return {
content: [{
type: "text",
text: "The file was not found. Check the path and try again.",
}],
details: { code: "NOT_FOUND", path },
isError: true,
};这里的结构是示意,具体字段应以当前 Pi 工具结果类型为准。原则是:文本帮助模型修正,details 帮助宿主统计;不要把完整堆栈、密钥或内部路径无条件暴露给模型。
4. 并行不等于 Promise.all#
一个 assistant message 可能同时请求多个工具。独立的只读查询可以并行,存在写入关系的调用则需要串行或按依赖分组。更重要的是,批次中一个调用被策略阻断时,其他调用是否继续,必须是明确的调度策略,而不是由 Promise 的偶然行为决定。
设计调度器时至少回答:
- 工具是否声明只读或有副作用?
- 批次是否允许并行?
- 取消一个调用会不会取消整个批次?
- 第一个失败是否阻止后续调用?
- 结果返回顺序是否与调用顺序稳定?
5. Operations 抽象解决什么#
如果工具直接调用 fs、child process 或网络客户端,测试和部署环境会被绑死。更好的方式是让工具依赖最小的 operations 接口:生产环境接真实实现,测试环境接内存或受控 fake。
type FileOps = {
read(path: string, signal?: AbortSignal): Promise<string>;
};
function makeReadTool(ops: FileOps) {
return {
async execute(_id: string, input: { path: string }, signal: AbortSignal) {
const text = await ops.read(input.path, signal);
return { content: [{ type: "text", text }], details: {} };
},
};
}这不是为了追求抽象数量,而是为了明确“工具决定要什么能力,宿主决定能力如何实现和受限”。
6. 工具描述也是控制面#
description 不是营销文案,它会影响模型选择工具。应说明适用场景、输入限制、输出形态和不能做什么。对容易误用的工具,可以用 promptSnippet 和 promptGuidelines 增加一条短规则,但规则不能替代执行层权限检查。
7. 取消与幂等#
工具执行应尽可能响应 AbortSignal。但取消发生在外部请求已经提交之后,并不能保证副作用被撤销。因此有副作用的工具需要幂等键、状态查询或事务边界;“Promise 被 reject”不等于外部世界没有变化。
8. 工具管道的安全不变量#
工具只有在“输入已验证、策略已允许、执行状态可取消、结果可归因”时才算完成一次调用。任何一个阶段失败,都应留下明确的失败原因和 call id;不能因为结果为空就把调用当作成功,也不能因为执行函数抛错就丢掉模型需要的上下文。这个不变量把工具从普通函数提升为 Agent 的可观测动作单元。
资料
本文依据 Pi Extensions 文档和 Agent 核心源码重新创作。