用户最烦 AI 在那干等。尤其智能体要调好几个工具------查库、算价、查物流------中间几秒钟界面一片死寂,用户以为卡了就刷新,一刷新上下文全没了。我给我们的 Agent 加了一个"过程可视化":把它正在调哪个工具、调到哪一步实时展示出来。这篇讲数据怎么来、前端怎么呈现。
先搞清楚:数据从哪来
智能体执行多步任务时,平台的流式响应里其实不只有最终答案,还夹着一连串"事件"------开始调工具、工具返回、开始生成等等。我接的这个平台,流里会带类似这样的事件标记:
vbnet
event: tool_call
data: {"tool":"query_stock","args":{"sku":"A1023"}}
event: tool_result
data: {"tool":"query_stock","ok":true}
event: message
data: {"content":"库存还有 120 件"}
关键是前端解析这条流时,别只挑 message 看,把 tool_call / tool_result 也接住,它们就是可视化的原料。
前端:一个事件驱动的状态机
我把每次对话维护成一个步骤列表,每个步骤有自己的状态:
lua
// steps: [{ id, type, label, status }]
function reduceEvent(steps, evt) {
switch (evt.type) {
case "tool_call":
return [
...steps,
{
id: evt.data.tool + Date.now(),
type: "tool",
label: toolLabel(evt.data.tool), // "正在查询库存..."
status: "running",
},
];
case "tool_result": {
// 把对应的 running 步骤标记成完成
const next = [...steps];
const last = [...next].reverse().find((s) => s.status === "running");
if (last) last.status = evt.data.ok ? "done" : "failed";
return next;
}
default:
return steps;
}
}
toolLabel 这个映射很重要------别把工具的英文函数名直接甩给用户。query_stock 要翻译成"正在查询库存",calc_price 翻成"正在计算价格"。用户看的是人话,不是你的函数名。
呈现:克制,别喧宾夺主
我第一版做得花里胡哨,每个步骤一个大卡片配动画,结果整个对话区被过程占满,真正的答案反而被挤到老下面。用户反馈"看着累"。
第二版我改成一行小字 + 一个状态点:
javascript
{steps.map((s) => (
<div key={s.id} className="step">
<span className={`dot ${s.status}`} />
<span className="label">{s.label}</span>
</div>
))}
running:转圈的小点done:变灰打勾,文字淡化failed:标红
整个过程区折叠在答案上方一个小区域,完成后还能点开看详情。低调但有信息量,这版反馈就好了。
一个真实的坑:失败步骤别藏
早期我把失败的工具调用静默掉了,想着"反正模型会重试"。结果有次物流接口连续挂,模型反复重试又反复失败,用户看到的就是一直转圈、最后蹦出个莫名其妙的答案。后来我把 failed 状态明确标红展示,用户至少知道"哦是物流系统出问题了",投诉都少了。透明比假装顺利更让人信任。
取舍
这套可视化我没做得太精细------比如工具调用的具体参数、耗时统计,这些对终端用户没意义,我都没展示,只在开发模式下打 console。给用户看的,就"它在做什么 + 做成没"两件事,够了。
小结
让用户"看见思考过程",本质是把焦虑换成耐心。原料来自流式事件,难点在翻译成人话和克制呈现。模型和工具编排我用的讯飞(用户最烦 AI 在那干等。尤其智能体要调好几个工具------查库、算价、查物流------中间几秒钟界面一片死寂,用户以为卡了就刷新,一刷新上下文全没了。我给我们的 Agent 加了一个"过程可视化":把它正在调哪个工具、调到哪一步实时展示出来。这篇讲数据怎么来、前端怎么呈现。
先搞清楚:数据从哪来
智能体执行多步任务时,平台的流式响应里其实不只有最终答案,还夹着一连串"事件"------开始调工具、工具返回、开始生成等等。我接的这个平台,流里会带类似这样的事件标记:
vbnet
event: tool_call
data: {"tool":"query_stock","args":{"sku":"A1023"}}
event: tool_result
data: {"tool":"query_stock","ok":true}
event: message
data: {"content":"库存还有 120 件"}
关键是前端解析这条流时,别只挑 message 看,把 tool_call / tool_result 也接住,它们就是可视化的原料。
前端:一个事件驱动的状态机
我把每次对话维护成一个步骤列表,每个步骤有自己的状态:
lua
// steps: [{ id, type, label, status }]
function reduceEvent(steps, evt) {
switch (evt.type) {
case "tool_call":
return [
...steps,
{
id: evt.data.tool + Date.now(),
type: "tool",
label: toolLabel(evt.data.tool), // "正在查询库存..."
status: "running",
},
];
case "tool_result": {
// 把对应的 running 步骤标记成完成
const next = [...steps];
const last = [...next].reverse().find((s) => s.status === "running");
if (last) last.status = evt.data.ok ? "done" : "failed";
return next;
}
default:
return steps;
}
}
toolLabel 这个映射很重要------别把工具的英文函数名直接甩给用户。query_stock 要翻译成"正在查询库存",calc_price 翻成"正在计算价格"。用户看的是人话,不是你的函数名。
呈现:克制,别喧宾夺主
我第一版做得花里胡哨,每个步骤一个大卡片配动画,结果整个对话区被过程占满,真正的答案反而被挤到老下面。用户反馈"看着累"。
第二版我改成一行小字 + 一个状态点:
javascript
{steps.map((s) => (
<div key={s.id} className="step">
<span className={`dot ${s.status}`} />
<span className="label">{s.label}</span>
</div>
))}
running:转圈的小点done:变灰打勾,文字淡化failed:标红
整个过程区折叠在答案上方一个小区域,完成后还能点开看详情。低调但有信息量,这版反馈就好了。
一个真实的坑:失败步骤别藏
早期我把失败的工具调用静默掉了,想着"反正模型会重试"。结果有次物流接口连续挂,模型反复重试又反复失败,用户看到的就是一直转圈、最后蹦出个莫名其妙的答案。后来我把 failed 状态明确标红展示,用户至少知道"哦是物流系统出问题了",投诉都少了。透明比假装顺利更让人信任。
取舍
这套可视化我没做得太精细------比如工具调用的具体参数、耗时统计,这些对终端用户没意义,我都没展示,只在开发模式下打 console。给用户看的,就"它在做什么 + 做成没"两件事,够了。
小结
让用户"看见思考过程",本质是把焦虑换成耐心。原料来自流式事件,难点在翻译成人话和克制呈现。模型和工具编排我用的讯飞(用户最烦 AI 在那干等。尤其智能体要调好几个工具------查库、算价、查物流------中间几秒钟界面一片死寂,用户以为卡了就刷新,一刷新上下文全没了。我给我们的 Agent 加了一个"过程可视化":把它正在调哪个工具、调到哪一步实时展示出来。这篇讲数据怎么来、前端怎么呈现。
先搞清楚:数据从哪来
智能体执行多步任务时,平台的流式响应里其实不只有最终答案,还夹着一连串"事件"------开始调工具、工具返回、开始生成等等。我接的这个平台,流里会带类似这样的事件标记:
vbnet
event: tool_call
data: {"tool":"query_stock","args":{"sku":"A1023"}}
event: tool_result
data: {"tool":"query_stock","ok":true}
event: message
data: {"content":"库存还有 120 件"}
关键是前端解析这条流时,别只挑 message 看,把 tool_call / tool_result 也接住,它们就是可视化的原料。
前端:一个事件驱动的状态机
我把每次对话维护成一个步骤列表,每个步骤有自己的状态:
lua
// steps: [{ id, type, label, status }]
function reduceEvent(steps, evt) {
switch (evt.type) {
case "tool_call":
return [
...steps,
{
id: evt.data.tool + Date.now(),
type: "tool",
label: toolLabel(evt.data.tool), // "正在查询库存..."
status: "running",
},
];
case "tool_result": {
// 把对应的 running 步骤标记成完成
const next = [...steps];
const last = [...next].reverse().find((s) => s.status === "running");
if (last) last.status = evt.data.ok ? "done" : "failed";
return next;
}
default:
return steps;
}
}
toolLabel 这个映射很重要------别把工具的英文函数名直接甩给用户。query_stock 要翻译成"正在查询库存",calc_price 翻成"正在计算价格"。用户看的是人话,不是你的函数名。
呈现:克制,别喧宾夺主
我第一版做得花里胡哨,每个步骤一个大卡片配动画,结果整个对话区被过程占满,真正的答案反而被挤到老下面。用户反馈"看着累"。
第二版我改成一行小字 + 一个状态点:
javascript
{steps.map((s) => (
<div key={s.id} className="step">
<span className={`dot ${s.status}`} />
<span className="label">{s.label}</span>
</div>
))}
running:转圈的小点done:变灰打勾,文字淡化failed:标红
整个过程区折叠在答案上方一个小区域,完成后还能点开看详情。低调但有信息量,这版反馈就好了。
一个真实的坑:失败步骤别藏
早期我把失败的工具调用静默掉了,想着"反正模型会重试"。结果有次物流接口连续挂,模型反复重试又反复失败,用户看到的就是一直转圈、最后蹦出个莫名其妙的答案。后来我把 failed 状态明确标红展示,用户至少知道"哦是物流系统出问题了",投诉都少了。透明比假装顺利更让人信任。
取舍
这套可视化我没做得太精细------比如工具调用的具体参数、耗时统计,这些对终端用户没意义,我都没展示,只在开发模式下打 console。给用户看的,就"它在做什么 + 做成没"两件事,够了。
小结
让用户"看见思考过程",本质是把焦虑换成耐心。原料来自流式事件,难点在翻译成人话和克制呈现。模型和工具编排我用的讯飞,走 MaaS 模式省去自建推理,流式事件结构也现成,我只管在前端把它翻译给用户看。),走 MaaS 模式省去自建推理,流式事件结构也现成,我只管在前端把它翻译给用户看。),走 MaaS 模式省去自建推理,流式事件结构也现成,我只管在前端把它翻译给用户看。