從零開始部署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 ollamacreate Create a model from a Modelfileshow Show information for a modelrun Run a modelpull Pull a model from a registrypush Push a model to a registrylist List modelsps List running modelscp Copy a modelrm Remove a modelhelp Help about any commandFlags:-h, --help help for ollama-v, --version Show version informationUse "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 datetimeapp = 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};// 配置Markedmarked.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支持
五、安全注意事項
- 端口防護:
export OLLAMA_HOST=127.0.0.1 # 禁止外部訪問
netstat -an | grep 11434 # 驗證監聽地址
- 防火墻規則:
sudo ufw deny 11434/tcp # 禁用Ollama外部端口
六、擴展應用場景
- 私有知識庫:接入LangChain處理本地文檔
- 自動化腳本:通過API實現代碼生成/自動Debug
- 硬件控制:結合HomeAssistant實現語音智能家居
完整代碼已開源(其實沒有):GitHub倉庫地址
通過本教程,您已掌握從模型部署到應用開發的全流程。本地化AI部署不僅降低成本,更保障了數據隱私,為開發者提供了真正的「AI自由」!