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 SDKuseChatmessage.partstool-${toolName},状态有 input-streaminginput-availableoutput-available
OpenAI Agents SDKRunItemStreamEventtool_calledtool_outputreasoning_item_created
OpenAI Chat Completions流式 delta 里带 tool_calls 字段,参数可能分片到达
Anthropic Messages APIcontent_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_calledtool_output。有个实际坑:部分 SDK 的 tool_called 会等工具跑完才推,UI 上「执行中」状态可能闪一下就没了——如果在意实时感,得读 raw stream 或在本地收到调用意图就先更新状态。

核心原则就一条:别把 tool_call 和最终 text 揉成一条消息,分开存、分开渲染。

两个容易踩的坑

展示过度。 每一步推理都展开,信息噪音比黑盒还大。工具调用展示摘要就行,详细日志放折叠或 debug 面板。

失败藏起来。 上面说过,值得再强调一次。用户宁可看到「第 2 步查询失败,结果可能不完整」,也不要一段看似流畅的瞎编答案。

今晚就能改的一版

已有 Agent 聊天页,按这个顺序来:

  1. phase 状态条,请求发出就先亮「思考中」
  2. 后端每调一次工具,前端收到后立刻插一张卡片(或先标 running,结果到了再更新)
  3. 最终回答和过程分开,过程默认折叠

不用等完美。三块 UI 上了,用户至少知道 AI 在干什么,而不是对着空白等一段结论。

模型还会继续卷。但在大多数业务场景里,让用户看见过程,往往比换一个更大的模型更能解决「不信」这个问题。


示例代码为简化演示,生产环境注意脱敏、权限控制和错误兜底。流式协议以你所用框架的官方文档为准:Vercel AI SDKOpenAI Agents SDK Streaming

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