零成本、全隐私:基于 React + Ollama 打造你的专属本地 AI 助手

在开发时,你是不是也有这样的顾虑:

  • 用 ChatGPT 处理公司文档,总担心数据泄露
  • 想在没有网络的环境下写代码或润色文章?
  • 看着每个月的 API 账单觉得肉疼

今天,我们要完成一个超酷的任务:拔掉网线,在自己的笔记本电脑上跑一个"私有版 ChatGPT" 。我们将使用 Ollama 部署大模型,并用 React 亲手写一个漂亮的聊天界面。

第一步:准备工作

在开始之前,我们需要两样核心东西:

  1. AI 大脑(Ollama)

    通常大模型需要昂贵的服务器,但 Ollama 这个工具能把模型压缩,让它运行在你的笔记本里。

    • 硬件建议:最好有 16GB 内存;如果是 Mac M 系列芯片体验最佳。
  2. 前端界面(React)

    用来和 AI 对话的窗口。我们将使用 React + Tailwind CSS。

安装 AI 大脑

  1. 去 Ollama 官网下载安装包。

  2. 安装完成后,打开终端(命令行),输入以下命令拉取运行 阿里开源的 Qwen2.5 (通义千问) 模型。它中文能力极佳,且体积小速度快:

    Bash 复制代码
    ollama pull qwen2.5:0.5b 
    ollama run qwen2.5:0.5b
  3. 此时,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 中,我们需要解决两个重要的交互问题:

  1. 自动滚动:每次有新消息,窗口要自动滚到底部。
  2. 输入锁定:当 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 应用!

为什么这个项目很有价值?

  1. 数据安全:你的所有对话数据都留在了本地内存里,没有上传到任何云端服务器。
  2. 解耦架构 :我们采用了标准的 React Hook 模式。未来如果你想把后台换成 OpenAI 的付费 API,只需要改动 ollamaApi.js 里的 URL,界面逻辑完全不用动。
相关推荐
阳艳讲ai2 小时前
九尾狐AI智能矩阵:重构企业获客新引擎
大数据·人工智能
Liue612312312 小时前
窗帘检测与识别_YOLOv26模型详解与应用_1
人工智能·yolo·目标跟踪
啊巴矲2 小时前
小白从零开始勇闯人工智能:计算机视觉初级篇(OpenCV进阶操作(下))
人工智能·opencv·计算机视觉
wuhen_n2 小时前
@types 包的工作原理与最佳实践
前端·javascript·typescript
我是伪码农2 小时前
Vue 1.27
前端·javascript·vue.js
秋名山大前端2 小时前
前端大规模 3D 轨迹数据可视化系统的性能优化实践
前端·3d·性能优化
玄同7652 小时前
SQLAlchemy 会话管理终极指南:close、commit、refresh、rollback 的正确打开方式
数据库·人工智能·python·sql·postgresql·自然语言处理·知识图谱
萤丰信息2 小时前
四大核心技术领航,智慧园区重构产业生态新范式
java·大数据·人工智能·智慧城市·智慧园区
言無咎2 小时前
从人工失误到AI精准:财务机器人如何重构企业财务数据体系
人工智能·重构·机器人
H7998742422 小时前
2026动态捕捉推荐:8款专业产品全方位测评
大数据·前端·人工智能