本地大模型升级 Qwen3.8 27B,接入 DeepSeek Harness,用 Cloudflare 开到公网
上一篇把 Qwen3.6 27B 跑在了 RX 7900 XTX 上,这套本地推理栈稳定跑了一段时间。这篇记三件事:模型升到 Qwen3.8 27B、接入 DeepSeek Harness(dsh)、再用 Cloudflare Tunnel + Access 把 Agent 安全开到公网。
一、Qwen3.6 27B 升 Qwen3.8 27B
升级最省事。整套链路是 OpenAI 兼容 API,模型换代对上层客户端基本透明。
换模型文件
新模型同样选 abliterated(去拒绝化)版本 + Q4_K_M 量化:
1 | 模型文件: huihui-qwen3.8-27b-abliterated-q4_k_m.gguf |
27B、Q4_K_M、16.8GB,放在 24GB 显存上仍然安全,沿用上一篇算好的预算(模型 ~16.8GB + KV cache q8_0 64K ~3.8GB + ROCm 开销 ~1-2GB,约 22-23GB),不用重新调参。
重启 llama-server
服务指向新文件、改下 alias:
1 | # /etc/systemd/system/llama.service(节选) |
1 | sudo systemctl restart llama.service |
上层客户端改 alias
端点是 OpenAI 兼容的 http://localhost:1234/v1,Hermes Agent 的 custom provider 里把 model 字段从 qwen3.6-27b 改成 qwen3.8-27b 就行,baseURL、apiKey 都不用动。
升级成本:一个模型文件 + 一行 alias。OpenAI 兼容 API 的好处就在这,模型层可以独立迭代,上层无感。
二、接入 DeepSeek Harness(dsh)
dsh 是 DeepSeek 开源的 Agent 编排框架(TypeScript,MIT,开发者预览版,会有 breaking changes)。它不是推理引擎,是编排层:工具、会话、沙箱、UI。本地模型(llama.cpp / vLLM)作为 OpenAI 兼容 provider 插进来。
dsh 还在
0.1.0-rc阶段,接口和配置会频繁变动,生产用建议锁版本。
dsh 只绑 127.0.0.1
dsh 出于安全考虑拒绝 --host 0.0.0.0,只能绑 127.0.0.1。但我想在局域网/公网访问它的 Web UI。解法:dsh 跑内部端口,nginx 代理对外端口。
两个容器(compose 编排):
| 容器 | 镜像 | 作用 |
|---|---|---|
dsh |
node:22-alpine | dsh 本体,host 网络,绑 127.0.0.1:3081 |
dsh-proxy |
nginx | 对外 192.168.x.x:3080,转发到 127.0.0.1:3081 |
dsh 启动命令:
1 | node --expose-internals /usr/local/bin/dsh web \ |
浏览器信任 403
setup 里最磨人的一段,nginx 改了 4 次才搞定。
dsh 对特权 RPC 方法(比如 settings.describe)有一道 isTrustedApiRequest 信任栅栏,要求同时满足:
Host头是 loopbacksec-fetch-site不是 cross-siteOrigin的 host 等于Host的 host
请求过一遍 nginx 后,Host 和 Origin 变成了公网地址,栅栏判定失败,403。
修复:在 nginx 的 /api/ location 里把 Host 和 Origin 都重写成 loopback:
1 | location /api/ { |
验证栅栏有没有过:
- 不带
Origincurl,200 - 带
Origin: http://<public>curl,403(栅栏拦了) - 修复后,400 “body is not JSON”(栅栏过了,400 只是 RPC 层在校验合成 body,正常)
bind mount 的 inode 陈旧
改了 nginx.conf 却没生效?因为覆盖写入 bind-mounted 文件时,容器里挂的还是旧 inode,你改的是宿主机新文件,容器看的是旧文件。
1 | # 宿主机 vs 容器内文件大小不一致,就是 inode 问题 |
改 bind mount 的配置后,docker restart 对应容器,再在容器内 grep / wc -c 验证,别只看宿主机。
配置 provider(in-container)
dsh 的配置在容器内:
/root/.dsh/settings.yaml,provider + 默认模型/root/.dsh/.credentials.yaml,实际的 API key 值
1 | # /root/.dsh/settings.yaml(节选) |
1 | # /root/.dsh/.credentials.yaml |
本地 llama.cpp 的 apiKey 其实可以随便填(它不校验),但 dsh 要求这个字段非空,给个占位值就行。
前端小坑:crypto.randomUUID
dsh 前端用到了 crypto.randomUUID,某些环境下缺失。要往 dsh-web-frontend/dist/index.html 注入一个 polyfill,并用一个标记(比如 DSH_UUID_PATCH)标注,方便更新后 grep 验证还在不在:
1 | docker exec dsh grep -c DSH_UUID_PATCH \ |
验证清单(全过才算成功)
docker ps,两个容器都 Up,docker inspect看 0 重启curl对外端口,HTTP 200- 模型后端:
curl http://192.168.x.x:1234/v1/models - polyfill:容器内 grep
DSH_UUID_PATCH,≥1 - 端到端证据:
/root/.dsh/storages/session_projcache.json里有 session 的sessionStats(turns/steps/decodeTokens/llmMs),真实调用过的铁证 docker logs dsh | grep -iE 'error|warn|fail',无异常
一个真实 session 证据(脱敏):turns=3, steps=9, llmMs=54836, decodeTokens=1387,模型确实在被调用,不是空转。
备注:dsh 的推理档位只接受 xhigh(默认)/ medium / low,写 high 会报 Jinja 错 Unexpected reasoning effort high。浏览器自动化工具对 dsh UI 时好时坏,curl + SSH 是最可靠的验证路径。
三、Cloudflare 安全公网(Tunnel + Access)
dsh 跑在 192.168.x.x:3080,局域网内访问没问题。但它是 Agent,谁能进 UI,谁就能读工作区文件、跑工具。开到公网,别裸开端口,前面挂一层真正的登录墙。
架构
1 | 浏览器 ──HTTPS──> Cloudflare 边缘 ──加密隧道──> cloudflared(token 模式, 仅出向) |
分层:
- Cloudflare Access 应用(真正的防护)。一个子域(比如
dsh.elric.top)指向http://192.168.x.x:3080。未认证请求先被 302 到 CF 登录页,根本到不了 nginx。认证用 OIDC(推荐)或 Email OTP(一次性验证码,零配置)。 - 边缘 TLS + 隧道通道。CF 在边缘终结 TLS,隧道是加密出向通道,路由器上不用开任何入站端口(契合”国内运营商爱扫 80/443”的顾虑)。
- 既有层仍生效。浏览器信任 403 栅栏还在,LLM 后端
192.168.x.x:1234保持仅局域网。 - 可选加固。CF 限流;子域起个不那么显眼的名字(想靠隐蔽就别叫
dsh.);token 最小权限、可随时吊销。
创建 Access 应用(Zero Trust 控制台)
- Applications → Add an application → Type: Self-hosted
- Application domain:
dsh.elric.top - Service address:
http://192.168.x.x:3080
- Application domain:
- Access policies → Add a policy
- 认证:One-time PIN (Email)(零配置起步)或 OIDC
- Include:你的邮箱
- 可选:MFA、IP 白名单、受信设备
- 后续可加 Service Token,让 CLI/API 免浏览器自动登录
CN 控制台小提示:中文界面下模板选择器没有现成的 “Edit Cloudflare Access” 模板。用自定义令牌,权限框按英文标识搜索(Access / Tunnel / DNS),等约 1s 异步下拉。权限给 Edit,作用域限定到具体 zone elric.top(别选 All resources),TTL 设 0。
端到端验证
curl -sL -o /dev/null -w '%{http_code}\n' https://dsh.elric.top,未认证请求应得到 CF Access 登录跳转(而不是 dsh UI)- 完成认证(OIDC / OTP),dsh UI 加载
- 确认登录页是 Cloudflare 的,不是应用的,证明墙在 nginx 前面
总结
- 模型升级零成本:OpenAI 兼容 API 让模型层独立迭代,换文件 + 改 alias 就完事。
- dsh 接入的核心问题:loopback-only 绑定要 nginx 代理,403 信任栅栏要重写 Host + Origin,bind mount 改配置要 docker restart 加容器内验证。
- 公网安全:别裸开端口,Cloudflare Tunnel(仅出向)加 Access 登录墙,分层防御,LLM 后端留在局域网。
这套组合打下来,本地大模型、Agent 编排、安全公网就齐了,基本能直接照抄。
本文环境:ai-server 上 llama.cpp 提供 qwen3.8-27b(Q4_K_M,64K ctx,16.8GB);dsh 为 0.1.0-rc 开发者预览版,接口可能变动。