DeepSeek-R1-Distill-Qwen-1.5B与Vue3前端集成:打造智能对话Web应用

最近在折腾AI应用的时候,发现很多开发者都在尝试把大模型集成到自己的产品里。但说实话,直接调用云端API虽然方便,成本和安全问题却让人头疼。有没有一种方法,既能享受大模型的智能,又能完全掌控在自己手里?

我试了试DeepSeek-R1-Distill-Qwen-1.5B这个轻量级模型,发现它特别适合本地部署。参数只有15亿,对硬件要求不高,但对话效果却出人意料的好。更关键的是,我想把它做成一个Web应用,让团队里的非技术人员也能轻松使用。

这就是今天要分享的内容:如何把这个模型和Vue3前端框架结合起来,搭建一个完全自主可控的智能对话Web应用。整个过程其实没有想象中那么复杂,跟着步骤走,你也能快速搭建起来。

1. 为什么选择这个技术组合?

在开始动手之前,我们先聊聊为什么选这套方案。市面上大模型那么多,前端框架也不少,为什么偏偏是DeepSeek-R1-Distill-Qwen-1.5B加Vue3?

先说模型选择。DeepSeek-R1-Distill-Qwen-1.5B是个蒸馏模型,你可以把它理解成“精华版”。原版的DeepSeek-R1参数太大,普通机器根本跑不动。而这个蒸馏版本保留了核心的对话能力,体积却小了很多。我实测下来,在一台普通的GPU服务器上就能流畅运行,生成回复的速度也很快,基本在2-3秒内就能完成。

更重要的是,它支持中文对话的效果很不错。很多小模型在处理中文时总感觉“词不达意”,但这个模型在理解上下文、保持对话连贯性方面表现很好。对于企业内部的知识问答、客服辅助这些场景,完全够用了。

再说前端选择。Vue3现在是前端开发的主流框架之一,它的组合式API写起来特别顺手。而且生态丰富,各种UI组件库、工具链都很成熟。最关键的是,Vue3对TypeScript的支持很好,这对于我们这种需要严格类型检查的项目来说太重要了。

还有一个考虑是团队协作。Vue3的学习曲线相对平缓,团队里即使有刚入门的前端同学,也能快速上手。而且Vue的响应式系统天然适合做实时对话应用——消息来了自动更新界面,用户输入了实时显示,这种体验很流畅。

2. 后端服务搭建:让模型跑起来

模型选好了,第一步就是让它先跑起来。这里我推荐用vLLM来部署,这是专门为大模型推理优化的工具,用起来比原生的transformers要快不少。

2.1 环境准备

首先你得有一台服务器,配置不用太高。根据我的经验,下面这个配置就足够了:

  • CPU:4核或6核处理器
  • 内存:16GB以上
  • GPU:显存8GB以上(RTX 3070或同等性能)
  • 系统盘:至少50GB空闲空间

如果暂时没有GPU,用CPU也能跑,就是速度会慢一些。模型本身只有6.7GB,加上一些运行时的开销,预留15GB空间比较稳妥。

系统方面,我习惯用Ubuntu 22.04,主要是各种依赖安装起来方便。当然用CentOS或者Alibaba Cloud Linux也可以,操作步骤大同小异。

2.2 安装依赖

登录服务器,先更新一下系统包:

sudo apt update
sudo apt upgrade -y

然后安装Docker,这是为了后面用容器化部署,省去配置环境的麻烦:

# 安装Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 将当前用户加入docker组,这样就不用每次都加sudo了
sudo usermod -aG docker $USER
# 需要重新登录生效

如果你用的是NVIDIA GPU,还需要安装NVIDIA容器工具包:

# 配置NVIDIA容器工具包仓库
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# 安装工具包
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

2.3 下载并运行模型

环境准备好了,现在来下载模型。我用的是阿里云魔搭社区上的版本,下载速度比较快:

# 创建模型存储目录
sudo mkdir -p /mnt/models/deepseek-1.5b
sudo chmod 777 /mnt/models/deepseek-1.5b

