当前位置: 首页 > news >正文

从零开始部署DeepSeek:基于Ollama+Flask的本地化AI对话系统

从零开始部署DeepSeek:基于Ollama+Flask的本地化AI对话系统


一、部署背景与工具选型

在AI大模型遍地开花的2025年,DeepSeek R1凭借其出色的推理能力和开源特性成为开发者首选。本文将以零基础视角,通过以下工具链实现本地化部署:

1.Ollama:轻量级模型管理工具,支持一键拉取、运行模型

Ollama 是一个功能强大的大语言模型管理端,专为下载、运行和调用大型语言模型(如DeepSeek)而设计。它提供了以下几个核心功能:

  • 模型下载:支持从官方仓库下载不同规模的模型。
  • 模型运行:通过API提供服务,让用户与模型进行交互。
  • 实时对话模拟:以流式方式展示模型回复。
2.DeepSeek : 平衡性能与资源消耗的中等规模模型

DeepSeek 是目标语言模型,可以根据您的硬件配置选择不同的模型规模(如7B、13B等)。它具有以下特点:

  • 可扩展性:根据内存和计算资源调整模型大小。
  • 高效训练:支持并行训练以加速模型收敛。
3.Python+Flask:构建Web交互界面,支持流式响应

Flask 是一个轻量级的Web框架,广泛应用于快速开发Web应用。结合Python,它为构建动态Web界面提供了灵活性:

  • API集成:通过Flask API调用Ollama服务。
  • 前端动态:使用JavaScript和HTML创建交互式对话框。
4.HTML+JavaScript:实现类ChatGPT的对话交互体验

HTML 是构建Web界面的基础语言,用于设计用户友好的对话框,并将其嵌入到Flask应用中。您可以通过以下方式集成:

  • 响应式布局:使用CSS样式表确保界面在不同设备上适配。
  • 动态内容展示:通过JavaScript更新页面内容,模拟模型回复,需要模拟打字机方式。

**备注说明**: 不想自己编程的话,可使用chatbox直接接入ollama

二、环境准备与模型部署

1. 安装Ollama

Windows/Mac用户
访问Ollama官网下载安装包,双击完成安装。 (github下载很慢,需要使用加速或从其他地方下载)

Linux用户

curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl start ollama
2. 下载DeepSeek模型

根据硬件配置选择模型(显存要求参考):

  • 基础版(1.5B):适合笔记本(8GB内存)
  • 标准版(7B):推荐配置(16GB内存+8GB显存)
  • 加强版(70B):需专业显卡(如RTX 5090)
# 拉取7B模型
ollama run deepseek-r1:7b

上述指令会完成下载和运行两个步骤(ollama支持断线续传功能,而且我发现速度慢下来的时候,手动停止然后再启动接着下载模型的话,速度会恢复上来),成功运行后可通过命令行直接向deepseek提问,如下:

另外,也可以在命令行手动执行ollama相关指令:

  • 无参数启动
C:\Users\arbboter\Desktop>ollama
Usage:
  ollama [flags]
  ollama [command]

Available Commands:
  serve       Start ollama
  create      Create a model from a Modelfile
  show        Show information for a model
  run         Run a model
  pull        Pull a model from a registry
  push        Push a model to a registry
  list        List models
  ps          List running models
  cp          Copy a model
  rm          Remove a model
  help        Help about any command

Flags:
  -h, --help      help for ollama
  -v, --version   Show version information

Use "ollama [command] --help" for more information about a command.
命令功能示例
ollama serve启动模型ollama serve
ollama pull下载模型ollama pull deepseek-r1:7b
ollama list查看本地模型ollama list
ollama rm删除模型ollama rm deepseek-r1:1.5b
ollama cp复制模型ollama cp deepseek-r1:7b my-backup

3.配置ollama

# 限制本地访问(默认配置)
export OLLAMA_HOST=127.0.0.1

# 允许局域网访问
export OLLAMA_HOST=0.0.0.0

# 启用调试
export OLLAMA_DEBUG=1

# 自定义模型路径(可能需重启机器生效)
export OLLAMA_MODELS=~\ollama\models

三、构建Web对话系统

