让用户理解 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]);
六、设计原则总结
- 透明化:不要隐藏 Agent 的思考过程。用户看到"正在思考...查询订单..."会更有耐心。
- 可操作:出错时提供重试、联系人工等选项,不要让用户卡住。
- 非侵入式:状态指示器不应该打断主消息流,最好放在气泡上方/下方,或作为独立消息。
- 性能友好:不要为了展示状态而频繁刷新整个组件,使用独立的小组件 + 原子化状态更新。
- 适配移动端:小屏幕下,工具调用状态可以简化为一个图标 + 简要文字。
七、完整状态转换图
下面这张图展示了前端状态机与后端事件的完整交互流程:

写在最后
智能体的状态指示不是锦上添花,而是基础体验的一部分。当你让用户看到 AI 在"调用订单查询工具"而不是干等一个模糊的加载圈时,用户对系统的信任感会明显提升。
实现上,关键是后端要提供细粒度的事件(thought、tool_call、tool_result、error、done),前端配合轻量级的状态机实时渲染。这套机制我们已经在生产环境中运行了半年,用户投诉"AI 没反应"的数量减少了 80%。