Chat template 不匹配导致输出全是乱码

本地 LLM 输出大量重复符号、乱码或毫无意义的 token 序列。通常是 chat template 格式与模型不匹配所致,定位模板来源并强制指定正确格式即可修复。

在 llama.cpp b3290 下加载 Mistral-7B-Instruct-v0.3 Q4_K_M,发送普通问题后输出连续的 [INST] 标签和重复感叹号,或者输出全是 <s> 嵌套。用 Ollama 运行同一模型没有这个问题,但用 Python 通过 llama.cpp 的 HTTP 服务端调用时出现乱码。这是典型的 chat template 不匹配问题:模型权重期望特定的对话格式,而调用层使用了错误的包装方式,导致模型从未见过的 token 序列开始生成,输出随机或重复内容。

常见原因

1. GGUF 文件内嵌了 chat template,但客户端没有读取

llama.cpp 现代版本(b3000 以后)会将 chat template 直接嵌入 GGUF 文件的元数据中。但较旧的客户端代码或某些 Python 绑定会忽略这个元数据,直接使用硬编码的默认模板(如 llama-2 格式)。

怎么判断python3 -c "import gguf; r = gguf.GGUFReader('model.gguf'); [print(f.name, f.parts) for f in r.fields.values() if 'chat_template' in f.name]" 查看 GGUF 内嵌的 chat template;与实际使用的模板对比。

2. 在 Modelfile 或脚本中手动拼接了错误格式

Ollama Modelfile 的 TEMPLATE 字段或 llama.cpp 的 --chat-template 参数被设置为错误的格式。例如,将 Llama-2 的 [INST]...[/INST] 格式用于 Llama-3(应该用 <|start_header_id|>...<|end_header_id|>),或将 ChatML 格式用于 Mistral(应该用 [INST])。

怎么判断:对比 HuggingFace 模型页面的官方 chat template(在 tokenizer_config.jsonchat_template 字段),与当前使用的模板逐字比较。

3. tokenizer_config.json 中的 Jinja2 模板语法未被正确解析

HuggingFace 的 chat template 使用 Jinja2 语法,llama.cpp 对 Jinja2 的支持不完整(截至 b3290,某些循环和条件语法会被跳过或错误解析)。若模板包含复杂的 Jinja2 逻辑,解析失败时 llama.cpp 会静默回退到默认模板。

怎么判断./llama-cli --model model.gguf --list-templates 查看当前识别的模板类型;--log-level debug 启动后搜索 chat template 相关日志。

4. 模型是 base 版本而非 instruct 版本

下载了模型的预训练(base)版本而非指令微调(instruct/chat)版本。Base 模型没有经过 RLHF/SFT 训练,对 chat template 没有响应,只会无限续写输入的 token 序列。

怎么判断:GGUF 文件名或 HuggingFace 仓库名中是否包含 instructchatit(instruction-tuned)字样;若只有 base 或无后缀,就是 base 模型。

5. 系统提示词在 template 中被插入两次

某些框架在组装 prompt 时会先在 system 消息里插入一次 system prompt,然后 chat template 又在渲染时再插入一次,导致 system 内容重复出现,破坏了模型期望的对话结构。

怎么判断:在 --verbose 模式下打印最终传给模型的完整 prompt 字符串,检查 system prompt 是否出现两次。

6. BOS/EOS token 在 template 外被额外添加

Python 绑定(如 llama-cpp-python)在调用 tokenizer 时默认在序列开头添加 BOS token(<s><|begin_of_text|>),而 chat template 本身也包含了 BOS。双重 BOS 会破坏模型的解码状态。

怎么判断:查看使用的 Python 代码中 Llama()tokenize() 调用的 add_bos_token / add_special_tokens 参数设置。

最短修复路径

Step 1:读取 GGUF 内嵌的 chat template

# 安装 gguf Python 包
pip install gguf

python3 << 'PYEOF'
import gguf
reader = gguf.GGUFReader("model-Q4_K_M.gguf")
for field in reader.fields.values():
    if "chat_template" in field.name:
        template_bytes = bytes(field.parts[-1])
        print("=== GGUF chat template ===")
        print(template_bytes.decode("utf-8"))
