嘿,各位未来的架构师们,还有那些正在为“让 AI 乖乖听话”而掉头发的开发者们,大家好!
欢迎来到今天的讲座,主题是——《React 驱动的 AI Agent 交互协议:实现声明式工具调用反馈》。
坐下来,喝口水。别紧张,虽然我们谈论的是 AI,但今天咱们不聊那些玄之又玄的神经网络权重,也不聊那些让你血压升高的 Prompt Engineering。我们要聊的是架构,是模式,是如何用 React 那我们最熟悉的 useState 和 useEffect,去驯服那只名叫 Agent 的野兽。
想象一下,AI Agent 不是一个只会聊天、稍微有点像 Siri 的机器人。它是一个真正的多面手。它需要查天气、算汇率、订机票,甚至帮你把隔壁老王家的狗叫回来。如果还是那种“用户问一句,AI 回一句”的聊天模式,那就太掉价了。那不是 Agent,那是个复读机。
我们要构建的是一个状态机驱动的交互系统。而 React,正是实现这个状态机的绝佳工具。
准备好了吗?让我们把那堆乱七八糟的回调函数扔进垃圾桶,开始正题。
第一部分:从“命令式”到“声明式”——告别手动挡
在深入协议之前,我们要先吐槽一下传统的 Agent 调用方式。如果你以前写过这种代码,你懂的。
传统的“手动挡”调用方式(也就是命令式编程的噩梦):
// 糟糕的代码示例,仅供怀念
const handleUserMessage = async (text: string) => {
setLoading(true);
try {
// 1. 把用户的话扔给 LLM
const response = await api.chat(text);
// 2. 解析 JSON(如果 LLM 没写错引号,或者多打了个 Tab,恭喜你,报错)
const toolCall = JSON.parse(response.toolCall);
// 3. 判断它要干嘛
if (toolCall.name === 'get_weather') {
// 4. 手动去调用函数
const weatherData = await getWeather(toolCall.args.city);
// 5. 把结果塞回去
await api.chatWithToolResult(response.id, weatherData);
// 6. 再次解析结果,显示给用户
const finalResponse = await api.chat(response.id);
addMessage(finalResponse.text);
}
} catch (error) {
console.error("我的天,它又疯了", error);
} finally {
setLoading(false);
}
};
看到没?这就是所谓的“面条代码”。你的 React 组件不得不去关心 LLM 的输出格式、去关心工具的执行顺序、去关心错误重试。这就像是你开了辆车,但你还得自己换轮胎、自己加油、自己修发动机。累不累?累死了!
我们想要的“自动挡”模式:
React 的核心理念是声明式。你只描述“UI 应该是什么样子的”,React 负责帮你把那该死的 DOM 更新了。
同样,对于 Agent,我们也需要声明式协议。我们要告诉 React:“嘿,Agent 现在处于 THINKING 状态,UI 就显示一个加载圈。当它调用 get_weather 工具时,UI 就把 TOOL 类型的消息渲染上去。当它完成任务时,渲染最终的回复。”
这就是我们要讲的协议的核心:将 Agent 的内部状态(Thinking -> Calling -> Result -> Done)映射到 React 的组件 Props 或 State 上。
第二部分:协议层设计——定义“语言”
在写代码之前,我们需要定义 Agent 说话的“语法”。这个语法就是我们的协议层。它不应该依赖任何特定的 LLM 后端,它应该是纯前端逻辑。
我们定义一个核心概念:Tool(工具)。在 React 的世界里,工具就是一个对象,它有名字、描述和参数 schema。
// 定义工具的契约
interface Tool<TInput = any, TOutput = any> {
name: string;
description: string;
// 执行逻辑,纯函数风格最好
execute: (input: TInput) => Promise<TOutput> | TOutput;
}
// 这是一个具体的工具:查天气
const getWeatherTool: Tool<{ city: string }> = {
name: 'get_weather',
description: '获取指定城市的实时天气信息',
execute: async ({ city }) => {
// 模拟网络请求
return new Promise(resolve => {
setTimeout(() => resolve({ city, temp: 26, condition: '晴朗' }), 1000);
});
}
};
现在,我们需要一个协议来描述 Agent 的“意图”。在 React 中,这个意图通常表现为Action或Event。
// Agent 的动作流
type AgentAction =
| { type: 'USER_INPUT'; payload: string }
| { type: 'AGENT_THINKING'; payload: string }
| { type: 'TOOL_CALL'; payload: { toolName: string; args: any } }
| { type: 'TOOL_RESULT'; payload: { toolName: string; result: any } }
| { type: 'AGENT_RESPONSE'; payload: string };
// Agent 的状态机
type AgentStatus = 'IDLE' | 'THINKING' | 'EXECUTING' | 'WAITING_FOR_INPUT';
第三部分:核心实现——自定义 Hook useAgent
这是今天的重头戏。我们将创建一个 React Hook,它像一个智能管家一样,管理着整个对话流程。这个 Hook 接收用户输入,处理状态流转,并负责将 Agent 的行为反馈给 UI。
import { useState, useCallback } from 'react';
// 假设我们有一个 LLM 服务
const mockLLM = async (input: string) => {
return {
text: "好的,我来帮你查一下北京的天气。",
tool_calls: [
{ name: 'get_weather', args: { city: '北京' } }
]
};
};
export const useAgent = (tools: Map<string, Tool>) => {
// UI 需要展示的消息列表
const [messages, setMessages] = useState<any[]>([]);
// Agent 当前所处的“精神状态”
const [status, setStatus] = useState<AgentStatus>('IDLE');
// 这是一个用来跟踪当前正在进行的工具调用的 ID(防止并发问题)
const [activeToolId, setActiveToolId] = useState<string | null>(null);
// 添加消息到 UI
const addMessage = useCallback((role: 'user' | 'agent' | 'tool', content: string) => {
setMessages(prev => [...prev, { role, content, timestamp: Date.now() }]);
}, []);
// 处理用户输入
const handleUserInput = useCallback(async (text: string) => {
// 1. 状态重置
setStatus('THINKING');
setMessages(prev => [...prev, { role: 'user', content: text }]);
try {
// 2. 请求 LLM
const response = await mockLLM(text);
// 3. 将 Agent 的思考过程渲染出来
addMessage('agent', response.text);
// 4. 如果 LLM 决定调用工具
if (response.tool_calls && response.tool_calls.length > 0) {
for (const toolCall of response.tool_calls) {
// 声明式反馈:告诉 UI "我要去执行工具了"
setStatus('EXECUTING');
addMessage('tool', `[正在调用: ${toolCall.name}...]`);
try {
const tool = tools.get(toolCall.name);
if (!tool) {
console.error(`工具不存在: ${toolCall.name}`);
addMessage('tool', `[错误: ${toolCall.name} 工具不可用]`);
continue;
}
// 执行工具
const result = await tool.execute(toolCall.args);
// 声明式反馈:告诉 UI "工具执行完毕,结果是..."
addMessage('tool', `[${toolCall.name} 结果: ${JSON.stringify(result)}]`);
// 这里通常需要把结果喂回 LLM,但在演示中我们简单处理
addMessage('agent', `查到了,${toolCall.name} 的结果是 ${JSON.stringify(result)}`);
} catch (err) {
addMessage('tool', `[工具 ${toolCall.name} 执行失败]`);
}
}
// 5. 最后给个总结
addMessage('agent', "以上就是我能提供的所有帮助。");
setStatus('IDLE');
} else {
setStatus('IDLE');
}
} catch (error) {
addMessage('agent', "哎呀,服务器抽风了,请稍后再试。");
setStatus('IDLE');
}
}, [tools, addMessage]);
return { messages, status, handleUserInput };
};
看懂了吗?这就是声明式的精髓。
在 Hook 内部,我们负责处理复杂的异步逻辑(LLM 交互、工具调度、错误处理)。但在外部组件中,我们只需要关心 messages 数组和 status 状态。
外部组件怎么写?简直简单得令人发指。
// 组件调用示例
const WeatherAgent = () => {
const { messages, status, handleUserInput } = useAgent(new Map([['get_weather', getWeatherTool]]));
return (
<div className="agent-container">
<div className="chat-window">
{messages.map(msg => (
<div key={msg.timestamp} className={`msg ${msg.role}`}>
{msg.content}
</div>
))}
{status === 'THINKING' && <div className="typing-indicator">Agent 正在思考...</div>}
{status === 'EXECUTING' && <div className="tool-indicator">Agent 正在拧螺丝...</div>}
</div>
<input
type="text"
onKeyPress={(e) => e.key === 'Enter' && handleUserInput(e.currentTarget.value)}
/>
</div>
);
};
就是这个! UI 代码里没有 JSON.parse,没有 await tool.execute,没有 try/catch 嵌套地狱。这就是我们要的“反馈协议”。
第四部分:优化交互——让反馈更“懂人心”
光有状态切换还不够。作为资深开发者,我们要追求极致的用户体验。当一个 Agent 调用工具时,用户可能会觉得卡住了,或者不知道它到底在干嘛。我们需要更细腻的反馈。
我们可以引入一个“中间状态”。比如,在 EXECUTING 之前,加一个 RECEIVING_INTENT 状态。
type AgentStatus =
| 'IDLE'
| 'RECEIVING_INTENT' // 正在接收指令
| 'EXECUTING' // 正在干活
| 'IDLE';
// 在 Hook 中扩展逻辑
const handleUserInput = useCallback(async (text: string) => {
// ...省略用户消息添加...
setStatus('RECEIVING_INTENT');
const response = await mockLLM(text); // 这里的 mockLLM 模拟网络延迟
// 如果模型返回了工具调用
if (response.tool_calls) {
setStatus('EXECUTING');
addMessage('tool', `[正在连接服务器: ${response.tool_calls[0].name}]`);
// 执行工具
const result = await tool.execute(...);
// 完成反馈
addMessage('tool', `[数据已返回: ${result.temp}度]`);
}
}, []);
这不仅仅是一个状态机,这是一个叙事流。我们在 UI 上构建了一个故事:用户说话 -> Agent 倾听 -> Agent 决定行动 -> Agent 行动 -> 结果展示。
第五部分:并发与流式反馈——让 Agent 看起来更聪明
很多 Agent 的工具调用(比如搜索、数据库查询)是很慢的。如果我们还是按顺序执行,用户就得盯着屏幕傻等。
React 支持并发模式,我们的协议也应该支持。我们可以把“流式反馈”加进去。
想象一下,我们定义一个 StreamEvent 协议:
type StreamEvent =
| { type: 'content_chunk', text: string }
| { type: 'tool_started', toolName: string }
| { type: 'tool_progress', percent: number, log: string }
| { type: 'tool_finished', result: any };
// 在 Hook 中处理流
const handleToolExecution = async (toolName: string) => {
const eventEmitter = new EventTarget(); // 或者用 RxJS
// 1. 立即反馈:开始
eventEmitter.dispatchEvent(new CustomEvent('tool_started', { detail: { toolName } }));
// 2. 模拟流式输出进度
for (let i = 0; i <= 100; i+=10) {
await new Promise(r => setTimeout(r, 200));
eventEmitter.dispatchEvent(new CustomEvent('tool_progress', { detail: { percent: i, log: `正在查询数据库...` } }));
}
// 3. 完成
eventEmitter.dispatchEvent(new CustomEvent('tool_finished', { detail: { result: 'OK' } }));
};
// 组件监听这些事件
const useAgentWithStreams = () => {
const [events, setEvents] = useState<StreamEvent[]>([]);
useEffect(() => {
// ...监听逻辑...
}, []);
return <AgentUI events={events} />;
};
当 Agent 调用 useAgentWithStreams 时,它会吐出一连串的 UI 事件。这就像看火车进站,一节一节地进,而不是等整列火车都进站才让你看。
第六部分:异常处理与重试机制——容错的艺术
Agent 不是神仙,它会犯错,LLM 会幻觉(一本正经地胡说八道)。
我们的协议必须包含错误边界。当工具调用失败时,Agent 需要有一个“自我纠错”的能力。
协议设计:
type AgentAction =
| { type: 'USER_INPUT'; payload: string }
| { type: 'TOOL_CALL'; payload: { toolName: string; args: any; retryCount: number } }
| { type: 'ERROR'; payload: { message: string; context: any } };
// 增强版的 Hook 逻辑
const handleToolCall = async (toolCall) => {
try {
const result = await tool.execute(toolCall.args);
return { type: 'TOOL_RESULT', payload: result };
} catch (error) {
// 如果是第一次调用失败,让 Agent 重试
if (toolCall.retryCount < 2) {
return { type: 'TOOL_CALL', payload: { ...toolCall, retryCount: toolCall.retryCount + 1 } };
}
// 重试两次还不行,那就报错吧
return {
type: 'ERROR',
payload: { message: `工具 ${toolCall.toolName} 执行失败: ${error.message}` }
};
}
};
在 UI 层,我们只需要处理 ERROR 这个 Action,然后弹出一个漂亮的 Toast,而不是控制台的一堆红字。这种错误信息的抽象,正是 React 驱动的协议带来的红利。
第七部分:实战演示——一个“智能旅行规划师”
让我们把所有东西串起来。创建一个能真正规划旅行的 Agent。
1. 定义工具集:
const travelTools = new Map([
[
'search_flights',
{
name: 'search_flights',
description: '搜索从 A 到 B 的航班',
execute: async (args) => {
// 真实的 API 调用逻辑
return {
flights: [
{ id: 'CA1234', price: 500, time: '10:00' },
{ id: 'MU5678', price: 480, time: '14:30' }
]
};
}
}
],
[
'book_hotel',
{
name: 'book_hotel',
description: '预订酒店',
execute: async (args) => {
return { success: true, bookingId: 'HOTEL_998877' };
}
}
]
]);
2. 定义协议处理器:
const agentProtocol = {
user: async (text) => {
// LLM 返回 JSON
return JSON.stringify({
thought: "用户想去巴黎,我需要查机票和订酒店",
tools: [
{ name: 'search_flights', args: { destination: 'Paris' } },
{ name: 'book_hotel', args: { city: 'Paris' } }
]
});
},
tool: async (toolName, args) => {
const tool = travelTools.get(toolName);
return tool ? await tool.execute(args) : "工具未定义";
}
};
3. UI 渲染(声明式!):
const TravelAgent = () => {
const { messages, status, handleUserInput } = useAgent(travelTools);
return (
<div className="travel-agent">
<header>🤖 智能旅行规划师</header>
<div className="logs">
{messages.map((msg, i) => {
if (msg.role === 'tool') {
// 简单的工具调用 UI
return (
<div key={i} className="log-entry tool-call">
<span className="icon">🔧</span>
<span>{msg.content}</span>
</div>
);
}
return <div key={i} className={`log-entry ${msg.role}`}>{msg.content}</div>;
})}
{/* 动态状态展示 */}
{status === 'THINKING' && <div className="status-pill">🧠 计划中...</div>}
{status === 'EXECUTING' && <div className="status-pill">⚙️ 执行中...</div>}
</div>
<input
placeholder="比如:我想去巴黎玩三天"
onKeyDown={e => e.key === 'Enter' && handleUserInput(e.target.value)}
/>
</div>
);
};
发生了什么?
用户输入 -> Hook 状态变 THINKING -> UI 显示 “🧠 计划中…” -> LLM 返回工具调用 -> Hook 状态变 EXECUTING -> UI 显示 “⚙️ 执行中…” -> UI 渲染工具调用的结果。
整个过程,React 组件完全不知道 search_flights 需要联网,也不知道 book_hotel 需要密码。它只知道:“哦,有一个 tool 类型的消息来了,把它渲染出来就行。”
这就是关注点分离的最高境界。
第八部分:进阶话题——流式解析与协议的演进
如果你觉得上面的代码太“同步”了,那是因为为了讲座的易懂性。在实际生产中,LLM 的输出是流式的。我们需要一个协议来处理这种“边吐边吃”的情况。
我们可以将协议升级为事件流:
// 协议:事件流
type AgentStreamEvent =
| { type: 'TEXT', chunk: string }
| { type: 'TOOL_START', name: string }
| { type: 'TOOL_PROGRESS', data: any };
// 组件监听流
const useAgentStream = () => {
const [events, setEvents] = useState<AgentStreamEvent[]>([]);
const streamAgent = async (prompt: string) => {
// 建立流连接
const response = await fetchLLMStream(prompt);
response.on('data', (chunk) => {
// 协议解析器
if (chunk.type === 'text') {
setEvents(prev => [...prev, { type: 'TEXT', chunk }]);
} else if (chunk.type === 'tool') {
setEvents(prev => [...prev, { type: 'TOOL_START', name: chunk.name }]);
executeTool(chunk.name, chunk.args);
}
});
};
};
这种方式让 Agent 的表现非常流畅。你可以一边看着它打字,一边看着它调用工具,就像看一部科技电影一样。
第九部分:调试——如何在 Agent 的世界里导航
写 React 你可以 console.log,写 Agent 协议你甚至可以 console.log,但你需要更好的方式。因为 Agent 的状态是复杂度爆炸的。
我推荐实现一个“上帝之眼”面板。
// 调试面板组件
const AgentDebugger = ({ agentState }) => {
return (
<div className="debug-panel">
<h3>Agent 内部状态</h3>
<div className="state-grid">
<div className="state-item">
<span>Status:</span>
<span className={agentState.status}>{agentState.status}</span>
</div>
<div className="state-item">
<span>Tools:</span>
<ul>
{agentState.currentTools.map(t => <li key={t.name}>{t.name}</li>)}
</ul>
</div>
<div className="state-item">
<span>Memory:</span>
<pre>{JSON.stringify(agentState.memory, null, 2)}</pre>
</div>
</div>
</div>
);
};
在开发阶段,把这个组件挂载在 App 根节点。当你点击“查天气”时,你能清晰地看到:Agent 先是在思考,然后决定调用 get_weather,它把 city: 'Beijing' 存进了内存,状态变成了 EXECUTING。
这种可视化的反馈机制,是调试复杂协议的神器。
第十部分:总结——拥抱未来
好了,朋友们,咱们讲得差不多了。
我们今天没有教你怎么训练一个 Transformer 模型,也没有教你如何优化 GPU 算力。我们教你的是一种思维方式。
传统的 Agent 开发,就像是在黑盒子里跳舞,你不知道机器什么时候会绊倒。而基于 React 的声明式协议,就像是给机器装上了透明玻璃。
- 工具调用 不再是一串神秘的 JSON 字符串,而是一个清晰的
ToolCallAction。 - 执行过程 不再是后台默默的进程,而是一个可观测的
Status状态。 - 反馈 不再是突兀的文字跳变,而是一个顺滑的、符合人类直觉的 UI 流程。
这种架构的优势是显而易见的:
- 可维护性:逻辑在 Hook 里,UI 在组件里,互不干扰。
- 可扩展性:加一个新的工具?只需要在 Map 里加一行配置,UI 无需改动。
- 用户体验:状态清晰,反馈及时,拒绝“转圈圈”式的等待。
所以,当你下次设计 Agent 系统时,问自己一个问题:“这看起来像是一个复杂的后端进程,还是一个响应式的前端状态机?”
如果是前者,请回头重学 React;如果是后者,恭喜你,你掌握了一种构建未来应用的利器。
祝你们的 Agent 们永远听话,你们的 UI 永远丝滑!
下课!