欢迎光临
我们一直在努力

DeepSeek-R1-Distill-Qwen-7B部署全栈指南:Ollama + FastAPI + Vue 构建前端交互界面

DeepSeek-R1-Distill-Qwen-7B部署全栈指南:Ollama + FastAPI + Vue 构建前端交互界面

你是不是已经用Ollama部署了DeepSeek-R1-Distill-Qwen-7B,但每次都要在命令行里敲代码才能用?或者想把这个强大的推理模型分享给团队其他成员,但他们看到命令行就头疼?

今天我就带你从零开始,搭建一个完整的全栈应用,把DeepSeek-R1-Distill-Qwen-7B包装成一个漂亮的Web界面。不用懂太多前端知识,跟着我做就行,一个小时就能搞定。

1. 项目整体架构

我们先来看看要搭建的这个系统长什么样。简单说,就是三层结构:

用户浏览器(Vue前端) ←→ FastAPI后端服务 ←→ Ollama本地模型

前端用Vue.js,做个漂亮的聊天界面,用户在这里输入问题、看到回答。

后端用FastAPI,负责接收前端请求,然后去调用Ollama的API,再把结果返回给前端。

模型层就是你已经部署好的DeepSeek-R1-Distill-Qwen-7B,通过Ollama来管理。

听起来复杂吗?其实每一步都很简单。我保证,就算你之前没怎么写过前端代码,也能跟着做出来。

1.1 你需要准备什么

在开始之前,确保你已经有这些环境:

  • Ollama已经安装并运行,DeepSeek-R1-Distill-Qwen-7B模型已经拉取完成
  • Python 3.8+ 环境(用来运行FastAPI后端)
  • Node.js 16+ 环境(用来运行Vue前端)
  • 一个代码编辑器(VS Code、PyCharm都行)
  • 基本的命令行操作知识

如果你还没安装Ollama或者拉取模型,先花几分钟搞定:

# 安装Ollama(如果你还没装)
curl -fsSL https://ollama.com/install.sh | sh

# 拉取DeepSeek-R1-Distill-Qwen-7B模型
ollama pull deepseek-r1-distill-qwen:7b

好了,准备工作完成,我们开始搭建。

2. 后端服务:用FastAPI连接Ollama

