6 min read教程

M05. 工具:从 schema 到执行结果

从 schema、校验、钩子、执行到结果协议,理解工具的安全执行边界。

M05. 工具:从 schema 到执行结果#

模型说“请调用 read”,并不等于程序可以直接执行一个函数。模型输出是不可信的结构化数据,工具又可能读写文件、运行命令或访问网络。Pi 把工具调用拆成多个阶段,目的不是增加仪式感,而是让每个风险点都有位置可检查。

1. 三层工具对象#

可以把工具理解成逐层增厚的契约:

  • Tool:名称、描述和参数 schema,模型据此决定是否调用。
  • AgentTool:再加上真正的异步 execute 能力。
  • ToolDefinition:产品层的标签、渲染、提示指导和额外行为。

底层类型不应该知道终端如何显示“正在读取文件”,产品层也不应该绕过 schema 直接调用 execute。

2. 五段执行管道#

一次调用可以画成:

text
模型 tool call
  → prepareArguments
  → validateToolArguments
  → beforeToolCall
  → tool.execute
  → afterToolCall
  → ToolResultMessage

prepareArguments 可以处理兼容旧格式的输入,但不应拿它替代验证;schema 验证负责结构正确;前置钩子负责策略判断;execute 负责动作;后置钩子负责审计、补充细节或统一结果。最后必须形成模型能理解的 tool result。

3. 为什么错误要变成结果#

工具常见错误包括:参数非法、资源不存在、权限拒绝、执行超时、进程退出非零和宿主异常。它们可以共享一种消息形态,但必须保留错误标记和可行动描述。

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

ts
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 不是营销文案,它会影响模型选择工具。应说明适用场景、输入限制、输出形态和不能做什么。对容易误用的工具,可以用 promptSnippetpromptGuidelines 增加一条短规则,但规则不能替代执行层权限检查。

7. 取消与幂等#

工具执行应尽可能响应 AbortSignal。但取消发生在外部请求已经提交之后,并不能保证副作用被撤销。因此有副作用的工具需要幂等键、状态查询或事务边界;“Promise 被 reject”不等于外部世界没有变化。

8. 工具管道的安全不变量#

工具只有在“输入已验证、策略已允许、执行状态可取消、结果可归因”时才算完成一次调用。任何一个阶段失败,都应留下明确的失败原因和 call id;不能因为结果为空就把调用当作成功,也不能因为执行函数抛错就丢掉模型需要的上下文。这个不变量把工具从普通函数提升为 Agent 的可观测动作单元。

资料

本文依据 Pi Extensions 文档Agent 核心源码重新创作。

相关文章