流式Tool-Use与前端状态
核对日期:2026-08-26。
1. 定义与边界
流式 Tool Use 是指模型在生成过程中决定调用工具,工具名称和参数也以增量到达。前端必须把「还在拼 JSON 的参数」和「已经校验可执行的参数」区分开。
本文件处理工具卡片的状态机、并行调用、参数增量解析。工具 schema、权限和错误码仍以 ../04-工具调用体系/README.md 为准。高风险执行前的审批 UI 见 HITL交互界面.md。
不适用:
- 在参数还不完整时执行工具。
- 把工具原始 JSON 直接
JSON.parse中间帧。 - 用
v-html渲染工具返回。
2. 为什么重要
文本流只影响观感;工具流影响对错和安全:
- 参数是 JSON,中间态不可解析。过早 parse 会抛错或执行半截对象。
- 用户需要知道 Agent 正在「查订单」还是「准备退款」,否则会以为死机。
- 并行工具调用如果共用一张卡片,失败归因会乱。
- AI SDK 5 起默认开启 tool call streaming;UI 若仍按「整段 message 到达再渲染」,会退回转圈体验。
3. 核心机制
Anthropic 流里,文本和工具参数走不同 delta:text_delta 与 input_json_delta。stop_reason 为 tool_use 时表示本轮要进入工具执行,而不是对话结束。OpenAI 兼容协议则是 tool_calls[].function.arguments 分片拼接。
网关应翻译成统一状态,而不是让 UI 分叉两套厂商事件。
每个 toolCallId 一张卡片。不要按「当前 message」聚合。
4. 架构模式
4.1 前端工具卡片模型
type ToolCardState =
| 'planned'
| 'streaming_args'
| 'args_ready'
| 'waiting_approval'
| 'executing'
| 'succeeded'
| 'failed'
| 'denied';
interface ToolCard {
toolCallId: string;
runId: string;
spanId: string;
toolName: string;
riskLevel: 'low' | 'medium' | 'high';
permission: 'read' | 'write';
state: ToolCardState;
partialJson: string;
args?: unknown;
resultSummary?: string;
errorCode?: string;
}
partialJson 只用于展示「正在生成参数」;执行和审批只用 args。
4.2 两种参数展示策略
| 策略 | 做法 | 适用 | 不适用 |
|---|---|---|---|
| 完整后再展示 | 拼字符串,结束帧再 JSON.parse + schema | 写工具、资金、外部发送 | 需要让用户看见长参数生成过程 |
| 部分 JSON 预览 | 用容错解析展示已完整的字段 | 只读检索、城市名等前缀字段 | 把预览对象拿去执行 |
生产默认用「完整后再执行」。预览可以提前,执行不行。
5. 工程实现
5.1 增量合并
interface ToolArgsDeltaEvent {
type: 'tool_args_delta';
toolCallId: string;
partialJson: string;
}
interface ToolCallReadyEvent {
type: 'tool_call_ready';
toolCallId: string;
args: unknown;
}
function applyToolEvent(
cards: Map<string, ToolCard>,
event: ToolArgsDeltaEvent | ToolCallReadyEvent,
): Map<string, ToolCard> {
const next = new Map(cards);
const current = next.get(event.toolCallId);
if (current === undefined) {
return next;
}
if (event.type === 'tool_args_delta') {
next.set(event.toolCallId, {
...current,
state: 'streaming_args',
partialJson: `${current.partialJson}${event.partialJson}`,
});
return next;
}
next.set(event.toolCallId, {
...current,
state: 'args_ready',
args: event.args,
partialJson: '',
});
return next;
}
服务端必须在 tool_call_ready 前完成 JSON 闭合和 schema 校验。客户端不要对 partialJson 做业务执行。
5.2 Vue 3 卡片渲染
<script setup lang="ts">
interface ToolCardView {
toolCallId: string;
toolName: string;
riskLevel: 'low' | 'medium' | 'high';
state: 'planned' | 'streaming_args' | 'args_ready' | 'executing' | 'succeeded' | 'failed';
args?: unknown;
resultSummary?: string;
}
defineProps<{ card: ToolCardView }>();
</script>
<template>
<section class="tool-card" :data-risk="card.riskLevel">
<header>
<strong>{{ card.toolName }}</strong>
<span>{{ card.state }}</span>
<span>{{ card.riskLevel }}</span>
</header>
<pre v-if="card.args !== undefined">{{ JSON.stringify(card.args, null, 2) }}</pre>
<p v-if="card.resultSummary">{{ card.resultSummary }}</p>
</section>
</template>
不要用 v-html 展示 resultSummary。摘要由服务端生成纯文本或 Markdown,前端走 Markdown 解析器的安全子集。
5.3 React 与 AI SDK 的关系
AI SDK 5 的 message parts 里,静态工具可能是 tool-getWeather 这类派生类型,动态工具是 dynamic-tool。这适合 Next.js 快速产品,不适合当企业 Agent 的长期契约。
建议:
- 内部产品可以用
@ai-sdk/react/@ai-sdk/vue加速。 - 网关仍输出自己的
toolCallId/riskLevel/spanId。 - UI 按
toolCallId渲染,不要按part.type字符串散落在页面各处。
5.4 并行工具
同一轮多个 tool_call_start 必须并行展示、独立失败:
- 一个检索失败不应关掉另一张查库卡片。
- 写工具默认串行,除非工具声明可并行且无共享幂等键。
- 前端用
toolCallId做key,禁止用数组下标。
6. 生产实践
| 实践 | 说明 |
|---|---|
| 风险色标 | 读工具中性,写工具警告,资金 / 外部发送用高风险样式 |
| 参数脱敏 | token、手机号、身份证在卡片里打码 |
| 结果摘要 | 工具返回 50KB JSON 时只展示 3–5 个可行动字段 |
| 耗时 | 显示工具 span 时长,便于发现慢依赖 |
| 重试入口 | 仅对声明可重试且幂等的失败卡片开放 |
| 和 trace 打通 | 卡片可跳到同一 span_id 的内部追踪,权限分开 |
7. 常见反模式
| 反模式 | 表现 | 后果 | 修正 |
|---|---|---|---|
中间帧 JSON.parse | 参数流到一半就执行 | 抛错或写坏数据 | 等 tool_call_ready |
| 一张卡片叠多次调用 | 按 message 而不是 toolCallId | 无法归因 | 一调用一卡片 |
| 展示完整工具返回 | 把数据库行推到浏览器 | 泄露、卡顿、注入 | 服务端摘要 |
| 流式参数可点执行 | 用户在 JSON 未闭合时确认 | 半截提交 | 按钮在 args_ready 才启用 |
| 写工具自动并行 | 同时改同一订单 | 竞态 | 写默认串行 |
| 用 prompt 保证「先展示再调用」 | 没有状态机 | 模型忽略 | 执行器看卡片状态,不看模型自觉 |
8. 评测方法
| 指标 | 说明 |
|---|---|
| Tool Card Start Latency | tool_call_start 相对本轮模型开始的时间 |
| Args Ready Accuracy | args 与最终执行参数是否一致 |
| Partial Parse Error Rate | 因提前 parse 导致的前端异常 |
| Parallel Isolation | 并行中单工具失败是否污染其他卡片 |
| Schema Block Rate | 参数未过 schema 就被执行的次数,目标为 0 |
| User-Visible Hang | 工具执行超过 N 秒仍无 executing 态 |
轨迹评测仍以 ../10-Agent评测体系/轨迹评测.md 为准;本文件只补「用户是否看见了正确的工具过程」。
样本:
{
"id": "ui_tool_stream_014",
"input": "帮我查订单 A123 并准备退款申请",
"expected_ui": {
"cards": [
{ "toolName": "get_order", "state": "succeeded" },
{ "toolName": "create_refund_request", "state": "waiting_approval" }
],
"forbidden": ["issue_payment executing"]
}
}
9. 安全与治理
- 工具参数流也可能带注入文本(文件名、备注、网页标题)。展示用纯文本。
riskLevel由策略引擎给,不由模型输出。- 客户端展示审批按钮不等于授权:真正执行必须在服务端复检。
- 失败卡片的错误信息不要回显内部 SQL / 绝对路径。
- 外部工具返回按不可信内容处理,见 ../12-安全与治理/Prompt-Injection.md。
10. 权威资料
- Vercel AI SDK tool usage: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-tool-usage (核对日期:2026-08-26)
- Vercel AI SDK Vue
useChat: https://ai-sdk.dev/docs/getting-started/nuxt (核对日期:2026-08-26) - Anthropic tool use: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview (核对日期:2026-08-26)
- Anthropic streaming: https://docs.anthropic.com/en/api/messages-streaming (核对日期:2026-08-26)
- OpenAI function calling: https://platform.openai.com/docs/guides/function-calling (核对日期:2026-08-26)
- OWASP Top 10 for LLM Applications: https://owasp.org/www-project-top-10-for-large-language-model-applications/ (核对日期:2026-08-26)