备注说明:选型flask及代码编写基本是由deepseek完成,本人只负责提问和修缮
1. 项目结构
deepseek-web/
├── app.py          # Flask主程序
├── templates/
│   └── index.html  # 聊天界面
2. Flask后端实现(app.py)
# app.py
from flask import Flask, render_template, request, Response
import ollama
import logging
from datetime import datetime

app = Flask(__name__)

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('chat.log'),
        logging.StreamHandler()
    ]
)

def chat_ollama(user_message, stream):
    host = 'http://192.168.1.10:11434'
    cli = ollama.Client(host=host)
    response = cli.chat(
        model='deepseek-r1:7b',
        messages=[{'role': 'user', 'content': user_message}],
        stream=stream,
        # options={'temperature': 0.7}
    )
    return response

@app.route('/')
def index():
    return render_template('index.html')


@app.route('/api/chat', methods=['POST'])
def chat():
    """流式聊天接口"""
    def generate(user_message):
        try:
            app.logger.info(f"流式处理开始: {user_message[:50]}...")

            stream = chat_ollama(user_message, True)
            for chunk in stream:
                content = chunk['message']['content']
                if content.startswith('<think>'):
                    content = content.replace('<think>', '', 1)
                elif content.startswith('</think>'):
                    content = content.replace('</think>', '\n', 1)
                app.logger.debug(f"发送数据块: {content}")
                yield f"{content}"
                
            app.logger.info("流式处理完成")

        except Exception as e:
            app.logger.error(f"流式错误: {str(e)}")
            yield f"[ERROR] {str(e)}\n\n"

    return Response(generate(request.json['message']), mimetype='text/event-stream')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=True)
