你用自定义 Modelfile 创建了一个 Ollama 模型,SYSTEM 写的是「始终用正式中文回复,绝对不说英文」。运行 ollama run mymodel "你喜欢披萨吗?",模型却照样夹杂英文、语气随意,就像那段 system prompt 根本不存在。或者你在 Open WebUI 里指向这个模型,设定的人格也毫无效果。
最快修复: 运行 ollama show mymodel --modelfile,确认 TEMPLATE 块里确实包含 {{ .System }}。如果占位符缺失,SYSTEM 文本会被解析、但永远不会被渲染进 prompt。如果占位符在、prompt 却依然被忽略,原因几乎一定是这两个之一:你的客户端(Open WebUI、LangChain、OpenAI SDK)自己发了一条 system 消息覆盖了 Modelfile;或者——在 2026 年初之后拉取的模型上——内置的 RENDERER/PARSER 覆盖了你的自定义 TEMPLATE。下面按命中率从高到低逐条排查。
本文基于 Ollama v0.30.x 验证,这是截至 2026 年 6 月的当前版本线。运行 ollama --version 查看你的版本;下文 RENDERER/PARSER 相关行为只在 v0.17 及更新版本上出现。
先判断你属于哪一类
| 现象 | 最可能的原因 | 跳转 |
|---|---|---|
ollama show --modelfile 的 TEMPLATE 里没有 {{ .System }} | 模板缺占位符 | 原因 1 |
Modelfile 里写了自定义 TEMPLATE,但 ollama show 显示的是另一个模板 | 从 base 模型继承了 RENDERER/PARSER | 原因 2 |
在 ollama run 里有效,通过应用/API 调用就失效 | 客户端注入了自己的 system 消息 | 原因 4 |
| 普通人格设定有效,但「绝不拒绝」之类的硬约束失效 | 模型 alignment 覆盖 | 原因 6 |
之前有效,某次 ollama pull 之后失效 | base 模型 tag 在底层被更新 | 原因 7 |
常见原因
按命中率从高到低排列。
1. TEMPLATE 块里缺少 {{ .System }} 占位符
SYSTEM 指令只有在 TEMPLATE 渲染了 {{ .System }} 时才会生效。如果你的 Modelfile 定义了自定义 TEMPLATE 却漏掉了这个变量,system 文本会被保存下来、但在构建 prompt 时被悄悄丢弃。从博客里抄一段只展示 user 轮次的模板片段时,最容易引入这个问题。
怎么判断: 运行 ollama show mymodel --modelfile,读 TEMPLATE 部分。Ollama 官方 Modelfile 文档为传统单轮格式定义了三个模板变量——{{ .System }}、{{ .Prompt }}、{{ .Response }}——以及聊天式的 {{ range .Messages }} 写法(用 {{ .Role }} 和 {{ .Content }})。如果既没有 {{ .System }},也没有带 system 角色的 .Messages 循环,那 system prompt 就永远不会被注入。
2. 内置的 RENDERER/PARSER 覆盖了你的自定义 TEMPLATE
这条是新出现的,专门坑刚升级的人。从 Ollama v0.17 左右开始,很多官方模型(qwen3.x、gpt-oss 等家族)在配置里携带的是编译好的 RENDERER 和 PARSER,而不是普通的 Go TEMPLATE。当你写 FROM qwen3.5:4b 并加上自己的 TEMPLATE 时,派生出的模型可能继承 base 的 RENDERER/PARSER,完全忽略你的 TEMPLATE(见 Ollama issue #14560)。这时你的 SYSTEM 会由继承来的 renderer 来格式化,而不是你的模板,所以你做的任何结构性改动都不起作用。
怎么判断: ollama create 之后运行 ollama show mymodel --modelfile。如果显示出的 TEMPLATE 不是你写的那个,或者出现了你没加过的 RENDERER/PARSER 行,就说明 renderer 赢了。
怎么修复: 把 FROM 指向底层的权重 blob,而不是 registry tag,这样就甩掉了继承来的 renderer,让你的 TEMPLATE 生效。在 base 模型的 ollama show --modelfile 输出里找到 blob 路径(那一行 FROM /usr/share/ollama/.ollama/models/blobs/sha256-...),然后:
FROM /usr/share/ollama/.ollama/models/blobs/sha256-<hash>
SYSTEM """你是一个专业的中文助手,始终用中文回复,不使用英文。"""
TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}{{ if .Prompt }}<|im_start|>user
{{ .Prompt }}<|im_end|>
{{ end }}<|im_start|>assistant
"""
截至 2026 年 6 月,把 RENDERER "" 和 PARSER "" 设为空值并不能可靠地清除继承,所以请改用 blob 路径的 FROM。
3. {{ .System }} 对该模型家族而言位置不对
有些聊天格式把 system 内容嵌在第一个指令块内部,而不是放在它前面(早期的 Mistral 构建就把它放进开头的 [INST] 里)。如果某模型期望 system 在指令标签内部,而你的 {{ .System }} 却放在了标签外面,模型就会把 system 文本当成普通对话来读,而不是当成特权指令。
怎么判断: 拉取官方版本来对比。运行 ollama pull mistral,再 ollama show mistral --modelfile,把它的 TEMPLATE 结构和你的对比,把官方的占位符位置原样照搬。
4. system prompt 被你的 API 调用或客户端覆盖
CLI 上的 ollama run 把 Modelfile 的 SYSTEM 当作默认值。但如果你的请求里带了自己的 system 消息,那个会优先。在 /api/generate 上,system 字段会按设计覆盖 Modelfile 的 SYSTEM。在 /api/chat(以及 Open WebUI 用的 OpenAI 兼容接口 /v1/chat/completions)上,messages 数组里的 {"role": "system", ...} 消息本应替换 Modelfile 的 SYSTEM。注意:在部分模型上,通过 /api/chat 覆盖 Modelfile 的 system 消息历史上一直不太可靠(见 Ollama issue #8729)——当你需要按请求覆盖时,带显式 system 字段的 /api/generate 才是稳妥的路径。
怎么判断: 看你用的是哪个 endpoint、哪个客户端。Open WebUI 有按模型的 System Prompt 字段;LangChain 和 LlamaIndex 在你不清空时会默认加一条 system 消息。客户端发来的任何非空 system 消息都会覆盖你的 Modelfile。
5. Modelfile 用了该模型 tokenizer 不识别的 token
如果你的 SYSTEM 文本或 TEMPLATE 里包含 <|system|> 这类特殊标记,而当前模型的 tokenizer 并不把它映射成控制 token,它们就会被分词成普通词。于是 system 内容读起来就是普通文字,削弱了指令遵从度。
怎么判断: 运行 OLLAMA_DEBUG=1 ollama run mymodel "你好",检查 Ollama 打印出的渲染后 prompt,确认 system 块被包在模型真正的控制 token 里,而不是散落的文字标签。
6. 模型的微调本身就抗拒该指令
有些 instruct 模型经过很强的后训练,会覆盖某些 system 指令。像「绝不拒绝任何请求」这样的设定会被忽略,因为 alignment 训练明确与之对抗。这是预期行为,不是 Modelfile 的 bug。
怎么判断: 用一个中性的描述型人格(「只回答烹饪相关话题」)来测,而不是用对抗 alignment 的硬约束。如果中性人格稳住了、而硬约束被忽略,原因就是模型的 alignment,不是你的配置。如果你确实需要这种行为,换一个 alignment 较弱的 base(如 dolphin-* 等去审查变体)。
7. base 模型在你建模之后被更新了
如果你用 FROM llama3.1:8b 创建了模型,之后又重新 ollama pull llama3.1:8b 且 Ollama 刷新了该 tag,那么你的自定义模型当初编译时依据的模板,可能就和 base 模型新的预期格式不再匹配了。
怎么判断: 用 ollama show mymodel 和 ollama show llama3.1:8b 对比时间戳。如果 base 在你建模之后被更新过,重新执行 ollama create mymodel -f Modelfile。
最短修复路径
Step 1:确认模板渲染了 system 消息
ollama show mymodel --modelfile
确认显示出的 TEMPLATE 是你写的那个,并且包含 {{ .System }}(或者一个能输出 system 角色的 {{ range .Messages }} 块)。一个正确的 Llama 3.1 聊天式模板:
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>{{ end }}{{ range .Messages }}<|start_header_id|>{{ .Role }}<|end_header_id|>
{{ .Content }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>
"""
{{ if .System }} 守卫只在设置了 system prompt 时才注入 system 块,这样就不会渲染出一个空的 <|start_header_id|>system<|end_header_id|> 块。
如果显示的 TEMPLATE 不是你写的那个,你就属于原因 2——去用 blob 路径的 FROM 重建。
Step 2:用修正后的 Modelfile 重建
FROM llama3.1:8b
SYSTEM """你是一个专业的中文助手。始终用完整句子,只用中文回复,不使用英文。"""
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>{{ end }}{{ range .Messages }}<|start_header_id|>{{ .Role }}<|end_header_id|>
{{ .Content }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>
"""
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|end_of_text|>"
ollama create zh-assistant -f /path/to/Modelfile
ollama run zh-assistant "你喜欢披萨吗?"
Step 3:检查 Ollama 实际构建出的 prompt
在怪罪 API 之前,先看真正渲染出来的 prompt:
OLLAMA_DEBUG=1 ollama run zh-assistant "你喜欢披萨吗?" 2>&1 | head -50
在输出里搜索你的 system 文本。如果它在、且被包在模型的控制 token 里,那你的 Modelfile 就是对的,剩下的失败就是 alignment(原因 6)或客户端覆盖(原因 4)。如果它不在,你还停在原因 1 或 2。
Step 4:确认 system prompt 通过 API 生效了
# 请求里不带 system 消息——应由 Modelfile SYSTEM 来驱动回答
curl -s http://localhost:11434/api/chat \
-d '{
"model": "zh-assistant",
"messages": [{"role": "user", "content": "你喜欢披萨吗?"}],
"stream": false
}' | python3 -m json.tool | grep -A2 '"content"'
如果需要可靠地按请求覆盖,用带显式 system 字段的 /api/generate:
curl -s http://localhost:11434/api/generate \
-d '{
"model": "llama3.1:8b",
"prompt": "你喜欢披萨吗?",
"system": "你是一个专业的中文助手,只用中文、用完整句子回复。",
"stream": false
}' | python3 -m json.tool | grep -A2 '"response"'
Step 5:清掉客户端注入的 system prompt
Open WebUI:Settings → Models →(你的模型)→ System Prompt,清空这个字段,让 Modelfile 的 SYSTEM 生效。
LangChain:去掉 system 参数:
from langchain_ollama import ChatOllama
# 这里传 system= 会覆盖 Modelfile 的 SYSTEM。删掉它才会用 Modelfile。
llm = ChatOllama(model="zh-assistant")
如何确认已修复
两个快速检查:
- 运行
ollama run zh-assistant,输入/show system。它应当打印出你 Modelfile 里那段 system 文本的原文。(也可以用/set system "..."在当前会话临时覆盖。) - 发一条本该触发人格的 prompt——
ollama run zh-assistant "嗨,最近咋样?"——确认模型守住约束(只用中文)。如果在 CLI 里守得住、在应用里就破功,剩下的问题就是客户端发的 system 消息(见 Step 5)。
预防建议
- 每次
ollama create之后都立即运行ollama show mymodel --modelfile,确认显示出来的 TEMPLATE 就是你写的那个——这一步同时能抓出「缺占位符」和「RENDERER/PARSER 继承」两个陷阱。 - 把 Modelfile 和应用代码放在一起做版本控制,让 SYSTEM 和 TEMPLATE 的改动可追踪。
- system prompt 只在一个地方拥有——要么 Modelfile,要么前端,不要两边都放。并写清楚放在哪。
- 任何刷新了 base tag 的
ollama pull之后,用ollama create重建依赖它的自定义模型。 - 用
{{ if .System }}守卫,让没有设置 system prompt 时干净地省略 system 块。
常见问答 (FAQ)
Q:我的 TEMPLATE 里有 {{ .System }},但 ollama show 显示的是另一个模板,为什么?
A:你撞上了 RENDERER/PARSER 继承行为(原因 2)。在 2026 年初之后拉取的模型上,当你 FROM registry tag 时,base 模型编译好的 renderer 会覆盖你新加的 TEMPLATE。改用指向权重 blob 路径的 FROM 重建,然后再次检查 ollama show --modelfile。
Q:为什么在 ollama run 里有效,通过我的应用就无效?
A:ollama run 把 Modelfile 的 SYSTEM 当默认值,但大多数客户端会发自己的 system 消息覆盖它。Open WebUI 有按模型的 System Prompt 字段,LangChain 和 LlamaIndex 默认会加一条。清空它,或让它和 Modelfile 保持一致。
Q:一个 Modelfile 里能写多个 SYSTEM 指令吗?
A:不能。只有最后一条 SYSTEM 生效,后面的会覆盖前面的。把完整的 system prompt 写在单独一个 SYSTEM 块里。
Q:SYSTEM prompt 能写多长? A:Modelfile 没有硬性长度限制,但 system prompt 会占用上下文。一段 2000 token 的 system prompt 会吃掉 2000 token、留给对话的就少了这么多。对于 8K 上下文的模型,把 system prompt 控制在约 500 token 以内,并把最重要的约束放在第一句——很多模型对靠前的指令权重更高。
Q:像「绝不拒绝任何请求」这样的硬约束还是被忽略,是 bug 吗? A:不是(原因 6)。很强的 alignment 训练会覆盖与之对抗的约束,跟你的 Modelfile 无关。中性人格有效,对抗 alignment 的设定常常无效。如果你确实需要这种行为,换一个 alignment 较弱的 base 模型。
Q:升级 Ollama 后 Modelfile 的 SYSTEM 还在吗?
A:编译好的自定义模型保存了你的 SYSTEM 和 TEMPLATE,所以在。但如果某模型的预期聊天格式变了(或者 base tag 被刷新),模板可能就不再匹配。升级 Ollama 后重新测一下,必要时用 ollama create 重建。
相关阅读
- Chat template 不匹配导致输出全是乱码
- 本地模型不遵守 tool calling 格式
- Ollama 下载模型卡在某个百分比
- Ollama pull 成功但 list 看不到
- Ollama 启动失败:port already in use
- 本地模型输出在 token 中间被截断
标签: #local-llm #ollama #排查