# 酒馆（SillyTavern）部署报告 — DSH 机 192.168.31.76

> 部署日期：2026-09-26
> 部署目标机：DSH 机（本机，192.168.31.76，Deepin 25）
> 结论：**已可用**。局域网打开 http://192.168.31.76:8000 即可聊天，后端已预接 GPU 机的无审核模型，端到端实测通过。

---

## 0. 一句话结果

| 项目 | 值 |
|---|---|
| 访问地址 | **http://192.168.31.76:8000** |
| 版本 | SillyTavern **1.19.0**（2026-09-14 发布，commit `7e8663c`） |
| 托管方式 | systemd 单元 `sillytavern.service`：**开机自启 + 崩溃自动拉起**（`Restart=always`） |
| 后端模型 | GPU 机 `http://192.168.31.31:8080/v1` · `bucoid-heretic-27b`（27B 无审核/heretic，IQ4_XS，**64K 上下文**） |
| 已配置 | 服务器监听 0.0.0.0、局域网白名单、后端连接、出站代理（GitHub 可达） |
| 实测 | 酒馆自己的后端接口 → GPU 机模型 → 返回中文角色扮演对白 ✅ |

---

## 1. 交付物一览（运维速查）

| 用途 | 路径 / 地址 |
|---|---|
| 网页入口 | http://192.168.31.76:8000 （局域网）；http://127.0.0.1:8000 （本机） |
| 安装目录 | `/home/zyw/SillyTavern`（527 MB，含 node_modules 400 MB） |
| 数据目录 | `/home/zyw/SillyTavern/data/default-user/` |
| ├ 角色卡 | `/home/zyw/SillyTavern/data/default-user/characters/`（自带示例 `default_Seraphina.png`） |
| ├ 聊天记录 | `/home/zyw/SillyTavern/data/default-user/chats/` |
| └ 全部设置 | `/home/zyw/SillyTavern/data/default-user/settings.json` |
| 配置文件 | `/home/zyw/SillyTavern/config.yaml` |
| 日志 | `/home/zyw/SillyTavern/sillytavern.log`（`journalctl -u sillytavern` 只有 systemd 自身消息） |
| systemd 单元 | `/etc/systemd/system/sillytavern.service`（本目录留了一份副本） |
| **控制脚本** | `bash ~/Downloads/sillytavern-ctl.sh {start\|stop\|restart\|status\|log\|test\|update\|info}` |
| 运行 node | `/usr/bin/node`（系统包 **v20.15.1**，满足 ST 要求 `engines.node >= 20`，**没动 nvm**） |
| 出站代理 | `socks5://127.0.0.1:10808`（本机 v2ray，由 `v2ray.service` 托管） |
| 设置备份 | `/home/zyw/SillyTavern/data/default-user/settings.json.bak-preconfigure` |

---

## 2. 部署步骤（可完整复现）

### 2.1 前提核对
本机已有：Node v22.23.2（nvm）与 **v20.15.1（系统包）**、git 2.51、v2ray 代理（`127.0.0.1:10809` HTTP / `10808` SOCKS5，google 探活 200）、8000 端口空闲、`/home` 剩余 446 GB。

> 注：GitHub 与 `raw.githubusercontent.com` 在本网**直连不通**（实测 `000`），克隆与 npm 必须走代理。

### 2.2 拉代码（走代理）
```bash
export https_proxy=http://127.0.0.1:10809 http_proxy=http://127.0.0.1:10809
cd /home/zyw
git clone --depth 1 --branch 1.19.0 https://github.com/SillyTavern/SillyTavern.git SillyTavern
```

### 2.3 装依赖（走代理，3 分钟）
```bash
cd /home/zyw/SillyTavern
export https_proxy=http://127.0.0.1:10809 http_proxy=http://127.0.0.1:10809
export npm_config_proxy=$http_proxy npm_config_https_proxy=$https_proxy
npm install --no-audit --no-fund        # 829 个包，exit 0
```

### 2.4 改 `config.yaml`（逐项及原因）

改动基于 `default/config.yaml` 复制而来：

