Agent 聊半天用户不信?加三块 UI 比换模型管用
Agent 聊半天用户不信?加三块 UI 比换模型管用
最近做内部小工具,上线头两天被骂最多的一句是:「这 AI 是不是在瞎编?」
后来没换模型,只改了聊天界面的三块 UI,投诉少了一大截。测试同事原话:「至少看得出它去查了订单接口,不是闭着眼编。」
问题通常不在模型强弱,而在黑盒感——用户只看到最后一段答案,中间搜没搜、调没调接口、哪步翻车了,一概不知道。不信,正常。
Agent 实际走的是这条链:
想一步 → 调工具 → 看结果 → 再想 → 再答
界面上却只显示最后那句「答」,信任很难建立。下面三块 UI 是我踩坑后留下的最小集,React / Vue 都能套,关键是数据结构和渲染时机,不是框架选型。
第一块:状态条——别让用户对着空白发呆
Agent 一启动就先给反馈。不用把模型内心独白全贴出来(涉密、噪音也大),一句「正在分析你的问题…」就够。
Cursor、ChatGPT、Claude 这类产品都有类似做法:运行中显示「Thinking…」「正在整理…」,或者一个可展开的推理区块。Cursor 里还可以在 Settings → Appearance → Agent Conversations → Tool Call Density 把详情密度调高,工具调用会 inline 展示,不用全挤在折叠里。
前端自己写的话,用 phase 状态机就够:
type AgentPhase = "idle" | "thinking" | "tool_running" | "answering" | "error";
function StatusBar({ phase }: { phase: AgentPhase }) {
const text = {
idle: "",
thinking: "正在分析问题…",
tool_running: "正在调用工具…",
answering: "整理结果中…",
error: "出错了,请稍后重试",
}[phase];
if (!text) return null;
return (
<div className="text-sm text-gray-500 py-2 flex items-center gap-2">
<span className="inline-block w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
{text}
</div>
);
}
等待焦虑很多时候不是等太久,是不知道在等什么。请求一发出去就先亮状态,比等 15 秒突然蹦出一大段文字体验好一截。
第二块:工具调用卡片——把黑盒拆开
每次工具调用,在消息流里插一张卡片。参数别全贴(涉密、太长),给摘要就行:工具名、city=杭州 这种关键字段、执行状态。
type ToolCall = {
id: string;
name: string;
argsSummary: string;
status: "running" | "ok" | "failed";
resultPreview?: string;
};
function ToolCallCard({ call }: { call: ToolCall }) {
const statusText = { running: "执行中", ok: "完成", failed: "失败" }[call.status];
return (
<div className="border rounded-lg p-3 my-2 bg-gray-50 text-sm">
<div className="flex justify-between">
<span className="font-medium">{call.name}</span>
<span className={call.status === "failed" ? "text-red-600" : ""}>{statusText}</span>
</div>
<div className="text-gray-600 mt-1">参数:{call.argsSummary}</div>
{call.resultPreview && (
<pre className="mt-2 text-xs bg-white p-2 rounded overflow-x-auto">
{call.resultPreview}
</pre>
)}
</div>
);
}
用户看到「哦,它真的去查了订单接口」,信任感会完全不一样。工具失败要在卡片上标红,别悄悄吞掉再给一个貌似正常的总结——这一步漏了,前面两块 UI 白做。
第三块:结论和过程分开——给两种人看
有人只想看结论,有人要审计过程。我的做法是:工具卡片实时插在过程区,最终回答单独展示,过程区默认折叠。
import { useState } from "react";
function AgentMessage({ answer, toolCalls }: { answer: string; toolCalls: ToolCall[] }) {
const [showDetail, setShowDetail] = useState(false);
return (
<div className="my-3">
<div className="whitespace-pre-wrap">{answer}</div>
{toolCalls.length > 0 && (
<>
<button
className="text-xs text-blue-600 mt-2"
onClick={() => setShowDetail((v) => !v)}
>
{showDetail ? "收起执行过程" : `查看执行过程(${toolCalls.length} 步)`}
</button>
{showDetail && toolCalls.map((c) => <ToolCallCard key={c.id} call={c} />)}
</>
)}
</div>
);
}
默认收起照顾体验,想审计的人点开。这跟 Cursor 把 Thinking 收成一行、点 chevron 再展开是同一个思路。
流式数据怎么接——别假设有统一协议
这点容易写错:SSE 和 WebSocket 只是传输层,事件字段没有行业标准。 不同框架各推各的:
| 框架 / 产品 | 工具调用相关事件(示意) |
|---|---|
Vercel AI SDK(useChat) | message.parts 里 tool-${toolName},状态有 input-streaming、input-available、output-available 等 |
| OpenAI Agents SDK | RunItemStreamEvent:tool_called、tool_output、reasoning_item_created 等 |
| OpenAI Chat Completions | 流式 delta 里带 tool_calls 字段,参数可能分片到达 |
| Anthropic Messages API | content_block_start / content_block_delta,工具块类型为 tool_use |
下面这段是前端自己的归一化层,把各家事件映射成统一的 ToolCall 结构,不是某个后端会直接推的格式:
type NormalizedEvent =
| { type: "phase"; phase: AgentPhase }
| { type: "tool_call"; call: ToolCall }
| { type: "tool_result"; id: string; preview: string; ok: boolean }
| { type: "text_delta"; content: string }
| { type: "done" };
function applyEvent(event: NormalizedEvent, state: ChatState): ChatState {
switch (event.type) {
case "phase":
return { ...state, phase: event.phase };
case "tool_call":
return {
...state,
phase: "tool_running",
toolCalls: [...state.toolCalls, event.call],
};
case "tool_result":
return {
...state,
toolCalls: state.toolCalls.map((c) =>
c.id === event.id
? { ...c, status: event.ok ? "ok" : "failed", resultPreview: event.preview }
: c
),
};
case "text_delta":
return { ...state, phase: "answering", answer: state.answer + event.content };
case "done":
return { ...state, phase: "idle" };
default:
return state;
}
}
接 Vercel AI SDK 的话,优先用 message.parts 渲染,别自己再造一套事件协议。接 OpenAI Agents SDK,监听 tool_called 和 tool_output。有个实际坑:部分 SDK 的 tool_called 会等工具跑完才推,UI 上「执行中」状态可能闪一下就没了——如果在意实时感,得读 raw stream 或在本地收到调用意图就先更新状态。
核心原则就一条:别把 tool_call 和最终 text 揉成一条消息,分开存、分开渲染。
两个容易踩的坑
展示过度。 每一步推理都展开,信息噪音比黑盒还大。工具调用展示摘要就行,详细日志放折叠或 debug 面板。
失败藏起来。 上面说过,值得再强调一次。用户宁可看到「第 2 步查询失败,结果可能不完整」,也不要一段看似流畅的瞎编答案。
今晚就能改的一版
已有 Agent 聊天页,按这个顺序来:
- 加
phase状态条,请求发出就先亮「思考中」 - 后端每调一次工具,前端收到后立刻插一张卡片(或先标 running,结果到了再更新)
- 最终回答和过程分开,过程默认折叠
不用等完美。三块 UI 上了,用户至少知道 AI 在干什么,而不是对着空白等一段结论。
模型还会继续卷。但在大多数业务场景里,让用户看见过程,往往比换一个更大的模型更能解决「不信」这个问题。
示例代码为简化演示,生产环境注意脱敏、权限控制和错误兜底。流式协议以你所用框架的官方文档为准:Vercel AI SDK、OpenAI Agents SDK Streaming。
更多推荐



所有评论(0)