搭建私有LLM助手平台

​ 难得翻新了博客,正好把最近整的新活记录分享一下。

1 效果

​ 先看效果再讲过程,给缺耐心的读者节省时间。

1.1 类APP效果

安卓手机上的类app效果
open-webui首页效果

​ 很像移动端app客户端吧,实际上这是PWA。PWA(Progressive Web App,渐进式网络应用)是一种结合了网页和原生应用优势的新型应用形态。它通过现代Web技术实现以下核心特性:

  1. 渐进增强
    所有用户都能访问基础网页功能,但支持现代浏览器的用户可获得增强体验(如离线访问、推送通知等)。
  2. 响应式设计
    自适应不同设备屏幕,提供无缝的移动体验。
  3. 离线支持
    通过Service Worker缓存资源,即使网络中断也能访问关键内容。
  4. 推送通知
    类似原生应用,可向用户发送实时通知(需用户授权)。
  5. 可安装性
    用户可将PWA添加到主屏幕,形成类似原生应用的快捷方式(无需应用商店)。
  6. 性能优化
    通过缓存策略和资源预加载,实现接近原生应用的加载速度。
特性 传统Web应用 原生应用 PWA
离线访问 不支持 支持 支持(通过缓存)
推送通知 不支持 支持 支持
安装方式 无需安装 需下载安装 可添加到主屏幕
跨平台兼容性 依赖浏览器 平台专属 一次开发多端运行
开发成本 高(需多平台开发) 中(Web技术)

​ open-webui毕竟是基于web的UI,open-webui添加PWA支持前,我也一直在浏览器里使用。不过,私用的LLM助手,就算有鉴权、CDN、防DDos,我也不会贴公网url就是了,非工业方案薄弱环节应该还是挺多的。

1.2 chat界面效果

chat效果1
chat效果2
chat效果3
chat效果4
chat效果5
chat效果6
  • chat界面右上角能点开参数配置面板,只影响当前会话,有大量LLM推理参数暴露。实际使用场景中,除了用Temperature调整文本生成的多样性倾向,我几乎不会调整这些参数。

  • chat界面左上角能点开模型选择列表,写文这会儿我的默认模型是本地的qwen3-14B,外部API接入模型和本地部署模型可以在同一个会话中交替选用,会话上下文对新加入会话的模型也保持可见。模型列表的搜索框也可以拉取ollama hub的新模型在本地部署。

  • MCP工具我多给点镜头,“+”号可以开启关闭模型可见的工具。所谓MCP(Model Context Protocol,模型上下文协议)就是令工具提示词使用标准的结构化形式,固化到工具中,同时这个工具调用形式在LLM训练阶段参与多任务模型微调。然而模型看着对话上下文和可用工具列表,最终怎么选用工具,就取决于具体模型的工具规划能力了。这里我用查询明天武汉天气作为例子,展示推理类模型利用时间查询工具和高德地图的城市天气查询工具,解决用户需求的过程。为什么我要分2步问?因为2次工具调用有顺序依赖关系,这类似一个工作流或者所谓的agentic任务规划过程。

  • 能看到“+”号点开后能截图和上传文件,发送按钮左边有个语音输入按钮,支持多模态输入。

  • 除了可以使用工具引入实时数据,也能用固定提示词和搜索引擎接口引入联网搜索结果作为上下文信息。实测这基本可以跟搜索引擎类MCP工具等同,但默认的联网搜索和RAG会在更大召回范围内先做文本编码,可以认为检索“视野”更宽,召回能力更强一些。

  • 代码解释器我还没玩过,看了下配置选项,是python沙箱,我参与的大工程比较多,不太有代码片段可以玩沙箱。

  • 模型响应框下方有TTS、重新生成、结果(Markdown格式)复制等等按钮,还挺全的。

  • 截图右上角能看到我用的是蜂巢网络(中国移动运营商),没开梯子,所以理论上来说,有网的地方我都能用这个私有LLM助手平台。

  • 右下方有open-webui的版本更新提示,拉新镜像重新部署一下就能更新。

1.3 平台配置效果