# 下载模型
sudo docker run -d -t --network=host --rm --name model-download \
  -v /mnt/models/deepseek-1.5b:/data \
  egs-registry.cn-hangzhou.cr.aliyuncs.com/egs/vllm:0.6.4.post1-pytorch2.5.1-cuda12.4-ubuntu22.04 \
  /bin/bash -c "git-lfs clone https://www.modelscope.cn/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B.git /data"

这个下载过程可能需要一些时间,大概10-20分钟,取决于你的网络速度。你可以用下面的命令查看下载进度:

sudo docker logs -f model-download

看到下载完成后,就可以启动推理服务了:

# 启动vLLM服务
sudo docker run -d -t --network=host --gpus all \
  --name deepseek-1.5b \
  -v /mnt/models/deepseek-1.5b:/data \
  egs-registry.cn-hangzhou.cr.aliyuncs.com/egs/vllm:0.6.4.post1-pytorch2.5.1-cuda12.4-ubuntu22.04 \
  /bin/bash -c "vllm serve /data \
    --port 8000 \
    --served-model-name deepseek-1.5b \
    --max-model-len 4096 \
    --dtype half"

这里有几个参数解释一下:

  • --port 8000:服务监听的端口号
  • --max-model-len 4096:模型支持的最大上下文长度
  • --dtype half:使用半精度浮点数,可以节省显存

服务启动后,检查一下是否正常运行:

sudo docker logs deepseek-1.5b

如果看到类似这样的输出,就说明成功了:

INFO:     Uvicorn running on http://0.0.0.0:8000

2.4 测试API接口

现在模型服务已经在8000端口运行了,我们可以先测试一下。vLLM提供了OpenAI兼容的API接口,用起来很方便。

在服务器上安装curl,然后发送一个测试请求:

curl http://localhost:8000/v1/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-1.5b",
    "prompt": "你好,请介绍一下你自己",
    "max_tokens": 100,
    "temperature": 0.7
  }'

如果一切正常,你会收到一个JSON格式的回复,里面包含了模型生成的文本。温度参数temperature控制着生成文本的随机性,值越大越有创意,值越小越稳定。对话场景一般设置在0.7-0.9之间比较合适。

3. 前端开发:用Vue3构建对话界面

后端服务跑起来了,现在来构建前端界面。Vue3的开发体验很好,我们一步步来。

3.1 创建Vue3项目

首先确保你的本地开发环境有Node.js(建议版本16以上)和npm。然后创建一个新的Vue项目:

# 使用Vite创建项目,这是现在最流行的方式
npm create vue@latest deepseek-chat-app

# 按照提示选择配置
# ✔ Project name: deepseek-chat-app
# ✔ Add TypeScript? Yes
# ✔ Add JSX Support? No
# ✔ Add Vue Router for Single Page Application? Yes
# ✔ Add Pinia for state management? Yes
# ✔ Add Vitest for Unit Testing? No
# ✔ Add an End-to-End Testing Solution? No
# ✔ Add ESLint for code quality? Yes
# ✔ Add Prettier for code formatting? Yes

# 进入项目目录并安装依赖
cd deepseek-chat-app
npm install

项目创建好后,我们还需要安装一些额外的依赖:

# 安装UI组件库 - 我用的是Element Plus,你也可以用其他喜欢的
npm install element-plus @element-plus/icons-vue

# 安装HTTP客户端 - Axios
npm install axios

# 安装Markdown渲染器 - 用于显示模型返回的格式化文本
npm install marked

# 安装代码高亮 - 如果模型返回代码块,可以高亮显示
npm install highlight.js

# 开发服务器启动
npm run dev

现在打开浏览器访问http://localhost:5173,应该能看到Vue的默认页面。

3.2 设计对话界面

一个好的对话界面应该简洁明了。我设计了一个类似ChatGPT的布局,左边是对话历史,右边是当前对话区域。

先创建几个基础组件。在src/components目录下创建ChatWindow.vue

