返回博客

QWen 3.8 27B 新手部署指南

作者 约 7 分钟读完

Qwen3.8-27B 本地部署新手指南:llama.cpp router + pi 在 M4 Pro 上从下模型到跑通 agent,实测 11.4 tok/s。四个真踩进去的坑,以及当模型每秒只吐 11 个 token 时,prompt 该怎么写才能一次成型。


QWen 3.8 27B 新手部署指南

最近 Qwen 3.8-27B 很火,因为它几乎能达到近似 Opus 4.6 的效果。去年底,使用 Opus 4.6 可以干很多活了,如果将 Qwen 3.8-27B 部署在本地,相当于有了无限 token 的 Opus 4.6。但在部署过程中还是遇到一些曲折,老金将它们总结成这篇新手指南文章,供大家阅读。


为什么是这套组合

老金本地部署的组合是 pi + llama.cpp + Qwen3.8-27B。

llama.cpp 负责把模型跑起来。相比 Ollama,它的 router 模式允许一个常驻进程管理多个 GGUF、按需装卸,并且暴露标准的 OpenAI 兼容接口,任何支持自定义 base URL 的客户端都能接。

pi 是 harness,把"模型"变成"能读写文件、能执行命令的 agent"的那一层。它自带 llama.cpp 的一等支持,这点后面会看到既是优势也是坑。

Qwen3.8-27B 在 Q4_K_M 量化下约 16.8 GB,64GB 内存的 M4 Pro 跑起来毫无压力。它带 vision projector 和原生 tool calling,后者是做 agent 的硬门槛。

前提:本文假设你已经装好 Homebrew 和 Node 22.19+(pi 通过 npm 分发),以及 18 GB 以上的可用磁盘空间。


部署三步:从下载到跑通

从模型下载到 pi agent 的部署链路

下载模型

要下两个文件:GGUF 本体(Q4_K_M 约 16.8 GB)和 vision projector(mmproj,约 1.4 GB)。后者别漏,router 靠它开多模态,缺了图片输入就是不可用。

别用浏览器点下载。17 GB 中途断一次网就得从头来,用官方 CLI 走,它自带断点续传和并发分片:

pip install -U "huggingface_hub[cli]"

mkdir -p ~/models/Qwen3.8-27B-Q4_K_M

# 新版命令是 hf,老的 huggingface-cli 还能用但已标记废弃
hf download Qwen/Qwen3.8-27B-GGUF \
  Qwen3.8-27B-Q4_K_M.gguf \
  mmproj-F16.gguf \
  --local-dir ~/models/Qwen3.8-27B-Q4_K_M

--local-dir 一定要加。不加的话文件会落进 ~/.cache/huggingface 的哈希目录结构里,你拿到的路径是一堆摘要文件名,router 扫不出来,之后还得自己捞。

国内网络慢的话换个镜像端点,命令不用改:

export HF_ENDPOINT=https://hf-mirror.com

下完先确认它确实是 GGUF,顺便看一眼架构标识:

head -c4 ~/models/Qwen3.8-27B-Q4_K_M/Qwen3.8-27B-Q4_K_M.gguf
# → GGUF
# 头部元数据里 general.architecture = qwen35

ls -lh ~/models/Qwen3.8-27B-Q4_K_M/
# Qwen3.8-27B-Q4_K_M.gguf   16.8G
# mmproj-F16.gguf            1.4G

mmproj 的文件名必须以 mmproj- 开头,router 认的是这个前缀,不是文件内容。仓库里如果叫别的名字,下完自己 mv 一下。

启动 llama.cpp router

brew install llama.cpp

装完先确认两件事。一是这个 build 认不认识 Qwen3.8 的架构标识 qwen35,新模型经常跑在旧版本前面;二是 Metal 后端在不在。

llama-server --list-devices
# Available devices:
#   BLAS: Accelerate
#   MTL0: Apple M4 Pro (53084 MiB, 53083 MiB free)   ← 要看到这行

然后以 router 模式启动。关键是不要传 -m 或 --model。一旦传了,它就退化成单模型模式,router 的按需装卸就没了。

llama-server \
  --models-dir ~/models \
  --no-models-autoload \
  --jinja \
  --host 127.0.0.1 --port 8081 \
  -ngl 999 -c 32768 --flash-attn on
  • --jinja 启用模型自带的 chat template,tool calling 依赖它
  • -ngl 999 尽可能多的层交给 GPU
  • -c 32768 每个 slot 的上下文。这个模型原生支持 262144,但全开的话 KV cache 会吃掉大量内存
curl -X POST http://127.0.0.1:8081/models/load \
  -H 'Content-Type: application/json' \
  -d '{"model":"Qwen3.8-27B-Q4_K_M"}'

# 27B 装载约 22 秒,轮询 GET /models 看 status

接入 pi

