将聊天模板视为模型合约的一部分:角色、特殊标记与双重分词
聊天模板决定了聊天模型所期望的精确标记序列,包括角色控制标记和特殊边界标记。将模板视为合约数据可以防止因格式不匹配或特殊标记重复而导致的静默质量下降。
本文内容
简明答案
应将聊天模板视为模型合约的声明部分:记录支持的角色(system, user, assistant)、每个角色映射的控制标记、分词器的特殊标记以及格式化时使用的精确标志。建议使用 tokenizer.apply_chat_template 构建提示词,并优先设置 tokenize=True,这样模板自身的特殊标记将是唯一输出的标记。如果先格式化为字符串,在随后分词时必须传递 add_special_tokens=False,因为模板已经包含了必要的边界标记。仅在启动全新的助手回复时使用 add_generation_prompt=True,仅在预填充最终消息时使用 continue_final_message,且两者绝不能同时传递。由于即使是基于同一基座微调的模型,其模板也可能不同,因此必须在每个模型变体上通过简短的生成测试来验证该合约。
为什么聊天模板属于模型合约
因果语言模型接收到的并非真正的对话,而是一个扁平的标记序列,并据此预测接下来的内容。聊天模板的作用是将一组包含角色和内容的字典列表转换为模型在聊天微调期间接触到的精确序列,其中包含如 <|user|>、<|assistant|> 等控制标记以及允许模型识别对话结构的端点标记。
由于两个基于同一基座微调的模型可能使用完全不同的格式,因此模板属于行为合约数据而非简单的演示格式。格式不匹配通常不会触发异常,而是会导致响应质量静默下降,这就是为什么合约必须明确指明模板及其对应标记的原因。
实用建议
在评估框架中将渲染后的提示词字符串与模型版本一同存储。当模型或库升级后输出质量下降时,解码存储的提示词并将其与新渲染的结果进行比对;这样可以在调查生成设置之前,将模板漂移与模型行为区分开来。
标准角色及其语义
三种角色涵盖了大多数常见场景。system 承载关于模型行为的指令,通常出现在首位;user 承载人类的查询;assistant 承载模型的回复。模板将每个角色映射到特定的控制标记,且这种映射是针对具体模型而定的:例如 Mistral-7B-Instruct 将用户回合包裹在 [INST] 和 [/INST] 之间,而 Zephyr-7B 则使用 <|user|> 和 <|assistant|> 风格的标记并配合序列结束分隔符。
因此,合约应将角色及其具体的标记拼写共同记录。仅命名角色是不够的,因为相同的角色名称在不同的检查点中渲染结果不同,而错误的标记集正是模板旨在防止的失效模式。
在分词器中定义特殊标记
分词器配置公开了模板所消耗的组件。相关属性包括用于格式化消息列表的 Jinja 模板字符串 chat_template,以及 bos_token、eos_token、unk_token、sep_token、pad_token、cls_token 和 mask_token 等特殊标记。模板读取这些属性而非硬编码其拼写,因此分词器和模板必须从同一个模型版本加载。
前提条件是分词器必须实际携带 chat_template 属性。如果没有,apply_chat_template 将没有可渲染的内容,此时你必须显式提供模板或选择另一个检查点。这是一个环境和版本问题,并不保证任何给定的模板都能与给定模型的训练格式相匹配。
使用 apply_chat_template 应用模板
标准操作流程为:构建一个包含 role 和 content 键的字典列表,调用 apply_chat_template,并根据需求选择参数。当你需要直接用于 generate() 的标记 ID 时,设置 tokenize=True;当你需要格式化字符串用于检查或记录时,设置 tokenize=False。仅当你打算稍后自行添加特殊标记时,才设置 add_special_tokens=False。
下面的示例加载了一个带有聊天模板的分词器,格式化了一条系统消息和一条用户消息,并请求生成提示词。打印的行是说明性输出:精确的空格取决于分词器版本和模板修订版本。
说明性输出(仅展示结构,非实际运行捕获):<|system|> You are a helpful assistant </s><|user|> What is 2+2? </s><|assistant|>
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('HuggingFaceH4/zephyr-7b-beta')
messages = [
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "What is 2+2?"}
]
ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors='pt'
)
print(tokenizer.decode(ids['input_ids'][0]))避免双重分词
聊天模板已经输出了模型所需的特殊标记。如果你使用 tokenize=False 进行渲染,然后将结果字符串通过分词器的常规调用进行处理,默认的 add_special_tokens=True 路径可能会第二次插入 bos 或 eos 标记。这种重复不会导致程序崩溃,但会改变模型在训练时所期望的序列。
安全模式是使用 apply_chat_template(tokenize=True),它会返回包含控制标记的 ID。如果无法避免使用中间字符串,请在分词时设置 add_special_tokens=False。在格式化和编码分属于不同服务的流水线中,这一区别至关重要。
生成提示词与最终消息处理
设置 add_generation_prompt=True 会附加宣布助手回复开始的标记,使模型能够进行回答而非继续补全用户的文本。对于像 Llama 这样没有特殊助手开始标记的模型,该参数没有效果,因此合约必须记录目标模型是否使用该标记。
continue_final_message 的作用相反:它会移除序列结束标记,以便生成过程在最终消息内部继续,这对于预填充已知的响应前缀或推理字段(如 reasoning_content)非常有用。这两个标志是互斥的,同时使用会触发错误。在训练期间,应设置 add_generation_prompt=False,因为助手开始标记在训练序列中并无帮助。
针对模型变体测试合约
仅凭语法无法证明正确性。对于每个模型变体,应渲染一个固定的两轮对话,对其进行解码,并确认控制标记和边界标记与该模型训练时的格式一致。随后运行简短的生成测试,检查模型是否作为助手进行回复,而不是在扩展用户的消息。
每当模型版本、transformers 版本或分词器来源发生变化时,都应运行此检查。外观相同的角色名称并不意味着相同的标记序列,一个适用于某个微调版本的模板在另一个源自相同基座的版本上可能会静默失效。
在合约中记录模板
一个可复现的合约应记录角色、所需的控制标记、生成提示词行为以及精确的模板字符串或其不可变的源版本。下面的片段是用于文档系统的模式片段,而非可运行的配置:它需要你填入模型标识符和模板文本。
应记录模板来源(分词器属性或显式字符串)而非仅记录其输出,因为当分词器库版本变化时,输出可能会随之改变。
model_contract:
model_id: <hugging-face-model-id>
tokenizer_revision: <revision-or-commit>
roles:
system: <role-token-or-pattern>
user: <role-token-or-pattern>
assistant: <role-token-or-pattern>
special_tokens:
bos: <token>
eos: <token>
add_generation_prompt_on_inference: true
add_generation_prompt_on_training: false
continue_final_message: prefill-only
chat_template_source: tokenizer.chat_template
chat_template_text: <exact-jinja-template>检查清单
- 确认所使用的具体模型修订版本的分词器具有非空的 chat_template 属性。
- 解码渲染后的提示词,验证每个角色的控制标记是否与模型的训练格式匹配。
- 验证分词后每个消息边界仅出现一次 bos/eos 边界标记。
- 如果先格式化为字符串,确认随后的分词调用使用了 add_special_tokens=False。
- 确认 add_generation_prompt 和 continue_final_message 绝未同时传递。
- 记录 add_generation_prompt 对目标模型是否有影响,因为某些模型缺乏助手开始标记。
- 为每个模型变体运行简短的生成测试,检查模型是否在回答而非延续用户回合。
- 将 transformers 版本和分词器修订版本与存储的模板文本一同固定。
适用范围
模板是模型特定的:为一个检查点编写的合约可能不适用于另一个检查点,即使两者源自同一基座模型。对于像 Llama 这样没有显式助手开始标记的模型,add_generation_prompt 无效,且将其与 continue_final_message 结合使用会报错。示例假设使用了具有 chat_template 属性的 Hugging Face 分词器并安装了 transformers 库;精确的渲染空格和标记 ID 取决于分词器和库版本,因此此处显示的解码字符串仅为说明性而非实际捕获输出。本文仅涵盖提示词格式化,不对生成质量、延迟或任务准确性做任何声明。