<template>
  <div class="chat-container">
    <!-- 消息列表 -->
    <div class="messages" ref="messagesRef">
      <div 
        v-for="(message, index) in messages" 
        :key="index"
        :class="['message', message.role]"
      >
        <div class="avatar">
          <el-avatar :size="32">
            <span v-if="message.role === 'user'">👤</span>
            <span v-else></span>
          </el-avatar>
        </div>
        <div class="content">
          <div class="markdown-content" v-html="renderMarkdown(message.content)"></div>
          <div v-if="message.role === 'assistant' && message.loading" class="loading">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
          </div>
        </div>
      </div>
    </div>

    <!-- 输入区域 -->
    <div class="input-area">
      <el-input
        v-model="inputText"
        type="textarea"
        :rows="3"
        placeholder="输入你的问题..."
        @keydown.enter.exact.prevent="handleSend"
        resize="none"
      />
      <div class="actions">
        <el-button 
          type="primary" 
          :loading="sending" 
          @click="handleSend"
          :disabled="!inputText.trim()"
        >
          发送
        </el-button>
        <el-button @click="handleClear">
          清空
        </el-button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, nextTick, onMounted } from 'vue'
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'

// 定义消息类型
interface Message {
  role: 'user' | 'assistant'
  content: string
  loading?: boolean
}

// 响应式数据
const messages = ref<Message[]>([
  { role: 'assistant', content: '你好!我是DeepSeek助手,有什么可以帮你的吗?' }
])
const inputText = ref('')
const sending = ref(false)
const messagesRef = ref<HTMLElement>()

// 配置marked
marked.setOptions({
  highlight: function(code, lang) {
    if (lang && hljs.getLanguage(lang)) {
      return hljs.highlight(code, { language: lang }).value
    }
    return hljs.highlightAuto(code).value
  },
  breaks: true,
  gfm: true
})

// 渲染Markdown
const renderMarkdown = (content: string) => {
  return marked(content)
}

// 发送消息
const handleSend = async () => {
  const text = inputText.value.trim()
  if (!text || sending.value) return

  // 添加用户消息
  messages.value.push({
    role: 'user',
    content: text
  })

  // 添加助手消息(占位)
  const assistantMessage: Message = {
    role: 'assistant',
    content: '',
    loading: true
  }
  messages.value.push(assistantMessage)

  // 清空输入框
  inputText.value = ''
  sending.value = true

  // 滚动到底部
  scrollToBottom()

  try {
    // 调用API
    const response = await fetch('http://你的服务器IP:8000/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'deepseek-1.5b',
        messages: messages.value
          .filter(msg => !msg.loading)
          .map(msg => ({ role: msg.role, content: msg.content })),
        stream: false,
        temperature: 0.7,
        max_tokens: 1000
      })
    })

    const data = await response.json()
    
    // 更新助手消息
    const lastIndex = messages.value.length - 1
    messages.value[lastIndex] = {
      role: 'assistant',
      content: data.choices[0].message.content
    }
  } catch (error) {
    console.error('请求失败:', error)
    const lastIndex = messages.value.length - 1
    messages.value[lastIndex] = {
      role: 'assistant',
      content: '抱歉,我遇到了一些问题,请稍后再试。'
    }
  } finally {
    sending.value = false
    scrollToBottom()
  }
}

// 清空对话
const handleClear = () => {
  messages.value = [
    { role: 'assistant', content: '你好!我是DeepSeek助手,有什么可以帮你的吗?' }
  ]
}

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

// 组件挂载时滚动到底部
onMounted(() => {
  scrollToBottom()
})
</script>

<style scoped>
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  background: #f5f5f5;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  background: white;
  border-radius: 8px;
  margin-bottom: 20px;
}

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

.message.user {
  flex-direction: row-reverse;
}

.message.user .content {
  margin-right: 12px;
  margin-left: 0;
  background: #e3f2fd;
}

.message.assistant .content {
  margin-left: 12px;
  margin-right: 0;
  background: #f5f5f5;
}

