200字
使用 Next.js 搭建 AI Agent 应用完整指南
2025-10-23
2026-02-23

前言

AI Agent(人工智能代理)是当前最热门的技术趋势之一。它不仅能理解用户指令,还能自主规划、调用工具并执行复杂任务。Next.js 作为 React 生态中最流行的全栈框架,结合其 API Routes 和 Server Actions 特性,是构建 AI Agent 应用的理想选择。

本文将带你从零开始,使用 Next.js 搭建一个功能完整的 AI Agent 应用。

目录

  1. 什么是 AI Agent
  2. 技术栈选择
  3. 项目初始化
  4. Agent 核心架构
  5. 实现基础对话功能
  6. 添加工具调用能力
  7. 记忆与上下文管理
  8. 流式输出优化
  9. 部署与最佳实践

什么是 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

性能优化建议

  1. 流式响应:使用 Server-Sent Events 提升用户体验
  2. 请求缓存:对重复问题缓存响应结果
  3. 并发控制:限制同时进行的 API 请求数量
  4. 错误重试:实现指数退避重试机制
  5. 成本监控:跟踪 Token 使用量,设置预算告警

安全注意事项

  1. API Key 保护:永远不要在前端暴露 API Key
  2. 输入验证:严格验证用户输入,防止注入攻击
  3. 速率限制:防止 API 滥用
  4. 内容过滤:对敏感内容进行过滤
  5. 用户隔离:确保不同用户的数据完全隔离

总结

通过本教程,我们使用 Next.js 构建了一个功能完整的 AI Agent 应用。主要内容包括:

  • ✅ 基础对话功能实现
  • ✅ 工具调用能力
  • ✅ 记忆管理系统
  • ✅ 流式输出优化
  • ✅ 状态管理最佳实践

下一步

  • 添加更多工具(文件操作、数据库查询等)
  • 实现多模态能力(图片、文档分析)
  • 集成向量数据库实现语义搜索
  • 添加用户认证和权限管理
  • 实现多 Agent 协作系统

参考资源


希望这篇教程对你有所帮助!如果你在构建过程中遇到问题,欢迎查阅相关文档或寻求社区支持。祝你构建出优秀的 AI Agent 应用!

评论