平台配置效果1
平台配置效果2
平台配置效果3
平台配置效果4
平台配置效果5
平台配置效果6
平台配置效果7
平台配置效果8
平台配置效果9
平台配置效果10
  • 首先open-webui有用户、鉴权、角色、用户组管理功能。私用嘛,现在整个平台就我一个用户。

  • 外部连接有两个用途,以OpenAI格式(注意这是LLM厂商的接口风格,跟OpenAPI的通用接口规范不是一回事)对接厂商模型API,以ollama格式对接本地部署模型。如果厂商不是OpenAI格式呢?后面我会讲。

  • 模型配置里,可以看到所有的外部接入、本地部署模型,可以单独配置模型是否启动、默认推理参数、系统提示词、默认可用工具列表、是否显示usage等配置项。右上角的下载按钮点开可以跟ollama交互增删本地模型。

  • 工具配置里可以给每个工具配置OpenAPI规范的API,可以看到其实我也配了有搜索引擎性质的github、arxiv、searxng MCP工具,来引入实时的领域或开放信息。如果工具不是OpenAPI规范呢?后面我会讲。

  • open-webui的这种工具接入形式,目前还只能扩展上下文,还属于RAG的变种形式,如果想实现agent,做多阶段的工具链规划与调用,需要用到open-webui的pipeline配置。这个我还没玩过,个人使用,还没那么多固化的复杂工作流想配。

  • 联网搜索能配相当多的搜索引擎接口类型,我是用自己部署的searxng这个元搜索引擎(能个性化定制、自行整合多个搜索引擎的结果)。

  • 代码解释器配置,能看到是两个python轻量沙箱,这类沙箱的通病是跑跑抽象代码片段还行,依赖包一复杂,工程体量一大就麻烦了,毕竟不是conda、docker这种能做一定程度环境隔离的重沙箱。

  • 提示词配置,是PE(Prompt Engineering,提示词工程)时代遗留的功能,我是没使用场景的。果然模型迭代一快,大量提示词都要变成摆设,而模型一定会快速迭代。

  • 语音功能配置,用来配语音转文本模型和文本转语音模型。我不太爱靠说话交流,保留默认配置。

2 契机与遗憾

​ 2024年11月左右,我终于有了自己梦寐以求的N卡台式机和NAS。彼时开源生态支持完善的qwen2.5刚发布不久,我想把这些烧钱设备变成生产力的热情高涨。