.avatar {
  flex-shrink: 0;
}

.content {
  flex: 1;
  padding: 12px 16px;
  border-radius: 12px;
  max-width: 70%;
  word-wrap: break-word;
}

.markdown-content {
  line-height: 1.6;
}

.markdown-content :deep(pre) {
  background: #f6f8fa;
  padding: 12px;
  border-radius: 6px;
  overflow-x: auto;
  margin: 8px 0;
}

.markdown-content :deep(code) {
  background: #f6f8fa;
  padding: 2px 4px;
  border-radius: 4px;
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}

.markdown-content :deep(p) {
  margin: 8px 0;
}

.markdown-content :deep(ul), 
.markdown-content :deep(ol) {
  padding-left: 20px;
  margin: 8px 0;
}

.input-area {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}

.actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 12px;
}

.loading {
  display: flex;
  align-items: center;
  height: 20px;
  margin-top: 8px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #999;
  margin: 0 4px;
  animation: bounce 1.4s infinite ease-in-out both;
}

.dot:nth-child(1) {
  animation-delay: -0.32s;
}

.dot:nth-child(2) {
  animation-delay: -0.16s;
}

@keyframes bounce {
  0%, 80%, 100% {
    transform: scale(0);
  }
  40% {
    transform: scale(1);
  }
}

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

这个组件实现了基本的对话功能,包括消息显示、Markdown渲染、代码高亮、加载动画等。界面设计得比较简洁,但该有的功能都有了。

3.3 添加配置管理

在实际项目中,我们不应该把API地址硬编码在组件里。更好的做法是使用环境变量。在项目根目录创建.env.development.env.production文件:

# .env.development - 开发环境
VITE_API_BASE_URL=http://localhost:8000
VITE_API_TIMEOUT=30000

# .env.production - 生产环境  
VITE_API_BASE_URL=http://你的服务器IP:8000
VITE_API_TIMEOUT=30000

然后创建一个API服务层,统一管理所有请求。在src/services目录下创建api.ts

import axios from 'axios'

// 创建axios实例
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    // 可以在这里添加token等认证信息
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    console.error('API请求错误:', error)
    
    // 统一错误处理
    if (error.response) {
      switch (error.response.status) {
        case 401:
          console.error('认证失败')
          break
        case 403:
          console.error('权限不足')
          break
        case 404:
          console.error('资源不存在')
          break
        case 500:
          console.error('服务器错误')
          break
        default:
          console.error('未知错误')
      }
    } else if (error.request) {
      console.error('网络错误,请检查连接')
    } else {
      console.error('请求配置错误')
    }
    
    return Promise.reject(error)
  }
)

// 对话相关API
export const chatApi = {
  // 发送消息
  sendMessage: (messages: Array<{ role: string; content: string }>) => {
    return api.post('/v1/chat/completions', {
      model: 'deepseek-1.5b',
      messages,
      stream: false,
      temperature: 0.7,
      max_tokens: 1000
    })
  },

  // 流式传输(如果需要)
  sendMessageStream: (messages: Array<{ role: string; content: string }>) => {
    return api.post('/v1/chat/completions', {
      model: 'deepseek-1.5b',
      messages,
      stream: true,
      temperature: 0.7,
      max_tokens: 1000
    }, {
      responseType: 'stream'
    })
  }
}

export default api

3.4 实现流式传输

上面的基础版本是一次性返回完整回复。如果想要更好的用户体验,可以实现流式传输,让回复像打字一样逐个字显示出来。

修改ChatWindow.vue,添加流式传输支持:

<script setup lang="ts">
// ... 其他导入和定义

