编写聊天模板
聊天模板是存储在分词器的 ~PreTrainedTokenizer.chat_template 属性中的一个 Jinja 模板。Jinja 是一种模板语言,允许你编写类似 Python 的代码和语法。
{%- for message in messages %}
{{- '<|' + message['role'] + '|>\n' }}
{{- message['content'] + eos_token }}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|assistant|>\n' }}
{%- endif %}如果你仔细研究一下,就会发现它其实非常像 Python,只是带有一些奇怪的 {%- 语法。该模板会遍历一个消息列表,对于每条消息,它会打印该消息的角色和内容,随后是一个序列结束标记。如果 add_generation_prompt=True,它会在对话末尾添加助手消息的起始头部。
将写入的模板作为字符串加载,并将其赋值给分词器的 chat_template 属性。设置完成后,每次调用 apply_chat_template() 时都会使用该模板。此外,当调用 save_pretrained() 或 push_to_hub() 时,该模板也会随分词器一起保存。模板会保存在分词器目录下的 chat_template.jinja 文件中。您可以直接编辑此文件来修改模板,这通常比操作模板字符串更简单。有关 Transformers 支持的其他磁盘存储格式,请参阅下文中的 存储和加载聊天模板。
模板编写技巧
开始编写 Jinja 模板最简单的方法是参考现有的模板。对任意聊天模型使用 print(tokenizer.chat_template) 即可查看其使用的模板。建议从不调用任何工具或支持 RAG 的简单模型入手,因为工具调用模型的模板可能非常复杂。最后,请查阅 Jinja 文档 以了解有关格式和语法的更多详细信息。
不过,在编写聊天模板时,你可能会遇到一些特定的技巧和陷阱,本节将更详细地介绍其中的一些内容。
编写多模态聊天模板
对于多模态模板,chat_template 属性设置在 processor 上,而不是 tokenizer 上。消息的 content 键通常是一个内容字典列表,而不仅仅是一个字符串。您可能需要检查列表中每个内容项的类型,并相应地进行处理。
通常,模板不应直接访问图像或视频数据。这通常由处理器在模板渲染完成后处理。相反,当模板遇到图像或视频内容时,应输出一个特殊的标记,例如 <|image|> 或 <|video|>。处理器随后会将这个单一的特殊标记扩展为一系列图像或视频标记。具体要输出哪些标记取决于你正在使用的模型。我们强烈建议加载一个现有的多模态处理器,以查看它是如何处理数据的。
下面的示例模板处理混合的图像和文本内容。
{%- for message in messages %}
{%- if loop.index0 == 0 %}
{{- bos_token }}
{%- endif %}
{{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }}
{%- if message['content'] is string %}
{{- message['content'] }}
{%- else %}
{%- for content in message['content'] %}
{%- if content['type'] == 'image' %}
{{- '<|image|>' }}
{%- elif content['type'] == 'text' %}
{{- content['text'] }}
{%- endif %}
{%- endfor %}
{%- endif %}
{{- '<|eot_id|>' }}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{%- endif %}此多模态模板与上方更简单的模板非常相似,但它会检查 content 列表,并对其进行迭代,在必要处渲染 <|image|> 标记。这使得图像能够“融入”用户文本的流程中。
并非所有模型都采用这种方式——例如,某些模型可能会将所有图像移至用户消息的末尾。聊天模板应始终与模型训练时所使用的格式保持一致。
修剪空白
Jinja 会输出文本块前后存在的任何空白字符。对于聊天模板而言,这可能是一个问题,因为添加模型训练时不存在的额外空白字符可能会损害性能。要去除这些空白字符,请在 Jinja 行语法中添加 -。这样,您就可以使用符合 Python 风格的缩进和换行来编写模板,而不会在渲染后的输出中意外地打印出缩进。
下面的示例模板未使用 -,导致输出中会打印出多余的空白。
{% for message in messages %}
{{ message['role'] + message['content'] }}
{% endfor %}我们强烈建议使用 - 以确保仅打印预期的内容。
{%- for message in messages %}
{{- message['role'] + message['content'] }}
{%- endfor %}特殊变量和可调用对象
模板中唯一的常量是 messages 变量和 add_generation_prompt 布尔值。不过,你可以访问传递给 apply_chat_template() 方法的任何其他关键字参数。
这提供了灵活性,并支持我们在设计规范时可能未曾想到的使用场景。最常见的附加变量是 tools,它包含一个以 JSON schema 格式表示的工具列表。虽然您可以使用任意变量名,但我们强烈建议遵循惯例,将此用途的变量命名为 tools。这使模板与标准 API 具有更好的兼容性。
您还可以访问 tokenizer.special_tokens_map 中包含的任何 token,其中通常包括 bos_token 和 eos_token 等特殊 token。您可以像 {{- bos_token }} 这样直接通过名称访问它们。
有两个可供调用的函数。要调用它们,请使用 {{- function_name(argument) }}。
raise_exception(msg)会引发一个TemplateException。这在调试或提醒用户模板使用有误时非常有用。strftime_now(format_str)以特定格式获取当前日期和时间,这在系统消息中经常需要。它等同于 Python 中的 datetime.now().strftime(format_str)。
与非 Python Jinja 的兼容性
Jinja 已用多种语言实现,且语法基本一致。在 Python 中编写模板时,你可以对字符串使用 lower 等方法,或对字典使用 items 等方法。但如果模板在非 Python 实现中使用(例如使用 Javascript 或 Rust 进行部署时),这些方法将不可用。
进行以下更改,以确保在所有 Jinja 实现中保持兼容性。
- 用 Jinja 过滤器替换 Python 方法。例如,将
string.lower()替换为string|lower,或将dict.items()替换为dict|dictitems。除string.strip()外,大多数更改都遵循相同的模式,string.strip()应替换为string|trim。完整的过滤器列表请参阅 内置过滤器。 - 将
True、False和None(这些是 Python 特有的)分别替换为true、false和none。 - 直接渲染 dict 或 list 在其他实现中可能会返回不同的结果。例如,字符串项可能从单引号变为双引号。为避免这种情况,请添加 tojson 过滤器以保持一致性。
大型模板
较新的模型或具备 tool-calling 和 RAG 等功能的模型需要更大的模板,其长度可能超过 100 行。在单独的文件中编写较大的模板可能更为方便。单独文件中的行号与模板解析或执行错误中的行号完全对应,从而更便于调试潜在问题。
将模板写入单独的文件,并将其提取到聊天模板中。
open("template.jinja", "w").write(tokenizer.chat_template)您也可以将编辑后的模板重新加载到分词器中。
tokenizer.chat_template = open("template.jinja").read()存储和加载聊天模板
Chat 模板以多种不同的格式存储在磁盘上。现代检查点将模板保存为独立的 .jinja 文件,而较早的检查点则将其嵌入到分词器或处理器配置中。
存储格式
模板可以存储为以下任意一种格式。
chat_template.jinja(推荐)。位于仓库根目录的独立 Jinja 文件,包含单个聊天模板。save_pretrained() 默认写入此文件。将模板存储在独立文件中便于检查、编辑和对比。tokenizer 和 processor 以相同方式加载chat_template.jinja。additional_chat_templates/<name>.jinja。一个包含独立 Jinja 文件的目录,用于模型附带多个命名模板的情况(例如,一个default模板和一个单独的tool_use模板)。default模板仍然放在仓库根目录的chat_template.jinja中,但其他所有命名模板都放在additional_chat_templates/<name>.jinja中,其中文件名的主干部分即为模板名称。
以下旧格式仅保留用于向后兼容的加载。请勿将聊天模板写入其中任何一种。
tokenizer_config.json中的chat_template字段。这是一种仅用于加载的旧版格式,在独立的.jinja文件出现之前使用。该模板以 JSON 字符串的形式嵌入在tokenizer_config.json中。当模型包含多个命名模板时,该字段是一个由{"name": ..., "template": ...}字典组成的列表,而不是单个字符串。使用此格式的现有仓库仍可正常加载,但 save_pretrained() 会写入现代的.jinja格式。chat_template.json。一种仅用于加载的旧版格式,由较早的多模态处理器检查点使用。它是一个 JSON 文件,形式为{"chat_template": "<template string>"}。使用此格式的现有仓库仍可正常加载,但 save_pretrained() 会写入现代的.jinja格式。如果处理器仓库中混合了旧版chat_template.json和现代.jinja文件,加载时会引发错误。
加载优先级
调用 from_pretrained() 时,Transformers 会按照固定的优先级解析存储格式。独立的 .jinja 文件优先于配置中嵌入的模板。加载器:
- 读取
tokenizer_config.json(或对于处理器,读取旧版的chat_template.json)中存在的任何chat_template字段。 - 如果仓库根目录下存在
chat_template.jinja,则读取该文件并将其用作default模板,覆盖步骤 1。 - 读取
additional_chat_templates/中的每个.jinja文件,以文件名主干作为键,并将它们合并。
如果结果中只有一个 default 模板,~PreTrainedTokenizer.chat_template 将被设置为该字符串。如果存在多个命名模板,chat_template 将变为一个 {name: template_string} 字典。在这种情况下,当传入工具时,apply_chat_template() 会选取 tool_use 条目,否则选取 default。
保存
save_pretrained() 和 push_to_hub() 默认以 .jinja 格式写入。单个字符串模板会写入 chat_template.jinja。包含多个命名模板的字典会将 default 条目写入 chat_template.jinja,并将其余每个条目分别写入 additional_chat_templates/ 下的独立文件。为避免重复,chat_template 字段会从 tokenizer_config.json 中移除。
目前没有受支持的方式将模板保存为旧版格式之一。这些格式仅用于加载旧版仓库。
更新旧版本仓库
通过重新加载并保存,将 tokenizer_config.json 或 chat_template.json 中嵌入的模板迁移到推荐的 .jinja 格式。
加载步骤会规范化仓库在 chat_template 中使用的任何旧格式,并且 ~PushToHubMixin.push_to_hub 会返回一个 chat_template.jinja 文件。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("your-org/your-model")
tokenizer.push_to_hub("your-org/your-model")工具模板
编写工具模板没有特定的格式,但最好遵循标准 API。这样可以确保模板在各类模型中广泛可用,而无需用户编写自定义代码来配合您的模型使用工具。
诸如空白和特殊标记等格式因模型而异。请确保所有内容都与模型训练时所使用的格式完全一致。
以下部分列出了用于编写工具模板的标准 API 元素。
工具定义
Tools 以 Python 函数或 JSON schema 的形式传入。当传入函数时,会自动生成 JSON schema 并传递给模板。当模板访问 tools 变量时,它始终是一个 JSON schema 列表。
尽管模板始终以 JSON schema 的形式接收工具,但在渲染时,你可能需要彻底更改该格式,以匹配模型训练时所使用的格式。例如,Command-R 是使用 Python 函数头定义的工具进行训练的。该模板会在内部将 JSON schema 类型进行转换,并将输入的工具渲染为 Python 函数头。
下面的示例展示了如何在 JSON schema 格式中定义一个工具。
{
"type": "function",
"function": {
"name": "multiply",
"description": "A function that multiplies two numbers",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "The first number to multiply"
},
"b": {
"type": "number",
"description": "The second number to multiply"
}
},
"required": ["a", "b"]
}
}
}以下示例展示了在聊天模板中处理工具定义的方法。具体的标记和布局应更改为与模型训练时所使用的标记和布局相匹配。
{%- if tools %}
{%- for tool in tools %}
{{- '<tool>' + tool['function']['name'] + '\n' }}
{%- for argument in tool['function']['parameters']['properties'] %}
{{- argument + ': ' + tool['function']['parameters']['properties'][argument]['description'] + '\n' }}
{%- endfor %}
{{- '\n</tool>' }}
{%- endfor %}
{%- endif %}工具调用
除了渲染工具定义外,您还需要在模板中渲染 工具调用 和 工具响应。
工具调用通常通过 "assistant” 消息中的 tool_calls 键传递。即使大多数工具调用模型仅支持单次工具调用,该值始终是一个列表,这意味着该列表通常只包含一个元素。
{
"role": "assistant",
"tool_calls": [
{
"type": "function",
"function": {
"name": "multiply",
"arguments": {
"a": 5,
"b": 6
}
}
}
]
}下面展示了一种处理工具调用的常见模式。你可以将其作为起点,但请务必确保你的模板与模型训练时使用的格式一致!
{%- if message['role'] == 'assistant' and 'tool_calls' in message %}
{%- for tool_call in message['tool_calls'] %}
{{- '<tool_call>' + tool_call['function']['name'] + '\n' + tool_call['function']['arguments']|tojson + '\n</tool_call>' }}
{%- endfor %}
{%- endif %}工具响应
工具响应是带有 tool 角色的消息字典。它们比工具调用简单得多,通常只包含 role、name 和 content 键。
{
"role": "tool",
"name": "multiply",
"content": "30"
}某些模板甚至可能不需要 name 键,在这种情况下,你可以将模板编写为仅读取 content 键。
{%- if message['role'] == 'tool' %}
{{- "<tool_result>" + message['content'] + "</tool_result>" }}
{%- endif %}贡献
模板准备就绪后,将其设置为分词器的 chat_template 属性,并使用 apply_chat_template() 进行测试。如果运行符合预期,则使用 push_to_hub() 将其上传到 Hub。
即使你不是模型的所有者,为缺少聊天模板或聊天模板不正确的模型添加模板仍然很有帮助。请在模型仓库中提交一个 pull request 来添加该模板!
tokenizer.chat_template = template
tokenizer.push_to_hub("amazing_company/cool_model", commit_message="Add chat template", create_pr=True)