你在 llama-server 里加载 Mistral-7B-Instruct-v0.3 并发一条聊天消息,结果模型不是干净地回答,而是把你整条消息原样吐回来,在回复中间夹着 [INST] 标记,接着开始自己编下一轮”用户提问”。又或者你加载 Llama 3,输出直接以纯文本 <|im_start|>assistant 开头。这些都是 chat-template 不匹配的症状:tokenizer 和推理引擎对”如何包装这段对话”没有达成一致,模型收到的 token 序列看起来像是被人从对话中间注入的,于是只能生成噪声。
最快的修复方式(截至 2026 年 6 月): 给 llama-server 加上 --jinja 参数。它会让 llama.cpp 使用 GGUF 文件里内嵌的 Jinja chat template(由其内置的 minja 引擎解析),而不是去猜一个命名模板。--jinja 在较新版本里默认开启,但建议显式写出,避免旧配置悄悄回退到错误格式。如果 GGUF 里根本没有内嵌模板,直接看 Step 1 读出模型的真实模板并传入正确的命名模板。
先判断你属于哪一类
在动手改之前,先把你看到的具体症状对应到最可能的原因。
| 你看到的症状 | 最可能的原因 | 跳转到 |
|---|---|---|
回复里出现纯文本 [INST]、<|im_start|>、<|eot_id|> | 给模型家族用错了命名模板 | 原因 1 / Step 2 |
| 输出连贯但停不下来,或一句话反复重复 | 该模板没注册正确的 stop/EOS token | 原因 6 / Step 5 |
| 模型只是续写你的文字,从不”回答” | 你下载的是 base 版而非 instruct 版 | 原因 2 |
Ollama 里正常,裸 llama.cpp 里乱码 | 引擎没套任何模板(走了 raw completion) | 原因 3 |
| 第一个 token 乱码或语言不对 | BOS token 被重复添加 | 原因 5 / Step 4 |
| 系统提示词漏到了可见回答里 | system 消息被放在模板结构之外 | 原因 4 |
常见原因
按命中率从高到低排列。
1. 给模型家族用错了命名模板
llama.cpp、llama-server、Ollama 各自带了一份内置模板名清单。如果你给 Llama 3 模型传 --chat-template llama2,引擎会把消息包进 [INST] ... [/INST],而不是 Llama 3 训练时用的 <|begin_of_text|>...<|eot_id|> 格式。模型从没见过 Llama 2 的包装方式,自然输出混乱。
2026 年常见的一个坑:模板名比大家想象的更”具体”。Mistral-7B-Instruct-v0.3 要用 mistral-v3,不是 mistral(根本没有这个名字)。Qwen2.5 用 chatml,没有 qwen2 这个值。Mistral Nemo 用 mistral-v3-tekken;Mistral Large 2411 用 mistral-v7。
怎么判断:运行 ./llama-cli --chat-template-help(或查看官方支持模板的 wiki),把这些名字和模型 HuggingFace tokenizer_config.json 里的 chat_template Jinja 字符串逐一对比。
2. 你下载的是 base 版,不是 instruct 版
Base(预训练)权重没有经过 SFT/RLHF,对任何 chat template 都不响应。它只会续写你喂进去的 token,读起来就是一段无视你问题的胡言乱语。
怎么判断:检查 GGUF 文件名和 HuggingFace 仓库名里有没有 Instruct、Chat、-it。如果写的是 base 或没有后缀,就是拿错了权重。base 模型再怎么换模板也救不回来。
3. GGUF 里没有内嵌模板,引擎用了通用模板
convert_hf_to_gguf.py 会把 tokenizer 的 chat_template 复制进 GGUF 元数据。如果转换时用了旧脚本或第三方转换器,模板可能丢失,引擎只能回退到一个和模型对不上的通用格式。这种情况在裸 llama-cli(默认不做模板包装)里会出乱码,但在 Ollama(自己套模板)里不会。
怎么判断:
python3 -c "import gguf; r = gguf.GGUFReader('model.gguf'); print(r.fields.get('tokenizer.chat_template'))"
如果输出是 None,说明 GGUF 里没有模板,你必须显式补上。
4. 系统提示词被放在模板结构之外
某些模型(Mistral v0.3、Qwen2.5)期望把系统提示嵌进第一个/最后一个 [INST] 块里,而不是作为独立的 system 轮次。如果你的客户端发了 {"role": "system", "content": "..."},而模板又不处理独立的 system 角色,系统文本就会漏到可见回答的错误位置。
怎么判断:去掉 system 消息,只发 user 消息。如果输出变好了,问题就出在 system 角色的位置上。
5. BOS(序列起始)token 被加了两次
有些封装层在调用 tokenizer 之前手动加了一次 BOS,而 tokenizer 自己也会加;Jinja 模板可能再加第三次。双重 BOS 会扰乱位置 0 的位置编码,症状是第一个 token 乱码、或者前缀冒出错误的语言。较新的 llama.cpp 会打印类似 added a BOS token to the prompt as the prompt already starts with a BOS token 的警告。
怎么判断:用 --verbose 启动 llama-server 看这行警告,或者检查开头的 token ID。如果模型的 BOS id(Llama 系是 1)在开头出现了两次,就有东西在重复添加它。
6. stop/EOS token 没注册,或对话历史角色顺序错了
如果引擎不知道模型的”轮次结束”token,模型答完之后会继续往下写,自己幻想出下一轮用户提问,或者把一句话循环到底。另外,如果你的客户端把角色发成 [assistant, user, assistant] 而不是严格交替的 [user, assistant, user],模型会看到一个没有提问就出现的 assistant 轮次,于是输出混乱、重复。
怎么判断:确认 --jinja 或命名模板注册了正确的 stop token(Llama 3 是 <|eot_id|>、ChatML 是 <|im_end|>、Mistral 是 </s>),并把发给 /v1/chat/completions 的实际 JSON 打出来,核对 role 序列是否严格交替。
最短修复路径
Step 1:读出模型真正的 chat template
权威来源是模型自己的 tokenizer,不是你的记忆。
# 从 HuggingFace hub 读取(公开仓库可用)
python3 - << 'EOF'
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
print(tok.chat_template)
EOF
对于需要登录的 gated 模型(Llama 3 需要 HuggingFace token),直接下载配置文件:
hf download meta-llama/Llama-3.1-8B-Instruct tokenizer_config.json
python3 -c "import json; d=json.load(open('tokenizer_config.json')); print(d['chat_template'])"
读出你这个 GGUF 里实际烧进去的模板:
python3 - << 'EOF'
import gguf
r = gguf.GGUFReader("model-Q4_K_M.gguf")
for f in r.fields.values():
if "chat_template" in f.name:
print("=== GGUF chat_template ===")
print(bytes(f.parts[-1]).decode("utf-8"))
EOF
Step 2:在 llama-server 中强制使用正确模板
优先用 --jinja,它直接使用 GGUF 内嵌模板,是 2026 年最可靠的做法:
./llama-server -m model-Q4_K_M.gguf --jinja
如果内嵌模板缺失或不对,就传一个显式的命名模板。请使用精确的现行名字(已对照 llama.cpp 2026 年 6 月核实):
# Llama 3 / 3.1 / 3.2
./llama-server -m models/Llama-3.1-8B-Instruct-Q4_K_M.gguf --chat-template llama3
# Mistral 7B Instruct v0.3(不是 "mistral")
./llama-server -m models/Mistral-7B-Instruct-v0.3-Q4_K_M.gguf --chat-template mistral-v3
# Mistral Nemo(tekken tokenizer)
./llama-server -m models/Mistral-Nemo-Instruct-2407-Q4_K_M.gguf --chat-template mistral-v3-tekken
# Qwen 2 / 2.5(用 ChatML,没有 "qwen2" 这个名字)
./llama-server -m models/Qwen2.5-7B-Instruct-Q4_K_M.gguf --chat-template chatml
# Gemma
./llama-server -m models/gemma-2-9b-it-Q4_K_M.gguf --chat-template gemma
如果你的模型需要一个不在内置清单里的模板,直接指向原始 Jinja 文件:
./llama-server -m model.gguf --jinja --chat-template-file ./my_template.jinja
Step 3:修正 Ollama Modelfile
在 Ollama Modelfile 里,TEMPLATE 块和 stop 参数必须和训练格式完全一致。以 Llama 3.1 为例:
FROM llama3.1:8b
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|>"
保存为 Modelfile.llama31,运行 ollama create myllama31 -f Modelfile.llama31,再用 ollama show myllama31 --modelfile 验证,并把 TEMPLATE 块对照该模型在 Ollama 官方库页面上的版本。别凭记忆手写模板,从官方来源复制。
Step 4:制止双重 BOS
如果 --verbose 打出了双 BOS 警告,就不要再自己加 BOS。在 Python 封装里,让聊天路径处理特殊 token:
from llama_cpp import Llama
llm = Llama(
model_path="model-Q4_K_M.gguf",
n_gpu_layers=35,
n_ctx=8192,
chat_format="llama-3", # 或 "chatml"、"mistral-instruct"
verbose=False,
)
# 不要手动加 BOS;chat_format 只会插入一次。
resp = llm.create_chat_completion(
messages=[{"role": "user", "content": "1 加 1 等于几?"}]
)
print(resp["choices"][0]["message"]["content"])
在 llama-server 里可以覆盖元数据标志,但注意它对少数模型家族会被忽略(截至 2026 年 6 月的已知 bug,见 llama.cpp issue #21786):
./llama-server -m model.gguf --jinja \
--override-kv tokenizer.ggml.add_bos_token=bool:false
llama-server 上没有 --no-bos 参数;那个参数只存在于部分较旧的 llama-cli 版本里。
Step 5:手动套用模板以便检查
当你不确定引擎到底发了什么,就自己渲染一遍 prompt 并读出来:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("path/to/model/or/hub/id")
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain Docker networking."},
]
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print(repr(prompt)) # 进引擎之前的精确字符串,含所有特殊 token
apply_chat_template 就是事实标准:BOS、角色 token、末尾的 generation prompt 都应当存在且顺序正确。
Step 6:确认确实修好了
发一条确定性的测试 prompt,检查响应体而不只是 HTTP 状态码:
curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "1 加 1 等于几?只回答数字。"}],
"max_tokens": 10,
"temperature": 0
}' | python3 -m json.tool | grep content
满足以下条件即为修复:
- 回复是
2(干净、首尾没有杂物); - 回复里没有以纯文本出现的角色标记(
[INST]、<|im_start|>、<|eot_id|>); - 生成自己停下来,而不是一直写到
max_tokens; - 服务端日志里没有双 BOS 警告。
如果还能看到角色 token 或被回吐的输入,说明模板仍然不对,回到 Step 1。
预防建议
- 默认加
--jinja,让引擎读 GGUF 内嵌模板,而不是去猜一个名字。 - 跑新模型前先读
tokenizer_config.json的chat_template(或 GGUF 元数据),再与引擎的模板名对齐。 - 如果内嵌模板缺失或过时,用最新的
convert_hf_to_gguf.py重新转换。 - Ollama Modelfile 里的
TEMPLATE块要从官方库页面复制,绝不凭记忆手写。 - 部署时锁定 llama.cpp 版本;模板和 Jinja 解析行为会随版本变化。
- 给部署里的每个模型维护一份模型到模板的映射文档。
常见问答 (FAQ)
Q:为什么 /completion 端点能正常工作,/v1/chat/completions 却输出乱码?
A:/completion 接收原始文本、不套任何 chat template;/v1/chat/completions 会用配置的模板包装你的消息。如果那个模板对模型来说是错的,就只有聊天端点坏掉。用 apply_chat_template(Step 5)渲染 prompt,和你发给 /completion 的内容对比一下。
Q:该信任 Ollama 的自动探测吗?
A:从 Ollama 官方库拉取的模型可以信任——模板是随模型一起带来的。对于你从 HuggingFace 自己转换的 GGUF,用 ollama show model --modelfile 验证并对照 tokenizer 配置;当 GGUF 没有内嵌模板时,自动探测会退化成启发式猜测。
Q:Mistral 7B Instruct v0.3 在 llama.cpp 里用哪个模板?
A:mistral-v3。没有叫 mistral 的模板名。v0.3 引入了原生工具调用控制 token;如果你套了 v1/v0.1 的模板,[TOOL_CALLS] 之类的 token 会以纯文本形式冒出来。Mistral Nemo 用 mistral-v3-tekken,Large 2411 用 mistral-v7。
Q:输出不乱码,但模型一句话重复个没完,这也是模板问题吗?
A:往往是,而且是 stop token 在作怪。如果模板没注册模型的轮次结束 token,生成就永远停不下来。确认正确的 stop token 已设置(Llama 3 是 <|eot_id|>、ChatML 是 <|im_end|>、Mistral 是 </s>),可以在 --jinja 输出或 Modelfile 的 PARAMETER stop 行里检查。
Q:错误的模板会悄悄绕过安全护栏吗? A:会。instruct/RLHF 对齐是针对特定角色 token 训练的。如果模板把内容放进错误的角色,模型可能不再把某条请求识别为用户轮次,其拒答行为就会失灵——这不是越狱,只是格式混乱。
相关阅读
- 本地模型输出在 token 中间被截断
- 本地模型不遵守 tool calling 格式
- Ollama Modelfile 里的 SYSTEM prompt 被忽略
- Tokenizer 版本不一致导致 token 计数对不上
- llama.cpp 换更激进量化后质量明显下降
标签: #local-llm #llama-cpp #排查