智能体状态指示:何时思考、何时调用工具、何时出错

让用户理解 AI 的"内心活动",体验感和信任感直接拉满

你有没有用过那种智能体:你问一个问题,界面转了五秒钟的圈,然后突然冒出一段回答。你看不到它在想什么,也不知道它是不是卡死了,更不知道为什么有时候回答很快、有时候很慢。这种"黑箱体验"会让用户焦虑,甚至怀疑 AI 是不是在摸鱼。

其实 Agent 在执行任务时,内部状态是很丰富的:它可能在推理(ReAct 的 Thought),可能在调用工具(比如查数据库、搜索网页),可能在等待外部 API 响应,也可能出错了需要重试或人工介入。如果能把这些状态实时地、可视化地展示给用户,用户的感知会完全不同------他不再觉得自己在跟一个黑盒对话,而是一个"正在认真思考并动手做事"的智能体。

这篇文章,我就把智能体状态指示的设计思路、实现方案和踩坑经验完整地讲一遍。包含完整的 React + TypeScript 代码,以及一套可扩展的状态机模型。

一、智能体有哪些状态?

一个典型的智能体(尤其是 ReAct 模式)在执行任务时,会经历以下几个阶段:

把这些状态映射到前端 UI,我们至少需要向用户传达:

  • 思考中:AI 正在分析问题、规划步骤(通常显示"正在思考..."或三个点跳动)。
  • 调用工具:AI 正在调用某个外部工具,比如"正在查询订单状态...""正在搜索文档..."。
  • 等待结果:工具调用后等待响应,可以显示进度或计时。
  • 生成回答:流式输出文字时,用户能看到逐字出现。
  • 出错:某一步失败,显示错误信息并提供重试或人工介入选项。
  • 完成:恢复正常状态。

二、状态指示器的 UI 设计

2.1 思考状态(Thinking)

最常见的做法是显示一个"正在输入"气泡,里面三个点跳动。同时可以附加一段文本,说明 AI 在思考什么(如果后端能返回 thought 内容)。

tsx 复制代码
// components/ThinkingIndicator.tsx
import { Loader2 } from 'lucide-react';

export function ThinkingIndicator({ thought }: { thought?: string }) {
  return (
    <div className="flex justify-start mb-4">
      <div className="bg-gray-100 dark:bg-gray-800 rounded-2xl rounded-bl-none px-4 py-3 max-w-[80%]">
        <div className="flex items-center gap-2">
          <Loader2 className="w-4 h-4 animate-spin text-gray-500" />
          <span className="text-sm text-gray-500">
            {thought ? `正在思考:${thought}` : 'AI 正在思考...'}
          </span>
        </div>
      </div>
    </div>
  );
}

2.2 工具调用状态(Tool Calling)

当 Agent 决定调用某个工具时,前端应该明确告诉用户:"AI 正在做某件事"。可以是内嵌在气泡中的一行提示,也可以是一个独立的卡片。

tsx 复制代码
// components/ToolCallStatus.tsx
import { Search, Database, Mail, Code, Wrench } from 'lucide-react';

const toolIcons: Record<string, React.ElementType> = {
  search: Search,
  query_order: Database,
  send_email: Mail,
  execute_code: Code,
};

export function ToolCallStatus({ toolName, args, status }: { toolName: string; args?: any; status: 'calling' | 'success' | 'error' }) {
  const Icon = toolIcons[toolName] || Wrench;
  const statusText = {
    calling: `正在调用工具 ${toolName}...`,
    success: `工具 ${toolName} 调用成功`,
    error: `工具 ${toolName} 调用失败`,
  };

  return (
    <div className={`flex justify-start mb-2 text-sm ${status === 'error' ? 'text-red-500' : 'text-gray-500'}`}>
      <div className="flex items-center gap-1 bg-gray-50 dark:bg-gray-800 rounded-full px-3 py-1">
        <Icon className="w-3 h-3" />
        <span>{statusText[status]}</span>
        {status === 'calling' && <Loader2 className="w-3 h-3 animate-spin ml-1" />}
      </div>
    </div>
  );
}

在对话流中,这些工具状态可以显示在消息气泡的上方或下方,作为辅助信息,不干扰主消息流。

2.3 等待结果(Waiting)

如果工具调用需要较长时间(比如调用外部 API 慢),可以显示进度条或计时。

tsx 复制代码
// components/WaitingIndicator.tsx
import { useEffect, useState } from 'react';

export function WaitingIndicator({ startTime }: { startTime: number }) {
  const [elapsed, setElapsed] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      setElapsed(Math.floor((Date.now() - startTime) / 1000));
    }, 1000);
    return () => clearInterval(timer);
  }, [startTime]);
  return (
    <div className="text-xs text-gray-400 mt-1">
      已等待 {elapsed} 秒...
    </div>
  );
}

