
介绍 🧠💬
人工智能(AI)正在重塑我们的交互方式,现在构建自己的 AI 助手变得前所未有的简单。在本指南中,我将带你了解如何使用 Next.js 、TailwindCSS 和 Ollama 构建一个简单的 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?
✅ 隐私 -- 一切都在本地运行。没有数据离开你的设备。
✅ 速度 -- 调用模型时没有网络延迟。
✅ 成本效益 -- 没有令牌限制或月度订阅费用。