// 添加流式传输方法
const handleSendStream = async () => {
  const text = inputText.value.trim()
  if (!text || sending.value) return

  // 添加用户消息
  messages.value.push({
    role: 'user',
    content: text
  })

  // 添加助手消息(占位)
  const assistantMessage: Message = {
    role: 'assistant',
    content: '',
    loading: true
  }
  messages.value.push(assistantMessage)

  inputText.value = ''
  sending.value = true
  scrollToBottom()

  try {
    const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/v1/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'deepseek-1.5b',
        messages: messages.value
          .filter(msg => !msg.loading)
          .map(msg => ({ role: msg.role, content: msg.content })),
        stream: true,  // 启用流式传输
        temperature: 0.7,
        max_tokens: 1000
      })
    })

    if (!response.body) throw new Error('No response body')

    const reader = response.body.getReader()
    const decoder = new TextDecoder('utf-8')
    let accumulatedText = ''
    const lastIndex = messages.value.length - 1

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      const chunk = decoder.decode(value)
      const lines = chunk.split('\n').filter(line => line.trim())

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6)
          if (data === '[DONE]') continue

          try {
            const parsed = JSON.parse(data)
            const content = parsed.choices[0]?.delta?.content || ''
            
            if (content) {
              accumulatedText += content
              // 更新消息内容,触发响应式更新
              messages.value[lastIndex] = {
                ...messages.value[lastIndex],
                content: accumulatedText,
                loading: false
              }
              // 滚动到底部
              scrollToBottom()
            }
          } catch (e) {
            console.error('解析流数据失败:', e)
          }
        }
      }
    }
  } catch (error) {
    console.error('流式请求失败:', error)
    const lastIndex = messages.value.length - 1
    messages.value[lastIndex] = {
      role: 'assistant',
      content: '抱歉,我遇到了一些问题,请稍后再试。'
    }
  } finally {
    sending.value = false
  }
}

// 修改handleSend方法,使用流式传输
const handleSend = async () => {
  await handleSendStream()
}
</script>

流式传输的体验要好很多,用户不用等待整个回复生成完毕,可以边生成边看。这对于长回复特别有用。

4. 功能增强:让应用更实用

基础功能有了,但要让这个应用真正有用,还需要添加一些增强功能。

4.1 对话历史管理

用户可能希望保存重要的对话,或者回顾之前的聊天记录。我们可以添加一个简单的历史管理功能。

创建src/components/ChatHistory.vue

<template>
  <div class="history-sidebar">
    <div class="header">
      <h3>对话历史</h3>
      <el-button type="primary" size="small" @click="handleNewChat">
        新对话
      </el-button>
    </div>
    
    <div class="history-list">
      <div 
        v-for="(history, index) in histories" 
        :key="index"
        :class="['history-item', { active: currentHistoryId === history.id }]"
        @click="selectHistory(history.id)"
      >
        <div class="title">
          {{ history.title || `对话 ${index + 1}` }}
        </div>
        <div class="preview">
          {{ history.preview }}
        </div>
        <div class="time">
          {{ formatTime(history.createdAt) }}
        </div>
        <el-button 
          class="delete-btn" 
          type="text" 
          size="small"
          @click.stop="deleteHistory(history.id)"
        >
          删除
        </el-button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { format } from 'date-fns'

interface ChatHistory {
  id: string
  title: string
  preview: string
  messages: Array<{ role: string; content: string }>
  createdAt: Date
}

// 从localStorage加载历史记录
const loadHistories = (): ChatHistory[] => {
  const saved = localStorage.getItem('chatHistories')
  if (saved) {
    try {
      return JSON.parse(saved).map((h: any) => ({
        ...h,
        createdAt: new Date(h.createdAt)
      }))
    } catch (e) {
      console.error('加载历史记录失败:', e)
    }
  }
  return []
}

// 保存历史记录
const saveHistories = (histories: ChatHistory[]) => {
  localStorage.setItem('chatHistories', JSON.stringify(histories))
}

const histories = ref<ChatHistory[]>(loadHistories())
const currentHistoryId = ref<string>('')

// 添加新对话
const handleNewChat = () => {
  const newHistory: ChatHistory = {
    id: Date.now().toString(),
    title: `新对话 ${histories.value.length + 1}`,
    preview: '开始新的对话...',
    messages: [],
    createdAt: new Date()
  }
  
  histories.value.unshift(newHistory)
  currentHistoryId.value = newHistory.id
  saveHistories(histories.value)
  
  // 触发事件,通知父组件
  emit('select', newHistory.messages)
}

