第 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 处于什么状态?
🛠️ 动手实践
- 写一个监听器实时渲染 write_file 工具的「路径 → 内容预览 → 完成」三段进度条。
- 构造一个会传错参数类型的场景(如 description 故意含糊),验证 validateToolCall 能拦住并让模型自愈。
- 对比 Anthropic 与(如有条件的)Google 模型的 toolcall_delta 到达模式差异并记录。
下一章第 8 章:AgentMessage 类型系统与 convertToLlm 桥接。