在开发时,你是不是也有这样的顾虑:
- 用 ChatGPT 处理公司文档,总担心数据泄露?
- 想在没有网络的环境下写代码或润色文章?
- 看着每个月的 API 账单觉得肉疼?
今天,我们要完成一个超酷的任务:拔掉网线,在自己的笔记本电脑上跑一个"私有版 ChatGPT" 。我们将使用 Ollama 部署大模型,并用 React 亲手写一个漂亮的聊天界面。
第一步:准备工作
在开始之前,我们需要两样核心东西:
-
AI 大脑(Ollama) :
通常大模型需要昂贵的服务器,但 Ollama 这个工具能把模型压缩,让它运行在你的笔记本里。
- 硬件建议:最好有 16GB 内存;如果是 Mac M 系列芯片体验最佳。
-
前端界面(React) :
用来和 AI 对话的窗口。我们将使用 React + Tailwind CSS。
安装 AI 大脑
-
去 Ollama 官网下载安装包。
-
安装完成后,打开终端(命令行),输入以下命令拉取 并运行 阿里开源的 Qwen2.5 (通义千问) 模型。它中文能力极佳,且体积小速度快:
Bashollama pull qwen2.5:0.5b ollama run qwen2.5:0.5b -
此时,Ollama 会在后台默默启动一个服务端口:
11434。这是我们后续代码连接的关键。
第二步:搭建"传声筒"
我们需要写一段代码,负责把网页上的文字传给后台的 Ollama。
在项目中创建 src/api/ollamaApi.js。我们使用 axios 来发送请求,这比原生 fetch 更方便管理。
JavaScript
import axios from 'axios';
// 1. 创建专线:直接连通本地的 Ollama 服务端口
const ollamaApi = axios.create({
baseURL: 'http://127.0.0.1:11434/v1', // Ollama 兼容 OpenAI 的接口格式
headers: {
'Authorization': 'Bearer ollama', // 格式上需要,实际本地不需要真 token
'Content-Type': 'application/json',
}
});
// 2. 发送消息的函数
export const chatCompletions = async (messages) => {
try {
const response = await ollamaApi.post('/chat/completions', {
model: 'qwen2.5:0.5b', // 必须确保你的 Ollama 里下载了这个模型
messages, // 把整个聊天历史发过去
stream: false, // 简化处理,让 AI 一次性把话说完
temperature: 0.7, // 控制 AI 的"创造力",0.7 比较平衡
});
// 取出 AI 回复的文本内容
return response.data.choices[0].message.content;
} catch(err) {
console.error('Ollama 请求失败', err);
throw err; // 把错误抛出去,让 UI 层展示给用户看
}
}
第三步:构建"记忆胶囊"
聊天应用最核心的逻辑是:记住聊了什么 和 知道现在的状态(是正在写,还是写完了)。
为了让代码整洁,我们将这些逻辑封装在一个自定义 Hook src/hooks/useLLM.js 中。
JavaScript
import { useState } from 'react';
import { chatCompletions } from '../api/ollamaApi.js';
export const useLLM = () => {
// 1. 初始化聊天记录,默认给一条 AI 的欢迎语
const [messages, setMessages] = useState([
{ role: 'user', content: '你好' },
{ role: 'assistant', content: '你好,我是 Qwen2.5 0.5b 模型' }
]);
// 2. 状态管理
const [loading, setLoading] = useState(false); // 是否正在思考
const [error, setError] = useState(null); // 是否报错
// 3. 核心动作:发送消息
const sendMessage = async (content) => {
// 如果内容为空或正在加载中,什么都不做
if (!content.trim() || loading) return;
setLoading(true);
setError(null);
// --- 关键点:乐观更新 (Optimistic UI) ---
// 在等待 AI 回复前,先把用户说的话显示在屏幕上,体验更好
const userMessage = { role: 'user', content };
const newHistory = [...messages, userMessage];
setMessages(newHistory);
try {
// --- 关键点:上下文传递 ---
// 必须把 newHistory (包含刚才用户说的话) 发给后端
// 否则 AI 记不住上一句说了什么
const assistantContent = await chatCompletions(newHistory);
// 收到回复后,追加到列表里
setMessages(prev => [
...prev,
{ role: 'assistant', content: assistantContent }
]);
} catch (err) {
setError('AI 暂时掉线了,请检查 Ollama 是否运行中');
} finally {
setLoading(false); // 无论成功失败,都要结束加载状态
}
};
// 4. 重置对话功能
const resetChat = () => {
setMessages([]);
};
// 把这些能力暴露给组件使用
return {
messages,
loading,
error,
sendMessage,
resetChat,
};
}
第四步:打造漂亮的界面
最后,我们需要一个像微信或 ChatGPT 一样的聊天窗口。在 App.jsx 中,我们需要解决两个重要的交互问题:
- 自动滚动:每次有新消息,窗口要自动滚到底部。
- 输入锁定:当 AI 正在思考时,锁住输入框,防止重复发送。
JavaScript
import { useEffect, useState, useRef } from 'react';
import { useLLM } from './hooks/useLLM.js';
export default function App() {
const [inputValue, setInputValue] = useState('');
// 引入我们刚才写的 Hook
const { messages, loading, error, sendMessage } = useLLM();
// 创建一个引用,用来定位聊天窗口的底部
const messagesEndRef = useRef(null);
// --- 自动滚动逻辑 ---
// 只要 messages 变了,就自动滚到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// --- 发送逻辑 ---
const handleSend = async (e) => {
e.preventDefault(); // 阻止表单刷新页面
if (!inputValue.trim()) return;
const text = inputValue;
setInputValue(''); // 立即清空输入框
await sendMessage(text);
}
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-6 px-4">
{/* 聊天主容器 */}
<div className="w-full max-w-[800px] bg-white rounded-lg shadow-md flex flex-col h-[90vh] max-h-[800px]">
{/* 顶部标题 */}
<div className="border-b p-4 text-center font-bold text-gray-700">
我的本地 AI 助手
</div>
{/* 1. 消息展示区 */}
<div className="flex-1 p-4 overflow-y-auto space-y-4 bg-gray-100">
{messages.map((msg, index) => {
const isUser = msg.role === 'user';
return (
<div key={index} className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
{/* 气泡样式:用户是蓝色,AI 是白色 */}
<div className={`max-w-[80%] px-4 py-2 rounded-lg shadow-sm ${
isUser
? 'bg-blue-600 text-white rounded-br-none'
: 'bg-white text-gray-800 border border-gray-200 rounded-bl-none'
}`}>
{msg.content}
</div>
</div>
)
})}
{/* Loading 动画提示 */}
{loading && (
<div className="flex justify-start">
<div className="bg-gray-200 px-4 py-2 rounded-lg text-sm text-gray-500 animate-pulse">
AI 正在思考...
</div>
</div>
)}
{/* 错误提示 */}
{error && <div className="text-red-500 text-center text-sm my-2">{error}</div>}
{/* 这是一个隐形的锚点,永远在列表最底部 */}
<div ref={messagesEndRef} />
</div>
{/* 2. 底部输入区 */}
<form className="p-4 border-t bg-white rounded-b-lg" onSubmit={handleSend}>
<div className="flex gap-2">
<input
type="text"
placeholder={loading ? "请等待 AI 回复..." : "输入消息...按回车发送"}
value={inputValue}
onChange={e => setInputValue(e.target.value)}
disabled={loading} // 思考时禁止输入
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition disabled:bg-gray-300 disabled:cursor-not-allowed"
disabled={loading || !inputValue.trim()}>
发送
</button>
</div>
</form>
</div>
</div>
)
}
总结
至此,你已经拥有了一个完全属于你的 AI 应用!
为什么这个项目很有价值?
- 数据安全:你的所有对话数据都留在了本地内存里,没有上传到任何云端服务器。
- 解耦架构 :我们采用了标准的 React Hook 模式。未来如果你想把后台换成 OpenAI 的付费 API,只需要改动
ollamaApi.js里的 URL,界面逻辑完全不用动。