DeepSeek-R1-Distill-Qwen-1.5B与Vue3前端集成:打造智能对话Web应用
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星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐



所有评论(0)