// 选择历史记录
const selectHistory = (id: string) => {
  currentHistoryId.value = id
  const history = histories.value.find(h => h.id === id)
  if (history) {
    emit('select', history.messages)
  }
}

// 删除历史记录
const deleteHistory = (id: string) => {
  const index = histories.value.findIndex(h => h.id === id)
  if (index !== -1) {
    histories.value.splice(index, 1)
    saveHistories(histories.value)
    
    if (currentHistoryId.value === id) {
      currentHistoryId.value = ''
      emit('select', [])
    }
  }
}

// 更新当前对话
const updateCurrentHistory = (messages: Array<{ role: string; content: string }>) => {
  if (!currentHistoryId.value) return
  
  const history = histories.value.find(h => h.id === currentHistoryId.value)
  if (history) {
    history.messages = messages
    history.preview = messages.length > 0 
      ? messages[messages.length - 1].content.slice(0, 50) + '...'
      : '空对话'
    
    // 如果第一条用户消息,可以设置为标题
    const firstUserMessage = messages.find(m => m.role === 'user')
    if (firstUserMessage && !history.title.startsWith('新对话')) {
      history.title = firstUserMessage.content.slice(0, 20) + '...'
    }
    
    saveHistories(histories.value)
  }
}

// 格式化时间
const formatTime = (date: Date) => {
  return format(date, 'MM-dd HH:mm')
}

const emit = defineEmits<{
  select: [messages: Array<{ role: string; content: string }>]
}>()

// 暴露方法给父组件
defineExpose({
  updateCurrentHistory
})

onMounted(() => {
  // 如果没有历史记录,创建一个默认的
  if (histories.value.length === 0) {
    handleNewChat()
  }
})
</script>

<style scoped>
.history-sidebar {
  width: 250px;
  height: 100vh;
  background: #2c3e50;
  color: white;
  display: flex;
  flex-direction: column;
}

.header {
  padding: 20px;
  border-bottom: 1px solid #34495e;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.header h3 {
  margin: 0;
  font-size: 16px;
}

.history-list {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
}

.history-item {
  padding: 12px;
  margin-bottom: 8px;
  background: #34495e;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.3s;
  position: relative;
}

.history-item:hover {
  background: #3d566e;
}

.history-item.active {
  background: #3498db;
}

.title {
  font-weight: bold;
  margin-bottom: 4px;
  font-size: 14px;
}