| 配置项 | 值 | 原因 |
|---|---|---|
| `listen` | `true` | **默认 false 只监听本机**，必须打开才能局域网访问 |
| `listenAddress.ipv4` | `0.0.0.0` | 监听所有网卡 |
| `browserLaunch.enabled` | `false` | 服务器无桌面，别去拉浏览器 |
| `port` | `8000` | 保持默认（已确认空闲） |
| `whitelistMode` | `true` | 保留白名单模式（安全兜底） |
| `whitelist` | 加 `192.168.31.0/24` | 否则除了 127.0.0.1 **局域网全被拒** |
| `enableForwardedWhitelist` | `false` | 无反向代理，关掉可防 `X-Real-IP` 伪造绕过白名单 |
| `basicAuthMode` | `false` | 保持免密（只用白名单）；要密码见 §6 |
| `logging.minLogLevel` | `1` | 0=DEBUG 太吵，1=INFO |
| `requestProxy.enabled` | **`true`** | 见下（GitHub 必需） |
| `requestProxy.url` | `socks5://127.0.0.1:10808` | 本机 v2ray SOCKS5 |
| `requestProxy.bypass` | `localhost,127.0.0.1,::1,192.168.31.31,192.168.31.76` | **关键**：让局域网 LLM 端点直连 |

⚠️ **`requestProxy` 为什么必须开**：酒馆的扩展安装、角色卡在线下载、以及若干后端都走 GitHub / `raw.githubusercontent.com`，本网直连全不通（实测 `raw.githubusercontent.com` → `000`）。开了代理后实测 GitHub 200。
⚠️ **`bypass` 为什么必须写具体 IP**：ST 内部用的是 `proxy-from-env`，它的 `no_proxy` **不支持 CIDR**（实测 `192.168.31.0/24` 不生效、`192.168.31.31` 生效）。**以后往局域网加新设备（另一台 GPU 机、NAS 等），必须把它的 IP 补进 bypass**，否则请求会被塞进代理而失败。

### 2.5 systemd 托管

```ini
[Unit]
Description=SillyTavern 酒馆 (AI 角色扮演前端, 端口 8000)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=zyw
Group=zyw
WorkingDirectory=/home/zyw/SillyTavern
Environment=NODE_ENV=production
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 关键：不要在这里设置 HTTP(S)_PROXY —— 酒馆需直连局域网 LLM (192.168.31.31:8080/1234)
Environment=NO_PROXY=localhost,127.0.0.1,192.168.31.0/24
ExecStart=/usr/bin/node /home/zyw/SillyTavern/server.js
Restart=always
RestartSec=5
TimeoutStopSec=15
LimitNOFILE=65535
StandardOutput=append:/home/zyw/SillyTavern/sillytavern.log
StandardError=append:/home/zyw/SillyTavern/sillytavern.log

[Install]
WantedBy=multi-user.target
```

把上面内容存成 `~/Downloads/sillytavern.service`（**必须是完整非空文件，0 字节会被 systemd 当成 masked**），然后：

```bash
sudo cp ~/Downloads/sillytavern.service /etc/systemd/system/sillytavern.service
sudo chown root:root /etc/systemd/system/sillytavern.service
sudo chmod 644 /etc/systemd/system/sillytavern.service
sudo systemctl daemon-reload
sudo systemctl enable --now sillytavern.service
```
> 用 `execStart=/usr/bin/node`（系统 v20）而不是 nvm 的 v22 绝对路径：nvm 路径带版本号，升级 node 就失效；v20.15.1 已满足 ST 的 `>= 20` 要求，实测运行正常。

### 2.6 预置后端连接（让酒馆开箱即用）

ST 的连接配置存在 `data/default-user/settings.json` 的 `oai_settings` 里。停止服务后改写：

```python
s['main_api'] = 'openai'
o['chat_completion_source'] = 'custom'
o['custom_url']   = 'http://192.168.31.31:8080/v1'   # ⚠️ 不要带 /chat/completions，ST 会自动拼
o['custom_model'] = 'bucoid-heretic-27b'
o['openai_max_context'] = 65536      # 与 GPU 机 n_ctx 对齐
o['openai_max_tokens']  = 4096   # 该模型 --reasoning-budget -1，思考链常吃 400~600 token
o['max_context_unlocked'] = True
o['show_thoughts'] = True            # 该模型是思考型，显示思维链
```
实测酒馆 UI 会加载到的配置（`POST /api/settings/get` 回读）：

```
main_api                = openai
chat_completion_source  = custom
custom_url              = http://192.168.31.31:8080/v1
custom_model            = bucoid-heretic-27b
openai_max_context      = 65536
openai_max_tokens       = 4096
show_thoughts           = True
```

---

## 3. 验证结果（实测证据）

| 验证项 | 方法 | 结果 |
|---|---|---|
| 版本 | `GET /version` | `SillyTavern:1.19.0` / commit `7e8663c` / `isLatest:true` ✅ |
| 本机访问 | `curl 127.0.0.1:8000` | HTTP **200**（734 KB 主页面） ✅ |
| **局域网访问** | `curl 192.168.31.76:8000` | HTTP **200** ✅ |
| 白名单 | `data/access.log` | 记录到 `192.168.31.76` 被放行 ✅ |
| **端到端（3 次）** | 调酒馆自己的 `/api/backends/chat-completions/generate` | 见下 ✅ |
| 出站走向 | 用 ST 的 `request-proxy.js` + 它用的 `node-fetch` 实测 | GitHub raw 200 / GitHub API 200（直连均为 000）；GPU LLM 200、本机 200（直连未被拐） ✅ |
| 崩溃自愈 | `kill -9` 主进程 | **10 秒内自动拉起**（新 PID，探活 200，`NRestarts=2`） ✅ |
| 配置持久化 | 回读 `settings.json` | 预置项未被覆盖 ✅ |

端到端实测返回（原文摘录，说明角色扮演+中文正常）：

| 提示 | 模型回复 |
|---|---|
| 系统：你是一家酒馆的老板娘，说话爽快热情，回答简短。用户：老板娘，来杯招牌酒！ | 「好嘞！招牌酒马上来，坐稳，别烫着！」 |
| 系统：你在酒馆里扮演一位沉默寡言的剑客，只说一句话。用户：客官，打哪儿来？ | 「死人堆里。」 |
| 用户：用一句话证明你在正常工作。 | 「我能够理解你的要求并生成连贯的中文回复，这说明我在正常工作。」 |

（该模型带思维链，`reasoning_content` 正常返回，酒馆会显示为「Thoughts」。）

---

## 4. 本次踩到的 6 个坑（都已在部署中处理）

1. **空文件 = masked**：我第一次用 `cat <<EOF | echo pw | sudo tee 单元文件` 写单元，管道顺序错导致写出 **0 字节**文件，systemd 把空单元视为 **`masked`**，报 `Unit file ... is masked` 而不是语法错误。→ 正确写法是先 `write` 到临时文件再 `sudo cp`。
2. **`pkill -f 'node server.js'` 会自杀**：排查脚本里用了这个模式，结果匹配到自己的 ssh/命令行走而自杀（本机 AGENTS.md 早有同类记录）。→ 一律用 `ss -ltnp` 取 PID 再 `kill`。
3. **`proxy-from-env` 不支持 CIDR**（见 §2.4）。
4. **`custom_url` 不能带 `/chat/completions`**：ST 源码 `src/endpoints/backends/chat-completions.js:2628` 会自己拼 `${apiUrl}/chat/completions`，写成完整端点会变成 `.../chat/completions/chat/completions`。只填到 `/v1`。
5. **ST 的 `requestProxy` 只对 `node-fetch` + 全局 agent 生效**：它替换的是 `http.globalAgent`/`https.globalAgent`，对 Node 原生 `fetch`(undici) 无效。好在 `chat-completions.js` 等关键路径确实 `import fetch from 'node-fetch'`，所以代理实际生效（已实测 GitHub 200）。**换/加功能时若发现某处不走代理，先确认它用的是不是 node-fetch。**
6. **首次启动慢**：ST 首次启动要用 webpack 编译前端库（首次约 1~2 分钟，之后缓存 1.2 秒），我的探活窗口一开始设太短，误判成「没起来」。→ 控制脚本里等待窗口统一给到 60 秒。

---

## 5. 日常使用

### 5.1 开始聊天
1. 浏览器打开 **http://192.168.31.76:8000**
2. 后端**已经配好**（OpenAI 兼容 → `http://192.168.31.31:8080/v1` → `bucoid-heretic-27b`），直接选角色卡开聊即可。
3. 若 GPU 机模型没在跑（`/v1/models` 无响应）：登录 GPU 机执行 `bash ~/switch-llm.sh`（默认 `ridge-long`；无审核档用 `bash ~/switch-llm.sh ko-long`），然后 `bash ~/Downloads/sillytavern-ctl.sh test` 复验。

### 5.2 换后端 / 换模型
- **推荐**：酒馆 UI 左侧「API 连接」→ API 选 `Custom (OpenAI-compatible)` → 改 URL / 模型名 → 连接。模型下拉可从 `/v1/models` 自动拉取。
- **LM Studio**（GPU 机 1234 端口）也是 OpenAI 兼容：URL 填 `http://192.168.31.31:1234/v1`，需先在 GPU 机启 llmster。
- 改完若想固化到服务端默认值，同 §2.6 改 `settings.json`（改前先 `stop`）。

### 5.3 角色卡 / 扩展
- **本地卡**：把 PNG/JSON 卡直接拖到酒馆「角色管理」里上传即可。
- **在线卡**：`config.yaml` 的 `whitelistImportDomains` 白名单内有 `cdn.discordapp.com`、`files.catbox.moe`、`raw.githubusercontent.com` 等；其余域名要先加白名单。**注意 chub.ai / janitorai 直连返回 403**（Cloudflare 拦 curl UA，浏览器里可能正常）。
- **扩展**：`extensions.autoUpdate: true`，装第三方扩展（如酒馆助手/JS-Slider-Runner）走 GitHub —— 代理已配好，可正常克隆。

### 5.4 控制脚本
```bash
bash ~/Downloads/sillytavern-ctl.sh status    # 单元/端口/本机+局域网探活/后端模型
bash ~/Downloads/sillytavern-ctl.sh test      # 端到端自检（真发一次请求）
bash ~/Downloads/sillytavern-ctl.sh restart
bash ~/Downloads/sillytavern-ctl.sh log 80    # 看日志
bash ~/Downloads/sillytavern-ctl.sh update    # 拉最新稳定版 tag + 装依赖 + 重启（走代理）
bash ~/Downloads/sillytavern-ctl.sh info
```

---

## 6. 已知限制与待办

- **无访问密码**：当前是「局域网白名单 + 免密」。同一局域网内任何人打开即用。若要密码：`config.yaml` 里把 `basicAuthMode: true` 并设 `basicAuthUser.{username,password}`，然后 `restart`。
- **GPU 机 LLM 不自启**：按用户既定策略，`llama-*` / LM Studio 单元都是 `disabled`，酒馆要能聊天需先在 GPU 机手动启动。
- **联网功能依赖 v2ray**：SS 节点轮换时（本机已多次发生）卡牌下载 / 扩展更新会失败；**本地聊天不受影响**（LLM 走 bypass 直连）。恢复办法：GPU 机或本机跑 `~/Downloads/v2ray-setup/refresh-sub.sh`。
- **未安装任何第三方扩展**，未配置智能体/工具调用、TTS、图像生成等扩展。
- `settings.json` 里 `firstRun: true` 保留默认，首次打开会看到酒馆的引导弹窗（一次性）。
- **跨机访问**：酒馆只监听 8000；`192.168.31.76:8000` 之外若要外网访问需自行做反向代理（届时记得开 `hostWhitelist` 并配 HTTPS）。

---

## 7. 回滚

```bash
sudo systemctl disable --now sillytavern.service
sudo rm /etc/systemd/system/sillytavern.service && sudo systemctl daemon-reload
mv /home/zyw/SillyTavern ~/SillyTavern.bak      # 或直接 rm -rf
# config.yaml 的 requestProxy 若要一起还原：把 enabled 改回 false
```
回滚不影响本机其他服务（下载中心 8899、相册 8900、DSH GUI 3080、迅雷 2345、v2ray）。

---

## 8. 附：本次新增/改动的文件清单

| 文件 | 说明 |
|---|---|
| `/home/zyw/SillyTavern/` | 酒馆 1.19.0 全量（git 仓库，detached HEAD 于 tag 1.19.0） |
| `/home/zyw/SillyTavern/config.yaml` | 由 `default/config.yaml` 复制后按 §2.4 修改 |
| `/home/zyw/SillyTavern/data/default-user/settings.json` | 预置后端连接（§2.6） |
| `/home/zyw/SillyTavern/data/default-user/settings.json.bak-preconfigure` | 改动前备份 |
| `/home/zyw/SillyTavern/proxy-selftest.mjs` | 出站走向自检脚本（用 ST 自己的 request-proxy 模块） |
| `/etc/systemd/system/sillytavern.service` | systemd 单元（本目录有副本） |
| `/home/zyw/Downloads/sillytavern-ctl.sh` | 控制脚本（start/stop/restart/status/log/test/update/info） |
| 本报告 | `dl-hub/55-SillyTavern酒馆部署/` |

---

## 9. 故障记录：`Jinja Exception: System message must be at the beginning.`

> 现象：部署完成后在酒馆里正常聊天，发消息即报错
> `Chat Completion API —— While executing CallExpression at line 106 ... raise_exception('System message must be at the beginning.')`

### 9.1 根因（已实测确证，非猜测）

GPU 机该模型的 Jinja 聊天模板里有一段**硬检查**（`/props` 取回的 `chat_template` 第 104-107 行）：

```jinja
{%- for message in messages %}
    {%- if message.role == "system" %}
        {%- if not loop.first %}
            {{- raise_exception('System message must be at the beginning.') }}
        {%- endif %}
```

即：**只允许第 0 条是 system，任何位置出现第二条 system 就直接 500**。这是 Qwen3.x 系模板的已知通病
（见 [llama.cpp #27367](https://github.com/ggml-org/llama.cpp/issues/27367)、
[unsloth/Qwen3.8-27B-GGUF 讨论](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/discussions/10)）。

而**酒馆默认 `squash_system_messages = false`**（不合并连续同角色消息）——于是「主提示 / 角色描述 / 角色性格 / 场景 / 世界书前后 / 对话示例」
被当成**多条连续的 system 逐条发送**，第 2 条就触发异常。这解释了为什么我用 `[system, user]` 两段式 curl 测试能通过、而酒馆里必错。

实测对照表（直接打 GPU 机 `/v1/chat/completions`）：

| 消息形态 | 结果 |
|---|---|
| 1 条 system + user | ✅ 通过 |
| **2 条连续 system + user**（酒馆默认真实形态） | ❌ 被拒（line 106） |
| system + user + 尾部 system（越狱块默认角色形态） | ❌ 被拒（line 106） |
| squash 合并后 + 尾部指令改 user | ✅ 通过 |

> 附带确认：默认的 `Post-History Instructions`(jailbreak) 与 `nsfw` 两个块**内容为空**，所以本次不是它们触发的；
> 但它们的默认角色是 `system` 且**注入在对话历史末尾**，一旦填内容就会以同样方式炸掉，因此一并预防性改掉。

### 9.2 已实施的修复（酒馆侧，安全可逆）

改 `data/default-user/settings.json` 的 `oai_settings`（改动前备份为 `settings.json.bak-before-squashfix`）：

| 键 | 改前 | 改后 | 作用 |
|---|---|---|---|
| `squash_system_messages` | `false` | **`true`** | **真凶修复**：把连续 system 合并成一条 |
| `prompts[].role`（`jailbreak`） | `system` | `user` | 预防：尾部注入不再是 system |
| `prompts[].role`（`nsfw`） | `system` | `user` | 同上 |

对应 UI 位置：`Advanced Formatting`(高级格式化) → **Squash system messages** 勾选框；以及 Prompt Manager 里每个提示块左侧的 **A / U / S** 角色按钮。

### 9.3 验证

- 服务端回读确认：`squash_system_messages = true`、`nsfw/jailbreak role = user` ✅
- 用**修复后酒馆会发出的真实消息形态**（合并后的单条 system + 多轮历史 + 尾部 user 指令）打酒馆自己的后端接口：
  ✅ 通过，模型正常回戏：
  > （笑着把两只杯都斟满，酒香顺着夜风飘过来）
  > 陪就陪！不过咱可说好，今晚你喝高兴了，我管你喝到尽兴，不许在店里撒酒疯啊。来，碰一个！

### 9.4 当时的剩余隐患（**已于同日用 GPU 机侧方案根治，见 §10**）

酒馆侧只能做到「合并连续 system + 把尾部块改成 user」；模板的限制本身只放宽到「system 只能在开头」，
所以**任何"注入到对话中间"的 system 消息**（下面这些正是酒馆的常用功能）仍会 500：

- **Author's Note（作者注释）**：在 AN 面板把角色从 **System 改成 User**（A/U/S 按钮）。
- **世界书 at-depth 条目**：源码 `public/scripts/world-info.js:3439` 写死默认 `entry.role ?? SYSTEM`，
  即**每条 at-depth 世界书条目默认就是 system** → 用世界书前要把条目角色改成 **User**（世界书编辑器里有角色图标）。
- 引擎/扩展注入的 system 提示同理。

**要彻底根治**得改 GPU 机侧（见 §10）。

---

## 10. GPU 机侧根治：`--chat-template-file`（✅ 已执行并验证）

该模型模板的硬检查位于 GPU 机单元 `llama-bucoid-heretic.service` 加载的 `bucoid_3.0_mtp.gguf` 内嵌模板中。
llama-server 支持 `--chat-template-file`（已确认该构建支持），可**在不改模型文件**的前提下覆盖模板：

1. 用 `/props` 取回原模板存成 `chat_template-fixed.jinja`；
2. 把第 105-107 行的 `raise_exception` 换成正常渲染一段内联 system 块：

```jinja
    {%- if message.role == "system" %}
        {%- if not loop.first %}
            {{- '<|im_start|>system\n' + content + '<|im_end|>' + '\n' }}
        {%- endif %}
```
   （首条 system 由模板前面的分支渲染，故仍保留 `not loop.first` 守卫，不会重复输出。）

3. 单元里加 `--chat-template-file /home/zyw/models/bucoid-heretic-27b/chat_template-fixed.jinja`；
4. `systemctl daemon-reload && systemctl restart llama-bucoid-heretic`（模型重新加载约 20~40s）。

**收益**：世界书 at-depth、Author's Note、任意 system 注入全部可用，酒馆不需要任何 role 上的将就，
其他客户端（Open WebUI 等）一并受益。
**代价/风险**：需重启 GPU 机正在跑的 14GB 模型服务；补丁只改 1 行、可秒回滚（保留原单元与模板备份）。
### 10.1 执行记录（2026-09-26，用户确认后执行）

| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | 从 `/props` 取回原模板存为 `chat_template-orig.jinja` | 170 行 |
| 2 | 只替换第 106 行 `raise_exception` → 渲染内联 system 块，存 `chat_template-fixed.jinja` | `diff` **仅 1 行不同**；md5 `7bb56f72782f5d02c9445a53e399cf95` |
| 3 | 传到 GPU 机 `/home/zyw/models/bucoid-heretic-27b/chat_template-fixed.jinja` | 远端 md5 **一致** |
| 4 | 备份单元 → `llama-bucoid-heretic.service.bak-20260926-pre-template` | 已备份 |
| 5 | 在 `ExecStart` 末尾追加 `--chat-template-file /home/zyw/models/bucoid-heretic-27b/chat_template-fixed.jinja` | 与备份 `diff` **仅该 1 行** |
| 6 | `daemon-reload` + `restart` | `/health` **5s 内 200**；`ps` 确认进程已带该 flag |

### 10.2 验证结果（改前全部 500 → 改后全部通过）

| 消息形态 | 改前 | 改后 |
|---|---|---|
| 2 条连续 system + user | ❌ 500 | ✅ 「哟，来啦！今儿想喝啥？」 |
| system + user + **尾部** system | ❌ 500 | ✅ 「来了，要哪种？」 |
| system + user + assistant + **中间** system（世界书 at-depth 形态） | ❌ 500 | ✅ 「本店招牌是桂花酿。要一壶？」← **正确用上了注入的世界书内容** |
| 常规 1 条 system（回归测试） | ✅ | ✅ 「（把杯碟往桌上一放）酒？有。」 |

> 说明：该模型的 MTP/推理配置未改动，只换了模板来源，因此**速度与显存不受影响**（依旧 64K + KV4 + MTP）。

### 10.3 副作用与回滚

- **副作用**：无。模板只多了一条渲染分支；`--reasoning-budget -1` 等参数未动。
- **注意**：单元 `Restart=no`，重启需手动；**改模板后必须 `daemon-reload`**，否则 flag 不生效。
- **回滚（30 秒）**：
  ```bash
  sudo cp /etc/systemd/system/llama-bucoid-heretic.service.bak-20260926-pre-template \
          /etc/systemd/system/llama-bucoid-heretic.service
  sudo systemctl daemon-reload && sudo systemctl restart llama-bucoid-heretic
  ```
- **影响面**：该 flag 是服务级的，所有连 8080 的客户端（酒馆、Open WebUI、脚本）一并受益。

### 10.4 酒馆侧是否还需要那两项设置？

`--chat-template-file` 生效后，酒馆的 `squash_system_messages` 与 `jailbreak/nsfw → user` **不再是必须的**，
但**建议保留**：① 合并连续 system 能少发冗余 token，无副作用；② 若把酒馆指向 GPU 机上**别的模型**
（`switch-llm.sh` 切 ridge / ko-ridge 等），那些模型可能同样是严格模板，保留这两项等于多一层保险。

### 10.5 配套调优：`openai_max_tokens` 2048 → 4096

联调时发现**回复正文为空**：该模型 `--reasoning-budget -1`（思考不设上限），单次思考常吃 **400~600 token**，
600 的预算会被思考链吃光、正文一字不出。这与备忘里 KO-Ridge 的已知问题同源。
已将酒馆 `openai_max_tokens` 调至 **4096**（实测 2000/4096 均能正常出正文，且质量良好、正确使用世界书）。
