Chat Template 说明
这篇文章解决什么问题
很多人在用聊天模型时,表面上看到的是这样一份数据:
json
[
{ "role": "system", "content": "你是一个严谨的助手" },
{ "role": "user", "content": "解释一下 chat template" }
]但模型真正接收的并不是这个数组,而是一串已经排好格式的 token。
chat template 解决的,就是这一步“翻译”问题:
- 上层程序习惯用
messages这种结构化数据表达对话 - 下层模型只认识一段线性的 token 序列
- 不同模型训练时看过的对话格式并不一样
如果这一步翻译错了,模型虽然还能生成内容,但常见结果是:
- 回答风格变差
- 把
user的话接着往下写 - 忽略
system指令 - 多轮对话边界混乱
tool call、JSON 预填充、停止条件都变得不稳定
先说结论
chat template本质上不是“提示词技巧”,而是“输入协议”。- 它的核心作用,是把结构化对话还原成模型训练阶段见过的那种文本格式。
- 它最重要的价值,不是好看,而是让训练格式和推理格式保持一致。
- 一个模型能不能稳定聊天,除了模型能力本身,很大一部分取决于你有没有把它喂对格式。
为什么聊天模型本质上仍然是“续写模型”
不管界面看起来多像聊天窗口,大多数文本大模型底层还是在做一件事:根据前面的 token,预测下一个 token。
也就是说,模型并不知道“这里是一个数组”“这里有三条消息”“这条消息来自 user”。
这些结构,都是人和框架在外面定义的抽象。
模型实际看到的更像是这样一段序列:
text
<BOS><SYS>你是一个严谨的助手</SYS>
<USER>解释一下 chat template</USER>
<ASSISTANT>上面这段只是示意,不代表某个具体模型的真实格式。
重点在于:system、user、assistant 这些角色,最后都必须被编码成模型认识的边界标记和文本顺序。
所以聊天不是一种新的模型类型,而是:
- 先把多轮消息排成模型认识的单段序列
- 再让模型从这个序列后面继续生成
chat template 就负责第 1 步。
Chat Template 到底是什么
可以把它理解成一份“渲染规则”:
- 输入:
messages - 输出:一段符合该模型习惯的文本
在 Hugging Face 这类生态里,它通常挂在 tokenizer 或 processor 上。
上层程序把消息列表传进去,模板会把它展开成最终字符串,然后再分词成 token ids。
完整链路通常是这样:
text
messages
-> chat template 渲染
-> 格式化后的文本
-> tokenizer
-> input_ids
-> 模型继续生成下一个 token所以它的位置很明确:
- 它不属于模型权重本身
- 它也不是普通业务 prompt
- 它是结构化对话和 tokenizer 之间的那一层协议
它通常包含哪些部分
一个可用的 chat template,通常至少会定义下面几类东西。
1. 角色头部
它要告诉模型:下面这段内容是谁说的。
常见角色有:
systemuserassistanttool
不同模型用的标记不同,有的用专门 token,有的用特殊字符串,有的靠固定分隔符。
2. 消息边界
模型必须知道一条消息从哪里开始,到哪里结束。
否则多轮对话会被视为一整段普通文本,角色关系容易串掉。
常见做法包括:
- 每轮前面加开始标记
- 每轮后面加结束标记
- 用固定换行和分隔符切开
- 给 assistant 回复单独留一个起始头
3. 特殊 token
比如:
bos_tokeneos_token- 角色控制 token
- 工具调用相关 token
- 多模态占位 token
这些 token 的意义不是“给人看”,而是给模型制造稳定边界。
4. 生成起点
很多模板会在最后追加一个“assistant 即将开始回复”的标记。
这件事很关键,因为模型是续写器,不给它这个起点,它可能会继续补完上一条 user 消息,而不是开始回答。
5. 可选扩展输入
较新的模板不只处理普通文本消息,还会处理:
- tools
- documents
- image / video / audio 占位
- 结构化内容块
这也是为什么 chat template 不只是“拼字符串”,而是逐渐变成一层正式接口。
它的工作原理
一句话版本
训练时模型见过什么格式,推理时就尽量喂回什么格式。
展开来看
1. 监督微调阶段,聊天数据本来就是序列化的
做 chat 模型训练时,数据集通常不会直接把消息数组原封不动送进模型。
它一定会先被整理成某种文本格式,再做 tokenization。
比如一组对话:
json
[
{ "role": "system", "content": "回答要简洁" },
{ "role": "user", "content": "什么是 RAG?" },
{ "role": "assistant", "content": "RAG 是检索增强生成。" }
]训练时真正送进去的,往往更像:
text
[SYSTEM]
回答要简洁
[/SYSTEM]
[USER]
什么是 RAG?
[/USER]
[ASSISTANT]
RAG 是检索增强生成。
[/ASSISTANT]上面依然只是示意。
重点不是具体长相,而是:模型学到的不是“消息对象”,而是“这些控制标记和文本之间的统计关系”。
2. 模型会把这些标记当成语义边界
如果一个模型长期在训练中看到:
system段通常定义行为规则user段通常提出任务assistant段通常产出答案
那么这些边界标记就不只是装饰,而会变成模型理解上下文的重要信号。
这会影响:
- 哪些内容被当作指令
- 哪些内容被当作待回答问题
- 生成应该从哪里开始
- 生成应该在什么地方结束
3. 推理时需要复现相同分布
如果训练时见过的是格式 A,推理时你却手写成格式 B,就会出现分布偏移。
分布偏移不一定直接报错,但很容易带来软性退化:
- 指令跟随变差
- 角色切换异常
- 回复啰嗦或跑偏
- 结束位置不稳定
很多人觉得“这个模型不太行”,其实问题先出在输入格式没对齐。
4. 模板本身通常是一段可执行规则
以 Hugging Face 为例,chat template 常见写法本质上是一段模板语言。
它会遍历 messages,按角色决定该输出哪些控制标记、换行和结束符。
示意如下:
jinja
{{ bos_token }}
{% for message in messages %}
<|start|>{{ message.role }}
{{ message.content }}<|end|>
{% endfor %}
{% if add_generation_prompt %}
<|start|>assistant
{% endif %}这段伪代码说明了三个关键点:
- 每条消息都要带角色
- 每条消息都要有边界
- 生成前通常要明确告诉模型“接下来轮到 assistant 了”
它解决了哪些实际问题
| 问题 | 没有 chat template 时会怎样 | 有 chat template 后解决什么 |
|---|---|---|
| 角色边界混乱 | 模型可能把用户话术继续补完,而不是回答 | 明确区分 system、user、assistant |
| 多轮消息串线 | 旧轮次和新轮次容易粘在一起 | 每轮都有稳定起止边界 |
| 训练/推理不一致 | 能生成,但效果不稳定 | 尽量对齐训练时的输入分布 |
| 停止条件混乱 | 生成可能拖长、截断不准 | 用结束 token 或回复头约束输出 |
| 框架适配成本高 | 每换一个模型都得手写 prompt 拼接 | 模板成为统一入口 |
| 工具调用难统一 | tool schema、tool result 容易乱 | 把工具信息也纳入正式格式 |
| 多模态输入不统一 | 图片、视频占位混乱 | 把非文本输入也按规则编排 |
它最核心解决的是“协议兼容”问题
这件事很像前端和后端之间的接口约定。
你当然可以不用正式 schema,直接手写字符串去拼,但只要:
- 换一个模型
- 换一个 tokenizer
- 换一个工具调用格式
- 换一个数据预处理流程
原来那套临时拼接就会开始出问题。
所以从工程角度看,chat template 的真正价值是:
- 把消息结构和模型输入解耦
- 把“怎么排格式”从业务逻辑中抽出来
- 让训练、推理、评测、微调都围绕同一份协议
为什么不同模型不能混用同一个模板
因为不同模型在训练时见过的“对话语法”未必相同。
同一组消息:
json
[
{ "role": "system", "content": "回答要简洁" },
{ "role": "user", "content": "解释 Transformer" }
]在不同模型家族里,可能会被排成完全不同的样子:
text
示意格式 A:
<sys>回答要简洁</sys><user>解释 Transformer</user><assistant>text
示意格式 B:
[INST] 回答要简洁
解释 Transformer [/INST]text
示意格式 C:
<|im_start|>system
回答要简洁
<|im_end|>
<|im_start|>user
解释 Transformer
<|im_end|>
<|im_start|>assistant这几种格式在人眼里都能看懂,但模型不只看“意思差不多”,它对具体 token 模式是敏感的。
所以不能简单认为:
- “都是聊天模型,模板应该通用”
- “只要 system/user/assistant 三个字段还在就行”
实际情况是:字段名可以相同,序列化协议仍然可能完全不同。
一个很容易被忽略的点:换行、空格、结束符也算协议的一部分
很多格式差异不是“有没有 <user> 这种标记”,而是一些更细的细节:
- 消息之间是否有空行
- 角色头后面有没有换行
- 每轮后面是否跟
eos - assistant 开头是否需要额外前缀
- system 是否允许单独成段
这些东西看起来像小事,但模型训练时确实看过。
对某些模型来说,少一个结束符,行为就会明显变差。
它和 Prompt、System Prompt、Tokenizer 分别是什么关系
| 概念 | 它负责什么 | 不负责什么 |
|---|---|---|
| Prompt | 具体让模型做什么 | 不决定角色边界协议 |
| System Prompt | 定义全局行为约束 | 不等于消息序列化格式 |
| Chat Template | 把消息结构排成模型期待的顺序和标记 | 不决定任务内容本身 |
| Tokenizer | 把文本切成 token ids | 不负责理解哪段是谁说的,除非模板先写进去 |
可以这样理解:
- prompt 是“内容”
- chat template 是“包装协议”
- tokenizer 是“编码器”
三者不是一回事,但会前后串起来。
常见误区
1. messages 数组就是模型输入
不是。messages 只是上层程序的方便表示,模型看到的始终是序列化之后的 token。
2. chat template 只是为了让文本更整齐
不是。
它首先是功能问题,其次才是格式问题。
3. 模板错一点也没关系,模型自己会理解
有时会“勉强能用”,但这和“格式正确”不是一回事。
很多性能退化都来自这种勉强。
4. 只要加上 system 字段,就等于 system prompt 生效
不一定。
如果模板根本没把 system 角色按训练习惯正确序列化,模型就不一定按预期理解它。
5. chat template 能解决所有对话问题
不能。
它不能替代:
- 更好的指令设计
- 上下文裁剪策略
- RAG
- 安全策略
- 长对话记忆管理
它只负责把输入“喂对”。
6. 渲染完字符串后,随便再加 special tokens 都行
这要非常小心。
有些模板本身已经把必要的特殊 token 放进去了,如果后面再额外补一遍,等于重复加边界,反而会伤害效果。
add_generation_prompt 和“继续补全最后一条消息”不是一回事
这是使用模板时最容易踩坑的地方之一。
有两种常见需求:
场景 1:开始一条新的 assistant 回复
这时通常要在模板末尾补一个“assistant 现在开始说话”的起始标记。
它的作用是把生成起点放对。
场景 2:让模型继续补完最后一条 assistant 内容
比如你已经给了一个 JSON 前缀:
json
{"name": "这时你不是要开始一条新消息,而是要模型沿着当前消息继续写。
这类场景通常不能再额外补一个新的 assistant 起始头,否则语义就冲突了。
所以“开始新回复”和“继续补完当前回复”是两种完全不同的控制方式。
什么场景下你必须认真看 chat template
以下场景,最好不要把它当黑盒:
- 你在本地跑开源聊天模型
- 你在多个模型家族之间切换
- 你在做 SFT、DPO 或数据预处理
- 你在接 tool calling
- 你在做 JSON 约束输出或预填充
- 你在处理多模态输入
- 你发现同一个 prompt 换模型后行为明显异常
如果你只是调用某些封装很深的闭源 API,平台往往已经把模板藏起来了。
但这不代表它不存在,只是这层被服务端替你处理了。
它不解决什么问题
把边界说清楚很重要。chat template 很关键,但它不负责下面这些事:
- 不负责让模型“更聪明”
- 不负责补足缺失知识
- 不负责延长上下文长度
- 不负责自动防 prompt injection
- 不负责决定回复质量上限
它解决的是“输入协议正确性”,不是“能力上限”。
我的判断
如果把聊天模型比作一个只会续写的引擎,那么 chat template 就是这台引擎的进料规格。
你给它的不是标准燃料,它未必马上熄火,但动力一定不稳定。
所以它在大模型体系里的位置很明确:
- 不是模型能力本身
- 不是提示词花活
- 是聊天抽象能够成立的基础协议
很多“为什么这个模型突然不听话”的问题,最后都能追到这里。
TODO:围绕 chat template 继续补的关联问题
- [ ] 不同模型家族的模板差异整理:Llama、Qwen、Mistral、ChatML 各自怎么组织消息
- [ ]
system prompt到底放哪一层最稳:消息层、模板层、服务层分别有什么差别 - [ ]
add_generation_prompt和继续补全最后一条消息,什么时候该用哪一个 - [ ] chat template 和
eos token、stop sequences的关系怎么排查 - [ ] SFT 数据集从 instruction 格式转 chat 格式时,应该在哪一步套模板
- [ ] tool calling 模型里,tool schema 和 tool result 应该如何进入模板
- [ ] 多模态模型的 chat template,怎么把 image / video / audio 占位混进消息序列
- [ ] 长对话裁剪时,应该按消息裁、按 token 裁,还是做摘要回填
- [ ] prompt injection 在模板层能缓解什么,哪些问题根本不是模板能解决的
- [ ] 自定义 chat template 后,如何做最小回归验证,避免“能跑但效果变差”