2.4 错误状态(Error)

当工具调用失败或 Agent 无法继续时,显示友好的错误提示,并提供重试或人工介入选项。

tsx 复制代码
// components/ErrorMessage.tsx
import { AlertCircle, RefreshCw, Headphones } from 'lucide-react';

export function ErrorMessage({ error, onRetry, onContactSupport }: { error: string; onRetry?: () => void; onContactSupport?: () => void }) {
  return (
    <div className="flex justify-start mb-4">
      <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 rounded-2xl px-4 py-3 max-w-[80%]">
        <div className="flex items-center gap-2 text-red-600 dark:text-red-400">
          <AlertCircle className="w-4 h-4" />
          <span className="text-sm font-medium">出错了</span>
        </div>
        <p className="text-sm mt-1">{error}</p>
        <div className="flex gap-3 mt-2">
          {onRetry && (
            <button onClick={onRetry} className="text-xs flex items-center gap-1 text-blue-500">
              <RefreshCw className="w-3 h-3" /> 重试
            </button>
          )}
          {onContactSupport && (
            <button onClick={onContactSupport} className="text-xs flex items-center gap-1 text-blue-500">
              <Headphones className="w-3 h-3" /> 联系人工
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

三、在流式对话中集成状态指示

实际对话中,Agent 的状态是动态变化的。我们需要一个统一的状态机来管理,并与 SSE / WebSocket 消息联动。

3.1 定义状态枚举

typescript 复制代码
// types/agentStatus.ts
export type AgentStatus =
  | 'idle'
  | 'thinking'
  | 'calling_tool'
  | 'waiting'
  | 'generating'
  | 'error'
  | 'done';

export interface AgentStatusInfo {
  status: AgentStatus;
  thought?: string;        // 当前思考内容
  toolName?: string;       // 正在调用的工具名
  toolArgs?: any;          // 工具参数
  errorMessage?: string;   // 错误信息
  startTime?: number;      // 开始时间(用于等待计时)
}

3.2 在聊天组件中使用

tsx 复制代码
// components/ChatInterface.tsx
import { useState } from 'react';
import { useStreamingChat } from '@/hooks/useStreamingChat';
import { ThinkingIndicator } from './ThinkingIndicator';
import { ToolCallStatus } from './ToolCallStatus';
import { ErrorMessage } from './ErrorMessage';
import { AgentStatusInfo } from '@/types/agentStatus';

export function ChatInterface() {
  const { sendMessage, isStreaming, currentAnswer } = useStreamingChat();
  const [agentStatus, setAgentStatus] = useState<AgentStatusInfo>({ status: 'idle' });
  const [messages, setMessages] = useState([]);

  const handleSend = async (userInput: string) => {
    // 添加用户消息
    setMessages(prev => [...prev, { role: 'user', content: userInput }]);
    setAgentStatus({ status: 'thinking', thought: '分析问题...' });

    // 调用流式 API,并监听事件
    const eventSource = new EventSource(`/api/agent/stream?prompt=${encodeURIComponent(userInput)}`);

    eventSource.addEventListener('thought', (e: any) => {
      const data = JSON.parse(e.data);
      setAgentStatus({ status: 'thinking', thought: data.content });
    });

    eventSource.addEventListener('tool_call', (e: any) => {
      const data = JSON.parse(e.data);
      setAgentStatus({
        status: 'calling_tool',
        toolName: data.toolName,
        toolArgs: data.args,
      });
    });

    eventSource.addEventListener('tool_result', (e: any) => {
      // 工具调用成功,短暂显示成功状态后继续
      setAgentStatus({ status: 'thinking', thought: '正在分析工具结果...' });
    });

    eventSource.addEventListener('error', (e: any) => {
      const data = JSON.parse(e.data);
      setAgentStatus({
        status: 'error',
        errorMessage: data.message,
      });
    });

    eventSource.addEventListener('done', () => {
      setAgentStatus({ status: 'done' });
      eventSource.close();
    });

    // 处理流式文本(generating 状态已在收到第一个 text 时设置)
    let firstChunk = true;
    eventSource.onmessage = (e) => {
      const data = JSON.parse(e.data);
      if (data.type === 'text') {
        if (firstChunk) {
          setAgentStatus({ status: 'generating' });
          firstChunk = false;
        }
        // 追加到当前 AI 消息...
      }
    };
  };

  return (
    <div className="flex flex-col h-screen">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((msg, idx) => (...))}
        
        {/* 状态指示器 */}
        {agentStatus.status === 'thinking' && (
          <ThinkingIndicator thought={agentStatus.thought} />
        )}
        {agentStatus.status === 'calling_tool' && (
          <ToolCallStatus toolName={agentStatus.toolName!} status="calling" />
        )}
        {agentStatus.status === 'waiting' && (
          <div className="flex justify-start">
            <div className="text-sm text-gray-400">⏳ 等待响应... <WaitingIndicator startTime={agentStatus.startTime!} /></div>
          </div>
        )}
        {agentStatus.status === 'error' && (
          <ErrorMessage
            error={agentStatus.errorMessage!}
            onRetry={() => handleSend(userInput)} // 重发相同消息
            onContactSupport={() => window.open('/support')}
          />
        )}
      </div>
      <ChatInput onSend={handleSend} disabled={agentStatus.status === 'calling_tool' || agentStatus.status === 'waiting'} />
    </div>
  );
}

四、后端需要提供哪些事件?

为了让前端能精确感知 Agent 状态,后端(智能体运行时)需要在关键节点推送特定事件。以 FastAPI + LangGraph 为例,可以在图的每个节点执行前后发送事件。

python 复制代码
async def agent_stream(prompt: str):
    # 思考事件
    yield f"event: thought\ndata: {json.dumps({'content': '分析用户意图'})}\n\n"
    
    # 决定调用工具
    yield f"event: tool_call\ndata: {json.dumps({'toolName': 'query_order', 'args': {'order_id': '123'}})}\n\n"
    
    # 模拟工具调用耗时
    await asyncio.sleep(1)
    
    # 工具结果事件
    yield f"event: tool_result\ndata: {json.dumps({'result': '订单状态: 已发货'})}\n\n"
    
    # 流式文本
    for chunk in ["订单", "已", "发货", ",", "预计", "明天", "送达"]:
        yield f"data: {json.dumps({'type': 'text', 'content': chunk})}\n\n"
        await asyncio.sleep(0.05)
    
    # 完成事件
    yield f"event: done\ndata: {{}}\n\n"

五、错误恢复与超时处理

除了展示错误,还需要让用户能够重试跳过。例如工具调用超时后,提供一个"重试"按钮,或者"跳过此步骤"按钮。

同时,前端应该设置一个全局超时:如果 Agent 在某个状态(如 calling_tool)超过 30 秒没有响应,自动触发超时错误,并提示用户。

tsx 复制代码
useEffect(() => {
  if (agentStatus.status === 'calling_tool' || agentStatus.status === 'waiting') {
    const timer = setTimeout(() => {
      setAgentStatus({
        status: 'error',
        errorMessage: `工具 ${agentStatus.toolName} 响应超时,请检查网络或稍后重试。`,
      });
    }, 30000);
    return () => clearTimeout(timer);
  }
}, [agentStatus]);

六、设计原则总结

  1. 透明化:不要隐藏 Agent 的思考过程。用户看到"正在思考...查询订单..."会更有耐心。
  2. 可操作:出错时提供重试、联系人工等选项,不要让用户卡住。
  3. 非侵入式:状态指示器不应该打断主消息流,最好放在气泡上方/下方,或作为独立消息。
  4. 性能友好:不要为了展示状态而频繁刷新整个组件,使用独立的小组件 + 原子化状态更新。
  5. 适配移动端:小屏幕下,工具调用状态可以简化为一个图标 + 简要文字。

七、完整状态转换图

下面这张图展示了前端状态机与后端事件的完整交互流程:

写在最后

智能体的状态指示不是锦上添花,而是基础体验的一部分。当你让用户看到 AI 在"调用订单查询工具"而不是干等一个模糊的加载圈时,用户对系统的信任感会明显提升。

实现上,关键是后端要提供细粒度的事件(thought、tool_call、tool_result、error、done),前端配合轻量级的状态机实时渲染。这套机制我们已经在生产环境中运行了半年,用户投诉"AI 没反应"的数量减少了 80%。

相关推荐
砍材农夫8 小时前
物联网 基于netty构建mqtt协议规范(主题通配符订阅)
java·前端·javascript·物联网·netty
广州华水科技8 小时前
单北斗GNSS变形监测在基础设施安全中的应用与维护
前端
木雷坞8 小时前
Home Assistant 升级翻车:一套 Docker Compose 回滚清单
后端
李小狼lee8 小时前
《spring如此简单》第四节--IOC思想的实现,spring启动后发生了什么
后端·面试
星栈8 小时前
Rust 全栈项目里,我写了一个不再重复造轮子的泛型表格组件
前端·前端框架·开源
SamDeepThinking8 小时前
面试官问Bean线程安全,你该从架构角度回答
java·后端·面试
008爬虫实战录8 小时前
【码上爬】 题九:webpack调试 堆栈分析
前端·webpack·node.js
用户713874229008 小时前
git fsck 深度解析 Git 仓库的体检医生
后端