.preview {
  font-size: 12px;
  color: #bdc3c7;
  margin-bottom: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.time {
  font-size: 11px;
  color: #95a5a6;
}

.delete-btn {
  position: absolute;
  right: 8px;
  top: 8px;
  color: #e74c3c;
  opacity: 0;
  transition: opacity 0.3s;
}

.history-item:hover .delete-btn {
  opacity: 1;
}
</style>

然后在主组件中集成这个历史管理功能。

4.2 添加系统设置

不同的使用场景可能需要不同的模型参数。我们可以添加一个设置面板,让用户调整温度、最大token数等参数。

创建src/components/SettingsPanel.vue

<template>
  <el-drawer
    v-model="visible"
    title="设置"
    size="300px"
  >
    <div class="settings-content">
      <el-form :model="settings" label-width="100px">
        <el-form-item label="温度">
          <el-slider
            v-model="settings.temperature"
            :min="0"
            :max="2"
            :step="0.1"
            show-input
          />
          <div class="tip">
            值越高越有创意,值越低越稳定。推荐0.7-0.9
          </div>
        </el-form-item>

        <el-form-item label="最大长度">
          <el-input-number
            v-model="settings.maxTokens"
            :min="100"
            :max="4000"
            :step="100"
          />
          <div class="tip">
            单次回复的最大token数
          </div>
        </el-form-item>

        <el-form-item label="流式传输">
          <el-switch v-model="settings.stream" />
        </el-form-item>

        <el-form-item label="API地址">
          <el-input v-model="settings.apiBaseUrl" />
        </el-form-item>

        <el-form-item>
          <el-button type="primary" @click="saveSettings">
            保存设置
          </el-button>
          <el-button @click="resetSettings">
            恢复默认
          </el-button>
        </el-form-item>
      </el-form>
    </div>
  </el-drawer>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

interface Settings {
  temperature: number
  maxTokens: number
  stream: boolean
  apiBaseUrl: string
}

// 默认设置
const defaultSettings: Settings = {
  temperature: 0.7,
  maxTokens: 1000,
  stream: true,
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
}

// 从localStorage加载设置
const loadSettings = (): Settings => {
  const saved = localStorage.getItem('chatSettings')
  if (saved) {
    try {
      return { ...defaultSettings, ...JSON.parse(saved) }
    } catch (e) {
      console.error('加载设置失败:', e)
    }
  }
  return { ...defaultSettings }
}

const settings = ref<Settings>(loadSettings())
const visible = ref(false)

// 保存设置
const saveSettings = () => {
  localStorage.setItem('chatSettings', JSON.stringify(settings.value))
  visible.value = false
  emit('change', settings.value)
}

// 恢复默认设置
const resetSettings = () => {
  settings.value = { ...defaultSettings }
}

// 打开设置面板
const open = () => {
  visible.value = true
}

const emit = defineEmits<{
  change: [settings: Settings]
}>()

// 暴露方法
defineExpose({
  open
})

// 监听设置变化,自动保存
watch(settings, (newSettings) => {
  localStorage.setItem('chatSettings', JSON.stringify(newSettings))
  emit('change', newSettings)
}, { deep: true })
</script>

<style scoped>
.settings-content {
  padding: 20px;
}

.tip {
  font-size: 12px;
  color: #999;
  margin-top: 4px;
}
</style>

4.3 添加快捷键支持

为了提高使用效率,我们可以添加一些快捷键:

<script setup lang="ts">
// 在ChatWindow组件中添加
import { onMounted, onUnmounted } from 'vue'

// 快捷键处理
const handleKeyDown = (e: KeyboardEvent) => {
  // Ctrl + Enter 发送
  if (e.ctrlKey && e.key === 'Enter') {
    handleSend()
    e.preventDefault()
  }
  
  // Ctrl + / 清空
  if (e.ctrlKey && e.key === '/') {
    handleClear()
    e.preventDefault()
  }
  
  // Ctrl + , 打开设置
  if (e.ctrlKey && e.key === ',') {
    // 这里需要调用SettingsPanel的open方法
    e.preventDefault()
  }
}

onMounted(() => {
  window.addEventListener('keydown', handleKeyDown)
})

onUnmounted(() => {
  window.removeEventListener('keydown', handleKeyDown)
})
</script>

5. 部署上线:让应用可访问

开发完成后,我们需要把应用部署到服务器上,让其他人也能访问。

5.1 构建前端应用

首先构建生产版本的前端代码:

npm run build

这会生成一个dist目录,里面是优化后的静态文件。

5.2 配置Nginx

在服务器上安装Nginx,并配置反向代理:

# 安装Nginx
sudo apt install nginx -y

# 创建Nginx配置
sudo nano /etc/nginx/sites-available/deepseek-chat

添加以下配置:

server {
    listen 80;
    server_name 你的域名或IP;
    
    # 前端静态文件
    location / {
        root /var/www/deepseek-chat;
        try_files $uri $uri/ /index.html;
        index index.html;
    }
    
    # 后端API代理
    location /api/ {
        proxy_pass http://localhost:8000/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

启用配置并重启Nginx:

# 创建目录并复制文件
sudo mkdir -p /var/www/deepseek-chat
sudo cp -r dist/* /var/www/deepseek-chat/

# 启用站点
sudo ln -s /etc/nginx/sites-available/deepseek-chat /etc/nginx/sites-enabled/

# 测试配置
sudo nginx -t

# 重启Nginx
sudo systemctl restart nginx

5.3 配置HTTPS(可选但推荐)

如果需要HTTPS,可以使用Let's Encrypt免费证书:

# 安装Certbot
sudo apt install certbot python3-certbot-nginx -y

# 获取证书
sudo certbot --nginx -d 你的域名

# 自动续期测试
sudo certbot renew --dry-run

5.4 使用PM2管理进程

为了确保服务稳定运行,可以使用PM2来管理Node.js进程(如果后端是Node.js的话):

# 安装PM2
npm install -g pm2

# 启动应用
pm2 start npm --name "deepseek-chat" -- run start

# 设置开机自启
pm2 startup
pm2 save

6. 实际应用与优化建议

整个应用搭建完成后,我在实际使用中发现了一些可以优化的地方,分享给大家参考。

6.1 性能优化

前端优化:

  • 使用虚拟滚动:如果对话历史很长,全部渲染会影响性能。可以考虑只渲染可视区域的消息。
  • 图片懒加载:如果对话中包含图片,可以使用懒加载技术。
  • 代码分割:将不常用的功能(如设置面板、历史管理)拆分成独立的chunk,按需加载。

后端优化:

  • 启用模型量化:如果显存紧张,可以使用8位或4位量化,能显著减少内存占用。
  • 调整批处理大小:根据实际并发情况调整--max-num-batched-tokens参数。
  • 使用缓存:对于常见问题,可以缓存模型回复,减少重复计算。

6.2 功能扩展想法

根据不同的使用场景,你可以考虑添加这些功能:

企业知识库集成:

// 简单的RAG(检索增强生成)实现
async function queryWithKnowledge(question: string) {
  // 1. 从向量数据库检索相关文档
  const relevantDocs = await searchKnowledgeBase(question)
  
  // 2. 将文档内容作为上下文
  const context = relevantDocs.map(doc => doc.content).join('\n\n')
  
  // 3. 构造提示词
  const prompt = `基于以下信息回答问题:
${context}

问题:${question}

回答:`
  
  // 4. 调用模型
  return await chatApi.sendMessage([{ role: 'user', content: prompt }])
}

多模型支持: 可以扩展支持多个模型,让用户根据需要选择。比如同时部署Qwen、Llama等模型,在前端提供切换选项。

插件系统: 设计一个插件架构,支持计算器、天气查询、网页搜索等工具调用。

团队协作功能:

  • 对话分享:生成分享链接
  • 协作编辑:多人同时编辑提示词
  • 权限管理:不同角色的访问权限

6.3 监控与维护

日志记录: 记录重要的用户操作和模型请求,便于问题排查和数据分析。

健康检查: 定期检查模型服务是否正常,自动重启失败的服务。

使用统计: 收集基本的使用数据(不涉及隐私),了解用户最常问的问题,优化模型表现。

7. 总结

走完这一整套流程,从模型部署到前端开发,再到最终上线,你会发现搭建一个智能对话应用并没有想象中那么难。DeepSeek-R1-Distill-Qwen-1.5B这个模型在轻量级应用中表现相当不错,Vue3的开发体验也很顺畅。

最关键的是,这套方案给了你完全的控制权。数据在自己服务器上,不用担心隐私泄露;模型可以随时更新调整,不用受制于第三方API的限制;前端界面可以根据业务需求任意定制。

实际用下来,这个应用在我们团队内部已经成了日常工具。新人来了,用它了解公司制度;开发遇到问题,用它搜索技术方案;甚至写周报、做总结,它都能帮上忙。虽然偶尔会有回答不准确的情况,但对于一个本地部署的轻量模型来说,已经超出预期了。

如果你也想尝试搭建自己的AI应用,建议先从简单的功能开始,跑通整个流程。遇到问题不用怕,现在开源社区的资源很丰富,大部分问题都能找到解决方案。最重要的是动手去做,在实践中学到的东西,比看多少教程都有用。


获取更多AI镜像

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