3. 前端实现(index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI 对话助手</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
    <style>
        :root {
            --primary-color: #10a37f;
            --bg-color: #f0f2f5;
        }
        body {
            margin: 0;
            padding: 20px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background-color: var(--bg-color);
            max-width: 800px;
            margin: 0 auto;
        }
        #chat-container {
            height: 70vh;
            overflow-y: auto;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            padding: 15px;
            background: white;
            margin-bottom: 20px;
        }
        .message {
            margin: 12px 0;
            display: flex;
            gap: 15px;
        }
        .user-message {
            justify-content: flex-end;
        }
        .message-content {
            max-width: 80%;
            padding: 12px 16px;
            border-radius: 8px;
        }
        .assistant-message .message-content {
            background: #f8f9fa;
            border: 1px solid #e0e0e0;
        }
        .user-message .message-content {
            background: var(--primary-color);
            color: white;
        }
        #input-container {
            display: flex;
            gap: 10px;
        }
        #user-input {
            flex: 1;
            padding: 12px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            resize: none;
            min-height: 44px;
        }
        button {
            background: var(--primary-color);
            color: white;
            border: none;
            padding: 0 20px;
            border-radius: 8px;
            cursor: pointer;
            transition: opacity 0.2s;
        }
        button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
        .typing-indicator {
            display: inline-block;
            padding: 8px 12px;
            background: #f8f9fa;
            border-radius: 8px;
            border: 1px solid #e0e0e0;
        }
        .dot {
            display: inline-block;
            width: 6px;
            height: 6px;
            margin-right: 3px;
            background: #ccc;
            border-radius: 50%;
            animation: bounce 1.4s infinite;
        }
        @keyframes bounce {
            0%, 80%, 100% { transform: translateY(0) }
            40% { transform: translateY(-6px) }
        }

        /* markdown基础样式 */
        .markdown-content {
            line-height: 1.6;
            transition: opacity 0.3s;
        }

        .markdown-content:not(.markdown-rendered) {
            opacity: 0.5;
        }

        .markdown-content h1 { font-size: 2em; margin: 0.67em 0; }
        .markdown-content h2 { font-size: 1.5em; margin: 0.83em 0; }
        .markdown-content pre { 
            background: #f5f5f5;
            padding: 1em;
            border-radius: 4px;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <div id="chat-container"></div>
    <div id="input-container">
        <textarea id="user-input" placeholder="输入消息..." rows="1"></textarea>
        <button id="send-btn" onclick="sendMessage()">发送</button>
    </div>

    <script>
        const chatContainer = document.getElementById('chat-container');
        const userInput = document.getElementById('user-input');
        const sendBtn = document.getElementById('send-btn');

        // 滚动到底部
        function scrollToBottom() {
            
        }

        function renderMarkdown(options = {}) {
            // 合并配置参数
            const config = {
                selector: options.selector || '.markdown-content',
                breaks: options.breaks ?? true,
                gfm: options.gfm ?? true,
                highlight: options.highlight || null
            };

            // 配置Marked
            marked.setOptions({
                breaks: config.breaks,
                gfm: config.gfm,
                highlight: config.highlight
            });

            // 渲染处理器
            const render = () => {
                document.querySelectorAll(config.selector).forEach(container => {
                    if (container.dataset.rendered) return;
                    
                    // 创建虚拟容器避免内容闪烁
                    const virtualDiv = document.createElement('div');
                    virtualDiv.style.display = 'none';
                    virtualDiv.innerHTML = container.innerHTML.trim();
                    
                    // 执行Markdown转换
                    container.innerHTML = marked.parse(virtualDiv.innerHTML);
                    container.dataset.rendered = true;
                    
                    // 添加加载动画
                    container.classList.add('markdown-rendered');
                });
            };

            // 自动执行渲染
            if (document.readyState === 'complete') {
                render();
            } else {
                document.addEventListener('DOMContentLoaded', render);
            }
        }

        // 添加用户消息
        function addUserMessage(content) {
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message user-message';
            messageDiv.innerHTML = `
                <div class="message-content">${content}</div>
            `;
            chatContainer.appendChild(messageDiv);
        }

        // 添加AI消息(流式)
        async function addAssistantMessageStream() {
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message assistant-message';
            messageDiv.innerHTML = `
                <div class="message-content markdown-content">
                    <div class="typing-indicator">
                        <span class="dot"></span>
                        <span class="dot" style="animation-delay: 0.2s"></span>
                        <span class="dot" style="animation-delay: 0.4s"></span>
                    </div>
                </div>
            `;
            chatContainer.appendChild(messageDiv);
            return messageDiv.querySelector('.message-content');
        }

        // 发送消息
        async function sendMessage() {
            const content = userInput.value.trim();
            if (!content) return;

            sendBtn.disabled = true;
            userInput.disabled = true;
            userInput.value = '';
            
            addUserMessage(content);
            const responseContainer = await addAssistantMessageStream();

            try {
                const response = await fetch('/api/chat', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        // 如果需要认证
                        // 'Authorization': 'Bearer YOUR_TOKEN'
                    },
                    body: JSON.stringify({ message: content })
                });

                if (!response.ok) throw new Error('请求失败');
                
                await this.createStreamTypewriter(response, responseContainer, {});
                this.scrollToBottom();
            } catch (error) {
                responseContainer.innerHTML = '❌ 请求出错: ' + error.message;
            } finally {
                sendBtn.disabled = false;
                userInput.disabled = false;
                userInput.focus();
            }
        }

        // 输入框事件处理
        userInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey) {
                e.preventDefault();
                sendMessage();
            } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
                userInput.value += '\n';
            }
        });

        async function createStreamTypewriter(stream, container, options = {}) {
                const config = {
                    baseSpeed: 50,
                    maxSpeedup: 3,
                    retryCount: 3,
                    ...options
                };
                this.reader = null;

                // 状态控制
                let isDestroyed = false;
                let cursorVisible = true;
                let renderQueue = [];
                let retryCounter = 0;

                // DOM元素初始化
                const cursor = document.createElement('span');
                cursor.className = 'typewriter-cursor';
                cursor.textContent = '▌';
                container.append(cursor);

                // 光标动画
                const cursorInterval = setInterval(() => {
                    cursor.style.opacity = cursorVisible ? 1 : 0;
                    cursorVisible = !cursorVisible;
                }, 600);

                // 核心渲染逻辑
                const renderEngine = () => {
                    if (renderQueue.length === 0 || isDestroyed) return;

                    // 动态调速算法
                    const speed = Math.max(
                        config.baseSpeed / config.maxSpeedup,
                        config.baseSpeed - renderQueue.length * 2
                    );

                    const fragment = document.createDocumentFragment();
                    while (renderQueue.length > 0) {
                        const char = renderQueue.shift();
                        fragment.append(document.createTextNode(char));
                    }

                    container.insertBefore(fragment, cursor);
                    setTimeout(() => requestAnimationFrame(renderEngine), speed);
                };

                // 流数据处理
                const processStream = async () => {
                    try {
                        this.reader = stream.body.getReader();
                        // Fetch模式处理
                        while (!isDestroyed) {
                            const { done, value } = await this.reader.read();
                            if (done) break;
                            renderQueue.push(...new TextDecoder().decode(value).split(''));
                            if (!renderQueue.length) continue;
                            requestAnimationFrame(renderEngine);
                        }
                    } catch (err) {
                        if (retryCounter++ < config.retryCount && !isDestroyed) {
                            processStream();
                        } else {
                            destroy();
                            throw new Error('Stream connection failed');
                        }
                    } finally {
                        container.innerHTML = marked.parse(container.textContent.replace('▌', ''));
                        destroy();
                    }
                };

                // 资源清理
                const destroy = () => {
                    if (isDestroyed) return;
                    isDestroyed = true;
                    clearInterval(cursorInterval);
                    cursor.remove();
                    if (stream.cancel) stream.cancel();
                    if (stream.close) stream.close();
                };

                // 启动引擎
                processStream();
                return { destroy };
            };
    </script>
