使用 Ollama 和 Next.js 构建 AI 助手

介绍 🧠💬

人工智能(AI)正在重塑我们的交互方式,现在构建自己的 AI 助手变得前所未有的简单。在本指南中,我将带你了解如何使用 Next.jsTailwindCSSOllama 构建一个简单的 AI 助手。

注意: 你可以从 ollama.com/models 上运行任何模型;但是,运行 7B 模型至少需要 8 GB 的 RAM,运行 13B 模型需要 16 GB,运行 33B 模型则需要 32 GB。

无论你是初学者,还是仅仅在寻找一种注重隐私的 AI 实现方式,你都会觉得这份指南通俗易懂。

🧰 使用的工具

在我们开始之前,先来了解一下这个项目使用的工具:

  • Next.js -- 我们选择的 React 框架,支持应用路由。
  • TailwindCSS -- 用于样式设计。
  • Cursor IDE -- 一个为 AI 辅助开发量身打造的现代编码环境。
  • Ollama-- 一种在本地运行大模型的简单方式。
  • Gemma 3:1B 模型 -- 一个轻量级模型,适合在大多数现代笔记本电脑上运行。

📦 第一步:设置你的 Next.js 应用

首先,让我们创建一个新的 Next.js 项目。打开终端并运行以下命令:

sql 复制代码
npx create-next-app@latest ollama-assistant --app
cd ollama-assistant
cursor .

安装 Next.js 后,你就可以设置 Tailwind、TypeScript 以及其他配置。

然后,使用以下命令运行应用:

arduino 复制代码
npm run dev

🤖 第二步:安装并运行带有 Gemma 的 Ollama

Ollama 让在本地运行模型变得非常简单。前往 ollama.com/download 并为你的操作系统安装它。

安装完成后,打开终端并运行以下命令:

arduino 复制代码
ollama run gemma3:1b

这将下载并启动 Gemma 3:1B 模型。下载完成后,模型将在终端中启动,如下图所示。

⚙️ 第三步:将你的应用连接到 Ollama

接下来,我们将添加一个简单的 API 路由,与本地 Ollama 服务器进行通信。Ollama 提供了 REST API 端点,允许你与模型进行交互。一旦终端运行起来,它会在 http://localhost:11434/api 暴露一个端口,你可以在该端口发送 HTTP 请求。

ts 复制代码
app/api/chat/route.ts

import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  try {
    const { message } = await req.json();

    // 向 Ollama API 发送请求
    const response = await fetch('http://localhost:11434/api/generate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'gemma3:1b',
        prompt: message,
        stream: false,
      }),
    });

    const data = await response.json();

    return NextResponse.json({
      response: data.response,
    });
  } catch (error) {
    console.error('Error:', error);
    return NextResponse.json(
      { error: 'Failed to process the request' },
      { status: 500 }
    );
  }
}

🖼️ 第四步:构建聊天界面

让我们创建一个简单的用户界面,用户可以在其中输入内容并从我们的助手中获取回复。

这些文件将位于名为 chat 的模块文件夹中。

tsx 复制代码
ChatInput.tsx

import React, { useState } from 'react';

interface ChatInputProps {
  onSendMessage: (message: string) => void;
}

const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage }) => {
  const [message, setMessage] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim()) {
      onSendMessage(message);
      setMessage('');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="border-t border-gray-200 dark:border-gray-700 p-4">
      <div className="flex items-center gap-2">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type your message..."
          className="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 p-2
          bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
          />
        <button
          type="submit"
          className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700
          transition-colors duration-200"
          >
          Send
        </button>
      </div>
    </form>
  );
};

export default ChatInput;
tsx 复制代码
ChatMessage.tsx

import React from 'react';

interface ChatMessageProps {
  message: string;
  isUser: boolean;
}

