Qwen3.8-27B 远程推理服务与 OpenCode 接入
这篇笔记记录的是一次完整的本地模型接入练习。重点不是“把模型启动起来”,而是分清模型、服务、接口和客户端各自负责什么,并用可重复的请求验证每一层。
先建立整体链路
Windows / OpenCode
│
│ OpenAI-compatible HTTP API
▼
SSH 隧道:127.0.0.1:8080
▼
远程主机 llama-server
▼
Qwen3.8-27B GGUF 模型
- 模型文件:只保存权重和推理所需的模型信息,本身不会监听端口。
- llama-server:加载 GGUF,分配上下文和推理资源,对外提供
/v1/...接口。 - OpenAI 兼容 API:把不同推理后端统一成客户端容易调用的格式。
- OpenCode:作为客户端调用模型,并把工具定义传给模型;它不是模型本身。
因此,“模型文件存在”不等于“服务已经可用”;“服务返回 200”也不等于工具调用和长上下文都已经验证。
这次实际使用的文件
远程主机上的模型目录是:
/home/lab304/models/efficientthink-q2/
├─ Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf
├─ mmproj-Qwen3.8-27B-Q4_K_M.gguf
└─ dflash2-qwen38-27b-Q4_K_M.gguf
主模型约 13 GB,属于 Qwen3.8-27B 的 GGUF 量化版本。mmproj 和 dflash2 是配套文件,是否能直接使用,仍要以当前 llama.cpp 版本和模型模板兼容性为准,不能只看文件名下结论。
服务程序:
/home/lab304/llama.cpp-efficientthink/build-cuda13-mmq/bin/llama-server
先验证服务,而不是先接 OpenCode
排错时先保留一份 CPU 基线。它能把“模型/模板/API 本身有问题”和“GPU 后端有问题”分开:
llama-server \
--model /home/lab304/models/efficientthink-q2/Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf \
--gpu-layers 0 \
--parallel 1 \
--ctx-size 4096 \
--jinja \
--reasoning-format none \
--reasoning off \
--host 127.0.0.1 \
--port 8080
这里每个关键参数都对应一个取舍:
| 参数 | 作用 |
|---|---|
--gpu-layers 0 |
不把模型层放到 GPU,先得到稳定的 CPU 基线 |
--parallel 1 |
只允许一个并发槽,降低内存压力,便于排查问题 |
--ctx-size 4096 |
上下文窗口为 4096,先控制内存和响应时间 |
--jinja |
使用模型模板组织聊天消息 |
--reasoning-format none / --reasoning off |
暂时关闭特殊 reasoning 输出,避免客户端解析不一致 |
--host 127.0.0.1 |
只监听远程主机本机,避免把推理服务直接暴露到局域网 |
启动后按三层顺序检查:
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v1/models
最后再发一条最小聊天请求,确认不只是端口打开,而是真的能生成:
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "/home/lab304/models/efficientthink-q2/Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf",
"messages": [{"role":"user","content":"只回答:部署测试成功。"}],
"temperature": 0,
"max_tokens": 32
}'
本次 CPU 基线中,/health 返回 {"status":"ok"},聊天请求 HTTP 200,并能生成“部署测试成功”。CPU 推理约为 1 token/s;它适合排错和保底,不适合高并发或追求快速交互。
Windows 端如何安全访问
由于服务最终绑定在远程主机的 127.0.0.1:8080,Windows 端使用 SSH 本地转发:
ssh -L 8080:127.0.0.1:8080 lab304@192.168.8.105
保持这个 SSH 窗口不关闭,Windows 上的客户端就可以访问:
http://127.0.0.1:8080/v1
这种方式比直接把 llama-server 绑定到 0.0.0.0 更安全。若以后确实要开放局域网访问,必须同时配置 --host 0.0.0.0、API key 和防火墙规则,并从另一台设备实测鉴权;不能只改一个 host 参数。
从本机使用扩展到局域网共享
后续测试发现,远端模型服务本身没有掉,真正出问题的是旧 SSH 隧道会话和远端地址变化。最初尝试连接 192.168.8.105 超时,自动发现后确认模型主机当前为 192.168.8.24。本机 WLAN 地址是 192.168.8.104。
现在的链路变成:
其他局域网电脑
│ http://192.168.8.104:18080/v1
▼
本机 qwen_ssh_tunnel.py
│ SSH + API Key
▼
192.168.8.24:22
│ 转发到远端 127.0.0.1:8080
▼
llama-server + Qwen3.8-27B
隧道监听 0.0.0.0:18080,但转发目标仍是远端 127.0.0.1:8080;代码只接受回环地址和 192.168.8.0/24 客户端,不应该把它理解成公网服务。验证结果是:
- 不带 API Key 请求
/v1/models返回401; - 带正确 API Key 的本机请求返回
200; - 另一台局域网设备请求也返回
200; - 实际模型对话请求返回
200和OK。
所以“Connection error”不能直接推断为模型掉线。排查顺序应是:检查 18080 监听 → 带 key 请求 /v1/models → 检查 SSH 隧道日志 → 再重载 Continue/VS Code。跨不同网络时使用 Tailscale 或 WireGuard,暂时不要把 18080 直接映射到公网。
当时尝试创建更严格的 Windows 防火墙规则时被系统拒绝(需要管理员权限),因此不能把“防火墙已限制”写成已完成事实;当前可确认的保护是 API Key 和隧道代码的来源地址过滤。
隧道脚本还加入了局域网主机发现:先尝试上次的 SSH 地址,再在 192.168.8.x 网段探测 SSH 服务和远端 8080,这样模型主机换地址后不必手工改每个客户端配置。
OpenCode 配置思路
先设置 API key(如果服务启用了 key):
$env:QWEN_REMOTE_API_KEY = (ssh lab304@192.168.8.105 "cat ~/.config/llama-server/api-keys").Trim()
通过 SSH 隧道访问时,OpenCode 的 provider 可以写成:
{
"$schema": "https://opencode.ai/config.json",
"model": "qwen-remote/qwen38",
"providers": {
"qwen-remote": {
"name": "Qwen3.8 Remote",
"package": "@opencode/ai/providers/openai-compatible",
"settings": {
"baseURL": "http://127.0.0.1:8080/v1",
"apiKey": "{env:QWEN_REMOTE_API_KEY}"
},
"models": {
"qwen38": {
"name": "Qwen3.8 27B EfficientThink Q2",
"modelID": "/home/lab304/models/efficientthink-q2/Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf",
"capabilities": {
"tools": true,
"input": ["text"],
"output": ["text"]
},
"limit": {
"context": 4096,
"output": 1024
}
}
}
}
}
}
baseURL 指向 /v1 而不是根路径;modelID 必须与 /v1/models 返回的模型 ID 一致。若绕过 SSH 隧道直接写 192.168.8.105:8080,前提是服务已经绑定局域网地址并完成鉴权验证,本次最终状态并不是这种配置。
Continue 配置时看懂字段,而不是照抄客户端名称
Continue 使用 YAML,但底层仍然是在调用同一个 OpenAI 兼容接口。字段可以按下面的关系理解:
models:
- name: Qwen3.8 27B EfficientThink Q2
provider: openai
model: /home/lab304/models/efficientthink-q2/Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf
apiBase: http://127.0.0.1:8080/v1
apiKey: ${QWEN_REMOTE_API_KEY}
contextLength: 4096
capabilities:
- tool_use
provider: openai指的是协议格式,不是必须调用 OpenAI 云服务;apiBase对应 OpenCode 的baseURL;model仍要写服务实际返回的模型 ID;capabilities只是告诉 Continue 可以尝试哪些能力,最终是否成功仍要用真实请求验证;- API key 应放环境变量或密钥管理中,不要写进公开仓库。
Continue、OpenCode 和手写 curl 请求可以共用同一条 SSH 隧道;排错时先用 curl 验证服务,再接客户端,能避免把客户端配置错误误判成模型错误。
工具调用验证:不能只测普通对话
Agent 客户端不仅要模型会回答,还要能正确返回结构化工具调用。测试时给 API 传入 tools 和 tool_choice: "required",本次返回了标准的 tool_calls:
{
"name": "get_weather",
"arguments": {"city": "上海"}
}
这证明当前服务在该测试请求下能输出 OpenAI 兼容的工具调用结构。它不代表所有复杂 schema、长上下文或多轮工具链都已经通过,后续仍应按实际 OpenCode 工作流测试。
Continue 的局域网配置与一键安装包
Continue 当前使用的远程模型配置要点是:
name: Qwen3.8 LAN Agent
version: 1.0.0
schema: v1
models:
- name: Qwen3.8 27B LAN - Agent + Code
provider: openai
model: /home/lab304/models/efficientthink-q2/Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf
apiBase: http://192.168.8.104:18080/v1
apiKey: ${{ secrets.QWEN_REMOTE_API_KEY }}
contextLength: 32768
capabilities:
- tool_use
这里的 provider: openai 仍然只是协议适配器,不表示请求一定发往 OpenAI;真正的地址是局域网隧道。当前配置把上下文设为 32768、单次输出上限设为 4096,API key 放在 Continue 的密钥环境中,不写入公开配置。
为了让另一台 Windows 电脑快速接入,已经在本机桌面生成两个安装包(它们没有上传到网站仓库):
Qwen3.8-LAN-Continue-OneClick.zip:安装时隐藏输入一次 API Key;Qwen3.8-LAN-Continue-OneClick-Private.zip:包含真实 key,只能交给可信电脑,不能上传网盘或代码仓库。
解压后双击 Install-Qwen3.8-Continue.cmd,安装器会备份已有的 .continue 配置、写入模型配置、复制网页 MCP、检测/安装 Python 和 Continue 扩展,并测试 /v1/models 是否返回 HTTP 200。模型主机地址变化时可执行:
.\Install-Qwen3.8-Continue.ps1 -ServerHost 新IP地址
其他电脑只共享模型 API;文件、终端、编辑器和网页工具仍在各自电脑本地运行,不会因为共用模型就自动共享本机文件。
受限网页检索:一次搜索、一次读取、停止
为避免模型反复调用网页工具,Continue 的内置 search_web 和 fetch_url_content 已禁用,改用本地 MCP 服务 qwen_web_mcp.py,只暴露两个工具:
web_search_once:对一个精确查询执行一次搜索,并返回候选 URL;read_url_once:读取指定 URL 一次,直接读取失败时只按规则尝试一次代理。
工具链的硬性边界是:搜索得到 URL 后只读取该 URL 一次;相同 URL 命中缓存;失败后返回停止信息,不更换搜索引擎、不重复重试。洛谷题目会先解析为唯一题面 URL,避免模型在不同搜索结果之间循环。
当前超时边界是直接读取 5 秒、代理读取 4 秒。实测洛谷 P10427 的搜索和题面读取约 1.2 秒完成,返回了题目描述、输入格式和输出格式;同一 URL 再次调用会命中缓存而停止。第一次在 Continue 中调用自定义工具时,需要在授权框按 Ctrl+Enter 执行。
工具编辑链也要有明确的提交点
这次还验证了 Continue 的本地工具链,正确顺序不是让模型直接“说已经改好”,而是:
发现文件
→ 读取目标
→ 新建或修改
→ 查看 diff
→ Apply / Accept
→ 保存文件
→ 终端读取或编译验证
→ 清理冒烟测试文件
edit_existing_file 第一次没有按工具要求重读时会失败,改用 single_find_and_replace 后成功;测试文件实际输出 SMOKE_V2,退出码为 0,随后确认测试目录已删除。这个过程说明“界面出现 diff”不等于“磁盘已经写入”,必须把 Apply/Accept 和落盘后的终端验证作为两个独立步骤。
GPU 路线:先失败,再定位并修复
这台远程主机有 RTX 5080 16 GB。最初使用 CUDA 13.2 + CUDA/MMQ 构建时,GPU 进程能够加载约 13 GB 的模型,显存占用也看起来合理,但生成阶段出现了不稳定行为:
The model produced output that does not match the expected peg-native format
日志中还出现了无法解析的 PEG-native 内容,最终聊天请求返回 HTTP 500。问题并不是“显存不够”,而是 RTX 5080(Blackwell)+ CUDA 13.2 + 当前 IQ3_S 量化/llama.cpp 组合的运行时兼容性问题。重新编译同一条 CUDA 13.2 路线仍可复现,所以不能把“模型成功装进显存”当成 GPU 部署成功。
这条失败路线的学习价值在于:
- 加载成功 ≠ 推理协议兼容:模板、reasoning 输出格式和客户端解析必须同时匹配。
- 显存足够 ≠ 运行稳定:还要检查生成内容、HTTP 状态码和日志。
- 先保留可用基线:停止异常 GPU 进程,恢复 CPU 服务,保证 API 和 OpenCode 仍能使用。
这次没有删除原来的 CUDA 13.2 环境,而是在用户目录安装 CUDA 12.8.2,重新编译出:
/home/lab304/llama.cpp-efficientthink/build-cuda128/bin/llama-server
最终 GPU 服务使用:
llama-server \
--model /home/lab304/models/efficientthink-q2/Qwen3.8-27B-EfficientThink-SimPO-Q2-LynnStyle.gguf \
--host 127.0.0.1 \
--port 8080 \
--gpu-layers all \
--parallel 1 \
--ctx-size 4096 \
--flash-attn on \
--jinja \
--reasoning off \
--reasoning-format none
这条 CUDA 12.8 路线实测:/health 正常,聊天接口 HTTP 200,返回“CUDA 修复测试成功”,nvidia-smi 显示服务实际占用约 12.4 GiB 显存。也就是说,当前推荐方案已经从 CPU 保底升级为 CUDA 12.8 + GPU 全层加载;CPU 命令仍应保留,作为升级或回归时的对照基线。
这里的错误路线要明确否定:
- 不能因为模型加载进显存,就断言 GPU 部署成功;必须继续测生成、HTTP 状态码和日志。
- 不能只反复重编 CUDA 13.2;如果错误与运行时/架构组合有关,应改变一个受控变量(这里是 CUDA 12.8.2)再回归。
- 不能直接删除系统 CUDA 13.2;用户目录并行安装更容易回滚,也不破坏其他项目。
最终验收表
| 层级 | 本次结论 |
|---|---|
| 模型文件 | GGUF 文件可被 llama-server 加载 |
| 服务健康 | /health 返回 status: ok |
| 模型接口 | /v1/models 可列出模型 |
| 普通生成 | CUDA 12.8 GPU 服务 HTTP 200,能返回测试文本 |
| 工具调用 | 标准 tool_calls 测试通过 |
| CUDA 13.2 GPU 生成 | 未通过,出现 PEG-native 解析错误和 HTTP 500 |
| CUDA 12.8 GPU 生成 | 通过,显存约 12.4 GiB,返回“CUDA 修复测试成功” |
| 当前推荐 | CUDA 12.8 + gpu-layers all + SSH 隧道 + OpenCode/Continue |
一套可复用的排错顺序
- 先看进程和端口:确认启动的是目标
llama-server,端口没有被旧进程占用。 - 再看
/health:不健康时不要继续调 OpenCode。 - 再看
/v1/models:核对真实的 model ID,避免配置文件写错。 - 发最小 chat 请求:只用一句话和很小的
max_tokens,区分服务问题与长上下文问题。 - 单独测工具调用:确认结构化输出能被客户端解析。
- 最后才换 GPU、CUDA、量化、上下文长度:一次只改一个变量,并保留能工作的基线。
参考
- OpenCode Providers
- OpenCode Models
- llama.cpp issue #21255:CUDA/Blackwell 相关兼容性讨论
- llama.cpp issue #28581:GPU 后端运行异常讨论
这次部署的结论不是“CPU 比 GPU 好”,而是:先用 CPU 建立可用基线,再定位 CUDA 13.2 的 GPU 兼容性问题,最后用用户目录中的 CUDA 12.8.2 完成 GPU 验收;两条路线都保留,便于后续回归。