当你调用一个大语言模型的API时,通常会传入类似这样的结构:
messages = [
{"role": "system", "content": "你是一个有帮助的助手。"},
{"role": "user", "content": "你好!"},
{"role": "assistant", "content": "你好!有什么可以帮助你的吗?"},
{"role": "user", "content": "2+2等于多少?"},
]
这个整洁的消息列表,在送入模型之前,会被转换成一段连续的文本。问题是:这段文本长什么样?
答案取决于模型。如果你用的是Qwen系列模型,这段文本会变成:
<|im_start|>system
你是一个有帮助的助手。<|im_end|>
<|im_start|>user
你好!<|im_end|>
<|im_start|>assistant
你好!有什么可以帮助你的吗?<|im_end|>
<|im_start|>user
2+2等于多少?<|im_end|>
<|im_start|>assistant
但如果你用的是Mistral模型,同样的对话会变成:
<s>[INST] 你好! [/INST] 你好!有什么可以帮助你的吗?</s>[INST] 2+2等于多少? [/INST]
这就是对话模板(Chat Template)——一个在大模型应用中至关重要却常被忽视的基础组件。它定义了如何将结构化的对话历史转换为模型能够理解的线性文本序列。
为什么对话模板如此重要?
对话模板不是装饰性的语法糖。它的设计直接影响模型的性能、安全性和行为一致性。
训练与推理的一致性
大语言模型在指令微调阶段,会使用特定的对话格式进行训练。模型学会了识别这种格式中的角色标记、消息边界和上下文关系。在推理时,如果输入的格式与训练时不一致,模型的表现会显著下降。
Hugging Face的研究表明,使用正确的对话模板可以使模型在IFEval基准测试上的分数显著提升。反之,格式不匹配会导致模型产生不连贯的回复、忽略上下文,甚至生成完全无关的内容。
安全对齐的脆弱性
2025年发表在AAAI会议上的一篇论文揭示了一个被命名为ChatBug的安全漏洞。研究者发现,对话模板的刚性格式要求——模型必须遵循,但用户不一定——可以被攻击者利用。
具体而言,攻击者可以通过两种方式绕过模型的安全对齐:
-
格式不匹配攻击(Format Mismatch Attack):修改或省略模板中的特殊控制token。实验显示,这种方法可以将生成有害内容的概率提高 $10^{10}$ 倍。
-
消息溢出攻击(Message Overflow Attack):在用户消息中插入模型回复的开头部分,诱导模型"续写"有害内容而非正常对话。
在对8个主流模型(包括Vicuna、Mistral、Llama-2、Llama-3、GPT-3.5、Gemini、Claude-2.1、Claude-3)的测试中,研究者成功通过ChatBug漏洞让所有模型产生了本应被拒绝的有害输出。
对话模板的技术实现
Jinja2模板语法
现代对话模板通常使用Jinja2模板语言实现。以下是一个简化的ChatML模板示例:
{%- for message in messages %}
<|im_start|>{{ message['role'] }}
{{ message['content'] }}<|im_end|>
{% endfor %}
{% if add_generation_prompt %}
<|im_start|>assistant
{% endif %}
Jinja2提供了条件判断、循环、变量输出等基本控制结构。模板中的 - 符号用于控制空白输出——这对于某些模型至关重要,因为额外的空格或换行可能导致性能下降。
核心变量
模板可以访问以下核心变量:
messages:消息列表,每条消息包含role和content字段add_generation_prompt:布尔值,指示是否在末尾添加助手回复的开头bos_token、eos_token:开始和结束序列tokentools:工具定义列表(用于函数调用)
apply_chat_template 函数
Hugging Face Transformers库提供了 apply_chat_template 方法来统一处理不同模型的格式转换:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-7B-Instruct")
formatted = tokenizer.apply_chat_template(
messages,
tokenize=False, # 返回字符串而非token ID
add_generation_prompt=True # 添加助手回复开头
)
这个方法会读取tokenizer配置中的 chat_template 属性,自动使用正确的格式进行渲染。
主流模板格式对比
ChatML:XML风格的经典方案
ChatML(Chat Markup Language)由OpenAI团队设计,是较早出现的对话格式标准之一。其核心思想是用XML风格的标签标记消息边界:
<|im_start|>role
message content<|im_end|>
<|im_start|> 和 <|im_end|> 中的 “im” 代表 “input message”。这种格式被Qwen、Yi等模型家族采用。
ChatML的优势在于结构清晰、易于解析,但其设计也存在一些问题:标签较长,增加了token消耗;不同实现中对空白的处理不一致,导致社区出现过多次争论。
Llama系列:不断演进的格式
Meta的Llama模型家族经历了多次模板格式的变化:
Llama 2及之前:使用类似旧版Llama的格式,标签包括 <|begin_of_text|>、<|start_header_id|>、<|end_header_id|>、<|eot_id|>。
Llama 3:简化为更紧凑的标签集合:<|header_start|>、<|header_end|>、<|eot|>。
一个完整的Llama 3格式示例:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
你是一个有帮助的助手。<|eot_id|><|start_header_id|>user<|end_header_id|>
你好!<|eot_id|><|start_header_id|>assistant<|end_header_id|>
这种演进反映了Meta在token效率和格式简洁性之间的持续优化。
Mistral:方括号的极简主义
Mistral选择了完全不同的设计哲学。其核心格式极其简洁:
[INST] user message [/INST] assistant message</s>
[INST] 和 [/INST] 起源于早期Llama模型的格式,但Mistral将其发扬光大。这种设计有几个有趣的特点:
-
控制token化:从Mistral的第二个tokenizer版本开始,
[INST]和[/INST]被设计为单独的控制token,而非普通文本。这意味着它们不会被分词器拆分。 -
空格的微妙之处:基于sentencepiece的tokenizer会在编码时自动添加前导空格。这导致了格式中空白位置的细微差异,曾引发社区的广泛讨论。
-
Tekken tokenizer的改进:Mistral Nemo等新模型采用基于tiktoken的Tekken tokenizer,消除了前导空格问题,使格式更加直观:
<s>[INST]user message[/INST]assistant message</s>
Alpaca:开源浪潮的起点
2023年3月,斯坦福大学发布了Alpaca模型,这是开源大模型历史上的一个里程碑。Alpaca的模板格式极其简单:
### Instruction:
{instruction}
### Response:
{response}
这种格式直接暴露了对话的"指令-响应"本质,虽然缺乏多轮对话的显式支持,但其简洁性启发了无数后续工作。Vicuna等模型在此基础上进行了扩展,使用 USER: 和 ASSISTANT: 标记角色。
Gemma:Google的回合制设计
Google的Gemma模型采用了独特的"回合"概念:
<start_of_turn>user
Hello!<end_of_turn>
<start_of_turn>model
Hi there!<end_of_turn>
值得注意的是,Gemma将助手角色称为 model 而非 assistant,这反映了Google内部不同的术语习惯。此外,Gemma的system prompt处理也与众不同:它不是独立的消息类型,而是被嵌入到第一个用户消息中。
角色约束:比格式更复杂的问题
如果说token格式的差异只是"语法"层面的不同,那么角色约束的规则则涉及更深层次的语义问题。
不同模型对消息序列有不同的要求:
| 模型 | System消息 | 角色交替要求 |
|---|---|---|
| OpenAI GPT | 任意位置,多个 | 无强制交替 |
| Gemini | 最多一个,必须首位 | 必须交替 |
| Anthropic Claude | 最多一个,必须首位 | 强制交替,连续同角色消息会被合并 |
| Qwen | 任意位置,多个 | 无强制交替 |
| Gemma | 无独立system角色 | 嵌入首个用户消息 |
这意味着,如果你想让代码在多个模型之间移植,最安全的策略是遵循最严格的约束:单个system消息在首位,之后严格交替user和assistant消息。
模板碎片化:一个正在演进的困境
随着大模型生态的爆发式增长,对话模板的碎片化问题日益严重。一个开发者在Reddit上的帖子这样描述现状:
“Jinja模板调试是世界上最无助的事情——没有显式类型、没有适当的异常处理、没有单元测试工具。你永远不知道它会在哪个输入上崩溃。”
碎片化带来的问题包括:
1. 跨模型兼容性差
同一段对话代码,换个模型可能就完全无法工作。开发者需要为每个模型维护不同的格式化逻辑。
2. 工具调用格式混乱
当涉及到函数调用时,格式差异更加显著。OpenAI的Harmony格式使用TypeScript风格定义工具,而ChatML则使用JSON Schema。模型输出的工具调用格式也各不相同——有的用XML标签包裹,有的用特殊token分隔。
3. 安全模型的复杂性
ChatBug论文揭示了一个更深层次的问题:对话模板的"语义模态"(semantic modality)是模糊的。system消息被模型视为"客观现实",user消息是"输入信息",而assistant的历史回复则可能被视为"上下文"或"风格示例"。
这种模糊性导致了意想不到的安全隐患。如果把对话历史摘要放入system消息,恶意用户能否通过污染摘要来实施越狱?如果把示例对话放入system消息,模型是否会将其视为事实?
从ChatML到Harmony:格式演进的新方向
2025年,OpenAI引入了新的Harmony格式,代表了对话模板设计的新思路。
Harmony的核心创新是多通道架构:
analysis通道:承载推理过程,不受安全过滤器约束commentary通道:工具调用的前言和中间结果final通道:面向用户的最终输出,受安全约束
一个完整的Harmony示例:
<|start|>user<|message|>2+2等于多少?<|end|>
<|start|>assistant<|channel|>analysis<|message|>
计算2+2,结果为4。
<|end|>
<|start|>assistant<|channel|>final<|message|>
2+2等于4。<|return|>
这种设计将推理过程和用户输出在格式层面分离,使得可以安全地展示推理过程而不暴露敏感内容。
另一个值得关注的改进是工具定义使用TypeScript风格而非JSON Schema。研究表明,代码形式的工具定义比JSON格式更简洁(约30%),更易于模型理解,并且支持更自然的变量管理。
实践指南:如何正确使用对话模板
始终使用apply_chat_template
不要手动拼接对话字符串。正确的做法是:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct")
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
检查模板是否存在
并非所有模型都有内置的对话模板:
if tokenizer.chat_template is None:
raise ValueError("该模型没有配置对话模板")
理解空白处理
对于Mistral等模型,空白字符的处理至关重要。确保在调用模板前正确处理消息内容的首尾空白。
为生产环境编写测试
Jinja模板的错误往往难以调试。在生产环境中,应该为模板渲染编写单元测试:
def test_chat_template():
test_cases = [
{"messages": [{"role": "user", "content": "test"}], "expected": "..."},
# 更多测试用例
]
for case in test_cases:
result = tokenizer.apply_chat_template(case["messages"], tokenize=False)
assert result == case["expected"]
保持角色约束的兼容性
如果你的应用需要支持多个模型,遵循最严格的约束:
def build_safe_messages(system_prompt, history, current_input):
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
for turn in history:
messages.append({"role": "user", "content": turn["user"]})
messages.append({"role": "assistant", "content": turn["assistant"]})
messages.append({"role": "user", "content": current_input})
return messages
结语
对话模板是大模型应用中最基础却又最容易被忽视的组件。它不仅是格式转换的工具,更是连接训练与推理、影响安全性与性能的关键桥梁。
当前生态中的模板碎片化问题,反映了开源大模型快速演进过程中的技术债务。从ChatML到Harmony的演进,展示了业界对更优雅、更安全的对话格式的持续探索。
对于开发者而言,理解对话模板的设计原理和正确使用方式,是构建稳定、可靠的大模型应用的基础。当你下次调用模型的API时,不妨花一点时间思考:这条消息在模型眼中究竟是什么样子?
参考资料
- Hugging Face. “Chat templates” - https://huggingface.co/docs/transformers/chat_templating
- Jiang et al. “ChatBug: A Common Vulnerability of Aligned LLMs Induced by Chat Templates” AAAI 2025
- Mistral AI. “Demystifying Mistral’s Instruct Tokenization & Chat Templates”
- Stanford CRFM. “Alpaca: A Strong, Replicable Instruction-Following Model” 2023
- altsoph. “What’s Wrong with Chat-Templates Format for LLM” 2025
- Hugging Face Blog. “ChatML vs Harmony: Understanding the new Format from OpenAI”
- LMSYS. “Vicuna: An Open-Source Chatbot Impressing GPT-4”
- Meta. “Llama 3 Model Cards and Prompt Formats”