pi 通过 npm 全局安装。这里有两个容易踩的点:包名和命令名不一样,以及它对 Node 版本有硬要求(engines 写的是 22.19+),Node 20 上启动会直接被引擎检查拦掉。

node -v    # 低于 v22.19 先升,nvm 用户记得 nvm use

npm i -g @earendil-works/pi-coding-agent
# 装的是 @earendil-works/pi-coding-agent,可执行文件叫 pi

pi --version

pi 内置了 llama.cpp provider,文档里写的是进 TUI 后执行 /login llama.cpp,填入 router 地址,之后用 /llama 装卸模型、/model 选择模型。这条路是通的,而且能在 pi 里直接搜索下载 HuggingFace 上的 GGUF,体验最好。

坑: provider_not_found 但这个内置 provider 只在 TUI 里注册。一旦你想用 pi -p 做非交互调用,或者跑 pi auth check --provider llama.cpp,都会得到 provider_not_found。脚本化场景下这条路直接堵死。

解法是再配一份 ~/.pi/agent/models.json,把 router 当作普通的 OpenAI 兼容服务接入。两条路指向同一个 router,互不冲突:

{
  "providers": {
    "llamacpp": {
      "baseUrl": "http://127.0.0.1:8081/v1",
      "api": "openai-completions",
      "apiKey": "local",
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false,
        "thinkingFormat": "qwen-chat-template"
      },
      "models": [{
        "id": "Qwen3.8-27B-Q4_K_M",
        "reasoning": true,
        "input": ["text", "image"],
        "contextWindow": 32768,
        "maxTokens": 16384,
        "samplingParams": {
          "temperature": 1.0, "top_p": 0.95,
          "top_k": 20, "min_p": 0.0
        }
      }]
    }
  }
}

几个字段值得解释:

  • apiKey 填个占位符就行,llama.cpp 不校验。但 pi 要求模型有 auth 配置才会在 /model 里显示,留空会导致模型"加载了但选不到"。
  • thinkingFormat: "qwen-chat-template" 是关键。Qwen3.8 的 chat template 里带 enable_thinking / preserve_thinking 两个开关,这个格式会把 pi 的思考等级翻译成对应的 chat_template_kwargs。填错的话思考链要么关不掉,要么开不起来。
  • supportsReasoningEffort: false,llama.cpp 不认 OpenAI 的 reasoning_effort 参数,不关掉会报错。
  • samplingParams 直接用了 Qwen 官方推荐值(模型卡和仓库里的 generation_config.json 都有)。

配好之后验证:

pi --list-models | grep llamacpp
# llamacpp  Qwen3.8-27B-Q4_K_M  32.8K  16.4K  yes  yes

pi -p --model llamacpp/Qwen3.8-27B-Q4_K_M "读取 calc.py 并修复其中的 bug"

性能实测:11.4 tok/s 意味着什么

同机对比,均为 4-bit 级别量化,均运行在 Metal 上:

指标llama.cpp Q4_K_MMLX 4-bit
解码速度11.4 tok/s15.4 tok/s
预填充(11.6k prompt)88.7 tok/s95.9 tok/s
相同 prompt 重发0.38 s需手动开启
权重体积16.8 GB16.1 GB

解码慢 35%,但这不是配置问题

llama.cpp 在 Apple Silicon 上的解码确实不如 MLX,瓶颈在权重的内存带宽,不在内存容量。每生成一个 token 都要把 16.8 GB 权重完整读一遍,M4 Pro 约 273 GB/s 的带宽算下来上限就在 16 tok/s 附近。内存够用就行,富余多少不影响。

怎么确认真的跑在 GPU 上 除了 --list-devices,更直接的办法是生成过程中看进程 CPU 占用。老金实测是 3–7%。如果掉回 CPU 推理,这个数字会是 600% 以上。

prompt cache 比解码速度重要

同一 prompt 冷启动 131 秒,命中缓存后 0.38 秒

预填充两者几乎持平,因为这个阶段是计算受限而非带宽受限。11.6k token 的长 prompt 冷启动要 131 秒,这个数字在 agent 场景里相当致命,每轮对话都要重新吃一遍上下文的话根本没法用。

好在 llama.cpp 的 prompt cache 默认就开着,效果极好:同一个 prompt 重发,131 秒直接变成 0.38 秒(11623 / 11627 命中缓存)。多轮 agent 循环里,这个差距的分量远大于解码那 35%。

所以纯对话、单轮生成我会用 MLX。多轮 agent 还是 llama.cpp 划算。


运行本地编码任务

老金用来测试的任务是让它写一个 AI 冥想应用的落地页。完整 prompt 如下:

请作为一名前端专家,为一款名为 'Aura' 的 AI 冥想应用
编写一个现代、极简风格的 Landing Page(落地页)。

