智能体状态指示:何时思考、何时调用工具、何时出错
让用户理解 AI 的“内心活动”体验感和信任感直接拉满你有没有用过那种智能体你问一个问题界面转了五秒钟的圈然后突然冒出一段回答。你看不到它在想什么也不知道它是不是卡死了更不知道为什么有时候回答很快、有时候很慢。这种“黑箱体验”会让用户焦虑甚至怀疑 AI 是不是在摸鱼。其实 Agent 在执行任务时内部状态是很丰富的它可能在推理ReAct 的 Thought可能在调用工具比如查数据库、搜索网页可能在等待外部 API 响应也可能出错了需要重试或人工介入。如果能把这些状态实时地、可视化地展示给用户用户的感知会完全不同——他不再觉得自己在跟一个黑盒对话而是一个“正在认真思考并动手做事”的智能体。这篇文章我就把智能体状态指示的设计思路、实现方案和踩坑经验完整地讲一遍。包含完整的 React TypeScript 代码以及一套可扩展的状态机模型。一、智能体有哪些状态一个典型的智能体尤其是 ReAct 模式在执行任务时会经历以下几个阶段把这些状态映射到前端 UI我们至少需要向用户传达思考中AI 正在分析问题、规划步骤通常显示“正在思考…”或三个点跳动。调用工具AI 正在调用某个外部工具比如“正在查询订单状态…”“正在搜索文档…”。等待结果工具调用后等待响应可以显示进度或计时。生成回答流式输出文字时用户能看到逐字出现。出错某一步失败显示错误信息并提供重试或人工介入选项。完成恢复正常状态。二、状态指示器的 UI 设计2.1 思考状态Thinking最常见的做法是显示一个“正在输入”气泡里面三个点跳动。同时可以附加一段文本说明 AI 在思考什么如果后端能返回 thought 内容。// components/ThinkingIndicator.tsx import { Loader2 } from lucide-react; export function ThinkingIndicator({ thought }: { thought?: string }) { return ( div classNameflex justify-start mb-4 div classNamebg-gray-100 dark:bg-gray-800 rounded-2xl rounded-bl-none px-4 py-3 max-w-[80%] div classNameflex items-center gap-2 Loader2 classNamew-4 h-4 animate-spin text-gray-500 / span classNametext-sm text-gray-500 {thought ? 正在思考${thought} : AI 正在思考...} /span /div /div /div ); }2.2 工具调用状态Tool Calling当 Agent 决定调用某个工具时前端应该明确告诉用户“AI 正在做某件事”。可以是内嵌在气泡中的一行提示也可以是一个独立的卡片。// components/ToolCallStatus.tsx import { Search, Database, Mail, Code, Wrench } from lucide-react; const toolIcons: Recordstring, 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 classNameflex items-center gap-1 bg-gray-50 dark:bg-gray-800 rounded-full px-3 py-1 Icon classNamew-3 h-3 / span{statusText[status]}/span {status calling Loader2 classNamew-3 h-3 animate-spin ml-1 /} /div /div ); }在对话流中这些工具状态可以显示在消息气泡的上方或下方作为辅助信息不干扰主消息流。2.3 等待结果Waiting如果工具调用需要较长时间比如调用外部 API 慢可以显示进度条或计时。// 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 classNametext-xs text-gray-400 mt-1 已等待 {elapsed} 秒... /div ); }2.4 错误状态Error当工具调用失败或 Agent 无法继续时显示友好的错误提示并提供重试或人工介入选项。// components/ErrorMessage.tsx import { AlertCircle, RefreshCw, Headphones } from lucide-react; export function ErrorMessage({ error, onRetry, onContactSupport }: { error: string; onRetry?: () void; onContactSupport?: () void }) { return ( div classNameflex justify-start mb-4 div classNamebg-red-50 dark:bg-red-900/20 border border-red-200 rounded-2xl px-4 py-3 max-w-[80%] div classNameflex items-center gap-2 text-red-600 dark:text-red-400 AlertCircle classNamew-4 h-4 / span classNametext-sm font-medium出错了/span /div p classNametext-sm mt-1{error}/p div classNameflex gap-3 mt-2 {onRetry ( button onClick{onRetry} classNametext-xs flex items-center gap-1 text-blue-500 RefreshCw classNamew-3 h-3 / 重试 /button )} {onContactSupport ( button onClick{onContactSupport} classNametext-xs flex items-center gap-1 text-blue-500 Headphones classNamew-3 h-3 / 联系人工 /button )} /div /div /div ); }三、在流式对话中集成状态指示实际对话中Agent 的状态是动态变化的。我们需要一个统一的状态机来管理并与 SSE / WebSocket 消息联动。3.1 定义状态枚举// types/agentStatus.tsexporttypeAgentStatus|idle|thinking|calling_tool|waiting|generating|error|done;exportinterfaceAgentStatusInfo{status:AgentStatus;thought?:string;// 当前思考内容toolName?:string;// 正在调用的工具名toolArgs?:any;// 工具参数errorMessage?:string;// 错误信息startTime?:number;// 开始时间用于等待计时}3.2 在聊天组件中使用// 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] useStateAgentStatusInfo({ 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 classNameflex flex-col h-screen div classNameflex-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!} statuscalling / )} {agentStatus.status waiting ( div classNameflex justify-start div classNametext-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 为例可以在图的每个节点执行前后发送事件。asyncdefagent_stream(prompt:str):# 思考事件yieldfevent: thought\ndata:{json.dumps({content:分析用户意图})}\n\n# 决定调用工具yieldfevent: tool_call\ndata:{json.dumps({toolName:query_order,args:{order_id:123}})}\n\n# 模拟工具调用耗时awaitasyncio.sleep(1)# 工具结果事件yieldfevent: tool_result\ndata:{json.dumps({result:订单状态: 已发货})}\n\n# 流式文本forchunkin[订单,已,发货,,预计,明天,送达]:yieldfdata:{json.dumps({type:text,content:chunk})}\n\nawaitasyncio.sleep(0.05)# 完成事件yieldfevent: done\ndata: {{}}\n\n五、错误恢复与超时处理除了展示错误还需要让用户能够重试或跳过。例如工具调用超时后提供一个“重试”按钮或者“跳过此步骤”按钮。同时前端应该设置一个全局超时如果 Agent 在某个状态如calling_tool超过 30 秒没有响应自动触发超时错误并提示用户。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%。