简介:这份指南围绕 DeepSeek 的多平台部署展开,面向希望将大模型落地到实际环境的开发人员与技术爱好者,覆盖机器学习、深度学习与自然语言处理场景下的模型部署需求。内容以单份 docx 文档交付,压缩包约 15KB,共 1 个文件,便于快速查阅和按需检索。现有 1521 人学习,说明其在 AI 工具实践群体中具备一定参考价值。文档按平台组织操作路径:本地环境先安装 Ollama 并拉取运行 DeepSeek 模型;iPhone 通过快捷指令配合 API Key 完成调用;Android 借助 Termux 配置依赖、编译并启动 Ollama 服务;Open WebUI 方案则结合 Docker 与 Ollama 搭建可视化交互界面。各环节给出明确命令、配置顺序与验证方式,并提示可能遇到的问题及处理思路,适合初学者按步骤实践,也方便有经验的开发者快速比较不同部署方案的适用条件,为个人研究或业务集成提供可操作的参考。
1. 一条命令背后:模型文件之外还藏着什么
大多数人第一次执行ollama run deepseek-r1:7b时,会以为自己在“下载一个模型”。实际下载的只是几 GB 字节流,真正的重头戏在之后:这些字节要先被 mmap 映射进虚拟内存,再由推理引擎按层做矩阵乘,最后由一个常驻进程把生成结果切成 token 流式吐回终端。模型部署的难点从来不在“拿到文件”,而在权重的数值格式、内核能不能吃满你手里那块芯片、以及服务层怎么把一次对话拆成可中断的请求。把这三层拆开看,你才会明白为什么同一个模型在笔记本上秒回、在手机上卡成幻灯片、在容器里又连不上——这些现象背后是同一套机制在不同约束下的表现。
2. 权重格式与推理引擎:Ollama 凭什么用一行命令跑起 7B 模型
2.1 GGUF 与量化:14GB 的权重是怎么缩到 4GB 的
先把账算清楚。一个 7B 参数的模型,如果每个权重用 fp16(2 字节)存,光权重就是 7×10⁹×2 ≈ 14GB。这个体积对消费级设备是致命的:16GB 内存的笔记本装得下但没法留出 KV cache 的空间,手机更是直接出局。所以任何能在本地跑的模型,第一步都是量化——把浮点权重压成低比特整数。
量化的原理不复杂,但细节决定了画质。最朴素的做法是整层共用一个 scale:w_int = round(w / scale),反量化时再乘回去。问题是一层里权重的动态范围可能差好几个数量级,全局 scale 会被少数大权重绑架,小权重全被压成 0,模型直接“变傻”。所以工业实现普遍走分块量化:把连续的 256 个权重划成一个 super-block,块内单独算 scale 和 min,块与块之间互不干扰。Q4_K_M 这类名字里的K指的就是这套 k-quant 分块方案,M表示中等粒度混合——部分关键张量(比如 attention 的 value 投影)用更高比特,其余用 4 bit。
常见的实现思路大致是这样:一个 Q4_K 的 super-block 存 256 个 4 bit 权重(128 字节),外加 6 bit 的 scale 和 6 bit 的 min,摊下来约 4.5 bit/权重。7B 参数乘 4.5 bit 约 3.9GB,再加上不参与量化的 embedding 和输出层,最终落在 4.4GB 上下。
| 量化类型 | 每权重比特 | 7B 模型体积 | 质量损失(主观) |
|---|---|---|---|
| F16 | 16 | ~14GB | 无(基准) |
| Q8_0 | 8.5 | ~7.2GB | 几乎不可感知 |
| Q5_K_M | 5.7 | ~5.1GB | 轻微,长文推理略降 |
| Q4_K_M | 4.8 | ~4.4GB | 可接受,日常问答无感 |
| Q4_0 | 4.5 | ~3.9GB | 明显,代码与数学题掉点 |
| Q2_K | 2.6 | ~2.7GB | 严重,不建议生产用 |
选哪一档不是玄学,是内存预算倒推出来的:模型体积 + KV cache 必须小于可用内存的 70% 左右,否则系统会在你聊到第三轮时把进程杀掉。
GGUF 这个容器格式的设计动机也在这儿。它是一个单文件自描述格式:文件头写明版本和 tensor 数量,紧跟一段 key-value 元数据(架构名、层数、head 数、上下文长度、tokenizer 词表),然后是每个 tensor 的名字、维度、类型、偏移量,最后才是连续排布的权重数据。这样做的直接好处是可以 mmap——推理时不需要把 4GB 一次性读进 RAM,而是把文件映射到虚拟地址空间,用到哪一页就缺页加载哪一页。这就是为什么ollama run看起来“秒启动”:它只是打开了文件,真正耗时的是首次推理时把权重页逐块换入。
2.2 内容寻址存储:磁盘上的模型其实是 manifest 加一堆 blob
如果你去翻 Ollama 的数据目录,会发现里面没有deepseek-r1-7b.gguf这种直白命名的文件,而是一堆以sha256-开头的无后缀文件。这不是设计得晦涩,是在抄容器镜像那套内容寻址存储(CAS):文件名就是内容的哈希,内容变一个字节,名字就变。
# Linux/macOS 下的默认位置,Windows 在 %USERPROFILE%\.ollama\models tree -L 3 ~/.ollama/models # ├── blobs/ # │ ├── sha256-3f9a... # 模型权重层(最大的那个) # │ ├── sha256-8c21... # 模板与参数配置层 # │ └── sha256-a04e... # 许可证/说明等小层 # └── manifests/ # └── registry.ollama.ai/library/deepseek-r1/7b # 一个 JSON 清单manifest 的结构和 OCI 镜像清单几乎同构:
{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "digest": "sha256:8c21...", "size": 4821 }, "layers": [ { "mediaType": "application/vnd.ollama.image.model", "digest": "sha256:3f9a...", "size": 4401234567 } ] }config指向的那个小 blob 里存的是模板(chat template)、默认参数(temperature、stop 序列)和系统提示。layers里的 model 层才是 GGUF 数据。这样拆的好处是层可复用:同一个底座的不同微调版本,如果 tokenizer 和模板没变,共享同一份小层;甚至不同 tag 如果指向同一个权重 blob,ollama pull会直接跳过下载。这也是为什么你拉第二个相近模型时,速度会快得让人怀疑它没下载完。
ollama create加 Modelfile 的玩法就建立在这套结构上:
# Modelfile 里的 FROM 可以指向已有模型,PARAMETER 会写进新的 config 层 cat > Modelfile <<'EOF' FROM deepseek-r1:7b PARAMETER temperature 0.6 PARAMETER num_ctx 8192 SYSTEM "你是一个只讲实现细节的工程师" EOF ollama create my-r1 -f Modelfile这里的关键是:新模型不会复制 4.4GB 权重,它只是生成一份新 manifest,权重层仍然指向原来那个 blob 的哈希。改温度、改系统提示这种操作,磁盘成本接近零。理解了这一点,你就不会在磁盘快满时去删blobs里的“重复文件”——按哈希命名意味着物理上根本不存在重复。
2.3 从 HTTP 到 token:一次 /api/chat 请求在 11434 端口里走了什么
ollama run只是个 CLI 壳子,真正干活的是一个常驻的 HTTP 服务,默认监听127.0.0.1:11434。你可以直接拿 curl 打它,看到的东西比 CLI 更接近本质:
curl http://localhost:11434/api/chat -d '{ "model": "deepseek-r1:7b", "messages": [ {"role": "user", "content": "用一句话说清楚 KV cache 的作用"} ], "stream": true, "options": {"num_ctx": 4096, "temperature": 0.6} }'响应不是一段完整 JSON,而是一串换行分隔的 JSON 对象(NDJSON),每行带一个message.content片段,最后一行done: true并附上统计字段:
{"model":"deepseek-r1:7b","message":{"role":"assistant","content":"KV"},"done":false} {"model":"deepseek-r1:7b","message":{"role":"assistant","content":" cache"},"done":false} {"model":"deepseek-r1:7b","done":true,"total_duration":8123456789,"eval_count":37,"eval_duration":5210000000}eval_count / eval_duration就是常说的 token/s,这是判断“跑得动跑不动”最硬的指标,比任何主观感受都可靠。
一次请求在服务内部的路径大致是五步。第一步,HTTP 层解析请求体,校验模型名是否已在本地 manifest 里注册,没注册就直接返回 404,不会偷偷去下载。第二步,进入调度器:它检查目标模型是否已经加载,已加载就看keep_alive有没有过期(默认 5 分钟),过期就等待回收;未加载则拉起一个runner 子进程。第三步,runner 进程把 GGUF 映射进内存,按num_ctx预分配 KV cache,然后开始 prefill——把整段 prompt 一次性过一遍前向,填满 KV cache。第四步,逐 token 解码,每生成一个就通过管道回传给主进程。第五步,主进程把它包装成 NDJSON 写回 HTTP 响应体。
这里有两个容易被忽略的机制。其一,runner 是独立进程而非线程:模型加载失败、显存不足、推理崩溃,都只会杀掉 runner,主服务还活着,所以你能看到ollama ps里模型状态变了但 API 仍在响应。其二,KV cache 是预分配的:num_ctx=8192意味着即使你只问了一句“你好”,也有一块对应 8192 token 的缓存被占住。粗略估算:32 层、8 个 KV head、head_dim 128、fp16 存 K 和 V,8192 token 下约 2×32×8×128×8192×2 byte ≈ 1GB。这个数字直接解释了后面所有的“内存不够”问题——模型 4.4GB 只是入场券,KV cache 才是隐形开销。
提示:
OLLAMA_KEEP_ALIVE、OLLAMA_NUM_PARALLEL、OLLAMA_MAX_LOADED_MODELS这三个环境变量决定了调度器行为。并行数每加一,KV cache 就翻一倍,笔记本上把它设成 4 是自找麻烦。
3. 手机端的两条岔路:iPhone 为什么只发请求,Android 却要自己编译
3.1 快捷指令的本质:一个跑在系统里的 HTTP 客户端
iPhone 那条路线看起来最“不像部署”:安装一个快捷指令,填入 API Key,就能对话。很多人以为这是某种黑科技,其实拆开看,快捷指令就是一个plist 描述的动作序列,里面的核心动作叫“获取 URL 内容”,支持指定 method、headers 和 request body。换句话说,它做的就是把你的输入拼成一个 JSON,POST 出去,再把返回的 JSON 解析出文本显示出来。
请求体长这样,和你在电脑上 curl 的东西一模一样:
{ "model": "deepseek-r1", "messages": [ {"role": "system", "content": "回答尽量简短"}, {"role": "user", "content": "快捷指令里填的这段文字"} ], "stream": false }返回后,用“获取字典值”动作按路径choices.0.message.content取内容。stream: false是有意为之——快捷指令处理流式响应很别扭,不如等一次性返回。
这条路线能成立的前提是推理发生在远端。手机侧不加载任何权重,不占内存,不发热,代价是每次对话都要走网络,且你的输入文本离开了设备。这个取舍在工程上是合理的:一台中端手机的算力,跟一台带独显的机器差两个数量级,硬要在端上跑 7B 模型,体验会很差。快捷指令真正的价值在于系统级入口:它可以挂到分享菜单(在任意 App 里选中文字直接问)、挂到 Siri、放到主屏或锁屏小组件,把“打开网页、登录、输入”压缩成一次点击。这是纯 Web 页面做不到的事。
3.2 Termux 编译链路:为什么 Android 上必须自己 go build
Android 那条路完全是另一回事。官方没有发布 Android/arm64 的预编译二进制,所以你只能拿源码自己编。这不是作者偷懒,是编译链的约束:Ollama 的服务层是 Go,但推理内核是 C/C++ 写的(ggml 那一套),中间靠 cgo 连接。cgo 一旦介入,就不再是“设个 GOOS/GOARCH 就能交叉编译”的纯 Go 场景了——你得有匹配目标平台的 C 工具链、C++ 标准库、以及正确版本的链接器。
Termux 提供的正好是这样一个用户态环境:一个跑在 Android 上、不依赖 root 的 Linux 前缀,所有包都装在/data/data/com.termux/files/usr下。整个流程的第一步不是装依赖,而是拿存储权限:
# 申请访问共享存储,成功后会在 ~ 下建立 storage 符号链接 termux-setup-storage # pkg 是 apt 的包装,先同步索引再升级已有包 pkg update && pkg upgrade -y # 编译 Ollama 需要 git 拉源码、golang 编译、cmake 构建 C 部分、 # clang 提供 cgo 所需的 C/C++ 编译器 pkg install -y git cmake golang clang libjpeg-turbotermux-setup-storage的作用常被误解成“给 Termux 开权限”,实际上它做的是把/storage/emulated/0(也就是文件管理器里看到的内部存储)软链到~/storage,同时触发系统的运行时权限弹窗。不做这一步,git clone到~没问题,但你想把编译好的二进制或模型文件放到能被其他 App 访问的位置就会失败。
接下来是拉源码和编译:
# --depth 1 只取最新一次提交,仓库历史对编译毫无用处,能省几百 MB git clone --depth 1 https://github.com/ollama/ollama.git cd ollama # go generate 会跑代码生成器,产出 cgo 需要的头文件绑定和内嵌资源 go generate ./... # 单线程构建,手机上并行编译极易被系统 OOM Killer 干掉 go build -p 1 -o ollama .go generate ./...这一步不能跳。它执行的是源码里//go:generate注释标记的命令,负责生成 cgo 的桥接代码和把一些静态资源嵌进二进制。跳过它,go build会在链接阶段报一堆 undefined symbol,报错信息还完全不指向真正的原因。
编完之后启动服务并验证:
# 后台启动,& 让它脱离当前 shell ./ollama serve & # 根路径会返回一句存活提示,能拿到就说明 HTTP 层起来了 curl http://localhost:11434 # 拉模型、跑模型,命令与桌面端完全一致 ./ollama pull deepseek-r1:8b ./ollama run deepseek-r1:8b这里有个血泪经验:Android 的后台管理比桌面激进得多,&挂起来的进程在息屏后几分钟就可能被冻结甚至回收。跑长任务前先执行termux-wake-lock拿一个唤醒锁,用完termux-wake-unlock释放,否则你会遇到“明明启动成功了,切个 App 回来就 Connection refused”的诡异现象。
3.3 8B 模型塞进手机的物理上限:内存、带宽与 token/s
编译成功不等于能用。手机端本地推理的天花板由两个硬件指标决定,跟软件优化关系不大。
第一个是内存容量。8B 模型 Q4 量化约 4.9GB,加上 4K 上下文的 KV cache 约 0.5GB,再算上 Termux 自身、Android 系统、其他后台应用,实际需要 7GB 以上的可用内存。8GB RAM 的手机理论够,实际常常不够——Android 的 LMK(低内存杀手)会在可用内存跌破阈值时按优先级杀进程,你的ollama大概率排在列表前列。这就是为什么很多人第一次pull完模型,run的时候直接闪退,日志里什么都没有。
第二个是内存带宽。自回归解码每生成一个 token,都要把全部权重从内存读一遍(batch=1 时矩阵乘退化成向量乘,算力不是瓶颈,带宽才是)。手机 LPDDR5 的带宽在 30~60GB/s 量级,4.9GB 权重除以 50GB/s 约 0.1 秒,理论上限 10 token/s。实际还要扣掉散热降频、系统调度抖动、非连续访存的开销,落到 2~5 token/s 是常态。对比之下,同样模型在带独显的机器上跑 30+ token/s 毫不费力。
所以手机端的定位要摆正:它是“离线可用、隐私可控、随时能问一句”的方案,不是“替代电脑”的方案。真要在这个尺寸上追求响应速度,正确做法是把推理放到远端,手机只做入口——也就是 iPhone 那条路的思路。这也是为什么同一份部署指南里,两个平台给出了完全不同的技术路线:不是作者偏心,是硬件约束把人逼到了两个方向。
4. 前后端解耦:Open WebUI 与 Ollama 之间的那条网络边界
4.1 两个容器、两个网络命名空间:localhost 为什么会指错
Open WebUI 的部署命令看起来平平无奇,但里面每个参数都在解决一个具体的网络问题:
docker run -d \ -p 3000:8080 \ --add-host=host.docker.internal:host-gateway \ -v open-webui:/app/backend/data \ --name open-webui \ --restart always \ ghcr.io/open-webui/open-webui:main-p 3000:8080是端口映射:容器内的 WebUI 监听 8080(它自己的默认端口),映射到宿主机的 3000。浏览器访问localhost:3000时,流量进的是宿主机网卡,Docker 的 iptables 规则把它转给容器。
--add-host=host.docker.internal:host-gateway是这条命令里最关键、也最容易被抄漏的一项。容器有独立的网络命名空间,容器里的localhost指的是容器自己,不是宿主机。所以你在 WebUI 的配置里填http://localhost:11434,它找来的是 WebUI 容器内部那个 11434 端口——那里什么都没有,于是报连接失败。host.docker.internal是 Docker 提供的一个特殊主机名,host-gateway告诉它解析到宿主机的网关 IP,这条--add-host就是往容器的/etc/hosts里写一行映射。Linux 上不加这个参数,这个名字根本不存在。
还有一层坑:即使地址写对了,Ollama 默认也只绑127.0.0.1:11434。宿主机上跑着的 Ollama 收到来自 Docker 网桥(通常是172.17.0.1)的连接时,会因为监听地址不匹配而拒绝。解决办法是启动 Ollama 时设OLLAMA_HOST=0.0.0.0:11434,让它监听所有网卡。
# 桌面端让 Ollama 对外暴露,容器才连得进来 export OLLAMA_HOST=0.0.0.0:11434 ollama serve另一条更干净的路是把两个容器放进同一个自定义网络,用服务名互访,完全不碰宿主机:
docker network create ai-net docker run -d --name ollama --network ai-net \ -v ollama:/root/.ollama -p 11434:11434 ollama/ollama docker run -d --name webui --network ai-net -p 3000:8080 \ -e OLLAMA_BASE_URL=http://ollama:11434 \ -v open-webui:/app/backend/data \ ghcr.io/open-webui/open-webui:main在ai-net里,Docker 内置的 DNS 会把ollama这个名字解析成对应容器的 IP,OLLAMA_BASE_URL直接指过去就行。这也是我现在更倾向的方式:不依赖宿主机的监听配置,换机器时只需改网络名。
4.2 OLLAMA_BASE_URL 与数据卷:WebUI 究竟保存了什么
-v open-webui:/app/backend/data这行决定了你的聊天记录能不能活过容器重建。镜像本身是无状态的,删掉容器再起一个新的,所有数据灰飞烟灭;挂上卷之后,/app/backend/data这个目录被持久化到宿主机,里面主要是三样东西:webui.db(SQLite,存用户、会话、消息、模型配置)、vector_db/(RAG 用的向量库,默认 Chroma)、以及上传的文件。
几个环境变量的作用值得单独说清楚:
| 变量 | 作用 | 常见取值 |
|---|---|---|
OLLAMA_BASE_URL | 后端调用 Ollama 的地址 | http://ollama:11434 |
OPENAI_API_BASE_URL | 接入 OpenAI 兼容端点时的地址 | 任意兼容服务 |
WEBUI_SECRET_KEY | 会话签名密钥,多副本部署必须一致 | 随机长字符串 |
DATA_DIR | 数据目录,默认/app/backend/data | 与挂载点保持一致 |
WEBUI_SECRET_KEY这一项在单机玩的时候无所谓,一旦你起两个副本做负载均衡,不统一这个值就会表现为“登录成功但立刻掉线”——JWT 是副本 A 签的,请求被转到副本 B 验证失败。这种问题排查起来非常费劲,因为它看起来像前端 bug。
4.3 点一下“下载模型”到底发生了什么:WebUI 对 /api/pull 的转发与 RAG 注入
WebUI 的管理界面里有个“拉取模型”的下拉框,选中 DeepSeek 点下载,进度条就开始走。这个过程里 WebUI 没有做任何下载逻辑,它只是替浏览器发了一个请求给 Ollama:
# WebUI 后端转发模型拉取请求的等价写法 import requests, json def pull_model(name: str): # Ollama 的 /api/pull 返回 NDJSON 流,每行一个进度事件 with requests.post( "http://ollama:11434/api/pull", json={"name": name, "stream": True}, stream=True, timeout=None, # 大模型下载可能几十分钟,不能设短超时 ) as resp: for line in resp.iter_lines(): if not line: continue evt = json.loads(line) # status 形如 "pulling manifest" / "downloading" # completed/total 是当前层的字节进度 print(f"{evt.get('status')} {evt.get('completed', 0)}/{evt.get('total', 0)}")关键点在于stream=True和timeout=None。Ollama 的拉取接口是按层流式返回进度的,如果后端用了默认超时,一个大模型下到一半连接就被掐断,前端看到的进度条会卡在某个百分比然后报错。很多人以为是网络问题,其实只是超时配置。
WebUI 真正增值的地方在编排层,不是皮肤。它把模型当成可路由的资源:你可以在同一个界面里挂本地 Ollama、远端兼容端点、甚至多个不同尺寸的模型,按会话切换。它还维护了 RAG 链路——上传的文档被切片、调用 embedding 模型转向量、塞进向量库,每次提问先检索 top-k 片段拼进 prompt。这条链路里的 embedding 调用同样走 Ollama 的/api/embeddings,所以一个nomic-embed-text之类的小模型是必需品,只拉对话模型会发现文档问答功能用不了。理解了这一层,你就知道 WebUI 为什么值得单独跑一个容器:它是有状态的编排器,和纯算子的 Ollama 职责完全不同。
5. 部署避坑:五条在终端里真实翻车的记录
5.1 拉模型卡在 pulling manifest 或者下到 90% 断掉
现象:ollama pull运行几十分钟,进度条走到某个百分比后长时间不动,最终报unexpected EOF或直接退出,重跑又从零开始。
原因:拉取是按层分块下载 + 边下边校验 sha256 的。任何一块校验不过,整个层作废。网络抖动、磁盘写满、或者目标分区剩余空间不足(blob 需要完整落盘后才改名)都会触发。Ollama 的断点续传能力有限,某些版本对中断的临时文件处理不干净。
解决:先df -h看模型目录所在分区的剩余空间,至少要留出模型体积 1.5 倍;再确认~/.ollama/models/blobs里没有残留的巨大临时文件;最后重试,网络不稳就换时间段。实在反复失败,用ollama pull拉一个更小的量化 tag 验证链路,能成功说明是体积问题不是网络问题。
5.2 模型加载到一半进程消失,日志里什么都没有
现象:ollama run后终端打印几行加载信息就退出,dmesg里能看到Out of memory: Killed process,但 Ollama 自己的日志干净得像没事发生。
原因:Linux 的 OOM Killer 在内存耗尽时按 oom_score 杀进程,被选中时进程收到 SIGKILL,没有任何机会写日志。触发点通常是“模型权重 + 预分配 KV cache”超过了可用内存,尤其是num_ctx设得很大或者OLLAMA_NUM_PARALLEL大于 1 的时候。
解决:把num_ctx从默认值降到 2048~4096 试一次;换更低的量化档(Q4_0 比 Q4_K_M 少几百 MB);确认OLLAMA_NUM_PARALLEL=1。用free -h在加载前看 available 而不是 free,page cache 是可回收的。如果是在 Docker 里跑,还要检查容器有没有内存上限(docker stats看 LIMIT 那一列)。
5.3 容器里的 WebUI 死活连不上 Ollama
现象:WebUI 界面能打开,但一发消息就报错,日志里是Connection refused或Max retries exceeded,而宿主机上curl localhost:11434一切正常。
原因:两个独立问题叠加。一是容器内的localhost指向容器自身,不是宿主机;二是即使地址写对了,Ollama 默认只绑127.0.0.1,来自 Docker 网桥的连接会被拒。
解决:优先用自定义网络 + 服务名(http://ollama:11434),一步解决解析和监听两个问题。只能用 host 模式时,两边都要改:Ollama 侧设OLLAMA_HOST=0.0.0.0:11434,WebUI 侧加--add-host=host.docker.internal:host-gateway并把地址改成http://host.docker.internal:11434。改完在容器里curl一下验证,别靠界面报错猜。
5.4 Termux 里 go build 报一堆 undefined symbol
现象:go build跑到链接阶段失败,错误里全是undefined reference to之类,指向一些看起来像 C 函数的名字。
原因:漏了go generate ./...。cgo 的桥接代码不是手写的,是生成出来的;不生成,Go 侧和 C 侧的函数签名对不上,链接器自然找不到符号。另一种可能是pkg install clang没装或者装了一半。
解决:严格按pkg install clang→go generate ./...→go build -p 1 -o ollama .的顺序走,一步不跳。-p 1限制并行编译数,手机上并行编译的内存峰值很容易触发 OOM,报错信息还会伪装成编译器内部错误。编译失败时先清go clean -cache再来一遍,别在脏缓存上反复试。
5.5 tag 写错,拉到的模型和预期不是一个东西
现象:文档里一处写deepseek-r1:7b,另一处写deepseek-r1:8b,两条命令都能跑,但一个 4.4GB 一个 4.9GB,跑起来内存表现完全不同;或者直接报manifest unknown。
原因:模型 tag 是精确匹配的,:7b和:8b是两个独立 manifest,指向不同的权重 blob。manifest unknown说明这个 tag 在注册表里根本不存在,可能是版本号记错了或者模型已改名。
解决:拉之前先ollama list看本地已有的,用ollama show <model>看参数量和量化类型确认是不是你要的。写部署脚本时把模型名抽成变量,别在多处硬编码。移动端尤其要注意,8B 在手机上比 7B 更容易被系统杀掉,文档里的两处不一致不是笔误就是历史遗留,按自己设备的实际内存选小的那一档。
6. 取舍:把推理放在本地还是远端,取决于你愿意付哪一笔账
走完这几条路线,会发现它们不是“同一个东西的不同装法”,而是三个不同位置的取舍。本地 Ollama 买的是数据不出机器,付的是内存、磁盘和一次性的环境配置成本;手机快捷指令买的是随时随地可用,付的是网络依赖和文本离开设备;Open WebUI 加容器买的是多模型编排和知识库,付的是多一层网络边界的排查成本。
| 维度 | 本地 Ollama | 手机端(远端 API) | Open WebUI + 容器 |
|---|---|---|---|
| 数据位置 | 完全本地 | 出端,到服务方 | 本地(含本地向量库) |
| 首次成本 | 装运行时 + 拉 GB 级权重 | 填一个 Key | 两个镜像 + 网络配置 |
| 响应速度 | 取决于本机带宽与算力 | 取决于网络 RTT | 同本地,多一层转发 |
| 离线可用 | 是 | 否 | 是 |
| 主要故障点 | 内存不足、量化选错 | Key 失效、超时 | 容器网络、监听地址 |
我一般会这么判断:如果对话内容涉及不能外发的资料,本地这条路没有替代品,剩下的只是把量化档位和num_ctx调到不 OOM 的平衡点;如果只是想要一个随手能问的入口,手机端那条最省事,但要清楚它本质是远端推理的封装,别指望断网还能用;如果需求是“团队共用一套、能传文档问”,那 WebUI 的编排价值就压过了它的部署复杂度。
验证是否真的搭对了,别只看界面能不能打开。三个动作按顺序做:curl http://<地址>:11434/api/tags能返回模型列表,说明服务和注册都正常;curl打一次/api/chat看返回里eval_count/eval_duration的比值,这是你这套环境的真实吞吐;最后跑一个长上下文的问题(比如让它总结一篇几千字的文档),看内存曲线会不会顶到天花板——短对话能跑、长上下文崩,是最常见的“假成功”。
从那以后我每次部署完,都会强制走一遍这三步再交付,不再相信“界面出来了就是好了”。很多所谓的环境问题,其实是在第一步就没通,只是被前端友好的错误提示盖住了。
希望帮到你。
本文还有配套的精品资源,点击获取