要求:
只输出一个单一的 index.html 文件,将 CSS 写在 <style> 标签内。
不要使用任何外部图片,可以使用 CSS 渐变、Emoji 或简单的 SVG 作为占位符。
页面包含:一个带有渐变文字的 Hero Section(主视区)、
一个包含三个特点的 Grid 布局功能区、以及一个底部的邮件订阅表单。
必须是纯响应式设计,在手机端和 PC 端都有完美的内边距和排版。
使用柔和的莫兰迪色系。

每条约束都在防一个具体的崩法

每条约束挡掉一种典型故障

这个 prompt 看起来平平无奇,但每条都对应本地模型的一种典型故障。

「作为一名前端专家」。对 27B 这个量级,角色设定的实际收益比对旗舰模型明显得多,它会改变默认的代码风格和细节密度。

「只输出一个单一的 index.html,CSS 写在 style 标签内」防的是跨文件协调,这是本地模型最容易崩的地方:生成 HTML 时引用了 style.css,写 CSS 时类名又对不上。单文件把这个失败模式整个删掉。

「不要使用任何外部图片」切断幻觉 URL。不加这条,模型几乎必然编造 unsplash.com/photo-xxx 之类根本不存在的图片地址,页面一打开全是碎图。同时要给出替代方案(渐变 / Emoji / SVG),否则它会卡住或留空白。

「Hero + 三特点 Grid + 邮件订阅表单」把开放式创作变成填空。明确的区块清单让模型不需要自己做信息架构决策,而这恰恰是小模型最不稳定的部分。

「纯响应式,手机端和 PC 端都有完美的内边距」,注意它同时点名了两种设备。只说"响应式"的话,模型往往只写一个 @media 就交差。

「柔和的莫兰迪色系」是唯一的审美约束,但极其具体。"好看"、"现代"这类词对模型没有信息量;"莫兰迪"是一个有明确色彩特征的专有名词,直接对应到低饱和度调色板。

核心思路 描述得详细没什么用,得把审美要求翻译成结构要求。"现代极简"你没法验证。"单文件 / 无外部资源 / 三个指定区块 / 双端响应式 / 莫兰迪"这五条,每一条都能用眼睛或脚本当场判定。

思考等级要开

这次运行老金把 pi 的 thinking level 设成了 medium,模型产出了 18,883 字符的思考过程,占掉了 20 分钟里相当大一部分。但这部分投入值得。落地页这类任务需要模型先把配色、断点、区块结构想清楚再动笔,否则写到一半改主意,以 11 tok/s 的速度重来一次代价太高。

模型自己写了个校验脚本

有个细节老金觉得比页面本身更有意思。写完 index.html 之后,模型没有直接收工,而是主动调用 bash 工具,现场写了一段 Python HTMLParser 脚本来检查标签闭合:

# 模型自发执行
python3 - <<'EOF'
from html.parser import HTMLParser
...
EOF
# → errors: none / unclosed: none

同样的模型放在纯聊天界面里只能吐一段文本,放进能执行命令的 agent 框架,它就会开始自我验证。功劳更多在 harness。这次运行总共三轮工具调用:写文件 → 校验结构 → 输出设计说明。


结果验收:20 分 07 秒之后

563 行、17 KB 的单文件 HTML,零外部依赖。

最终效果如下:

命中的部分

  • 渐变文字用 background-clip: text 实现,落在标题的关键词上而非整句
  • 功能区用 repeat(auto-fit, minmax(min(260px, 100%), 1fr)),是真流体网格,不是三个写死的断点
  • 间距和字号大量使用 clamp(),在断点之间也能平滑过渡
  • 外部资源引用数:0

还有两处是我没要求、但它自己加上的:9 处 aria / label 无障碍标注,包括给订阅输入框配了视觉隐藏的 label;以及 prefers-reduced-motion 支持,呼吸光晕动画会对晕动敏感用户自动关闭。

没做好的部分

中文标题的换行处理不到位。桌面端 Hero 的"交还给此刻的呼吸"里,"吸"字被孤零零挤到了第三行;移动端"安静的窗"也有同样的孤字问题。模型对 CJK 断行规则没有概念,这需要一句追加 prompt 来修:

给所有标题加上 text-wrap: balance,
并确保渐变关键词不被拆到两行。

所以本地 27B 到底能干什么

我会交给它的活儿:单文件、结构明确的产出,落地页、配置文件、脚本这一类;有客观验收标准的任务;涉及敏感代码、不能出网的场景。还有批量的、不赶时间的重复劳动。

暂时别指望的:

  • 需要快速多轮试错的探索型任务,一轮 20 分钟
  • 跨文件的大型重构
  • 需要最新框架知识的工作
  • 你盯着屏幕等结果的交互式编码

11 tok/s 很慢,但它可以整晚不停地跑,不计费,不上传任何东西。派给它那些"结果明确、就是懒得写"的活儿,体验会好很多。