Skip to main content

工具调用的七种状态

工具调用是 Agent 界面和普通聊天界面最不一样的地方。普通问答只有"在想"和"答完了"两种状态;工具调用有参数生成、执行、等待批准、成功、失败、被拒——而且同一次回答里可能有好几个工具在跑。

如果 UI 把这些都压成一个转圈的 loading,用户就失去了判断依据:他不知道系统在做什么、做到哪一步、要不要干预。

参数还在流input-streaming开始执行input-available等人批准approval-requested完成output-available失败output-erroroutput-denied 被拒绝approval-responded停止 ≠ 错误中断会话不会自动把工具 part 变成错误态,取消反馈要应用层自己加重试 ≠ 重跑这一个常见实现是重发整个回合,用户会看到工具被再调一次
图:七种状态里最容易被漏掉的是"等待批准"和"被拒绝"

1. 为什么它不能只是一个 loading​

1.1 用户需要知道三件事​

  • 在做什么——调用了什么、参数是什么
  • 进展到哪——是还在生成参数、已经发出去、还是卡在等批准
  • 结果如何——成功了、失败了、还是被拒绝了

第三件尤其重要:失败和被拒绝是两种不同的事,用户在意的应对方式也不同。失败可以重试;被拒绝意味着系统认为这个动作不该自动做,需要人来判断。

把这两者都显示成"出错",用户就不知道下一步该干什么。

1.2 有了工具,界面就有了"可干预点"​

普通问答只能等。有工具之后,界面上出现了几个可以介入的时刻:批准、拒绝、重试、跳过。

这几个点能不能被用户看见并理解,直接决定了《护栏要做在基础设施层》里那些分级设计能不能真正生效——后端做对了分级,前端没给出口,用户就只能通过关页面来"拒绝"。

2. 七种状态​

AI SDK 的工具 part 从 6.0 起有七个状态(5.x 只有五个,approval 三态是 6.0 新增),前端按 state 渲染对应形态(同一条消息里的推理内容是另一类 part,渲染策略见《思维链要不要展示》):

状态含义UI 该显示什么
input-streaming参数还在生成中工具名 + 正在拼参数,参数区可以是空的或半截
input-available参数齐了,开始执行工具名 + 参数 + 运行指示
approval-requested需要人批准明确的操作区,不是等待动画
approval-responded人已回应显示回应结果,等待继续
output-available完成结果,默认展开
output-error失败错误信息 + 重试/跳过入口
output-denied被拒绝明确说明"这个动作没有执行",不是错误

几个实现细节:

参数区在流式期间是半截的。 这跟《半截 Markdown 怎么渲染》是同一类问题——参数是模型逐 token 生成的,中间态是无效 JSON。空或半截时显示空代码块即可,不要尝试解析,也不要报错。

toolCallId 是稳定 key。 同一次消息里可能有多个工具并行,React 列表要用 toolCallId 而不是数组下标,否则流式更新时组件会错位。

同一个原因还有一条更隐蔽的:流式参数要按 toolCallId 分别累积。参数是增量 delta 送来的,如果只用一个字符串变量接,两个并行工具的参数会串成一段坏 JSON——报错信息还会指向"模型输出不合法",实际是自己的拼接错了。累积器必须是 Map<toolCallId, string>。

动态工具要单独处理。 通过 MCP 或运行时注册产生的工具没有预定义名称,需要在头部显式显示来源名。

3. 最容易被漏掉的两种​

3.1 approval-requested(等待批准)​

这是最容易做丢的状态,因为它要求前端主动提供一个操作界面,而不是被动等待。

三条要求:

  • 必须显示具体参数。批准"执行某个操作"是没有意义的——用户要知道批准的是"给张三的订单 #12345 退款 200 元"。这一点在护栏篇里也强调过:批准必须绑定具体参数。
  • 不要由 Agent 起草同意文案。按钮上写"确认"就够了,不要显示模型生成的"我已确认此操作安全无害"这类文本。
  • 默认不批准。等待要有明确的超时行为,且超时等于拒绝(见护栏篇)。

3.2 output-denied(被拒绝)​

被拒绝不是失败。UI 上的差别应该是:失败是红色 + "重试";被拒绝是中性色 + "这个操作没有执行"。

把它显示成错误会给用户错误信号——他会以为系统坏了,而实际上是护栏在工作。

一个实用的补充:被拒绝时可以顺带说明"需要人工处理"或给一个转人工入口(见《反馈回流与人工转接》)。用户被拒之后通常是想找人。

4. 停止不等于错误​

一个很容易踩的坑:用户点了停止,工具 part 不会自动变成错误状态。