后端是整个系统的桥梁,它要做三件事:

  • 接收前端发来的问题
  • 把问题传给Ollama模型
  • 把模型的回答返回给前端
  • 2.1 创建后端项目

    先找个地方创建项目文件夹,我习惯在桌面或者文档里建:

    # 创建项目文件夹
    mkdir deepseek-web-app
    cd deepseek-web-app

    # 创建后端文件夹
    mkdir backend
    cd backend

    现在创建Python虚拟环境,这样不会搞乱你系统里的其他Python项目:

    # 创建虚拟环境
    python -m venv venv

    # 激活虚拟环境
    # Windows系统用这个:
    venv\\Scripts\\activate
    # Mac/Linux系统用这个:
    source venv/bin/activate

    激活后,命令行前面会出现(venv)字样,说明虚拟环境生效了。

    2.2 安装必要的包

    我们需要两个主要的包:FastAPI用来创建Web服务,requests用来调用Ollama的API:

    pip install fastapi uvicorn requests

    uvicorn是ASGI服务器,用来运行FastAPI应用。requests是用来发送HTTP请求的库。

    2.3 编写后端代码

    在backend文件夹里创建一个main.py文件,把下面的代码复制进去:

    from fastapi import FastAPI, HTTPException
    from fastapi.middleware.cors import CORSMiddleware
    from pydantic import BaseModel
    import requests
    import json

    # 创建FastAPI应用
    app = FastAPI(
    title="DeepSeek-R1-Distill-Qwen-7B API",
    description="基于Ollama的DeepSeek模型Web接口",
    version="1.0.0"
    )

    # 配置CORS(跨域资源共享)
    # 这样前端才能访问后端
    app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"], # 开发时可以用*,生产环境要改成具体的前端地址
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    )

    # 定义请求和响应的数据结构
    class ChatRequest(BaseModel):
    """聊天请求的数据结构"""
    message: str # 用户输入的消息
    model: str = "deepseek-r1-distill-qwen:7b" # 默认使用这个模型
    stream: bool = False # 是否使用流式响应

    class ChatResponse(BaseModel):
    """聊天响应的数据结构"""
    response: str # 模型的回复
    model: str # 使用的模型名称
    time_taken: float # 处理时间(秒)

    # Ollama API的基础URL
    OLLAMA_BASE_URL = "http://localhost:11434"

    @app.get("/")
    async def root():
    """根路径,返回服务状态"""
    return {
    "status": "running",
    "service": "DeepSeek-R1-Distill-Qwen-7B API",
    "version": "1.0.0"
    }

    @app.get("/api/models")
    async def get_models():
    """获取可用的模型列表"""
    try:
    response = requests.get(f"{OLLAMA_BASE_URL}/api/tags")
    if response.status_code == 200:
    return response.json()
    else:
    raise HTTPException(status_code=500, detail="无法获取模型列表")
    except requests.exceptions.ConnectionError:
    raise HTTPException(status_code=503, detail="Ollama服务未启动")

    @app.post("/api/chat", response_model=ChatResponse)
    async def chat(chat_request: ChatRequest):
    """处理聊天请求"""
    try:
    # 构建请求Ollama的数据
    ollama_data = {
    "model": chat_request.model,
    "prompt": chat_request.message,
    "stream": chat_request.stream
    }

    # 记录开始时间
    import time
    start_time = time.time()

    # 发送请求到Ollama
    response = requests.post(
    f"{OLLAMA_BASE_URL}/api/generate",
    json=ollama_data,
    timeout=300 # 5分钟超时
    )

    # 计算处理时间
    end_time = time.time()
    time_taken = round(end_time – start_time, 2)

    if response.status_code == 200:
    result = response.json()
    return ChatResponse(
    response=result.get("response", ""),
    model=chat_request.model,
    time_taken=time_taken
    )
    else:
    raise HTTPException(
    status_code=response.status_code,
    detail=f"模型请求失败: {response.text}"
    )

    except requests.exceptions.Timeout:
    raise HTTPException(status_code=504, detail="请求超时,请重试")
    except requests.exceptions.ConnectionError:
    raise HTTPException(status_code=503, detail="无法连接到Ollama服务")
    except Exception as e:
    raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")

    @app.get("/api/health")
    async def health_check():
    """健康检查端点"""
    try:
    # 检查Ollama服务是否可用
    response = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5)
    if response.status_code == 200:
    return {"status": "healthy", "ollama": "running"}
    else:
    return {"status": "degraded", "ollama": "unavailable"}
    except:
    return {"status": "unhealthy", "ollama": "down"}

    if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

    这段代码做了几件重要的事:

  • 创建了FastAPI应用,并配置了CORS,这样前端才能访问
  • 定义了数据模型,确保前后端数据格式一致
  • 实现了几个关键接口:
    • /:根路径,检查服务状态
    • /api/models:获取可用的模型列表
    • /api/chat:核心的聊天接口
    • /api/health:健康检查
  • 2.4 启动后端服务

    保存文件后,在命令行里运行:

    python main.py

    你会看到类似这样的输出:

    INFO: Started server process [12345]
    INFO: Waiting for application startup.
    INFO: Application startup complete.
    INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

    现在打开浏览器,访问 http://localhost:8000/docs,你会看到自动生成的API文档页面。这说明后端服务已经正常运行了。

    重要提示:确保Ollama服务也在运行。打开另一个命令行窗口,运行:

    ollama serve

    保持这个窗口打开,不要关闭。

    3. 前端界面:用Vue创建聊天应用

    后端搞定了,现在来做个漂亮的前端界面。我用Vue 3 + Element Plus,这两个组合用起来特别顺手。

    3.1 创建Vue项目

    回到项目根目录(deepseek-web-app),创建前端项目:

    # 回到项目根目录
    cd ..

    # 使用Vite创建Vue项目
    npm create vue@latest frontend

    创建过程中会问一些问题,按下面这样选择:

    ✔ Project name: … frontend
    ✔ Add TypeScript? … No
    ✔ Add JSX Support? … No
    ✔ Add Vue Router for Single Page Application development? … No
    ✔ Add Pinia for state management? … No
    ✔ Add Vitest for Unit Testing? … No
    ✔ Add an End-to-End Testing Solution? … No
    ✔ Add ESLint for code quality? … No

    创建完成后,进入前端目录并安装Element Plus:

    cd frontend
    npm install element-plus @element-plus/icons-vue
    npm install axios # 用于HTTP请求

    3.2 编写前端代码

    我们需要修改几个文件。先打开src/App.vue,把里面的内容全部替换:

    <template>
    <div id="app">
    <div class="app-container">
    <!– 顶部导航栏 –>
    <el-header class="app-header">
    <div class="header-content">
    <div class="logo-section">
    <el-icon size="28" color="#409EFF"><ChatLineRound /></el-icon>
    <h1>DeepSeek-R1-Distill-Qwen-7B</h1>
    </div>
    <div class="status-section">
    <el-tag :type="statusType" size="large">
    <el-icon><Connection /></el-icon>
    {{ statusText }}
    </el-tag>
    <el-button
    type="primary"
    :icon="Refresh"
    @click="checkHealth"
    :loading="checkingHealth"
    >
    检查状态
    </el-button>
    </div>
    </div>
    </el-header>

    <!– 主内容区 –>
    <el-main class="app-main">
    <div class="chat-container">
    <!– 左侧模型信息 –>
    <el-aside width="300px" class="model-info">
    <el-card class="model-card">
    <template #header>
    <div class="card-header">
    <span>模型信息</span>
    </div>
    </template>

    <div class="model-details">
    <div class="model-item">
    <el-icon><Cpu /></el-icon>
    <span class="label">模型名称:</span>
    <span class="value">{{ currentModel }}</span>
    </div>

    <div class="model-item">
    <el-icon><DataAnalysis /></el-icon>
    <span class="label">模型大小:</span>
    <span class="value">7B参数</span>
    </div>

    <div class="model-item">
    <el-icon><Star /></el-icon>
    <span class="label">模型特点:</span>
    <span class="value">推理增强型</span>
    </div>

    <div class="model-item">
    <el-icon><Clock /></el-icon>
    <span class="label">响应时间:</span>
    <span class="value">{{ lastResponseTime }}秒</span>
    </div>
    </div>

    <div class="model-controls">
    <el-select
    v-model="currentModel"
    placeholder="选择模型"
    class="model-select"
    >
    <el-option
    v-for="model in availableModels"
    :key="model.name"
    :label="model.name"
    :value="model.name"
    />
    </el-select>

    <el-button
    type="primary"
    @click="loadModels"
    :loading="loadingModels"
    class="refresh-btn"
    >
    <el-icon><Refresh /></el-icon>
    刷新模型
    </el-button>
    </div>
    </el-card>

    <el-card class="usage-card">
    <template #header>
    <div class="card-header">
    <span>使用说明</span>
    </div>
    </template>

    <div class="usage-content">
    <p>1. 在下方输入框输入问题</p>
    <p>2. 按Enter或点击发送按钮</p>
    <p>3. 等待模型生成回答</p>
    <p>4. 对话历史会自动保存</p>
    <p>5. 可以随时切换模型</p>
    </div>
    </el-card>
    </el-aside>

    <!– 右侧聊天区域 –>
    <el-main class="chat-main">
    <el-card class="chat-card">
    <!– 聊天消息区域 –>
    <div class="messages-container" ref="messagesContainer">
    <div
    v-for="(message, index) in messages"
    :key="index"
    :class="['message', message.type]"
    >
    <div class="message-header">
    <el-avatar :size="32" :class="message.type">
    <el-icon v-if="message.type === 'user'"><User /></el-icon>
    <el-icon v-if="message.type === 'assistant'"><Robot /></el-icon>
    </el-avatar>
    <div class="message-info">
    <span class="sender">{{ message.type === 'user' ? '你' : 'DeepSeek' }}</span>
    <span class="time">{{ formatTime(message.timestamp) }}</span>
    </div>
    </div>
    <div class="message-content">
    <pre>{{ message.content }}</pre>
    </div>
    </div>

    <!– 加载指示器 –>
    <div v-if="loading" class="loading-indicator">
    <el-icon class="loading-icon"><Loading /></el-icon>
    <span>DeepSeek正在思考中…</span>
    </div>
    </div>

    <!– 输入区域 –>
    <div class="input-area">
    <el-input
    v-model="inputMessage"
    type="textarea"
    :rows="3"
    placeholder="输入您的问题…(按Ctrl+Enter发送)"
    @keydown.ctrl.enter="sendMessage"
    :disabled="loading"
    class="message-input"
    />

    <div class="input-controls">
    <div class="input-tips">
    <el-tag size="small" type="info">支持代码、数学、推理问题</el-tag>
    <el-tag size="small" type="info">按Ctrl+Enter快速发送</el-tag>
    </div>

    <div class="input-actions">
    <el-button
    @click="clearChat"
    :disabled="messages.length === 0 || loading"
    >
    <el-icon><Delete /></el-icon>
    清空对话
    </el-button>

    <el-button
    type="primary"
    @click="sendMessage"
    :loading="loading"
    :disabled="!inputMessage.trim()"
    >
    <el-icon><Promotion /></el-icon>
    发送消息
    </el-button>
    </div>
    </div>
    </div>
    </el-card>
    </el-main>
    </div>
    </el-main>

    <!– 底部信息 –>
    <el-footer class="app-footer">
    <div class="footer-content">
    <span>基于 Ollama + FastAPI + Vue 构建</span>
    <span>模型:DeepSeek-R1-Distill-Qwen-7B</span>
    <span>服务状态:{{ serviceStatus }}</span>
    </div>
    </el-footer>
    </div>
    </div>
    </template>

    <script setup>
    import { ref, onMounted, nextTick, computed } from 'vue'
    import axios from 'axios'
    import {
    ChatLineRound,
    Connection,
    Refresh,
    Cpu,
    DataAnalysis,
    Star,
    Clock,
    User,
    Robot,
    Loading,
    Delete,
    Promotion
    } from '@element-plus/icons-vue'

    // 响应式数据
    const inputMessage = ref('')
    const messages = ref([])
    const loading = ref(false)
    const currentModel = ref('deepseek-r1-distill-qwen:7b')
    const availableModels = ref([])
    const loadingModels = ref(false)
    const checkingHealth = ref(false)
    const lastResponseTime = ref(0)
    const messagesContainer = ref(null)

    // 计算属性
    const statusType = computed(() => {
    return serviceStatus.value === '运行正常' ? 'success' : 'danger'
    })

    const statusText = computed(() => {
    return serviceStatus.value === '运行正常' ? '服务正常' : '服务异常'
    })

    const serviceStatus = ref('检查中…')

    // API基础URL
    const API_BASE_URL = 'http://localhost:8000'

    // 初始化
    onMounted(() => {
    checkHealth()
    loadModels()
    loadChatHistory()
    })

    // 检查服务健康状态
    const checkHealth = async () => {
    checkingHealth.value = true
    try {
    const response = await axios.get(`${API_BASE_URL}/api/health`)
    if (response.data.status === 'healthy') {
    serviceStatus.value = '运行正常'
    ElMessage.success('服务连接正常')
    } else {
    serviceStatus.value = '服务异常'
    ElMessage.warning('服务状态异常')
    }
    } catch (error) {
    serviceStatus.value = '连接失败'
    ElMessage.error('无法连接到后端服务')
    console.error('健康检查失败:', error)
    } finally {
    checkingHealth.value = false
    }
    }

    // 加载可用模型
    const loadModels = async () => {
    loadingModels.value = true
    try {
    const response = await axios.get(`${API_BASE_URL}/api/models`)
    if (response.data.models) {
    availableModels.value = response.data.models
    ElMessage.success(`加载了 ${availableModels.value.length} 个模型`)
    }
    } catch (error) {
    ElMessage.error('加载模型列表失败')
    console.error('加载模型失败:', error)
    } finally {
    loadingModels.value = false
    }
    }

    // 发送消息
    const sendMessage = async () => {
    const message = inputMessage.value.trim()
    if (!message || loading.value) return

    // 添加用户消息
    const userMessage = {
    type: 'user',
    content: message,
    timestamp: new Date()
    }
    messages.value.push(userMessage)

    // 清空输入框
    inputMessage.value = ''

    // 滚动到底部
    scrollToBottom()

    // 发送到后端
    loading.value = true
    try {
    const response = await axios.post(`${API_BASE_URL}/api/chat`, {
    message: message,
    model: currentModel.value,
    stream: false
    })

    // 添加助手回复
    const assistantMessage = {
    type: 'assistant',
    content: response.data.response,
    timestamp: new Date()
    }
    messages.value.push(assistantMessage)

    // 更新响应时间
    lastResponseTime.value = response.data.time_taken

    // 保存聊天记录
    saveChatHistory()

    ElMessage.success(`响应时间: ${response.data.time_taken}秒`)
    } catch (error) {
    console.error('发送消息失败:', error)
    ElMessage.error('发送消息失败,请检查服务状态')

    // 添加错误消息
    const errorMessage = {
    type: 'assistant',
    content: '抱歉,我暂时无法回答这个问题。请检查服务是否正常运行。',
    timestamp: new Date()
    }
    messages.value.push(errorMessage)
    } finally {
    loading.value = false
    scrollToBottom()
    }
    }

    // 清空聊天记录
    const clearChat = () => {
    ElMessageBox.confirm(
    '确定要清空所有聊天记录吗?',
    '确认清空',
    {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
    }
    ).then(() => {
    messages.value = []
    localStorage.removeItem('deepseek_chat_history')
    ElMessage.success('聊天记录已清空')
    }).catch(() => {
    // 用户取消
    })
    }

    // 保存聊天记录到本地存储
    const saveChatHistory = () => {
    const history = {
    messages: messages.value,
    model: currentModel.value,
    timestamp: new Date().toISOString()
    }
    localStorage.setItem('deepseek_chat_history', JSON.stringify(history))
    }

    // 加载聊天记录
    const loadChatHistory = () => {
    const history = localStorage.getItem('deepseek_chat_history')
    if (history) {
    try {
    const data = JSON.parse(history)
    messages.value = data.messages || []
    currentModel.value = data.model || 'deepseek-r1-distill-qwen:7b'
    } catch (error) {
    console.error('加载聊天记录失败:', error)
    }
    }
    }

    // 滚动到底部
    const scrollToBottom = () => {
    nextTick(() => {
    if (messagesContainer.value) {
    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
    })
    }

    // 格式化时间
    const formatTime = (date) => {
    if (!date) return ''
    const d = new Date(date)
    return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`
    }
    </script>

    <style scoped>
    .app-container {
    height: 100vh;
    display: flex;
    flex-direction: column;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }

    .app-header {
    background: rgba(255, 255, 255, 0.95);
    border-bottom: 1px solid #e4e7ed;
    padding: 0 20px;
    height: 60px;
    display: flex;
    align-items: center;
    }

    .header-content {
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    }

    .logo-section {
    display: flex;
    align-items: center;
    gap: 12px;
    }

    .logo-section h1 {
    margin: 0;
    font-size: 20px;
    font-weight: 600;
    color: #303133;
    }

    .status-section {
    display: flex;
    align-items: center;
    gap: 12px;
    }

    .app-main {
    flex: 1;
    padding: 20px;
    overflow: hidden;
    }

    .chat-container {
    display: flex;
    height: 100%;
    gap: 20px;
    }

    .model-info {
    display: flex;
    flex-direction: column;
    gap: 20px;
    }

    .model-card, .usage-card {
    height: auto;
    }

    .card-header {
    font-weight: 600;
    color: #303133;
    }

    .model-details {
    display: flex;
    flex-direction: column;
    gap: 16px;
    }

    .model-item {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    }

    .model-item .label {
    color: #606266;
    min-width: 80px;
    }

    .model-item .value {
    color: #303133;
    font-weight: 500;
    }

    .model-controls {
    margin-top: 20px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    }

    .model-select {
    width: 100%;
    }

    .refresh-btn {
    width: 100%;
    }

    .usage-content {
    font-size: 13px;
    color: #606266;
    line-height: 1.6;
    }

    .usage-content p {
    margin: 8px 0;
    }

    .chat-main {
    padding: 0;
    overflow: hidden;
    }

    .chat-card {
    height: 100%;
    display: flex;
    flex-direction: column;
    }

    .messages-container {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    background: #f8f9fa;
    border-radius: 8px;
    margin-bottom: 20px;
    max-height: 60vh;
    }

    .message {
    margin-bottom: 20px;
    animation: fadeIn 0.3s ease;
    }

    @keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
    }

    .message-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
    }

    .message-info {
    display: flex;
    flex-direction: column;
    }

    .sender {
    font-weight: 600;
    font-size: 14px;
    color: #303133;
    }

    .time {
    font-size: 12px;
    color: #909399;
    }

    .message-content {
    background: white;
    padding: 16px;
    border-radius: 8px;
    border: 1px solid #e4e7ed;
    margin-left: 44px;
    }

    .message-content pre {
    margin: 0;
    white-space: pre-wrap;
    word-wrap: break-word;
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
    font-size: 14px;
    line-height: 1.6;
    }

    .message.user .message-content {
    background: #ecf5ff;
    border-color: #b3d8ff;
    }

    .message.assistant .message-content {
    background: #f0f9eb;
    border-color: #c2e7b0;
    }

    .message.user .el-avatar {
    background: #409eff;
    }

    .message.assistant .el-avatar {
    background: #67c23a;
    }

    .loading-indicator {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 20px;
    color: #606266;
    }

    .loading-icon {
    animation: spin 1s linear infinite;
    }

    @keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
    }

    .input-area {
    border-top: 1px solid #e4e7ed;
    padding-top: 20px;
    }

    .message-input {
    margin-bottom: 16px;
    }

    .input-controls {
    display: flex;
    justify-content: space-between;
    align-items: center;
    }

    .input-tips {
    display: flex;
    gap: 8px;
    }

    .input-actions {
    display: flex;
    gap: 12px;
    }

    .app-footer {
    background: rgba(255, 255, 255, 0.95);
    border-top: 1px solid #e4e7ed;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    }

    .footer-content {
    display: flex;
    gap: 30px;
    font-size: 12px;
    color: #909399;
    }

    /* 响应式设计 */
    @media (max-width: 768px) {
    .chat-container {
    flex-direction: column;
    }

    .model-info {
    width: 100%;
    }

    .footer-content {
    flex-direction: column;
    gap: 8px;
    text-align: center;
    }
    }
    </style>

    接下来,修改src/main.js文件:

    import { createApp } from 'vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import * as ElementPlusIconsVue from '@element-plus/icons-vue'
    import App from './App.vue'

    const app = createApp(App)

    // 注册Element Plus
    app.use(ElementPlus)

    // 注册所有图标
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
    }

    app.mount('#app')

    最后,修改index.html的<head>部分,添加一些meta标签和标题:

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DeepSeek-R1-Distill-Qwen-7B Web界面</title>
    <meta name="description" content="基于Ollama + FastAPI + Vue构建的DeepSeek模型Web交互界面">
    </head>
    <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    </body>
    </html>

    3.3 启动前端服务

    保存所有文件后,在命令行里运行:

    npm run dev

    你会看到类似这样的输出:

    VITE v4.5.0 ready in 320 ms

    ➜ Local: http://localhost:5173/
    ➜ Network: http://192.168.1.100:5173/
    ➜ press h to show help

    现在打开浏览器,访问 http://localhost:5173,就能看到我们刚刚创建的聊天界面了!

    4. 测试完整流程

    现在整个系统已经搭建完成,我们来测试一下:

    4.1 测试步骤

  • 确保所有服务都在运行:

    • Ollama服务(在命令行运行 ollama serve)
    • 后端FastAPI服务(在backend目录运行 python main.py)
    • 前端Vue服务(在frontend目录运行 npm run dev)
  • 打开浏览器,访问 http://localhost:5173

  • 检查服务状态:

    • 页面右上角应该显示"服务正常"
    • 左侧模型信息区域应该显示"DeepSeek-R1-Distill-Qwen-7B"
  • 发送测试消息:

    • 在输入框里输入:"用Python写一个快速排序算法"
    • 点击"发送消息"按钮或按Ctrl+Enter
    • 等待几秒钟,应该能看到模型的回复
  • 测试其他功能:

    • 尝试清空对话
    • 检查聊天记录是否自动保存(刷新页面看看)
    • 如果有其他模型,可以尝试切换
  • 4.2 常见问题解决

    如果遇到问题,可以按下面步骤排查:

    问题1:前端无法连接到后端

    • 检查后端服务是否运行(访问 http://localhost:8000/docs)
    • 检查前端代码中的API地址是否正确(应该是 http://localhost:8000)
    • 检查浏览器控制台是否有CORS错误

    问题2:后端无法连接到Ollama

    • 检查Ollama服务是否运行(访问 http://localhost:11434)
    • 检查模型是否已下载(运行 ollama list)
    • 检查后端代码中的Ollama地址是否正确

    问题3:模型响应很慢或超时

    • DeepSeek-R1-Distill-Qwen-7B是7B参数的模型,需要一定的计算资源
    • 复杂问题可能需要更长时间
    • 可以调整后端的超时时间(代码中的timeout=300)

    5. 项目优化与扩展

    基础功能已经完成了,但我们可以做得更好。这里给你几个优化建议:

    5.1 添加流式响应

    现在的实现是等模型完全生成后再返回,用户需要等待较长时间。我们可以改成流式响应,让用户看到模型一个字一个字地生成回答。

    修改后端main.py,添加流式响应支持:

    from fastapi.responses import StreamingResponse
    import asyncio

    @app.post("/api/chat/stream")
    async def chat_stream(chat_request: ChatRequest):
    """流式聊天接口"""
    async def generate():
    try:
    # 构建请求数据
    ollama_data = {
    "model": chat_request.model,
    "prompt": chat_request.message,
    "stream": True # 启用流式
    }

    # 发送流式请求
    response = requests.post(
    f"{OLLAMA_BASE_URL}/api/generate",
    json=ollama_data,
    stream=True,
    timeout=300
    )

    # 流式返回数据
    for line in response.iter_lines():
    if line:
    line = line.decode('utf-8')
    if line.strip():
    try:
    data = json.loads(line)
    if 'response' in data:
    yield f"data: {json.dumps({'chunk': data['response']})}\\n\\n"
    except:
    continue

    except Exception as e:
    yield f"data: {json.dumps({'error': str(e)})}\\n\\n"

    return StreamingResponse(
    generate(),
    media_type="text/event-stream",
    headers={
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    }
    )

    前端也需要相应修改,使用EventSource接收流式数据。

    5.2 添加对话历史管理

    我们可以让用户保存不同的对话会话:

    # 在后端添加对话历史管理
    from typing import Dict, List
    import uuid

    # 内存中存储对话历史(生产环境应该用数据库)
    conversations: Dict[str, List[dict]] = {}

    @app.post("/api/conversation/new")
    async def new_conversation():
    """创建新的对话会话"""
    conversation_id = str(uuid.uuid4())
    conversations[conversation_id] = []
    return {"conversation_id": conversation_id}

    @app.get("/api/conversation/{conversation_id}")
    async def get_conversation(conversation_id: str):
    """获取对话历史"""
    return {"messages": conversations.get(conversation_id, [])}

    @app.post("/api/conversation/{conversation_id}/message")
    async def add_message(conversation_id: str, message: dict):
    """添加消息到对话"""
    if conversation_id in conversations:
    conversations[conversation_id].append(message)
    return {"status": "success"}
    return {"status": "error", "message": "Conversation not found"}

    5.3 添加模型参数调整

    让用户可以调整生成参数:

    class ChatRequest(BaseModel):
    message: str
    model: str = "deepseek-r1-distill-qwen:7b"
    stream: bool = False
    temperature: float = 0.7 # 新增:温度参数
    max_tokens: int = 2048 # 新增:最大token数

    # 在chat函数中使用这些参数
    ollama_data = {
    "model": chat_request.model,
    "prompt": chat_request.message,
    "stream": chat_request.stream,
    "options": {
    "temperature": chat_request.temperature,
    "num_predict": chat_request.max_tokens
    }
    }

    5.4 部署到生产环境

    开发完成后,你可能想部署到服务器上:

    后端部署:

    # 使用Gunicorn + Uvicorn部署
    pip install gunicorn
    gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app –bind 0.0.0.0:8000

    前端部署:

    # 构建生产版本
    npm run build

    # 生成的静态文件在dist目录
    # 可以用Nginx或Apache部署

    使用Docker部署(推荐): 创建Dockerfile:

    # 后端Dockerfile
    FROM python:3.9-slim

    WORKDIR /app

    COPY requirements.txt .
    RUN pip install –no-cache-dir -r requirements.txt

    COPY . .

    CMD ["uvicorn", "main:app", "–host", "0.0.0.0", "–port", "8000"]

    创建docker-compose.yml:

    version: '3.8'

    services:
    backend:
    build: ./backend
    ports:
    – "8000:8000"
    environment:
    – OLLAMA_HOST=ollama:11434
    depends_on:
    – ollama

    frontend:
    build: ./frontend
    ports:
    – "80:80"
    depends_on:
    – backend

    ollama:
    image: ollama/ollama:latest
    ports:
    – "11434:11434"
    volumes:
    – ollama_data:/root/.ollama

    volumes:
    ollama_data:

    6. 总结

    通过这个教程,我们完成了一个完整的全栈应用:

  • 后端服务:用FastAPI搭建了RESTful API,连接Ollama模型服务
  • 前端界面:用Vue 3 + Element Plus创建了美观的聊天界面
  • 完整功能:实现了消息发送、历史记录、模型切换等核心功能
  • 优化扩展:讨论了流式响应、对话管理、参数调整等进阶功能
  • 这个项目的价值在于:

    对个人用户:终于不用在命令行里敲代码了,有个漂亮的Web界面可以用 对团队协作:可以轻松分享给不懂技术的同事或朋友使用 对学习提升:理解了前后端分离的架构,掌握了FastAPI和Vue的基本用法 对项目扩展:这个框架可以轻松扩展到其他Ollama模型

    最棒的是,整个项目都是开源的,你可以随意修改、扩展。比如:

    • 添加用户登录系统
    • 实现多模型同时对话
    • 添加文件上传功能(让模型处理文档)
    • 集成到现有的业务系统中

    现在你已经有了一个完整的DeepSeek-R1-Distill-Qwen-7B Web应用,快去试试它的推理能力吧!从简单的数学题到复杂的编程问题,看看这个7B参数的模型能给你什么惊喜。


    获取更多AI镜像

    想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

    赞(0)
    未经允许不得转载:171主机测评 » DeepSeek-R1-Distill-Qwen-7B部署全栈指南:Ollama + FastAPI + Vue 构建前端交互界面
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址