Skip to content

第 7 章 · 工具调用事件流

本章目标:理解模型生成工具调用的流式过程,处理 toolcall_delta 部分参数与 validateToolCall 校验。

7.1 两层「工具调用」

先厘清命名,避免混淆:

事件含义
pi-ai 流层toolcall_start/delta/end模型正在生成工具调用(JSON 参数逐步到达)
pi-agent-core 执行层tool_execution_start/update/end运行时正在执行已完成的工具调用

本章聚焦第一层:模型如何「边想边写」出一次工具调用。

7.1 流式中的部分 JSON

工具参数是逐字符流出的。pi-ai 会做尽力解析——每收到一个 delta 都给出当前的部分解析结果:

typescript
// 直接用 pi-ai 层观察原始流
const s = models.stream(model, context);

for await (const event of s) {
  if (event.type === "toolcall_delta") {
    // partial.content[contentIndex] 是正在生成的块
    const toolCall = event.partial.content[event.contentIndex];

    if (toolCall.type === "toolCall" && toolCall.arguments) {
      // arguments 是部分解析结果:字段可能缺失、字符串可能截断
      if (toolCall.name === "write_file" && toolCall.arguments.path) {
        console.log(`即将写入: ${toolCall.arguments.path}`);
        // content 可能还没传完 —— 必须防御性检查!
        if (toolCall.arguments.content) {
          console.log(`内容预览: ${toolCall.arguments.content.slice(0, 100)}...`);
        }
      }
    }
  }

  if (event.type === "toolcall_end") {
    // 此时 arguments 已完整(但尚未校验)
    console.log("工具调用完成:", event.toolCall.name, event.toolCall.arguments);
  }
}

部分参数的铁律

  • 字段可能缺失或不完整——使用前必须存在性检查
  • 字符串可能截断在半词、数组可能不完整;
  • 保底是空对象 {},永远不会是 undefined
  • Google Provider 不支持函数调用流式:只会一次性收到完整的 toolcall_delta

7.3 validateToolCall:执行前必校验

自己写执行循环时,用 validateToolCall 按 schema 校验参数:

typescript
import { validateToolCall, type Tool } from "@earendil-works/pi-ai";

const tools: Tool[] = [weatherTool, calculatorTool];
const s = models.stream(model, { messages, tools });

for await (const event of s) {
  if (event.type === "toolcall_end") {
    const toolCall = event.toolCall;
    try {
      // 按工具的 TypeBox schema 校验并转换值(失败抛错)
      const validatedArgs = validateToolCall(tools, toolCall);
      const result = await executeMyTool(toolCall.name, validatedArgs);
      // 正常回填 toolResult……
    } catch (error) {
      // 校验失败也回填为 toolResult(isError:true)→ 模型可自行修正重试
      context.messages.push({
        role: "toolResult",
        toolCallId: toolCall.id,
        toolName: toolCall.name,
        content: [{ type: "text", text: error.message }],
        isError: true,
        timestamp: Date.now(),
      });
    }
  }
}

把校验错误作为 toolResult 回给模型而不是抛到顶层,能让模型自主修复参数问题再试一次——这是 Agent 鲁棒性的关键设计。

7.4 在 Agent 层观察全流程

typescript
// Agent 类用户看到的事件序列
agent.subscribe((e) => {
  switch (e.type) {
    case "message_update": {
      const inner = e.assistantMessageEvent;
      // 流层事件透传在 message_update 里
      if (inner.type === "toolcall_delta") {
        process.stdout.write("▍");   // 参数还在生成中
      }
      break;
    }
    case "tool_execution_start":
      console.log(`\n▶ 执行 ${e.toolName}(${JSON.stringify(e.args)})`);
      break;
    case "tool_execution_end":
      console.log(`◀ 完成`);
      break;
  }
});

7.5 本章小结

  • toolcall_* 是模型生成阶段的事件,tool_execution_* 是运行时执行阶段的事件;
  • toolcall_delta 期间 arguments 是尽力解析的部分 JSON:必须防御性读取、保底 {}
  • validateToolCall(tools, call) 按 schema 校验;校验失败应作为 isError 的 toolResult 回填让模型自愈;
  • Google Provider 不支持工具调用流式,会一次性收到完整参数。

🧪 随堂测验

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

1. toolcall_delta 事件中,partial 解析出的 arguments 最坏情况是什么?

2. validateToolCall 抛错后,最佳实践是什么?

3. 关于 Google Provider 的工具调用流式支持,正确的是?

4. toolcall_end 时拿到的 event.toolCall.arguments 处于什么状态?

🛠️ 动手实践

  1. 写一个监听器实时渲染 write_file 工具的「路径 → 内容预览 → 完成」三段进度条。
  2. 构造一个会传错参数类型的场景(如 description 故意含糊),验证 validateToolCall 能拦住并让模型自愈。
  3. 对比 Anthropic 与(如有条件的)Google 模型的 toolcall_delta 到达模式差异并记录。

下一章第 8 章:AgentMessage 类型系统与 convertToLlm 桥接。