我们在腾讯云服务器上尝试部署DeepSeek蒸馏版,服务器配置8核32G内存,用Docker部署Ollama、Open-Webui、Searxng、AnythingLLM。
这些都是什么东西呢?我一个个来解释下,首先在Ollama容器内部署蒸馏版deepseek-r1:1.5b,就是说deepseek是运行在ollama环境中,deepseek有两个版本r1和v3,分别代表深度思考模型和日常通用模型。而我们常用1.5b、7b分别代表15亿个参数和70亿个参数的模型,当然越大对服务器要求越高,能力越强。
Open-Webui是接入deepseek的网页,我们就可以在网页上同deepseek对话了,不然只能在命令行中对话。但是经过测试网页版反应很慢,命令行中反应很快就答复了,不知道原因。
有了上面Ollama和Open-Webui部署完成后其实就可以正常聊天了,那Searxng和AnythingLLM的作用是什么呢?
Searxng的作用是提供给deepseek联网搜索的能力,因为deepseek是不具备联网搜索能力的,就是用Searxng对接各种搜索引擎,提供赋予实时搜索能力,比如你们今天天气怎么样,今天几号,如果不联网那就找不到答案。
AnythingLLM的作用是给deepseek继续训练的能力,将 PDF、Markdown、Word 等多格式文件索引进系统,将本地文档或数据源整合进一个可检索、可对话的知识库,这样就不断学习训练了专业领域内的知识库,为了让AI能力与时俱进不断增强,那就需要持续维护扩大知识库。
安装Docker
### 设置国内镜像
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://mirror.centos.org|baseurl=http://mirrors.aliyun.com|g' \
-i.bak \
/etc/yum.repos.d/CentOS-*.repo
# 清理缓存
yum makecache
# 配置 docker-ce 国内 yum 源(阿里云)
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 安装工具库
yum install -y yum-utils
#安装 docker
yum install -y docker-ce docker-ce-cli containerd.io
#启动
systemctl enable docker
systemctl start docker
安装Ollama和DeepSeek
国外镜像都基本访问不了,都要靠国内代理,我已经把Ollama放到的腾讯云公开镜像仓库中,可以直接使用还很快 ccr.ccs.tencentyun.com/rootegg/public:ollama-20250212,后面要最新可以用ghcr.io/open-webui/open-webui:main
# 运行ollama容器并挂载11434端口到宿主机
docker run -d --name ollama -p 11434:11434 ccr.ccs.tencentyun.com/rootegg/public:ollama-20250212
# 上面ollama容器启动后,进入容器去运行deepseek模型
docker exec -it ollama bash
# 后面已经进入容器后,看到这里没有运行任何模型,这句执行后是空的
ollama list
# 运行deepseek-r1:1.5b这个模型,也可以用7b
ollama run deepseek-r1:1.5b
最后下载很慢很慢很慢很慢很慢很慢很慢很慢,我大概下载完用了两个小时
命令行测试
这里测试1+1等于几,速度很快就响应了,但是后面安装Open-Webui回答就很慢,估计Open-Webui有问题,还是命令行反应最快
Ollama官网
可以到ollama官网ollama.com/search?q=de… 看到deepseek-r1和deepseek-v3两个模型,r1我们做实验一般选择1.5b或7b,有条件的可以用7b
安装Open-Webui,这一步可忽略
同样用我最新公开镜像 ccr.ccs.tencentyun.com/rootegg/public:open-webui-20250212
注意这里设置环境变量HF_ENDPOINT,必须设置为 hf-mirror.com ,否则依然会报错,等10多分钟启动成功直到容器状态变成healthy
注意刚开始我看docker ps状态显示unhealthy,我一直以为有问题,重装多次都是unhealthy,但是其实没有问题了,大概等待10几分钟后就变成healthy成功了
# 安装Open-Webui
docker run -d -e HF_ENDPOINT=https://hf-mirror.com -p 300:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ccr.ccs.tencentyun.com/rootegg/public:open-webui-20250212
访问访问3000端口就可以使用deepseek了,登录进去后会等待几分钟是白板,我有以为出问题了,但是看有个 models接口返回很慢,等几分钟就可以对话了
等界面出来就好了
浏览器插件Page Assist
上面用Open-Webui来网页对话deepseek发现速度很慢,我改用谷歌浏览器插件Page Assist来进行对话,同时里面能上传学习资料,能开启联网,比Open-Webui好多了
www.crxsoso.com/webstore/de… 下载 Page Assist 插件,自动安装失败,就手动拖到Chrome的扩展程序中吧
配置中文
进入右上角设置,修改后保存
配置网络搜索
搜索引擎改为baidu,修改后保存
设置RAG模型
配置Ollama地址
前面容器部署的Ollama地址是11434端口,修改后保存
成功
postman调用测试SSE流模式
POST http://IP地址:11434/v1/chat/completions
header头增加 Accept: text/event-stream
JSON请求 {"model": "deepseek-r1:1.5b","stream": true,"messages": [{"role": "user","content": "解释什么是大模型"}]}
Html页面测试SSE流
因为本地页面和服务器有跨域问题,所以有两种方式解决,两种方式html种端口使用不一样。
第一种方式启chrome临时关闭cors安全限制
此时连接的端口还是原本ollama开放的端口 11434
先启动一个无安全显示的chrome 命令行中打开
"C:\Users\Lenovo\AppData\Local\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir="C:\ChromeDevSession"
再启动html页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>HTML SSE 大模型流式对话</title>
<style>
.container {
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}
#promptInput {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
#sendBtn {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
#sendBtn:disabled {
background: #ccc;
cursor: not-allowed;
}
#responseContainer {
margin-top: 20px;
padding: 16px;
border: 1px solid #eee;
border-radius: 4px;
min-height: 200px;
font-size: 16px;
line-height: 1.6;
}
.loading {
color: #666;
font-style: italic;
}
.error {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<input type="text" id="promptInput" placeholder="请输入你的问题(如:介绍 SSE 的应用场景)" />
<button id="sendBtn" onclick="startStream()">发送请求</button>
<div id="responseContainer"></div>
</div>
<script>
let eventSource = null; // 存储 SSE 连接实例
const promptInput = document.getElementById("promptInput");
const sendBtn = document.getElementById("sendBtn");
const responseContainer = document.getElementById("responseContainer");
// 替换上面的 startStream 函数,支持 POST 方法
function startStream() {
const prompt = promptInput.value.trim();
if (!prompt) {
alert("请输入问题!");
return;
}
responseContainer.innerHTML = '<div class="loading">正在生成回复...</div>';
sendBtn.disabled = true;
promptInput.disabled = true;
// 用 fetch 发起 POST 流式请求
fetch("http://服务器IP地址:11434/v1/chat/completions", {
// 你的 POST 接口地址
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream", // 告知服务端返回 SSE 格式
"Cache-Control": "no-cache",
Authorization: "Bearer sk-xxx", // 接口鉴权(如 API 密钥)
},
body: JSON.stringify({
// POST 请求体(按接口要求调整)
// prompt: prompt,
messages: [
{
role: "user",
content: prompt,
},
],
stream: true,
model: "deepseek-r1:1.5b",
temperature: 0.7,
}),
keepalive: true,
})
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
// 解析流式响应(核心:读取 ReadableStream)
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = ""; // 缓存未完整解析的分片
while (true) {
const { done, value } = await reader.read();
if (done) break; // 流结束
// 解码二进制数据为字符串,追加到缓存
buffer += decoder.decode(value, { stream: true });
// 按 SSE 格式分割分片(每段以 \n\n 结束)
const chunks = buffer.split("\n\n");
// 处理所有完整的分片(最后一个可能不完整,留到下次)
for (let i = 0; i < chunks.length - 1; i++) {
const chunk = chunks[i].trim();
if (!chunk) continue;
// 移除开头的 "data: " 前缀
const dataStr = chunk.startsWith("data: ") ? chunk.slice(6) : chunk;
if (dataStr === "[DONE]") continue; // 跳过结束标识
// 解析数据并渲染
try {
const data = JSON.parse(dataStr);
const content = data.content || data.choices[0].delta.content;
if (content) {
responseContainer.innerHTML = responseContainer.innerHTML.replace('<div class="loading">正在生成回复...</div>', "") + content;
}
} catch (err) {
// 纯文本分片直接渲染
responseContainer.innerHTML = responseContainer.innerHTML.replace('<div class="loading">正在生成回复...</div>', "") + dataStr;
}
}
// 保留未完整解析的部分(最后一个分片)
buffer = chunks[chunks.length - 1];
}
// 流结束后清理
responseContainer.innerHTML = responseContainer.innerHTML.replace('<div class="loading">正在生成回复...</div>', "");
sendBtn.disabled = false;
promptInput.disabled = false;
})
.catch((err) => {
responseContainer.innerHTML = `<div class="error">回复生成失败!${err.message}</div>`;
sendBtn.disabled = false;
promptInput.disabled = false;
console.error("POST 流式请求错误:", err);
});
}
</script>
</body>
</html>
效果
第二种在服务器上搭建nginx转发
nginx上监听11435端口,转发到ollma的11434上,所以html中端口要改为11435
nginx配置文件,因为ollama本身要返回response头,要nginx中去做覆盖操作
server {
listen 11435;
location /v1/chat/completions {
proxy_pass http://127.0.0.1:11434/v1/chat/completions;
# 允许跨域
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "*" always;
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Credentials "true" always;
# 3. 处理 SSE 流式关键配置(避免分片缓存、断开连接)
proxy_buffering off; # 关闭缓冲(核心!SSE 必须关闭,否则分片会被缓存后一次性返回)
proxy_cache off; # 关闭缓存
proxy_http_version 1.1; # 启用 HTTP/1.1(长连接必需)
proxy_set_header Connection "keep-alive"; # 保持长连接
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 4. 处理预检请求(OPTIONS):直接返回 204 无内容(避免目标接口返回重复 CORS 头)
if ($request_method = OPTIONS) {
return 204; # 预检请求成功,无需转发到目标接口(关键:避免目标接口返回重复 Access-Control-Allow-Origin)
}
# 5. 转发响应时,覆盖目标接口的重复 CORS 头(若目标接口仍返回多余头,用此强制覆盖)
# 原理:Nginx 的 add_header 会覆盖后端返回的同名头(前提是配置了 always)
proxy_hide_header Access-Control-Allow-Origin; # 隐藏目标接口返回的 Access-Control-Allow-Origin 头
proxy_hide_header Access-Control-Allow-Methods; # 隐藏目标接口返回的方法头
proxy_hide_header Access-Control-Allow-Headers; # 隐藏目标接口返回的头字段
}
}
html内容
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>HTML SSE 大模型流式对话</title>
<style>
.container {
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}
#promptInput {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
#sendBtn {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
#sendBtn:disabled {
background: #ccc;
cursor: not-allowed;
}
#responseContainer {
margin-top: 20px;
padding: 16px;
border: 1px solid #eee;
border-radius: 4px;
min-height: 200px;
font-size: 16px;
line-height: 1.6;
}
.loading {
color: #666;
font-style: italic;
}
.error {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<input type="text" id="promptInput" placeholder="请输入你的问题(如:介绍 SSE 的应用场景)" />
<button id="sendBtn" onclick="startStream()">发送请求</button>
<div id="responseContainer"></div>
</div>
<script>
let eventSource = null; // 存储 SSE 连接实例
const promptInput = document.getElementById("promptInput");
const sendBtn = document.getElementById("sendBtn");
const responseContainer = document.getElementById("responseContainer");
// 替换上面的 startStream 函数,支持 POST 方法
function startStream() {
const prompt = promptInput.value.trim();
if (!prompt) {
alert("请输入问题!");
return;
}
responseContainer.innerHTML = '<div class="loading">正在生成回复...</div>';
sendBtn.disabled = true;
promptInput.disabled = true;
// 用 fetch 发起 POST 流式请求
fetch("http://IP地址:11435/v1/chat/completions", {
// 你的 POST 接口地址
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream", // 告知服务端返回 SSE 格式
"Cache-Control": "no-cache",
Authorization: "Bearer sk-xxx", // 接口鉴权(如 API 密钥)
},
body: JSON.stringify({
// POST 请求体(按接口要求调整)
// prompt: prompt,
messages: [
{
role: "user",
content: prompt,
},
],
stream: true,
model: "deepseek-r1:1.5b",
temperature: 0.7,
}),
keepalive: true,
})
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
// 解析流式响应(核心:读取 ReadableStream)
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = ""; // 缓存未完整解析的分片
while (true) {
const { done, value } = await reader.read();
if (done) break; // 流结束
// 解码二进制数据为字符串,追加到缓存
buffer += decoder.decode(value, { stream: true });
// 按 SSE 格式分割分片(每段以 \n\n 结束)
const chunks = buffer.split("\n\n");
// 处理所有完整的分片(最后一个可能不完整,留到下次)
for (let i = 0; i < chunks.length - 1; i++) {
const chunk = chunks[i].trim();
if (!chunk) continue;
// 移除开头的 "data: " 前缀
const dataStr = chunk.startsWith("data: ") ? chunk.slice(6) : chunk;
if (dataStr === "[DONE]") continue; // 跳过结束标识
// 解析数据并渲染
try {
const data = JSON.parse(dataStr);
const content = data.content || data.choices[0].delta.content;
if (content) {
responseContainer.innerHTML = responseContainer.innerHTML.replace('<div class="loading">正在生成回复...</div>', "") + content;
}
} catch (err) {
// 纯文本分片直接渲染
responseContainer.innerHTML = responseContainer.innerHTML.replace('<div class="loading">正在生成回复...</div>', "") + dataStr;
}
}
// 保留未完整解析的部分(最后一个分片)
buffer = chunks[chunks.length - 1];
}
// 流结束后清理
responseContainer.innerHTML = responseContainer.innerHTML.replace('<div class="loading">正在生成回复...</div>', "");
sendBtn.disabled = false;
promptInput.disabled = false;
})
.catch((err) => {
responseContainer.innerHTML = `<div class="error">回复生成失败!${err.message}</div>`;
sendBtn.disabled = false;
promptInput.disabled = false;
console.error("POST 流式请求错误:", err);
});
}
</script>
</body>
</html>
现在任意可以访问了
服务器部署ollama和nginx转发index页面
通常你会遇到403问题,ollama接口被nginx转发后出现403报错解决方法
server {
listen 11436;
server_name localhost;
server_tokens off;
# 前端静态文件服务
location / {
root /var/www/deepseek;
index index.html;
try_files $uri $uri/ /index.html;
autoindex off;
charset utf-8;
expires 1d;
# 新增:允许前端访问静态文件的跨域(避免前端资源加载时的隐性跨域)
add_header Access-Control-Allow-Origin "*" always;
}
# Ollama 接口代理(核心修复)
location /v1/chat/completions {
# 关键:加上这句解决403问题
proxy_set_header origin http://127.0.0.1:11434;
proxy_pass http://127.0.0.1:11434;
# 关键:只保留一个 Origin 配置(用 * 测试,排除地址不匹配问题)
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Credentials "false" always;
# SSE 流式必需配置(补充 Ollama 所需的 Host 头)
proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
proxy_set_header Host $proxy_host; # 关键修改:传递目标服务的 Host(Ollama 可能校验)
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
# 优化 OPTIONS 预检请求(确保头信息完整)
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Credentials "false" always;
add_header Content-Length 0;
return 204;
}
# 强制隐藏 Ollama 自带的 CORS 头(彻底避免重复)
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
}