前言
AI Agent(人工智能代理)是当前最热门的技术趋势之一。它不仅能理解用户指令,还能自主规划、调用工具并执行复杂任务。Next.js 作为 React 生态中最流行的全栈框架,结合其 API Routes 和 Server Actions 特性,是构建 AI Agent 应用的理想选择。
本文将带你从零开始,使用 Next.js 搭建一个功能完整的 AI Agent 应用。
目录
什么是 AI Agent
AI Agent 是一个能够:
- 理解自然语言:解析用户意图
- 自主规划:将复杂任务分解为步骤
- 调用工具:执行搜索、计算、API 请求等操作
- 记忆管理:维持对话历史和长期记忆
- 持续迭代:根据结果调整策略
的智能系统。
技术栈选择
后端框架
- Next.js 14+:App Router + Server Actions
- Anthropic SDK:Claude API 集成
前端
- React 18:UI 组件
- Tailwind CSS:样式管理
- shadcn/ui:高质量组件库
状态管理
- Zustand:轻量级状态管理
- React Query:服务端状态同步
数据存储
- Upstash Redis:对话历史缓存
- PostgreSQL(可选):持久化存储
项目初始化
1. 创建 Next.js 项目
npx create-next-app@latest my-ai-agent \
--typescript \
--tailwind \
--app \
--src-dir \
--import-alias "@/*"
cd my-ai-agent
2. 安装依赖
npm install @anthropic-ai/sdk uuid
npm install zustand @tanstack/react-query
npm install lucide-react class-variance-authority clsx tailwind-merge
3. 配置环境变量
创建 .env.local:
ANTHROPIC_API_KEY=your_api_key_here
NEXT_PUBLIC_APP_URL=http://localhost:3000
Agent 核心架构
┌─────────────────────────────────────────────────┐
│ Frontend │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ Chat Panel │ │ Tool Panel │ │Settings │ │
│ └─────────────┘ └─────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Next.js API Layer │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Chat Route │ │ Agent Server Action │ │
│ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Agent Engine │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Messages │ │ Tools │ │ Memory │ │
│ │ Manager │ │ Executor│ │ Manager │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Claude API │
│ (Anthropic SDK) │
└─────────────────────────────────────────────────┘
实现基础对话功能
1. 创建 Agent 核心
// src/lib/agent/core.ts
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
export interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
export interface AgentConfig {
model?: string;
maxTokens?: number;
temperature?: number;
systemPrompt?: string;
}
export class Agent {
private config: AgentConfig;
private history: Message[] = [];
constructor(config: AgentConfig = {}) {
this.config = {
model: 'claude-3-5-sonnet-20241022',
maxTokens: 4096,
temperature: 0.7,
systemPrompt: 'You are a helpful AI assistant.',
...config,
};
}
async chat(userMessage: string): Promise<string> {
// 添加用户消息到历史
this.history.push({
role: 'user',
content: userMessage,
timestamp: Date.now(),
});
try {
const response = await anthropic.messages.create({
model: this.config.model!,
max_tokens: this.config.maxTokens!,
temperature: this.config.temperature,
system: this.config.systemPrompt,
messages: this.history.map(({ role, content }) => ({
role,
content,
})),
});
const assistantMessage =
response.content[0].type === 'text'
? response.content[0].text
: 'Unable to process response';
// 添加助手响应到历史
this.history.push({
role: 'assistant',
content: assistantMessage,
timestamp: Date.now(),
});
return assistantMessage;
} catch (error) {
console.error('Agent chat error:', error);
throw new Error('Failed to get response from agent');
}
}
getHistory(): Message[] {
return this.history;
}
clearHistory(): void {
this.history = [];
}
}
2. 创建 Server Action
// src/app/actions/chat.ts
'use server';
import { Agent } from '@/lib/agent/core';
import { cache } from 'react';
export type Message = {
role: 'user' | 'assistant';
content: string;
timestamp: number;
};
// 使用 React cache 创建单例 Agent
const getAgent = cache(() => new Agent({
systemPrompt: `You are a helpful AI assistant with access to various tools.
You can help users with information retrieval, calculations, and other tasks.`,
}));
export async function sendMessage(message: string): Promise<Message> {
const agent = getAgent();
const response = await agent.chat(message);
return {
role: 'assistant',
content: response,
timestamp: Date.now(),
};
}
export async function clearChat(): Promise<void> {
const agent = getAgent();
agent.clearHistory();
}
export async function getChatHistory(): Promise<Message[]> {
const agent = getAgent();
return agent.getHistory();
}
3. 创建前端界面
// src/components/chat/ChatPanel.tsx
'use client';
import { useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Send, Trash2 } from 'lucide-react';
import { sendMessage, clearChat, getChatHistory } from '@/app/actions/chat';
import type { Message } from '@/app/actions/chat';
export function ChatPanel() {
const [input, setInput] = useState('');
const { data: messages = [], refetch } = useQuery({
queryKey: ['chat-history'],
queryFn: () => getChatHistory(),
});
const sendMessageMutation = useMutation({
mutationFn: sendMessage,
onSuccess: () => {
refetch();
},
});
const clearChatMutation = useMutation({
mutationFn: clearChat,
onSuccess: () => {
refetch();
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
const userMessage: Message = {
role: 'user',
content: input,
timestamp: Date.now(),
};
setInput('');
await sendMessageMutation.mutateAsync(input);
};
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold">AI Agent</h1>
<button
onClick={() => clearChatMutation.mutate()}
className="p-2 hover:bg-gray-100 rounded-lg"
title="Clear chat"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
</div>
))}
{sendMessageMutation.isPending && (
<div className="flex justify-start">
<div className="bg-gray-200 rounded-lg px-4 py-2">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={sendMessageMutation.isPending}
/>
<button
type="submit"
disabled={sendMessageMutation.isPending || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
<Send className="w-5 h-5" />
</button>
</form>
</div>
);
}
添加工具调用能力
1. 定义工具接口
// src/lib/agent/tools.ts
export interface Tool {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, any>;
required?: string[];
};
execute: (params: any) => Promise<any>;
}
export const availableTools: Tool[] = [
{
name: 'get_weather',
description: 'Get the current weather for a specific location',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'The city and state, e.g. San Francisco, CA',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'The temperature unit',
},
},
required: ['location'],
},
execute: async ({ location, unit = 'celsius' }) => {
// 模拟 API 调用
return {
location,
temperature: unit === 'celsius' ? 22 : 72,
condition: 'Partly cloudy',
humidity: 65,
};
},
},
{
name: 'search_web',
description: 'Search the web for information',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query',
},
num_results: {
type: 'number',
description: 'Number of results to return (default: 5)',
},
},
required: ['query'],
},
execute: async ({ query, num_results = 5 }) => {
// 实际项目中可以使用真实搜索 API
return {
query,
results: [
{ title: 'Example result 1', url: 'https://example.com/1' },
{ title: 'Example result 2', url: 'https://example.com/2' },
].slice(0, num_results),
};
},
},
{
name: 'calculate',
description: 'Perform mathematical calculations',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: 'Mathematical expression to evaluate',
},
},
required: ['expression'],
},
execute: async ({ expression }) => {
try {
// 安全的数学表达式求值
const result = Function('"use strict"; return (' + expression + ')')();
return { expression, result };
} catch (error) {
return { expression, error: 'Invalid expression' };
}
},
},
];
2. 增强 Agent 以支持工具调用
// src/lib/agent/core.ts (更新)
export class Agent {
// ... 之前的代码
async chat(userMessage: string): Promise<string> {
this.history.push({
role: 'user',
content: userMessage,
timestamp: Date.now(),
});
let maxIterations = 5;
let finalResponse = '';
for (let i = 0; i < maxIterations; i++) {
const response = await anthropic.messages.create({
model: this.config.model!,
max_tokens: this.config.maxTokens!,
temperature: this.config.temperature,
system: this.config.systemPrompt,
tools: availableTools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.parameters,
})),
messages: this.history.map(({ role, content }) => ({
role,
content,
})),
});
// 处理工具使用
const toolUseBlocks = response.content.filter(
block => block.type === 'tool_use'
) as Array<any>;
if (toolUseBlocks.length > 0) {
// 执行工具调用
for (const toolUse of toolUseBlocks) {
const tool = availableTools.find(
t => t.name === toolUse.name
);
if (tool) {
const result = await tool.execute(toolUse.input);
this.history.push({
role: 'assistant',
content: '',
timestamp: Date.now(),
});
this.history.push({
role: 'user',
content: JSON.stringify({
tool_use_id: toolUse.id,
result,
}),
timestamp: Date.now(),
});
}
}
} else {
// 获取最终响应
const textBlock = response.content.find(
block => block.type === 'text'
);
finalResponse = textBlock?.type === 'text' ? textBlock.text : '';
break;
}
}
this.history.push({
role: 'assistant',
content: finalResponse,
timestamp: Date.now(),
});
return finalResponse;
}
}
记忆与上下文管理
1. 使用 Zustand 管理状态
// src/store/chat-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
toolCalls?: any[];
}
interface ChatStore {
messages: ChatMessage[];
currentSessionId: string | null;
sessions: string[];
addMessage: (message: Omit<ChatMessage, 'id' | 'timestamp'>) => void;
clearMessages: () => void;
setSession: (sessionId: string) => void;
createNewSession: () => string;
}
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
messages: [],
currentSessionId: null,
sessions: [],
addMessage: (message) =>
set((state) => ({
messages: [
...state.messages,
{
...message,
id: crypto.randomUUID(),
timestamp: Date.now(),
},
],
})),
clearMessages: () => set({ messages: [] }),
setSession: (sessionId) =>
set({ currentSessionId: sessionId }),
createNewSession: () => {
const sessionId = crypto.randomUUID();
set((state) => ({
sessions: [...state.sessions, sessionId],
currentSessionId: sessionId,
messages: [],
}));
return sessionId;
},
}),
{
name: 'chat-storage',
}
)
);
2. 实现长期记忆
// src/lib/agent/memory.ts
export interface Memory {
id: string;
content: string;
importance: number; // 0-1
timestamp: number;
embedding?: number[];
}
export class MemoryManager {
private memories: Memory[] = [];
private maxMemories = 100;
async addMemory(content: string, importance = 0.5): Promise<void> {
const memory: Memory = {
id: crypto.randomUUID(),
content,
importance,
timestamp: Date.now(),
};
this.memories.push(memory);
// 按重要性和时间排序,保留最重要的记忆
this.memories.sort((a, b) => {
const scoreA = a.importance * 0.7 + (1 - a.timestamp / Date.now()) * 0.3;
const scoreB = b.importance * 0.7 + (1 - b.timestamp / Date.now()) * 0.3;
return scoreB - scoreA;
});
this.memories = this.memories.slice(0, this.maxMemories);
}
getRelevantMemories(query: string, limit = 5): Memory[] {
// 简单实现:基于关键词匹配
// 实际项目中可以使用向量数据库进行语义搜索
const queryLower = query.toLowerCase();
const keywords = queryLower.split(/\s+/);
return this.memories
.map(memory => ({
memory,
relevance: this.calculateRelevance(memory.content, keywords),
}))
.filter(({ relevance }) => relevance > 0)
.sort((a, b) => b.relevance - a.relevance)
.slice(0, limit)
.map(({ memory }) => memory);
}
private calculateRelevance(content: string, keywords: string[]): number {
const contentLower = content.toLowerCase();
const matches = keywords.filter(keyword =>
contentLower.includes(keyword)
).length;
return matches / keywords.length;
}
getAllMemories(): Memory[] {
return this.memories;
}
}
流式输出优化
// src/app/actions/stream-chat.ts
'use server';
import Anthropic from '@anthropic-ai/sdk';
import { availableTools } from '@/lib/agent/tools';
export async function* streamChat(
messages: Array<{ role: string; content: string }>
): AsyncGenerator<string, void, unknown> {
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
try {
const stream = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4096,
messages,
tools: availableTools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.parameters,
})),
stream: true,
});
for await (const event of stream) {
switch (event.type) {
case 'text_delta':
yield event.delta.text;
break;
case 'content_block_delta':
if (event.delta.type === 'tool_use') {
yield JSON.stringify({
type: 'tool_use',
name: event.delta.name,
});
}
break;
case 'message_stop':
yield '[DONE]';
break;
}
}
} catch (error) {
console.error('Streaming error:', error);
throw error;
}
}
// src/components/chat/StreamingChat.tsx
'use client';
import { useState, useRef, useEffect } from 'react';
export function StreamingChat() {
const [messages, setMessages] = useState<Array<{
role: string;
content: string;
}>>([]);
const [input, setInput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isStreaming) return;
const userMessage = { role: 'user', content: input };
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsStreaming(true);
// 添加空助手消息
setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
try {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [...messages, userMessage],
}),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error('No reader available');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
setIsStreaming(false);
continue;
}
try {
const json = JSON.parse(data);
if (json.content) {
setMessages(prev => {
const newMessages = [...prev];
newMessages[newMessages.length - 1].content += json.content;
return newMessages;
});
}
} catch (e) {
console.error('Parse error:', e);
}
}
}
}
} catch (error) {
console.error('Chat error:', error);
setIsStreaming(false);
}
};
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
{/* Messages display */}
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
{message.content || <span className="animate-pulse">▊</span>}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Input form */}
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isStreaming}
/>
<button
type="submit"
disabled={isStreaming || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300"
>
{isStreaming ? '...' : 'Send'}
</button>
</form>
</div>
);
}
部署与最佳实践
部署到 Vercel
# 安装 Vercel CLI
npm i -g vercel
# 部署
vercel
性能优化建议
- 流式响应:使用 Server-Sent Events 提升用户体验
- 请求缓存:对重复问题缓存响应结果
- 并发控制:限制同时进行的 API 请求数量
- 错误重试:实现指数退避重试机制
- 成本监控:跟踪 Token 使用量,设置预算告警
安全注意事项
- API Key 保护:永远不要在前端暴露 API Key
- 输入验证:严格验证用户输入,防止注入攻击
- 速率限制:防止 API 滥用
- 内容过滤:对敏感内容进行过滤
- 用户隔离:确保不同用户的数据完全隔离
总结
通过本教程,我们使用 Next.js 构建了一个功能完整的 AI Agent 应用。主要内容包括:
- ✅ 基础对话功能实现
- ✅ 工具调用能力
- ✅ 记忆管理系统
- ✅ 流式输出优化
- ✅ 状态管理最佳实践
下一步
- 添加更多工具(文件操作、数据库查询等)
- 实现多模态能力(图片、文档分析)
- 集成向量数据库实现语义搜索
- 添加用户认证和权限管理
- 实现多 Agent 协作系统
参考资源
希望这篇教程对你有所帮助!如果你在构建过程中遇到问题,欢迎查阅相关文档或寻求社区支持。祝你构建出优秀的 AI Agent 应用!