</body>
</html>

四、启动与优化

1. 运行系统
flask run --port 5001

访问 http://localhost:5001 即可开始对话 ,如下:
主页面

2. 性能优化技巧
  • 量化加速:使用ollama run deepseek-r1:7b --quantize q4_0 减少显存占用
  • GPU加速:在Ollama配置中启用CUDA支持

五、安全注意事项

  1. 端口防护
export OLLAMA_HOST=127.0.0.1  # 禁止外部访问
netstat -an | grep 11434      # 验证监听地址
  1. 防火墙规则
sudo ufw deny 11434/tcp      # 禁用Ollama外部端口

六、扩展应用场景

  1. 私有知识库:接入LangChain处理本地文档
  2. 自动化脚本:通过API实现代码生成/自动Debug
  3. 硬件控制:结合HomeAssistant实现语音智能家居

完整代码已开源(其实没有):GitHub仓库地址
通过本教程,您已掌握从模型部署到应用开发的全流程。本地化AI部署不仅降低成本,更保障了数据隐私,为开发者提供了真正的「AI自由」!

相关文章:

  • Everything——你的文件搜索效率革命
  • 如何在 VS Code 中快速使用 Copilot 来辅助开发
  • Java高频面试之SE-22
  • 【LeetCode】560.和为K的子数组
  • 使用 MySQL 从 JSON 字符串提取数据
  • 内部知识库:安全协作驱动数字化转型新路径
  • 【Linux AnolisOS】关于Docker的一系列问题。尤其是拉取东西时的网络问题,镜像源问题。
  • 百达翡丽(Patek Philippe):瑞士制表的巅峰之作(中英双语)
  • C++和OpenGL实现3D游戏编程【总览】
  • spring cloud 微服务部署(2025年)第一章:Nacos、LoadBalancer、GateWay、Ribbon集成之Nacos部署
  • 鼠标悬浮到某个 <li> 元素时,将 hoverLiData 更新为当前 item 的 id
  • mac 本地安装deepseek
  • Flutter:动态表单(在不确定字段的情况下,生成动态表单)
  • Linux 进程控制(进程创建,进程等待)
  • 手机功耗BugReport字段含义介绍
  • 软件考研,选择华科还是科软?
  • Java与C语言中取模运算符%的区别对比
  • 如何使用 Ollama 和 Docker 设置 DeepSeek
  • Query String 传递 json 对象参数、map参数
  • 【设计模式】【结构型模式】组合模式(Composite)
  • 柬埔寨果农:期待柬埔寨榴莲走进中国市场
  • 著名心血管病学专家李国庆教授逝世,享年63岁
  • 350种咖啡主题图书集结上海,20家参展书店买书送咖啡
  • 特朗普促卡塔尔说服伊朗放弃核计划,伊朗总统:你来吓唬我们?
  • 陕西宁强县委书记李宽任汉中市副市长
  • 首次采用“顶置主星+侧挂从星”布局,长二丁“1箭12星”发射成功