​ 我的显卡是NVIDIA 4070Ti Super,NAS是群晖DS224+。现在看这两个型号选择都留了点暗坑,后来人引以为戒:

  • 4070tis虽然算力OK,但显存只有16GB,复现不了工业场景常用的24GB显存(比如4090卡)轻量部署方案。通常只够部署14B规模(140亿参数量,模型文件大概9GB出头,模型推理占显存10GB出头)的模型。模型参数量到显存空间的换算过程我在这里先不展开聊。

  • DS224+这NAS算群晖最新的入门款,2开头就是2盘位,够用,何况之前我为了从复旦的NLP实验室搞语料买了个16T黑盘,虽然不能用来组RAID但可以用来外接扩展。坑在于我家明明做了全屋超6类网线,具备户内万兆通信的基础,可这款NAS只有2个1Gb的网口,也就是最多做双通道的千兆通信,顶天2千兆带宽,读写大文件时,NAS网口成IO瓶颈了┓(´∀` )┏。

3 核心方案选型

​ 我长期在微博上跟进AI开源圈的消息,爱可可-爱生活(号主是北邮模式识别实验室的陈老师)、黄建同学(前微软高工)、宝玉xp(前微软高工),都是不错的大V,2024年整年AI开源壳工具层出不穷。

​ 优秀、高性能的通用本地LLM推理工具ollama脱颖而出,迅速占据本地LLM推理事实开源方案标准的地位。ollama的配套生态迅速发展,它的官推UI壳open-webui也进入我的视线,后来这个UI壳更是迅速扩展成了相当流行的通用AI壳甚至是小规模AI应用平台(能chat,能多模态交互,能建工作流,能RAG,能接工具包括后来的MCP工具)。

​ 于是,ollama搞定部署、open-webui搞定前后端的方案主体敲定了。

4 完善方案

4.1 痛点

​ 本地部署ollama和open-webui很顺利,完成demo验证后,一些瓶颈问题浮现:

  1. 各家厂商的开源模型中,我希望本地部署体验的模型很多,会迅速占用大量磁盘空间;

  2. 我当然希望在上班或者不在家时也能随时随地使用LLM助手;

  3. 除了本地部署的算力受限的几种开源模型,必要的时候我也希望在同一个应用里白嫖厂商开放的LLM;

  4. 我的PC不可能永远用来跑模型,有时会关机,有时会用来切win系统打游戏(我装了双系统,win用来娱乐,ubuntu用来开发和当服务器)。

4.2 解法

需求 解决方案 备注
本地模型存储 NAS NFS挂载 空间不受限,RAID可靠,14B模型初次换入加载在1min左右耗时可接受。
外网暴露 frp + 自有域名 + 自有VPS frp写文时已经94.3k star的golang开源老牌内网穿透工具,有自有域名和VPS的情况下,比运营商固定IP灵活很多,性能要求不苛刻,实测时延几乎不可感知。
模型API归一化 one-api 写文时已经25.4k star的开源LLM API 管理 & 分发系统,适配了国内外几乎所有让人有接入意愿的通用LLM厂商API。
部署拓扑 见下图 ollama在PC,open-webui、frpc在NAS,frps、one-api、nginx、ssl在VPS,兼顾可维护性、网络可达要求、VPS轻量原则。

BipedalBit Open-WebUI部署拓扑

5 实现详情

5.1 PC

​ 按照我的个人习惯,给PC装双系统,win平台娱乐,ubuntu系统下做开发,也适合作为服务长期运行(例如我出门在外时)的服务器环境。

5.1.1 NAS挂载

安装NFS客户端

1
2
sudo apt update
sudo apt install nfs-common

挂载NFS共享目录

1
2
3
sudo mount -t nfs <NFS服务器IP>:/<共享路径> /<本地挂载点>
# 示例
# sudo mount -t nfs 192.168.1.100:/shared_folder /mnt/nfs
  • <NFS服务器IP>:NFS服务器的IP地址
  • /<共享路径>:NFS服务器上共享的目录路径
  • /<本地挂载点>:Ubuntu系统上用于挂载的本地目录(需提前创建)

检查挂载点

1
ls /mnt/nfs

配置开机自动挂载

​ 编辑 /etc/fstab 文件,添加以下行:

1
<NFS服务器IP>:/<共享路径>  /<本地挂载点>  nfs  defaults  0  0

示例:

1
192.168.1.100:/shared_folder  /mnt/nfs  nfs  defaults  0  0

5.1.2 ollama

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
services:
ollama:
volumes:
- ${OLLAMA_DATA_DIR-~/workspace/docker-data/ollama-data}:/root/.ollama
container_name: ollama
pull_policy: always
tty: true
restart: unless-stopped
image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ['0']
capabilities: [gpu]
ports:
- ${OLLAMA_WEBAPI_PORT-11434}:11434
environment:
- 'NVIDIA_VISIBLE_DEVICES=all'
- 'CUDA_MPS_ENABLE=1'
- 'CUDA_CACHE_PATH=/tmp'
# - 'CUDA_LAUNCH_BLOCKING=1'
- 'OLLAMA_MAX_GPU_MEMORY=16000'
- 'OLLAMA_USE_GPU=1'
- 'HTTP_PROXY=http://****:10809'
- 'HTTPS_PROXY=http://****:10809'
# 容器内忽然无法使用GPU问题,已经用以下方案解决:
# 在 /etc/docker/daemon.json 中添加 "exec-opts": ["native.cgroupdriver=cgroupfs"]
# 等待5s防止nfs未挂载完成
entrypoint: ["/bin/bash", "-c"]
command: ["sleep 5 && exec /bin/ollama serve"]
  • pull_policy: always让我每次重启ollama时都能自动更新。
  • HTTP_PROXY=http://****:10809HTTPS_PROXY=http://****:10809是在配代理,从ollama的模型hub拉开源模型是ollama自身的行为,所以它需要用梯子的。配置的是局域网里的梯子v2ray客户端地址,比如,可以在NAS上。
  • OLLAMA_MAX_GPU_MEMORY=16000是因为我显卡显存16GB,大家看自己情况调整。
  • /etc/docker/daemon.json中添加"exec-opts": ["native.cgroupdriver=cgroupfs"]解容器偶发掉卡问题还挺难查的。
  • 用sleep解重新开机时容器启动早于NFS挂载就绪的问题挺偷懒的,我觉得应该有更优雅的解法。

5.2 NAS

​ 群晖NAS让我很喜欢的一点就是可以玩docker容器,甚至docker-compose项目也支持。这样一些希望常驻且磁盘IO或容量要求高的应用就可以部署在NAS上了。

5.2.1 v2ray(客户端)

​ 2025年海外docker镜像仓库包括docker官方仓库都在国内有访问障碍,为了方便拉镜像做部署,最简单的解决方案就是利用VPS搭个梯子了,有现成梯子当然也可以用。早些年,梯子客户端还是shadowsocks,这两年我也紧跟时代换成了v2ray生态的各种变种客户端和clash,个人感觉相比起来v2ray会比clash上手更方便、灵活一些。
​ 首先要准备一个目录,来装几个必要文件:

1
2
3
4
5
6
7
8
9
v2ray
├── localtime
├── timezone
└── v2ray
├── config.json
├── geoip.dat
├── geoip.metadb
├── geoip-only-cn-private.dat
└── geosite.dat
  • localtime、timezone是为了保证v2ray客户端和服务端的时钟保持一致,不一致通信会不通的。起容器时会挂载进去替换docker镜像里的原始文件。
  • v2ray子目录里是v2ray客户端依赖的资源文件,其中geoip是各种网络局域辅助判定规则集,跟GFW打交道的。
  • v2ray/config.json是v2ray客户端的核心配置文件,通常可以用图形界面的v2ray客户端配置完后复制出来用,或者网上找一份改改用。大概长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
{
"log": {
"access": "",
"error": "",
"loglevel": "warning"
},
"inbounds": [
{
"tag": "socks",
"port": 10808,
"listen": "0.0.0.0",
"protocol": "socks",
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
],
"routeOnly": false
},
"settings": {
"auth": "noauth",
"udp": true,
"allowTransparent": false
}
},
{
"tag": "http",
"port": 10809,
"listen": "0.0.0.0",
"protocol": "http",
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
],
"routeOnly": false
},
"settings": {
"auth": "noauth",
"udp": true,
"allowTransparent": false
}
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "****",
"settings": {
"vnext": [
{
"address": "****",
"port": ****,
"users": [
{
"id": "****",
"alterId": ****,
"email": "****",
"security": "auto"
}
]
}
]
},
"streamSettings": {
"network": "tcp"
},
"mux": {
"enabled": false,
"concurrency": -1
}
},
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
{
"tag": "block",
"protocol": "blackhole",
"settings": {
"response": {
"type": "http"
}
}
}
],
"dns": {
"hosts": {
"dns.google": "8.8.8.8",
"proxy.example.com": "127.0.0.1"
},
"servers": [
{
"address": "223.5.5.5",
"domains": [
"geosite:cn",
"geosite:geolocation-cn"
],
"expectIPs": [
"geoip:cn"
]
},
"1.1.1.1",
"8.8.8.8",
"https://dns.google/dns-query",
{
"address": "223.5.5.5",
"domains": [
"bipedalbit.net"
]
}
]
},
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "proxy",
"domain": [
"domain:googleapis.cn",
"domain:gstatic.com"
]
},
{
"type": "field",
"port": "443",
"network": "udp",
"outboundTag": "block"
},
{
"type": "field",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"type": "field",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"type": "field",
"outboundTag": "direct",
"ip": [
"223.5.5.5/32",
"223.6.6.6/32",
"2400:3200::1/128",
"2400:3200:baba::1/128",
"119.29.29.29/32",
"1.12.12.12/32",
"120.53.53.53/32",
"2402:4e00::/128",
"2402:4e00:1::/128",
"180.76.76.76/32",
"2400:da00::6666/128",
"114.114.114.114/32",
"114.114.115.115/32",
"180.184.1.1/32",
"180.184.2.2/32",
"101.226.4.6/32",
"218.30.118.6/32",
"123.125.81.6/32",
"140.207.198.6/32",
"geoip:cn"
]
},
{
"type": "field",
"outboundTag": "direct",
"domain": [
"domain:dns.alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:doh.360.cn",
"domain:dot.360.cn",
"geosite:cn",
"geosite:geolocation-cn",
"domain:bipedalbit.net"
]
},
{
"type": "field",
"port": "0-65535",
"outboundTag": "proxy"
}
]
}
}

​ 接下来是docker-compose.yaml:

1
2
3
4
5
6
7
8
9
v2ray-client:
mage: v2fly/v2fly-core
container_name: v2ray-client
restart: unless-stopped
network_mode: host
volumes:
- ./v2ray:/etc/v2ray
- ./localtime:/etc/localtime
- ./timezone:/etc/timezone

5.2.2 open-webui

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
open-webui:
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
container_name: open-webui
pull_policy: always
volumes:
- .:/app/backend/data
depends_on:
- ollama
ports:
- 8080:8080
extra_hosts:
- host.docker.internal:host-gateway
environment:
- 'HTTP_PROXY=http://****:10809'
- 'http_proxy=http://****:10809'
- 'HTTPS_PROXY=http://****:10809'
- 'https_proxy=http://****:10809'
- 'OLLAMA_BASE_URL=http://****:11434'
- 'WEBUI_SECRET_KEY='
- 'WEBUI_URL=https://****'
- 'ENABLE_WEBSOCKET_SUPPORT=true'
restart: unless-stopped

5.2.3 frpc

1
2
3
4
5
6
7
frpc:
image: snowdreamtech/frpc
container_name: frpc
restart: unless-stopped
network_mode: host
volumes:
- ./frpc.toml:/etc/frp/frpc.toml

​ frp客户端配置文件frpc.toml长这样:

1
2
3
4
5
6
7
8
9
10
serverAddr = "bipedalbit.net"
serverPort = ****
auth.token = "****"

[[proxies]]
name = "open-webui"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 8080

​ 不清楚的地方可以看frp官方文档

5.2.4 mcpo2

mcpo是open-webui团队发起的开源项目,用python FastAPI框架写的,旨在快速启动OpenAPI规范接口的mcpserver,有鉴权功能,自解释、更方便部署在云上,方便各种平台接入。贴几张图让大家理解下OpenAPI的好处:

OpenAPI格式的MCP服务 /docs
OpenAPI格式的MCP服务 接口列表
OpenAPI格式的MCP服务 接口详情

​ NAS上这个mcpo服务跟VPS上的从部署方式看一模一样,用来对接只限国内的mcp工具服务,比如高德地图。

​ 我魔改了一下原镜像,换了个启动脚本,方便用环境变量配API_KEY:

1
2
3
4
5
6
#!/bin/bash
# @Author: zhangyipeng1
# @Date: 2025-05-15 18:12:11
# @Last Modified by: wps
# @Last Modified time: 2025-05-15 18:12:23
mcpo --config /app/config.json --api-key "$API_KEY"

​ 通过套壳Dockerfile把启动脚本放入镜像:

1
2
3
4
5
6
FROM ghcr.io/open-webui/mcpo:main

COPY ./start.sh /app/start.sh
RUN chmod +x /app/start.sh

ENTRYPOINT ["/app/start.sh"]

​ mcp配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"mcpServers": {
"amap-maps": {
"args": [
"-y",
"@amap/amap-maps-mcp-server"
],
"command": "npx",
"env": {
"AMAP_MAPS_API_KEY": "****"
}
},
"time": {
"args": [
"mcp-server-time",
"--local-timezone=Asia/Shanghai"
],
"command": "uvx"
},
"fetch": {
"args": ["mcp-server-fetch"],
"command": "uvx"
}
}
}

​ 接下来是docker-compose.yaml,不指定镜像而是从Dockerfile构建镜像:

1
2
3
4
5
6
7
8
9
10
11
services:
mcpo:
build:
context: .
dockerfile: Dockerfile
ports:
- "8090:8000"
environment:
API_KEY: ****
volumes:
- ./config.json:/app/config.json

5.3 VPS

​ 如果只打算维护一台云主机,那么选择一个海外VPS更能满足全球资源拉取、全球信息获取、搭建全球化应用的需求。VPS厂商挺多的就不细说了,我选了个洛杉矶的机房,全球主流资源访问速度和回国网速都不错。

​ VPS也装ubuntu,开发环境尽量跟PC保持统一,给自己降低心智负担。

5.3.1 v2ray(服务端)

一个很方便的v2ray服务端一键部署工具

5.3.2 frps

1
2
3
4
5
6
7
8
9
10
services:
frps:
image: snowdreamtech/frps
container_name: frps
restart: always
ports:
- "127.0.0.1:8080:8080"
- "****:****"
volumes:
- ~/frp/frps.toml:/etc/frp/frps.toml

​ frp服务端配置文件frps.toml长这样:

1
2
bindPort = ****
auth.token = "****"

​ 不清楚的地方可以看frp官方文档

5.3.3 one-api

​ one-api是用来汇聚各厂商的LLM接口,将各种厂商LLM接口统一为OpenAI格式,方便统一接入和灵活切换,渠道管理功能挺完善的:

one-api

​ 但是需要注意:

  • 如果把one-api部署在海外,可能连不上部分国内厂商的接口,例如阿里的百炼平台。
  • VPS上用容器部署服务时,如果不打算让服务直接暴露到外网,务必注意暴露端口要带上ip限制,例如127.0.0.1:3000:3000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
version: '3.4'
services:
redis:
image: "${REGISTRY:-docker.io}/redis:latest"
container_name: redis
restart: unless-stopped

db:
image: mysql:latest
container_name: mysql
restart: unless-stopped
environment:
TZ: "Asia/Shanghai"
MYSQL_ROOT_PASSWORD: "****"
MYSQL_USER: "oneapi"
MYSQL_PASSWORD: "****"
MYSQL_DATABASE: "one-api"
volumes:
- ~/mysql-data:/var/lib/mysql

one-api:
image: "${REGISTRY:-docker.io}/justsong/one-api:latest"
container_name: one-api
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
TZ: "Asia/Shanghai"
SQL_DSN: "oneapi:****@tcp(db:3306)/one-api?charset=utf8mb4"
REDIS_CONN_STRING: "redis://redis"
SESSION_SECRET: "****"
# NODE_TYPE: slave # 多机部署时从节点取消注释该行
# SYNC_FREQUENCY: 60 # 需要定期从数据库加载数据时取消注释该行
# FRONTEND_BASE_URL: "https://****" # 多机部署时从节点取消注释该行
volumes:
- ~/one-api:/data
depends_on:
- redis
- db
healthcheck:
test: >
wget -qO- http://localhost:3000/api/status |
grep -o '\"success\":\\s*true' > /dev/null && echo 'healthy' || echo 'unhealthy'
interval: 30s
timeout: 10s
retries: 3
command:
["sh", "-c", "env && exec /one-api"]

5.3.4 searxng

searxng是一个开源的元搜索引擎,能配置多个搜索引擎作为自己的数据源,保护隐私,无广告。

searxng

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
version: "3.7"
services:
searxng-redis:
container_name: searxng-redis
image: docker.io/valkey/valkey:8-alpine
command: valkey-server --save 30 1 --loglevel warning
restart: unless-stopped
networks:
- searxng
volumes:
- /root/searxng/valkey-data:/data:rw
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "1"

searxng:
container_name: searxng
image: docker.io/searxng/searxng:latest
restart: unless-stopped
networks:
- searxng
ports:
- "127.0.0.1:8081:8080"
volumes:
- /root/searxng/searxng:/etc/searxng:rw
environment:
- SEARXNG_BASE_URL=https://****/
- SEARXNG_REDIS_URL=redis://@searxng-redis:6379/0
- UWSGI_WORKERS=${SEARXNG_UWSGI_WORKERS:-4}
- UWSGI_THREADS=${SEARXNG_UWSGI_THREADS:-4}
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "1"

networks:
searxng:

​ 虽然挂载了一个目录,暴露了几个配置文件出来,但其实我也没修改过里面的配置。

5.3.5 mcpo1

​ 跟NAS上的mcpo2没什么大区别,只是里面配的mcp连海外工具。我只贴个配置文件吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
"mcpServers": {
"arxiv-mcp-server": {
"args": [
"tool",
"run",
"arxiv-mcp-server",
"--storage-path",
"/app/arxiv-data"
],
"command": "uv"
},
"fetch": {
"args": [
"mcp-server-fetch"
],
"command": "uvx"
},
"github": {
"args": [
"-y",
"@modelcontextprotocol/server-github"
],
"command": "npx",
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "****"
}
},
"mcp-server-firecrawl": {
"args": [
"-y",
"firecrawl-mcp"
],
"command": "npx",
"env": {
"FIRECRAWL_API_KEY": "****"
}
},
"searxng": {
"args": [
"-y",
"mcp-searxng"
],
"command": "npx",
"env": {
"SEARXNG_URL": "https://****"
}
},
"time": {
"args": [
"mcp-server-time",
"--local-timezone=Asia/Shanghai"
],
"command": "uvx"
}
}
}

5.3.6 nginx+ssl

​ 我VPS上部署的所有开放外网访问的服务,统一经由nginx暴露,顺便配上SSL证书。现在的SSL证书不像早年要么花钱要么搞自签的形式证书了,我们有了Let’s Encrypt这个SSL证书公益签发机构,还有certbot工具可以快速配置证书。现在自建站还花钱买SSL证书的我觉得就有点花冤枉钱了。

​ nginx配置文件我贴些open-webui相关的关键片段吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
server {
listen 443 ssl;
server_name ****;

ssl_certificate /etc/letsencrypt/live/****/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/****/privkey.pem;

charset utf-8;

location /ws/socket.io/ {
add_header 'Access-Control-Allow-Origin' 'wss://****';
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization, Upgrade, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Extensions';
add_header 'Access-Control-Allow-Credentials' 'true';

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'wss://****';
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization, Upgrade, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Extensions';
add_header 'Access-Control-Allow-Credentials' 'true';
return 204; # No Content
}

proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_cache_bypass $http_upgrade;

proxy_pass http://localhost:8080;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 3600s;
}
location / {
client_max_body_size 64m;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding gzip;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache off;
proxy_buffering off;
chunked_transfer_encoding on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 60s;

proxy_pass http://localhost:8080;
proxy_redirect off;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}

}

注意里面有些专门为websocket添加的配置,是趟过坑才成型的。

5.3.7 域名

​ 域名服务商也挺多的,负责维护域名归属关系,有资质能出具域名归属证书,国内的域名服务商还能代办域名备案。费用从一年十来块到一年几百块的都有,看自己需要选择就好。

5.3.8 DNS

​ 自建站有很多人爱用cloudfare做DNS、CDN、对象存储、AI网关,这家赛博活菩萨有很多免费云基建服务。我最早也用cloudfare做域名解析,后来发现在国内偶发不可用,可能国内白嫖的人太多,节点负载太高了。现在我用同样免费的DNSPod做域名解析,最早是独立DNS,现在被腾讯云吞了。

6 总结

​ 这一套操作下来,你应该也能搭一套自己的AI助手平台。即使是缺显卡,也能部署整套系统的主体部分(不要ollama,只要one-api甚至连one-api都不要,只配国内厂商的接口)。

​ 整个系统陆续添砖加瓦搭了几个月,想想踩了这么多坑,不记录下来确实挺可惜。

​ 时隔9年又开始写博客,写完才发现这篇文格外的长。欢迎爱折腾的同好讨论~


搭建私有LLM助手平台
https://blog.bipedalbit.net/2025/05/28/搭建私有LLM助手平台/
作者
Bipedal Bit
发布于
2025年5月28日
许可协议