ollama离线环境部署deepseek及对话网站开发
ollama离线环境部署deepseek及局域网对话网站开发
需要在离线环境下面部署deepseek大模型,而且局域网内用户能在浏览器直接对话,主机的操作系统是win10
经不断探索,找到一条能走通的路,大致流程和思路如下:
- 局域网服务器上下载安装ollama,ollama运行下载的离线模型
- 开放局域网服务器ollama的主机地址和端口
- 编写前端对话窗口,并设置反向代理处理跨域请求
一、ollama及离线模型下载
1. 下载模型
ollama下载下来后,正常安装即可
离线模型下载,需要下载guff文件,国内可以在huggingface镜像站或者魔塔社区下载,由于主机算力非常差,我下了个DeepSeek-R1-Distill-Qwen-1.5B-Q5_K_M.gguf模型,在魔塔社区下的
下载完成后,需要写模型文件,也就是Modelfile
新建一个Modelfile文件,不需要后缀名,文件写入以下内容:
FROM ./DeepSeek-R1-Distill-Qwen-1.5B-Q5_K_M.gguf
SYSTEM """你是一个由 DeepSeek 提供支持的人工智能助手。"""
TEMPLATE """{{- if .System }}{{ .System }}{{ end }}
{{- range .Messages }}
{{- if eq .Role "user" }}[INST] {{ .Content }} [/INST]
{{- else if eq .Role "assistant" }}{{ .Content }}
{{- end }}
{{- end }}"""
PARAMETER stop "[INST]"
PARAMETER stop "[/INST]"
PARAMETER num_ctx 4096
PARAMETER temperature 0.7
PARAMETER top_p 0.9
如果熟悉的话,可以根据自己的需求自己写
2. 创建模型
将Modelfile和下载的guff文件放到同一个目录下,然后终端执行以下命令:
ollama create deepseek-qwen1.5b -f Modelfile
如果终端不报错,就表明离线模型部署成功了
然后执行ollama run deepseek-qwen1.5b
可以在终端使用模型
二、主机开放IP
ollama在运行过程中,创建了一个后端服务,其运行在localhost:11434上,本机可以正常访问,但是局域网内不能访问,需要在本机的高级系统设置中新建两个环境变量,变量名和值如下:
- 变量名:OLLAMA_HOST,值:0.0.0.0,作用是开放本机IP
- 变量名:OLLAMA_ORIGINS,值:*,作用是运行局域网内所有用户访问,这一步忘了配置,导致卡了半天。。
三、编写前端页面
这里借助[Semi Design UI](Getting Started 快速开始 | semi-design-vue),因为它提供了快速使用的对话窗口,而且我个人觉得它的UI风格比ant design还要漂亮,但是它是基于JSX写的,用起来不是特别顺手
1. 对话组件
用的是上述UI框架中的Chat组件,全部代码如下:
<template>
<div class="container">
<Chat
:key="align + mode"
:align="align"
:mode="mode"
:style="commonOuterStyle"
:chats="message"
:roleConfig="roleInfo"
@chatsChange="onChatsChange"
@messageSend="onMessageSend"
@messageReset="onMessageReset"
class="chat"
/>
<Spin
v-show="loadingStatus"
tip="loading..."
size="large"
class="loading"
/>
</div>
</template>
<script setup>
import { ref, defineComponent } from "vue";
import { Chat, Spin } from "@kousum/semi-ui-vue";
import { getLargeModelAPI } from "@/apis/chat";
const defaultMessage = [
{
role: "system",
id: "1",
createAt: new Date(),
content: "您好,我是您的智能助理,您可以问我任何问题.",
},
];
const roleInfo = {
user: {
name: "用户",
avatar: "/user.png",
},
assistant: {
name: "系统",
avatar: "/model.png",
},
system: {
name: "系统",
avatar: "/model.png",
},
};
const commonOuterStyle = {
border: "1px solid var(--semi-color-border)",
borderRadius: "16px",
margin: "8px 16px",
height: 550,
};
let id = 0;
function getId() {
return `id-${id++}`;
}
// const uploadProps = { action: "https://api.semi.design/upload" };
// const uploadTipProps = { content: "自定义上传按钮提示信息" };
const message = ref(defaultMessage);
const mode = ref("bubble");
const align = ref("leftRight");
// 加载状态
const loadingStatus = ref(false);
const question = ref("");
const onMessageSend = async (content, attachment) => {
question.value = content;
loadingStatus.value = true;
try {
const res = await getLargeModelAPI(content);
loadingStatus.value = false;
const newAssistantMessage = {
role: "assistant",
id: getId(),
createAt: Date.now(),
content: res.choices[0].text.replace("]", ""),
};
setTimeout(() => {
message.value = [...message.value, newAssistantMessage];
}, 200);
} catch (error) {
loadingStatus.value = false;
}
};
const onChatsChange = (chats) => {
message.value = chats;
};
const onMessageReset = () => {
setTimeout(async () => {
const lastMessage = message.value[message.value.length - 1];
loadingStatus.value = true;
const res = await getLargeModelAPI({ prompt: question.value });
loadingStatus.value = false;
const newLastMessage = {
...lastMessage,
status: "complete",
content: res.generated_text,
};
message.value = [...message.value.slice(0, -1), newLastMessage];
}, 200);
};
</script>
<style lang="scss" scoped>
.container {
position: relative;
width: 100vh;
height: 100vh;
display: flex;
justify-content: center;
.chat {
width: 100%;
height: 100%;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>
关键逻辑是调用getLargeModelAPI
方法,也就是ollama的调用接口,获取对话信息,并将消息堆入对话数组
2. getLargeModelAPI调用ollama接口
这里不再封装axios,直接使用axios,代码如下:
import axios from "axios";
import { Toast } from '@kousum/semi-ui-vue';
export const getLargeModelAPI = async (prompt) => {
try {
// const url = "http://localhost:11434/v1/completions"
const url = "/api/v1/completions"
const headers = {
"Content-Type": "application/json",
}
const request = {
prompt,
model: "deepseek-qwen1.5b",
max_tokens: 1000,
temperature: 0.7,
top_p: 1,
n: 1
}
const response = await axios.post(url, request, headers)
return response.data
} catch (error) {
console.error(error);
Toast.error("Failed to get large model API")
}
}
值得关注的是,这里调用的地址并非直接是ollama的地址(http://localhost:11434/v1/completions),而是代理的地址,因为ollama的端口是11434,与页面地址的端口不一样,所以会出现跨域问题(实际上,本机上我直接调用http://localhost:11434/v1/completions也能得到数据,但是局域网内其他用户就不行了),所以还需要配置反向代理
3. 配置反向代理
在vite的配置文件中设置代理
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
proxy: {
"/api": {
// target: "http://localhost:11434",
target: "http://192.168.xxx.xxx:11434",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});
代理地址替换成本机ip即可
至此,大功告成,前端访问页面如下: