最近几天在本地用 OpenCode 接 llama.cpp 服务器跑 Swift 和 Occamy 模型时,遇到一个反复出现的 500 错误:System message must be at the beginning。查了一整天根因,最终通过前置合并多条 system 消息解决了问题。把过程写下来,也给自己留个记录——毕竟我记性不太好(疑似 ADHD,面对大量文字时海马体就不工作)。
问题复现
10 月 10 日,用户报告 Swift-1.5-Qwen3.8-27B 接入 OpenCode 时报 500,错误信息指向 System message must be at the beginning,定位到官方 chat_template 第 106 行。
紧接着 Occamy 模型也报同样的错:System message must be at the beginning,这次在 occamy-chat_template.jinja 第 85 行。
两个不同模型、不同模板,却指向同一个错误模式——这说明问题不在某个特定模板,而在于 OpenCode 的请求结构。
根因分析
OpenCode 在发送请求时,开头会带两条 system 消息:一条是主系统提示(主 prompt),另一条是环境/工具提示(告诉模型当前有哪些可用工具)。而 Swift 和 Occamy 的官方 chat_template 都只允许 index 0 位置存在一条 system 消息,用 not loop.first 来校验,遇到第二条 system 就抛错。
关键洞察:这不是模板本身的 bug,而是模板设计时没考虑到「多条开头 system」的使用场景。 OpenCode 的环境注入 + 用户主 prompt 的 system 部分,在模板侧看到了两条连续的 system 消息块。
修复思路
参考 Tiel chat_template.jinja 的 head.count 思路:不修改官方 body 模板(保持内容块验证和后续 system 拒绝逻辑),而是在前置层把连续的多条 system 消息合并进一个 </system> 块(\n\n 连接),主循环校验从 not loop.first 改为 loop.index0 >= ns_lead.count,即仍拒绝非开头 system,但允许并合并多条开头 system。
修复实现
以 Occamy 为例,新增 /home/cao/scripts/llama/occamy-chat_template.jinja:
- 在文件头统计「开头连续 system」数量
ns_lead.count(namespace,遇非 system 停止) - tools 分支和非 tools 分支都改为把所有开头 system 合并进一个
</system>块(\n\n连接) - 主循环校验从
not loop.first改为loop.index0 >= ns_lead.count
即仍拒绝非开头 system,但允许并合并多条开头 system。这是照搬 Tiel chat_template.jinja 的 head.count 思路。
Swift 模型(Swift-1.5-Qwen3.8-27B)的情况类似但更简单:官方模板内置了 8952 字符的 chat template,与本地运行子服务器的 57627 端口 /props 返回的内置模板完全一致(SHA256 验证通过)。修复新增 /home/cao/scripts/llama/swift-chat_template.jinja,同样采用前置合并连续 leading system 为一个内容块数组、双换行连接的方式。
验证过程
Occamy 验证:
- 单 system(有/无 tools)输出与修复前逐字节相同
- 两条开头 system 合并成功
- 非开头 system 仍抛错(保留拒绝逻辑)
- 完整 agent 对话(双 system + tool_calls + tool 响应 + user)渲染正常
- 真实 router e2e(双 system + tools + user,model=occamy-1.0-3050)返回
finish=stop content=SYSTEM-OK,tg 21.92s
Swift 验证:
benchmarks/swift-template-20261010/verify.pyPASS 44 项渲染- 真实 llama.cpp direct Jinja + common 路径覆盖旧错误、新合并、单/无 system 逐字一致
- tools 和 history、reasoning options、system image 拒绝均验证通过
注意事项
- 服务器启动时缓存模板:改完 chat_template 后必须重启 llama-server/router 才生效。已有进程会缓存旧 preset,用户需重启 launcher 激活。
- 同一文件被多预设共用:occamy-chat_template.jinja 也被
[occamy-1.0]双 GPU 预设共用,修改影响所有引用该文件的预设。 - 备份习惯:修改前先备份原文件(如
occamy-chat_template.jinja.before-sysfix-20260929),出问题可快速回退。 - 证据留存:验证日志放在
benchmarks/目录下(如benchmarks/occamy-3050-20260929/sysfix-e2e.log),方便后续追溯。
延伸:OpenCode 配置相关经验
同一天还解决了两个 OpenCode 配置问题,简要记录:
DeepSeek Flash 图片输入:在 opencode.json 中添加 provider.deepseek.models.deepseek-flash: {attachment:true, modalities:{input:[text,image],output:[text]}},通过本机 /session/:id/message 接口附 PNG 图验证模型正确识别图中数字。注意 CLI 的 opencode run -f 会额外加入 tool use 文本,纯色图曾被误判,不能仅凭 -f 代表 Web 附件路径。
Exa MCP schema 冲突:OpenCode 接 DeepSeek flash 时报 Invalid schema for function exa_agent_run。将 mcp.exa.url 改为 https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,明确只暴露搜索和抓取工具,避免 agent_run schema 进入模型请求。
这些天折腾下来最大的体会:LLM 本地部署的工具链调试,一半时间在查 template 渲染,另一半时间在确认”改完要重启”。希望这篇记录能帮到同样遇到 system 消息 500 错误的朋友。