const ChatMessage: React.FC<ChatMessageProps> = ({ message, isUser }) => {
  return (
    <div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`}>
      <div
        className={`${
          isUser
          ? 'bg-blue-600 text-white rounded-l-lg rounded-tr-lg'
          : 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-r-lg rounded-tl-lg'
        } px-4 py-2 max-w-[80%]`}
        >
        <p className="text-sm">{message}</p>
      </div>
    </div>
  );
};

export default ChatMessage;
tsx 复制代码
ChatPage.tsx

"use client"
import React, { useEffect, useRef, useState } from 'react';
import ChatInput from './ChatInput';
import ChatMessage from './ChatMessage';

interface Message {
  text: string;
  isUser: boolean;
}

const ChatPage: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const handleSendMessage = async (message: string) => {
    // 添加用户消息
    setMessages(prev => [...prev, { text: message, isUser: true }]);
    setIsLoading(true);

    try {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message }),
      });

      const data = await response.json();

      // 添加 AI 回复
      setMessages(prev => [...prev, { text: data.response, isUser: false }]);
    } catch (error) {
      console.error('Error:', error);
      setMessages(prev => [...prev, { text: 'Sorry, there was an error processing your request.', isUser: false }]);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto">
      <div className="bg-white dark:bg-gray-800 shadow-lg rounded-lg m-4 flex-1 flex flex-col overflow-hidden">
        <div className="p-4 border-b border-gray-200 dark:border-gray-700">
          <h1 className="text-xl font-semibold text-gray-800 dark:text-white">AI Assistant</h1>
        </div>
        <div className="flex-1 overflow-y-auto p-4">
          {messages.map((msg, index) => (
      <ChatMessage key={index} message={msg.text} isUser={msg.isUser} />
    ))}
          {isLoading && (
            <div className="flex justify-start mb-4">
              <div className="bg-gray-200 dark:bg-gray-700 rounded-lg px-4 py-2">
                <div className="animate-pulse flex space-x-2">
                  <div className="w-2 h-2 bg-gray-400 rounded-full"></div>
                  <div className="w-2 h-2 bg-gray-400 rounded-full"></div>
                  <div className="w-2 h-2 bg-gray-400 rounded-full"></div>
                </div>
              </div>
            </div>
          )}
          <div ref={messagesEndRef} />
        </div>
        <ChatInput onSendMessage={handleSendMessage} />
      </div>
    </div>
  );
};

export default ChatPage;

然后,在应用文件夹中的 page.tsx 文件如下所示:

tsx 复制代码
import ChatPage from '@/modules/ChatPage';

export default function Chat() {
  return <ChatPage />;
}

在终端中运行 npm run dev 后,用户界面将如下图所示:

****

🧠 工作原理

简要总结:

  • 你在文本区域输入消息。
  • 应用将你的消息发送到 /api/chat 接口。
  • 该端点将其转发到 Ollama 的本地 API(localhost:11434)。
  • Ollama 返回模型的回复。
  • 前端立即显示 AI 回复。

💡 为什么使用 Ollama?

✅ 隐私 -- 一切都在本地运行。没有数据离开你的设备。

✅ 速度 -- 调用模型时没有网络延迟。

✅ 成本效益 -- 没有令牌限制或月度订阅费用。

dev.to/abayomijohn...

相关推荐
excel2 小时前
Nginx 与 Node.js(PM2)的对比优势及 HTTPS 自动续签配置详解
后端
bobz9653 小时前
vxlan 为什么一定要封装在 udp 报文里?
后端
bobz9654 小时前
vxlan 直接使用 ip 层封装是否可以?
后端
郑道5 小时前
Docker 在 macOS 下的安装与 Gitea 部署经验总结
后端
3Katrina5 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
汪子熙5 小时前
HSQLDB 数据库锁获取失败深度解析
数据库·后端
高松燈6 小时前
若伊项目学习 后端分页源码分析
后端·架构
没逻辑6 小时前
主流消息队列模型与选型对比(RabbitMQ / Kafka / RocketMQ)
后端·消息队列
倚栏听风雨7 小时前
SwingUtilities.invokeLater 详解
后端
Java中文社群7 小时前
AI实战:一键生成数字人视频!
java·人工智能·后端