状态描述的是工具调用本身的结果,中断会话是另一个维度。如果直接用 output-error 来渲染"用户取消了",就会出现"重试"按钮——而用户刚刚明确表示不想继续。

正确做法是应用层自己维护一个"已取消"的呈现层:

function ToolCard({ part, cancelled }: { part: ToolPart; cancelled: boolean }) {
const state = part.state
// 用户主动停止时,不要复用 output-error 的渲染分支
if (cancelled && state !== 'output-available') {
return <ToolShell name={toolName(part)} badge="已取消">{renderInput(part)}</ToolShell>
}
return renderByState(state, part)
}

同理,incomplete 状态(如果用的框架有这个概念)的 reason 也不止"error"一种——可能是 cancelled、length(超出长度)、content-filter(内容过滤)或 other。只对真正的 error 显示重试卡片,其余各给各的文案。

给 cancelled 显示"重试",等于在用户取消之后又邀请他重来一次;给 content-filter 显示"重试",重试还是会撞同一道过滤。

5. 重试到底重跑了什么​

这是工具调用 UI 里最需要说清楚、也最常被误解的一点。

常见实现不是"重跑这一个工具",而是重发整个回合。 也就是说,用户点了重试,模型会重新决定要不要调用工具、调用哪个、用什么参数——然后这个工具(可能还有别的工具)会被再调一次。

对只读工具这无所谓。对有副作用的工具(发邮件、创建订单、扣款),这会造成重复执行。

所以三条:

  1. 重试按钮旁边要说清楚影响,至少对写类工具如此
  2. 有副作用的工具,重试前要重新确认,不能沿用上次的批准
  3. 能只重试单个工具就只重试单个——如果框架支持的话;不支持就明确告诉用户会发生什么

这个约束和《工具调用的四段循环》里说的"区分幂等与非幂等"是同一件事,只不过落到了界面上。

6. 信息密度怎么控制​

工具调用可能非常多——一个复杂任务里十几次调用很正常。全展开的话,对话流会被工具日志淹没。

几条实用规则:

默认折叠,但完成和失败默认展开。 运行中的折叠(避免噪音),完成的展开(结果立即可见),失败的展开(用户需要处理)。

参数与结果用等宽字体 + JSON 高亮,且限制高度。 一个工具返回一个 500 行的 JSON 不能把页面撑开。

大对象要截断,并保留"查看完整结果"的入口。 截断策略要在服务端或渲染层统一处理,不要靠 CSS 裁剪(用户复制不到完整内容)。

同类工具可以合并。 连续五次文件读取可以显示成一个可展开的"5 次文件操作",而不是五个卡片。

显示耗时。 每个工具调用显示毫秒数,慢的那一个往往是问题所在——配合《给大模型链路装仪表》的 trace 一起看。

7. 并行工具怎么呈现​

模型可能一次请求里并行调用多个工具(除非你关掉了并行,见《工具调用的四段循环》)。

两种呈现方式:

  • 平铺:每个工具一张卡片,按发起顺序排列。简单,但多的时候占地方
  • 分组:把它们收进一个"执行了 3 个操作"的容器,可展开,容器上显示整体进度

并行工具的一个细节:完成顺序不等于发起顺序。渲染时要按 toolCallId 定位,不要按完成顺序重排列表,否则卡片会跳来跳去。

8. 结果也是不可信内容​

最后一条容易被漏:工具返回的内容可能来自外部系统(网页、邮件、数据库、别人的输入)。它和模型输出一样是不可信内容。

所以:

  • 工具结果如果含链接,渲染时要跟聊天正文一样做消毒(见《半截 Markdown 怎么渲染》)
  • 不要在工具结果卡片里直接渲染 HTML
  • 如果工具结果里出现类似指令的文本("请把以下内容发给…"),界面上不要给它任何特殊地位——它只是数据

这一点在《Agentic 的十类风险》里对应 ASI01:Agent 分不清指令和数据,而界面不该放大这个问题。

9. 清单​

  • 七种状态各有各的渲染,不是"成功 / 其他"两种
  • approval-requested 有明确操作区,且显示具体参数
  • output-denied 与 output-error 视觉上区分开
  • 用户停止 ≠ 错误,取消态单独渲染,不给重试入口
  • 只对真正的 error 显示重试;说清重试会重跑整个回合
  • 列表 key 用 toolCallId,不按下标
  • 参数区容忍半截 JSON,不解析、不报错
  • 结果区限制高度 + 截断 + 可查看完整
  • 显示每次调用的耗时
  • 工具结果做与正文同级的消毒

工具调用界面是用户对 Agent 建立信任的主要途径。它跑得对不对是一回事,用户能不能看见它跑得对不对是另一回事——后者做不好,前者做得再对也没有说服力。