目录
什么是 AI Agent
AI Agent(AI 智能体)是能够自主理解目标、规划步骤、执行任务的 AI 系统。与简单的问答不同,Agent 可以:
用户: "帮我查一下上海今天的天气,然后告诉我穿什么衣服"
传统 LLM: "上海今天晴,气温 25-30 度,建议穿短袖"
(需要人工查询天气数据)
AI Agent: ① 调用天气 API
② 分析温度范围
③ 生成穿衣建议
④ 返回完整答案
Agent 的核心能力
| 能力 | 说明 | |------|------| | 工具调用 (Tool Use) | Agent 可以调用外部工具:搜索、计算、API、文件操作等 | | 多步骤推理 (Chain of Thought) | 将复杂任务拆解为多个步骤逐步执行 | | 记忆 (Memory) | 跨对话保持上下文,支持长期记忆 | | 规划 (Planning) | 自动规划执行路径,失败时回退重试 |
主流接入方案
| 方案 | 难度 | 灵活性 | 适用场景 | |------|:----:|:------:|---------| | 直接调用 LLM API | ⭐ | ⭐⭐⭐ | 简单对话、生成任务 | | MCP 协议 | ⭐⭐ | ⭐⭐⭐⭐ | 工具调用、多源数据 | | 前端 Agent 框架 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 复杂自动化、复杂工作流 |
方案一:直接调用 LLM API
最简单的方式,直接通过 HTTP 调用 LLM 的 Chat API。
支持的前端 HTTP 请求
// 使用 fetch(原生,无需依赖)
async function chat(messages: ChatMessage[]): Promise<string> {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4o',
messages,
max_tokens: 2048,
}),
});
const data = await response.json();
return data.choices[0].message.content;
}
// 定义消息格式
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
React Hook 封装
// hooks/useChat.ts
import { useState, useCallback } from 'react';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
createdAt: Date;
}
export function useChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const send = useCallback(async (content: string) => {
const userMsg: Message = {
id: crypto.randomUUID(),
role: 'user',
content,
createdAt: new Date(),
};
setMessages(prev => [...prev, userMsg]);
setLoading(true);
setError(null);
try {
const reply = await chat([...messages, userMsg].map(m => ({
role: m.role,
content: m.content,
})));
const assistantMsg: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: reply,
createdAt: new Date(),
};
setMessages(prev => [...prev, assistantMsg]);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
}, [messages]);
return { messages, send, loading, error };
}
使用示例
import { useChat } from '@/hooks/useChat';
function ChatBox() {
const { messages, send, loading, error } = useChat();
const [input, setInput] = useState('');
const handleSend = () => {
if (!input.trim()) return;
send(input);
setInput('');
};
return (
<div>
<div className="messages">
{messages.map(m => (
<div key={m.id} className={`msg msg-${m.role}`}>
{m.content}
</div>
))}
</div>
{loading && <div className="typing">AI is typing...</div>}
{error && <div className="error">{error}</div>}
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSend()}
placeholder="Ask me anything..."
/>
<button onClick={handleSend} disabled={loading}>Send</button>
</div>
);
}
优点:简单直接,零依赖,适合简单对话场景
缺点:需要自己实现工具调用逻辑,不适合复杂 Agent 场景
方案二:MCP 协议接入
MCP (Model Context Protocol) 是 Anthropic 提出的开放协议,用于连接 LLM 与外部工具和数据源。
MCP 工作原理
┌──────────────┐ MCP Protocol ┌─────────────────────┐
│ LLM │◄────────────────────►│ MCP Server │
│ (Claude/ │ │ (Node.js / Python) │
│ GPT) │ │ │
└──────────────┘ │ ┌───────────────┐ │
│ │ File System │ │
│ │ HTTP / APIs │ │
│ │ Database │ │
│ │ Custom Tools │ │
│ └───────────────┘ │
└─────────────────────┘
MCP Server 示例 (Node.js)
// mcp-server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio';
const server = new McpServer({
name: 'weather-server',
version: '1.0.0',
});
// 注册天气查询工具
server.tool(
'get_weather',
'查询指定城市的天气信息',
{
city: { type: 'string', description: '城市名称' },
},
async ({ city }) => {
const res = await fetch(`https://api.weather.com/v3/wx/conditions/current?city=${city}`);
const data = await res.json();
return {
content: [
{
type: 'text',
text: `${city} 当前天气:${data.temp}°C,${data.condition},湿度 ${data.humidity}%`,
},
],
};
}
);
// 启动服务
const transport = new StdioServerTransport();
server.run(transport);
MCP 客户端调用
// 前端通过 WebSocket 连接本地 MCP Server
import { Client } from '@modelcontextprotocol/sdk/client';
const client = new Client({
name: 'frontend-client',
version: '1.0.0',
});
// 连接本地 MCP Server
await client.connect(transport);
const result = await client.callTool({
name: 'get_weather',
arguments: { city: '上海' },
});
console.log(result.content[0].text);
// 输出: "上海 当前天气:30°C,晴,湿度 65%"
优点:标准化协议,工具可复用,支持多数据源
缺点:需要额外部署 MCP Server,增加了架构复杂度
方案三:前端 Agent 框架
对于复杂的 Agent 场景,使用专用框架可以大大减少开发工作量。
Vercel AI SDK(推荐)
Vercel AI SDK 是目前最成熟的前端 AI 开发套件,支持所有主流 LLM。
安装
pnpm add ai @ai-sdk/openai @ai-sdk/anthropic
基本对话
import { useChat } from 'ai/react';
function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/chat', // 发送到自己的 API 路由
maxSteps: 5, // 最多 5 步推理
});
return (
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}: </strong>
{m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Ask me anything..."
/>
<button type="submit">Send</button>
</form>
</div>
);
}
服务端 API 路由(Next.js App Router)
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
system: '你是一个专业的助手,回答简洁准确。',
maxSteps: 5, // 启用多步骤推理
});
return result.toDataStreamResponse();
}
启用工具调用
// 服务端 - 定义工具
import { tool } from 'ai';
const weatherTool = tool({
description: '查询城市天气',
parameters: z.object({
city: z.string().describe('城市名称'),
}),
execute: async ({ city }) => {
const res = await fetch(`https://api.weather.com/v3/wx/conditions/current?city=${city}`);
const data = await res.json();
return `${city} 当前气温 ${data.temp}°C,天气:${data.condition}`;
},
});
const searchTool = tool({
description: '搜索互联网',
parameters: z.object({
query: z.string().describe('搜索关键词'),
}),
execute: async ({ query }) => {
// 调用搜索 API
const results = await webSearch(query);
return results.join('\n');
},
});
// API 路由
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
tools: { weather: weatherTool, search: searchTool },
});
return result.toDataStreamResponse();
}
// 前端 - 使用工具调用
import { useChat } from 'ai/react';
function AgentChat() {
const { messages, input, handleInputChange, handleSubmit, addToolResult } = useChat({
api: '/api/chat',
maxSteps: 5,
onToolCall({ toolCall }) {
// 工具被调用时显示加载状态
console.log(`Calling tool: ${toolCall.toolName}`);
},
});
return (
<div>
{messages.map(m => (
<div key={m.id}>
{m.role === 'user' ? '你' : 'AI'}: {m.content}
{/* 显示工具调用结果 */}
{m.toolInvocations?.map(tool => (
<div key={tool.toolName} className="tool-result">
🔧 {tool.toolName}: {tool.result}
</div>
))}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">发送</button>
</form>
</div>
);
}
工具调用流程图
用户: "上海天气怎么样?"
▼
┌─────────────────────────┐
│ GPT-4o 收到消息 │
└────────────┬────────────┘
▼
"需要调用 weather 工具"
▼
┌─────────────────────────┐
│ 前端收到 tool_call │
│ { name: 'weather', │
│ args: { city: '上海' } } │
└────────────┬────────────┘
▼
┌─────────────────────────┐
│ 执行天气查询 API │
└────────────┬────────────┘
▼
┌─────────────────────────┐
│ 返回结果给 GPT-4o │
└────────────┬────────────┘
▼
"上海今天 30°C,晴"
实战:构建一个 AI 助手组件
完整实现一个带工具调用能力的 AI 助手界面。
文件结构
src/
├── components/
│ └── AIAgentChat.tsx ← 主组件
├── hooks/
│ └── useAIAgent.ts ← Agent 逻辑 Hook
├── app/
│ └── api/
│ └── agent/
│ └── route.ts ← API 路由
└── types/
└── agent.ts ← 类型定义
Agent 类型定义
// types/agent.ts
export interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
toolInvocations?: ToolInvocation[];
createdAt: Date;
}
export interface ToolInvocation {
id: string;
toolName: string;
args: Record<string, unknown>;
status: 'pending' | 'result' | 'error';
result?: string;
error?: string;
}
export interface Tool {
name: string;
description: string;
params: Record<string, ToolParam>;
}
export interface ToolParam {
type: 'string' | 'number' | 'boolean';
description: string;
required?: boolean;
}
Agent Hook
// hooks/useAIAgent.ts
import { useState, useCallback, useRef } from 'react';
import type { Message, ToolInvocation } from '@/types/agent';
export function useAIAgent() {
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
// 发送消息
const send = useCallback(async (content: string) => {
// 添加用户消息
const userMsg: Message = {
id: crypto.randomUUID(),
role: 'user',
content,
createdAt: new Date(),
};
setMessages(prev => [...prev, userMsg]);
setLoading(true);
setError(null);
try {
// 调用 API
const res = await fetch('/api/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [...messages, userMsg] }),
signal: abortRef.current?.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const reader = res.body?.getReader();
if (!reader) throw new Error('No response body');
// 创建助手消息占位
const assistantMsg: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: '',
toolInvocations: [],
createdAt: new Date(),
};
setMessages(prev => [...prev, assistantMsg]);
setLoading(false);
// SSE 流式读取
const decoder = new TextDecoder();
let partial = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
partial += decoder.decode(value, { stream: true });
const lines = partial.split('\n');
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') {
reader.cancel();
return;
}
try {
const event = JSON.parse(data);
if (event.type === 'text') {
// 文本片段
setMessages(prev => prev.map(m =>
m.id === assistantMsg.id
? { ...m, content: m.content + event.text }
: m
));
} else if (event.type === 'tool_call') {
// 工具调用
const invocation: ToolInvocation = {
id: crypto.randomUUID(),
toolName: event.toolName,
args: event.args,
status: 'pending',
};
setMessages(prev => prev.map(m =>
m.id === assistantMsg.id
? { ...m, toolInvocations: [...(m.toolInvocations || []), invocation] }
: m
));
} else if (event.type === 'tool_result') {
// 工具结果
setMessages(prev => prev.map(m =>
m.id === assistantMsg.id
? {
...m,
toolInvocations: m.toolInvocations?.map(t =>
t.toolName === event.toolName
? { ...t, status: 'result' as const, result: event.result }
: t
),
}
: m
));
}
} catch {
// 忽略解析错误
}
}
}
} catch (e) {
if ((e as Error).name === 'AbortError') return;
setError((e as Error).message);
} finally {
setLoading(false);
}
}, [messages]);
// 中断生成
const abort = useCallback(() => {
abortRef.current?.abort();
setLoading(false);
}, []);
// 清空对话
const clear = useCallback(() => {
setMessages([]);
setError(null);
}, []);
return { messages, send, abort, clear, loading, error };
}
React 组件
// components/AIAgentChat.tsx
'use client';
import { useState, useRef, useEffect } from 'react';
import { useAIAgent } from '@/hooks/useAIAgent';
import { SendOutlined, StopOutlined, DeleteOutlined, RobotOutlined, UserOutlined } from '@ant-design/icons';
export default function AIAgentChat() {
const { messages, send, abort, clear, loading, error } = useAIAgent();
const [input, setInput] = useState('');
const bottomRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// 自动滚动到底部
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 发送消息
const handleSend = () => {
if (!input.trim() || loading) return;
send(input);
setInput('');
textareaRef.current?.focus();
};
// 回车发送(Shift+Enter 换行)
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
{/* Header */}
<div className="flex items-center justify-between pb-4 border-b">
<div className="flex items-center gap-2">
<RobotOutlined style={{ fontSize: 24, color: '#6366f1' }} />
<h1 className="text-xl font-bold">AI Agent</h1>
</div>
<button onClick={clear} className="text-gray-500 hover:text-red-500">
<DeleteOutlined /> Clear
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto py-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-gray-400 mt-20">
<RobotOutlined style={{ fontSize: 48 }} />
<p className="mt-2">Send a message to start the conversation</p>
</div>
)}
{messages.map(msg => (
<div key={msg.id} className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}>
{/* Avatar */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
msg.role === 'user' ? 'bg-indigo-500 text-white' : 'bg-gray-200 text-gray-600'
}`}>
{msg.role === 'user' ? <UserOutlined /> : <RobotOutlined />}
</div>
{/* Content */}
<div className={`max-w-[75%] space-y-2 ${
msg.role === 'user' ? 'items-end' : 'items-start'
}`}>
{/* Tool Calls */}
{msg.toolInvocations?.map(tool => (
<div key={tool.id} className="bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 text-sm">
<div className="flex items-center gap-2 text-amber-700">
<span className="font-mono">🔧 {tool.toolName}</span>
{tool.status === 'pending' && <span className="text-xs animate-pulse">Running...</span>}
</div>
<div className="text-xs text-amber-600 mt-1 font-mono">
{JSON.stringify(tool.args)}
</div>
{tool.result && (
<div className="mt-1 text-green-700 text-xs">
✓ {tool.result}
</div>
)}
</div>
))}
{/* Message */}
<div className={`rounded-2xl px-4 py-2 ${
msg.role === 'user'
? 'bg-indigo-500 text-white rounded-tr-sm'
: 'bg-gray-100 text-gray-800 rounded-tl-sm'
}`}>
<pre className="whitespace-pre-wrap text-sm leading-relaxed font-sans">
{msg.content || (loading && msg.role === 'assistant' ? '思考中...' : '')}
</pre>
</div>
</div>
</div>
))}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg px-4 py-2 text-red-700 text-sm">
Error: {error}
</div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<div className="pt-4 border-t">
<div className="flex gap-2 items-end">
<textarea
ref={textareaRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask me anything... (Enter to send, Shift+Enter for newline)"
className="flex-1 resize-none rounded-xl border border-gray-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
rows={1}
style={{ maxHeight: 120 }}
/>
{loading ? (
<button
onClick={abort}
className="w-12 h-12 rounded-xl bg-red-500 text-white hover:bg-red-600 flex items-center justify-center"
>
<StopOutlined />
</button>
) : (
<button
onClick={handleSend}
disabled={!input.trim()}
className="w-12 h-12 rounded-xl bg-indigo-500 text-white hover:bg-indigo-600 disabled:opacity-40 flex items-center justify-center"
>
<SendOutlined />
</button>
)}
</div>
<p className="text-xs text-gray-400 mt-2 text-center">
AI may make mistakes. Consider verifying important information.
</p>
</div>
</div>
);
}
API 路由
// app/api/agent/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';
export const runtime = 'edge';
const weatherTool = tool({
description: '查询城市天气信息',
parameters: z.object({
city: z.string().describe('城市名称,中文或英文'),
}),
execute: async ({ city }) => {
// 模拟天气查询
return `查询结果:${city} 今天气温 28°C,多云,空气质量良好。`;
},
});
const calculatorTool = tool({
description: '进行数学计算',
parameters: z.object({
expression: z.string().describe('数学表达式,如 2+2*3'),
}),
execute: async ({ expression }) => {
try {
// 安全计算(不要用 eval,改用数学库或 VM)
const result = new Function(`return (${expression})`)();
return `计算结果:${expression} = ${result}`;
} catch {
return `计算错误:无法计算表达式 "${expression}"`;
}
},
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: `你是一个智能助手,名字叫小智。你有以下工具可用:
- weather: 查询城市天气
- calculator: 进行数学计算
当用户询问天气时,先调用 weather 工具。
当用户进行数学计算时,调用 calculator 工具。
其他问题直接回答。`,
messages,
tools: {
weather: weatherTool,
calculator: calculatorTool,
},
maxSteps: 5,
});
return result.toDataStreamResponse();
}
生产环境注意事项
1. API Key 安全
// ❌ 错误:前端直接暴露 Key
const API_KEY = 'sk-xxxx'; // 任何人 F12 都能看到
// ✅ 正确:前端发请求到自己服务器,服务器持有 Key
// Next.js API Route /server
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, // 服务端环境变量
},
// ...
});
2. 限流防护
// 基于 IP 的简单限流(生产环境用 Redis)
const rateLimit = new Map<string, { count: number; resetAt: number }>();
function checkRateLimit(ip: string, max = 20, windowMs = 60000): boolean {
const now = Date.now();
const record = rateLimit.get(ip);
if (!record || now > record.resetAt) {
rateLimit.set(ip, { count: 1, resetAt: now + windowMs });
return true;
}
if (record.count >= max) return false;
record.count++;
return true;
}
// 使用
if (!checkRateLimit(req.headers.get('x-forwarded-for') || 'unknown')) {
return new Response('Too Many Requests', { status: 429 });
}
3. 流式响应处理
// 使用 ReadableStream 配合 TransformStream
export const stream = new TransformStream();
const writer = stream.writable.getWriter();
const encoder = new TextEncoder();
async function* generate() {
// 生成内容...
yield 'data: ' + JSON.stringify({ type: 'text', text: 'Hello' }) + '\n\n';
yield 'data: [DONE]\n\n';
}
for await (const chunk of generate()) {
await writer.write(encoder.encode(chunk));
}
writer.close();
4. 用户输入过滤
import { z } from 'zod';
const userMessageSchema = z.object({
content: z.string()
.min(1, 'Message cannot be empty')
.max(4000, 'Message too long')
.refine(s => !/<script|javascript:/i.test(s), 'Invalid content'),
});
const validated = userMessageSchema.safeParse(req.body);
if (!validated.success) {
return new Response(JSON.stringify({ error: validated.error.message }), { status: 400 });
}
许可与成本
主流 LLM API 对比
| 模型 | 提供商 | 前端免费额度 | 生产价格 | 工具调用支持 | |------|--------|------------|---------|:-----------:| | GPT-4o | OpenAI | $5 免费额度 | $5/1M tokens | ✅ | | Claude 3.5 Sonnet | Anthropic | $5 免费额度 | $3/1M tokens | ✅ | | Gemini 1.5 Pro | Google | Yes | $1.25/1M tokens | ✅ | | DeepSeek V3 | DeepSeek | $10 免费 | ¥1/1M tokens | ✅ | | Qwen 2.5 | 通义千问 | Yes | 较低 | ✅ | | GLM-4 | 智谱 AI | Yes | 较低 | ✅ |
成本优化建议
-
使用更小的模型处理简单任务:
- 简单对话 → GPT-4o-mini / Claude Haiku
- 复杂推理 → GPT-4o / Claude Sonnet
-
缓存常用回复:
// 用 hash(content) 作为 key 缓存结果 const cache = new Map<string, string>(); function getCachedResponse(prompt: string): string | null { const key = crypto.createHash('sha256').update(prompt).digest('hex'); return cache.get(key) ?? null; } -
减少 Token 数量:
- 截断过长的对话历史
- 使用更简洁的系统提示词
下一步
- [ ] 接入向量数据库实现 RAG(检索增强生成)
- [ ] 添加多模态能力(图片理解、文档分析)
- [ ] 实现 Agent 记忆持久化
- [ ] 对接 MCP 协议连接更多外部工具
- [ ] 添加用户认证和对话历史管理