PYEOF

Step 2:在 llama.cpp 中使用正确的 chat template

# 方法 1:让 llama.cpp 自动从 GGUF 读取模板(推荐)
./llama-cli -m model-Q4_K_M.gguf \
  --chat-template auto \
  -cnv \
  --temp 0.7

# 方法 2:手动指定 chatml 格式(适用于 Qwen / Mistral v2+)
./llama-cli -m model-Q4_K_M.gguf \
  --chat-template chatml \
  -cnv

# 方法 3:手动指定 llama3 格式
./llama-cli -m model-Q4_K_M.gguf \
  --chat-template llama3 \
  -cnv

Step 3:Ollama Modelfile 中设置正确模板

# 错误示例(Llama-3 模型用了 Llama-2 模板)
TEMPLATE """[INST] {{ .System }} {{ .Prompt }} [/INST]"""

# 正确示例(Llama-3 instruct 格式)
TEMPLATE """<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|><|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"

Step 4:llama-cpp-python 中禁用重复 BOS

from llama_cpp import Llama

llm = Llama(
    model_path="model-Q4_K_M.gguf",
    n_gpu_layers=35,
    n_ctx=8192,
    chat_format="chatml",  # 或 "llama-3", "mistral-instruct"
    verbose=False
)

# 不要手动添加 BOS,让 chat_format 处理
response = llm.create_chat_completion(
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "你好"}
    ]
)
print(response["choices"][0]["message"]["content"])

Step 5:验证模板正确性

# 使用简单问题验证,正确的模型应该给出连贯的中文回答
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local",
    "messages": [{"role": "user", "content": "1加1等于几?"}],
    "max_tokens": 50
  }'
# 期望回答类似"1加1等于2。"而非乱码

预防建议

  • 下载 GGUF 时优先选择 instruct 或 chat 后缀的版本,明确标识已经过指令微调。
  • 在 llama.cpp 中始终使用 --chat-template auto,让其自动从 GGUF 元数据读取正确模板。
  • 切换新模型时,先用 gguf-info 检查内嵌 chat template,与 HuggingFace tokenizer_config.json 比对。
  • llama-cpp-python 中指定 chat_format 参数,不依赖库的自动探测(自动探测有时会选错格式)。
  • 保存每个模型使用的 chat template 到项目文档,避免团队成员重复踩同一个坑。
  • 更新 llama.cpp 版本后重新验证 template 是否正确解析,新版本可能改变了 Jinja2 解析行为。
  • 遇到乱码输出时,第一步打印完整 raw prompt 确认格式,而不是调整采样参数。

常见问答 (FAQ)

Q: 同一个模型在 Ollama 里正常,在 llama.cpp 里乱码,区别是什么? A: Ollama 内置了主流模型的 chat template 映射,会自动选择正确格式;llama.cpp 命令行默认不应用 chat template(除非用 -cnv 对话模式或显式指定 --chat-template)。在 llama.cpp 中直接拼接原始 prompt 而不使用模板会触发这个问题。

Q: ChatML 格式是什么?哪些模型用它? A: ChatML 格式使用 <|im_start|>role\ncontent<|im_end|> 标记对话轮次。Qwen2/2.5、Mistral v2+、Phi-3、InternLM2 等模型都使用 ChatML。这是目前使用最广泛的 instruct 格式之一。

Q: 如果不确定模型用哪个模板,怎么快速测试? A: 最快的方法是访问 HuggingFace 模型页面,点击”Use this model”标签,在”with transformers”示例代码中找到 tokenizer.apply_chat_template() 的用法,就能看到正确格式。或者直接查看模型仓库的 tokenizer_config.json

Q: 输出不是乱码但模型一直重复同一句话,也是 template 问题吗? A: 可能是 template 问题,也可能是 stop token 没有设置导致模型在 EOS 处继续生成。检查 Modelfile 中的 PARAMETER stop 或 llama.cpp 的 --reverse-prompt 参数是否包含正确的 EOS/EOT token。

相关阅读

标签: #local-llm #llama-cpp #排查