from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1")
chat = [
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing great. How can I help you today?"},
{"role": "user", "content": "I'd like to show off how chat templating works!"},
]
tokenizer.apply_chat_template(chat, tokenize=False)
输出结果:
"<s>[INST] Hello, how are you? [/INST]I'm doing great. How can I help you today?</s> [INST] I'd like to show off how chat templating works! [/INST]"
注意分词器是如何添加控制标记 [INST] 和 [/INST] 来表示用户消息的开始和结束(但不包括助手消息!),并将整个聊天内容压缩成一个字符串。如果我们使用 tokenize=True(这是默认设置),该字符串也会被分词。
现在,尝试相同的代码,但换成 HuggingFaceH4/zephyr-7b-beta 模型,你应该会得到:
Zephyr 和 Mistral-Instruct 都是从同一个基础模型 Mistral-7B-v0.1 微调而来的。然而,它们在训练时使用了完全不同的聊天格式。如果没有聊天模板,你将不得不为每个模型编写手动格式化代码,并且很容易犯下影响性能的小错误!聊天模板为你处理了格式化的细节,允许你编写适用于任何模型的通用代码。
如何使用聊天模板?¶
如上例所示,聊天模板使用起来非常简单。只需构建一个包含 role 和 content 键的消息列表,然后将其传递给 apply_chat_template() 方法。一旦你这样做,你将得到可以直接使用的输出!当使用聊天模板作为模型生成的输入时,使用 add_generation_prompt=True 来添加生成提示也是一个好主意。
以下是使用 Zephyr 模型准备输入以进行 model.generate() 的示例:
from transformers import AutoModelForCausalLM, AutoTokenizer
checkpoint = "HuggingFaceH4/zephyr-7b-beta"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint) # 你可能想在这里使用 bfloat16 并将模型移动到 GPU
messages = [
{
"role": "system",
"content": "You are a friendly chatbot who always responds in the style of a pirate",
},
{"role": "user", "content": "How many helicopters can a human eat in one sitting?"},
]
tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt")
print(tokenizer.decode(tokenized_chat[0]))
这将生成 Zephyr 期望的输入格式字符串。
现在我们的输入已经为 Zephyr 正确格式化,我们可以使用模型生成对用户问题的响应:
outputs = model.generate(tokenized_chat, max_new_tokens=128)
print(tokenizer.decode(outputs[0]))
这将生成:
是否有自动化的聊天管道?¶
是的,有!我们的文本生成管道支持聊天输入,这使得使用聊天模型变得容易。过去,我们使用专门的“ConversationalPipeline”类,但现在它已被弃用,其功能已合并到 TextGenerationPipeline 中。让我们再次尝试 Zephyr 示例,但这次使用管道:
from transformers import pipeline
pipe = pipeline("text-generation", "HuggingFaceH4/zephyr-7b-beta")
messages = [
{
"role": "system",
"content": "You are a friendly chatbot who always responds in the style of a pirate",
},
{"role": "user", "content": "How many helicopters can a human eat in one sitting?"},
]
print(pipe(messages, max_new_tokens=128)[0]['generated_text'][-1]) # 打印助手的响应
输出结果:
管道将为你处理所有分词和调用 apply_chat_template 的细节——一旦模型有了聊天模板,你只需要初始化管道并传递消息列表!
什么是“生成提示”?¶
你可能已经注意到 apply_chat_template 方法有一个 add_generation_prompt 参数。这个参数告诉模板添加标记,指示机器人响应的开始。例如,考虑以下聊天:
messages = [
{"role": "user", "content": "Hi there!"},
{"role": "assistant", "content": "Nice to meet you!"},
{"role": "user", "content": "Can I ask a question?"}
]
对于使用标准“ChatML”格式的模型,如果没有生成提示,它将如下所示:
tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
输出结果:
而有生成提示时,它将如下所示:
tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
输出结果:
注意这次我们添加了指示机器人响应开始的标记。这确保了当模型生成文本时,它将写一个机器人响应,而不是做一些意外的事情,比如继续用户的消息。记住,聊天模型仍然是语言模型——它们被训练来继续文本,而聊天只是它们的一种特殊文本!你需要用适当的控制标记来引导它们,以便它们知道它们应该做什么。
并非所有模型都需要生成提示。一些模型,如 LLaMA,在机器人响应之前没有任何特殊标记。在这些情况下,add_generation_prompt 参数将没有效果。add_generation_prompt 的确切效果将取决于所使用的模板。
“continue_final_message”有什么作用?¶
当将消息列表传递给 apply_chat_template 或 TextGenerationPipeline 时,你可以选择格式化聊天,以便模型继续聊天中的最后一条消息,而不是开始一条新消息。这是通过移除指示最后一条消息结束的任何结束标记来实现的,以便模型在开始生成文本时简单地扩展最后一条消息。这对于“预填充”模型的响应很有用。
以下是一个示例:
chat = [
{"role": "user", "content": "Can you format the answer in JSON?"},
{"role": "assistant", "content": '{"name": "'},
]
formatted_chat = tokenizer.apply_chat_template(chat, tokenize=True, return_dict=True, continue_final_message=True)
model.generate(**formatted_chat)
模型将生成继续 JSON 字符串的文本,而不是开始一条新消息。当你知道你希望模型如何开始其回复时,这种方法可以非常有助于提高模型遵循指令的准确性。
由于 add_generation_prompt 添加了开始新消息的标记,而 continue_final_message 移除了最后一条消息的结束标记,因此同时使用它们是没有意义的。因此,如果你尝试这样做,将会得到一个错误!
TextGenerationPipeline 的默认行为是设置 add_generation_prompt=True,以便它开始一条新消息。然而,如果输入聊天中的最后一条消息具有“assistant”角色,它将假设该消息是预填充的,并切换到 continue_final_message=True,因为大多数模型不支持连续的助手消息。你可以通过在调用管道时显式传递 continue_final_message 参数来覆盖此行为。
我可以在训练中使用聊天模板吗?¶
是的!这是一个确保聊天模板与模型在训练期间看到的标记匹配的好方法。我们建议你将聊天模板作为数据集的预处理步骤应用。之后,你可以像其他语言模型训练任务一样继续。在训练时,通常应设置 add_generation_prompt=False,因为添加的提示助手响应的标记在训练期间没有帮助。让我们看一个示例:
from transformers import AutoTokenizer
from datasets import Dataset
tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta")
chat1 = [
{"role": "user", "content": "Which is bigger, the moon or the sun?"},
{"role": "assistant", "content": "The sun."}
]
chat2 = [
{"role": "user", "content": "Which is bigger, a virus or a bacterium?"},
{"role": "assistant", "content": "A bacterium."}
]
dataset = Dataset.from_dict({"chat": [chat1, chat2]})
dataset = dataset.map(lambda x: {"formatted_chat": tokenizer.apply_chat_template(x["chat"], tokenize=False, add_generation_prompt=False)})
print(dataset['formatted_chat'][0])
输出结果:
从这里开始,只需像使用标准语言建模任务一样继续训练,使用 formatted_chat 列。
默认情况下,一些分词器在分词时会添加特殊标记,如 <bos> 和 <eos>。聊天模板应该已经包含了它们所需的所有特殊标记,因此额外的特殊标记通常是不正确的或重复的,这会损害模型性能。
因此,如果你使用 apply_chat_template(tokenize=False) 格式化文本,你应该在稍后分词该文本时设置 add_special_tokens=False。如果你使用 apply_chat_template(tokenize=True),则无需担心这一点!
高级:聊天模板的额外输入¶
apply_chat_template 唯一需要的参数是 messages。然而,你可以传递任何关键字参数给 apply_chat_template,它将在模板内部可访问。这为你提供了很大的自由度,可以使用聊天模板做很多事情。这些参数的名称或格式没有限制——你可以传递字符串、列表、字典或任何其他你想要的东西。
也就是说,有一些常见的用例,例如传递用于函数调用的工具,或用于检索增强生成的文档。在这些常见情况下,我们有一些关于这些参数名称和格式的建议,这些建议在下面的章节中描述。我们鼓励模型作者使其聊天模板与这种格式兼容,以便在模型之间轻松转移工具调用代码。
高级:工具使用 / 函数调用¶
“工具使用”LLMs 可以选择在生成答案之前调用外部工具作为函数。当将工具传递给工具使用模型时,你可以简单地将函数列表传递给 tools 参数:
import datetime
def current_time():
"""获取当前本地时间作为字符串。"""
return str(datetime.now())
def multiply(a: float, b: float):
"""
一个乘以两个数字的函数
参数:
a: 要乘的第一个数字
b: 要乘的第二个数字
"""
return a * b
tools = [current_time, multiply]
model_input = tokenizer.apply_chat_template(
messages,
tools=tools
)
为了使其正常工作,你应该以如上格式编写你的函数,以便它们可以被正确解析为工具。具体来说,你应该遵循以下规则:
- 函数应该有一个描述性名称
- 每个参数必须有一个类型提示
- 函数必须有一个标准的 Google 风格的文档字符串(即,初始函数描述后跟一个描述参数的
Args:块,除非函数没有任何参数) - 不要在
Args:块中包含类型。换句话说,写a: The first number to multiply,而不是a (int): The first number to multiply。类型提示应该放在函数头中。 - 函数可以在文档字符串中有一个返回类型和一个
Returns:块。然而,这些是可选的,因为大多数工具使用模型会忽略它们。
将工具结果传递给模型¶
上面的示例代码足以列出模型可用的工具,但如果模型想要实际使用其中一个工具呢?如果发生这种情况,你应该:
- 解析模型的输出以获取工具名称和参数。
- 将模型的工具调用添加到对话中。
- 使用这些参数调用相应的函数。
- 将结果添加到对话中。
完整的工具使用示例¶
让我们逐步完成一个工具使用示例。在这个示例中,我们将使用一个 8B 的 Hermes-2-Pro 模型,因为它是撰写本文时在其大小类别中性能最高的工具使用模型之一。如果你有内存,可以考虑使用更大的模型,如 Command-R 或 Mixtral-8x22B,它们也支持工具使用并提供更强的性能。
首先,让我们加载我们的模型和分词器:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
checkpoint = "NousResearch/Hermes-2-Pro-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint, torch_dtype=torch.bfloat16, device_map="auto")
接下来,让我们定义一个工具列表:
def get_current_temperature(location: str, unit: str) -> float:
"""
获取指定位置的当前温度。
参数:
location: 要获取温度的位置,格式为 "City, Country"
unit: 返回温度的单位。(选项: ["celsius", "fahrenheit"])
返回:
指定位置的当前温度,以指定单位表示,为浮点数。
"""
return 22.0 # 一个真正的函数应该实际获取温度!
def get_current_wind_speed(location: str) -> float:
"""
获取指定位置的当前风速,单位为 km/h。
参数:
location: 要获取风速的位置,格式为 "City, Country"
返回:
指定位置的当前风速,单位为 km/h,为浮点数。
"""
return 6.0 # 一个真正的函数应该实际获取风速!
tools = [get_current_temperature, get_current_wind_speed]
现在,让我们为我们的机器人设置一个对话:
messages = [
{"role": "system", "content": "You are a bot that responds to weather queries. You should reply with the unit used in the queried location."},
{"role": "user", "content": "Hey, what's the temperature in Paris right now?"}
]
现在,让我们应用聊天模板并生成一个响应:
inputs = tokenizer.apply_chat_template(messages, tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt")
inputs = {k: v.to(model.device) for k, v in inputs.items()}
out = model.generate(**inputs, max_new_tokens=128)
print(tokenizer.decode(out[0][len(inputs["input_ids"][0]):]))
我们得到:
模型已经用有效的参数调用了函数,格式符合函数文档字符串的要求。它推断我们最有可能指的是法国的巴黎,并且它记得,作为国际单位制的故乡,法国的温度应该以摄氏度显示。
输出格式如上所示,是我们在本示例中使用的 Hermes-2-Pro 模型的特定格式。其他模型可能会输出不同的工具调用格式,你可能需要在此步骤进行一些手动解析。例如,Llama-3.1 模型会输出稍微不同的 JSON,使用 parameters 而不是 arguments。无论模型输出什么格式,你都应该以如下格式将工具调用添加到对话中,包含 tool_calls、function 和 arguments 键。
接下来,让我们将模型的工具调用添加到对话中。
tool_call = {"name": "get_current_temperature", "arguments": {"location": "Paris, France", "unit": "celsius"}}
messages.append({"role": "assistant", "tool_calls": [{"type": "function", "function": tool_call}]})
如果你熟悉 OpenAI API,你应该注意这里的一个重要区别——tool_call 是一个字典,但在 OpenAI API 中它是一个 JSON 字符串。传递字符串可能会导致错误或奇怪的模型行为!
现在我们已经将工具调用添加到对话中,我们可以调用函数并将结果添加到对话中。由于我们只是使用一个总是返回 22.0 的虚拟函数,我们可以直接添加该结果。
messages.append({"role": "tool", "name": "get_current_temperature", "content": "22.0"})
某些模型架构,特别是 Mistral/Mixtral,还需要在这里使用 tool_call_id,它应该是 9 个随机生成的字母数字字符,并分配给工具调用字典的 id 键。相同的键也应该分配给工具响应字典的 tool_call_id 键,以便工具调用可以与工具响应匹配。因此,对于 Mistral/Mixtral 模型,上面的代码将是:
tool_call_id = "9Ae3bDc2F" # 随机 ID,9 个字母数字字符
tool_call = {"name": "get_current_temperature", "arguments": {"location": "Paris, France", "unit": "celsius"}}
messages.append({"role": "assistant", "tool_calls": [{"type": "function", "id": tool_call_id, "function": tool_call}]})
messages.append({"role": "tool", "tool_call_id": tool_call_id, "name": "get_current_temperature", "content": "22.0"})
最后,让我们让助手读取函数输出并继续与用户聊天:
inputs = tokenizer.apply_chat_template(messages, tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt")
inputs = {k: v.to(model.device) for k, v in inputs.items()}
out = model.generate(**inputs, max_new_tokens=128)
print(tokenizer.decode(out[0][len(inputs["input_ids"][0]):]))
我们得到:
尽管这是一个使用虚拟工具和单次调用的简单演示,但相同的技术适用于多个真实工具和更长的对话。这可以是一种强大的方式,通过实时信息、计算工具(如计算器)或访问大型数据库来扩展对话代理的能力。
理解工具模式¶
你传递给 apply_chat_template 的 tools 参数的每个函数都会被转换为 JSON 模式。这些模式随后会被传递给模型聊天模板。换句话说,工具使用模型不会直接看到你的函数,也不会看到函数内部的实际代码。它们关心的是函数的定义和它们需要传递给它们的参数——它们关心的是工具的功能和如何使用它们,而不是它们的工作原理!你需要读取它们的输出,检测它们是否请求使用工具,将它们的参数传递给工具函数,并将响应返回给聊天。
生成 JSON 模式传递给模板应该是自动且不可见的,只要你的函数遵循上述规范,但如果你遇到问题,或者你只是想更多地控制转换,你可以手动处理转换。以下是一个手动模式转换的示例。
from transformers.utils import get_json_schema
def multiply(a: float, b: float):
"""
一个乘以两个数字的函数
参数:
a: 要乘的第一个数字
b: 要乘的第二个数字
"""
return a * b
schema = get_json_schema(multiply)
print(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"]
}
}
}
如果你愿意,你可以编辑这些模式,或者甚至完全不使用 get_json_schema 从头开始编写它们。JSON 模式可以直接传递给 apply_chat_template 的 tools 参数——这为你提供了很大的能力来为更复杂的函数定义精确的模式。但要小心——你的模式越复杂,模型在处理它们时就越容易混淆!我们建议尽可能使用简单的函数签名,将参数(尤其是复杂、嵌套的参数)保持在最低限度。
以下是一个手动定义模式并通过 apply_chat_template 直接传递它们的示例:
# 一个不带参数的简单函数
current_time = {
"type": "function",
"function": {
"name": "current_time",
"description": "Get the current local time as a string.",
"parameters": {
'type': 'object',
'properties': {}
}
}
}
# 一个带两个数值参数的更完整的函数
multiply = {
'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']
}
}
}
model_input = tokenizer.apply_chat_template(
messages,
tools = [current_time, multiply]
)
高级:检索增强生成¶
“检索增强生成”或“RAG”LLMs 可以在响应查询之前搜索文档语料库以获取信息。这使得模型能够将其知识库扩展到其有限的上下文大小之外。我们对 RAG 模型的建议是,它们的模板应该接受一个 documents 参数。这应该是一个文档列表,其中每个“文档”是一个包含 title 和 contents 键的单个字典,这两个键都是字符串。由于这种格式比用于工具的 JSON 模式简单得多,因此不需要辅助函数。
以下是一个 RAG 模板实际操作的示例:
from transformers import AutoTokenizer, AutoModelForCausalLM
# 加载模型和分词器
model_id = "CohereForAI/c4ai-command-r-v01-4bit"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
device = model.device # 获取模型加载的设备
# 定义对话输入
conversation = [
{"role": "user", "content": "What has Man always dreamed of?"}
]
# 定义用于检索增强生成的文档
documents = [
{
"title": "The Moon: Our Age-Old Foe",
"text": "Man has always dreamed of destroying the moon. In this essay, I shall..."
},
{
"title": "The Sun: Our Age-Old Friend",
"text": "Although often underappreciated, the sun provides several notable benefits..."
}
]
# 使用 RAG 模板对对话和文档进行分词,返回 PyTorch 张量。
input_ids = tokenizer.apply_chat_template(
conversation=conversation,
documents=documents,
chat_template="rag",
tokenize=True,
add_generation_prompt=True,
return_tensors="pt").to(device)
# 生成响应
gen_tokens = model.generate(
input_ids,
max_new_tokens=100,
do_sample=True,
temperature=0.3,
)
# 解码并打印生成的文本以及生成提示
gen_text = tokenizer.decode(gen_tokens[0])
print(gen_text)
documents 输入用于检索增强生成并不广泛支持,许多模型的聊天模板会简单地忽略此输入。
要验证模型是否支持 documents 输入,你可以阅读其模型卡,或 print(tokenizer.chat_template) 以查看是否使用了 documents 键。
支持此功能的一个模型类别是 Cohere 的 Command-R 和 Command-R+,通过其 rag 聊天模板。你可以在其模型卡中看到使用此功能的接地生成示例。
高级:聊天模板如何工作?¶
模型的聊天模板存储在 tokenizer.chat_template 属性中。如果没有设置聊天模板,则使用该模型类的默认模板。让我们看一下 Zephyr 聊天模板,尽管请注意,这个模板比实际的模板简化了一些!
如果你以前从未见过这个,这是一个 Jinja 模板。Jinja 是一种允许你编写生成文本的简单代码的模板语言。在很多方面,代码和语法类似于 Python。在纯 Python 中,这个模板看起来像这样:
for message in messages:
print(f'<|{message["role"]}|>')
print(message['content'] + eos_token)
if add_generation_prompt:
print('<|assistant|>')
实际上,模板做了三件事:
- 对于每条消息,打印角色,用
<|和|>括起来,如<|user|>或<|assistant|>。 - 接下来,打印消息内容,后跟结束标记。
- 最后,如果设置了
add_generation_prompt,打印助手标记,以便模型知道开始生成助手响应。
这是一个相当简单的模板,但 Jinja 为你提供了很大的灵活性来做更复杂的事情!让我们看一个 Jinja 模板,它可以像 LLaMA 那样格式化输入(注意,真正的 LLaMA 模板包括默认系统消息的处理和稍微不同的系统消息处理——不要在你的实际代码中使用这个!)
现在,只需设置 tokenizer.chat_template 属性。下次你使用 apply_chat_template() 时,它将使用你的新模板!此属性将保存在 tokenizer_config.json 文件中,因此你可以使用 push_to_hub() 将你的新模板上传到 Hub,并确保每个人都在为你的模型使用正确的模板!
template = tokenizer.chat_template
template = template.replace("SYS", "SYSTEM") # 更改系统标记
tokenizer.chat_template = template # 设置新模板
tokenizer.push_to_hub("model_name") # 将你的新模板上传到 Hub!
使用你的聊天模板的方法 apply_chat_template() 被 TextGenerationPipeline 类调用,因此一旦你设置了正确的聊天模板,你的模型将自动与 TextGenerationPipeline 兼容!
如果你正在为一个聊天模型进行微调,除了设置聊天模板外,你可能还应该将任何新的聊天控制标记作为特殊标记添加到分词器中。特殊标记永远不会被分割,确保你的控制标记总是作为一个整体处理,而不是被分词成多个部分。你还应该将分词器的 eos_token 属性设置为模板中标记助手生成的结束标记。这将确保文本生成工具能够正确判断何时停止生成文本。
为什么有些模型有多个模板?¶
有些模型为不同的用例使用不同的模板。例如,它们可能为普通聊天使用一个模板,而为工具使用或检索增强生成使用另一个模板。在这些情况下,tokenizer.chat_template 是一个字典。这可能会引起一些混淆,因此我们建议尽可能为所有用例使用单个模板。你可以使用 Jinja 语句,如 if tools is defined 和 {% macro %} 定义,轻松地将多个代码路径包装在一个模板中。
当分词器有多个模板时,tokenizer.chat_template 将是一个 dict,其中每个键是模板的名称。apply_chat_template 方法对某些模板名称有特殊处理:具体来说,它会在大多数情况下查找名为 default 的模板,如果找不到则会引发错误。然而,如果用户传递了 tools 参数并且存在名为 tool_use 的模板,它将使用该模板。要访问其他名称的模板,请将模板名称传递给 apply_chat_template() 的 chat_template 参数。
我们发现这可能会让用户感到困惑——因此,如果你自己编写模板,我们建议尽可能将所有内容放在一个模板中!
我应该使用什么模板?¶
当为一个已经训练用于聊天的模型设置模板时,你应该确保模板与模型在训练期间看到的聊天格式完全匹配,否则你可能会遇到性能下降。即使你正在进一步训练模型——如果你保持聊天标记不变,你可能会获得最佳性能。这与分词化非常相似——当你精确匹配训练期间使用的分词化时,通常会获得最佳的推理或微调性能。
如果你从头开始训练一个模型,或者为聊天微调一个基础语言模型,那么你有很大的自由度来选择合适的模板!LLMs 足够聪明,可以处理许多不同的输入格式。一个流行的选择是 ChatML 格式,这是许多用例的一个好选择。它看起来像这样:
如果你喜欢这个模板,这里有一个单行代码形式,可以直接复制到你的代码中。单行代码还包括对生成提示的支持,但请注意,它不会添加 BOS 或 EOS 标记!如果模型需要这些标记,它们不会被 apply_chat_template 自动添加——换句话说,文本将以 add_special_tokens=False 进行分词。这是为了避免模板和 add_special_tokens 逻辑之间的潜在冲突。如果模型需要特殊标记,请确保将它们添加到模板中!
tokenizer.chat_template = "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
这个模板将每条消息包裹在 <|im_start|> 和 <|im_end|> 标记中,并将角色作为字符串写入,这允许你在训练时使用灵活的角色。输出看起来像这样:
“user”、“system”和“assistant”角色是聊天的标准角色,我们建议在有意义的情况下使用它们,特别是如果你想让你的模型与 TextGenerationPipeline 良好配合。然而,你并不局限于这些角色——模板非常灵活,任何字符串都可以是一个角色。
我想添加一些聊天模板!我应该如何开始?¶
如果你有任何聊天模型,你应该设置它们的 tokenizer.chat_template 属性并使用 apply_chat_template() 进行测试,然后将更新后的分词器推送到 Hub。即使你不是模型所有者——如果你使用的模型有一个空的聊天模板,或者仍在使用默认类模板,请打开一个 pull request 到模型仓库,以便正确设置此属性!
一旦设置了属性,就完成了!tokenizer.apply_chat_template 现在将正确工作,这意味着它也在 TextGenerationPipeline 等地方自动支持!
通过确保模型具有此属性,我们可以确保整个社区能够充分利用开源模型的全部功能。格式不匹配已经困扰该领域太久了,并且默默地损害性能——是时候结束它们了!
高级:模板编写技巧¶
开始编写 Jinja 模板的最简单方法是查看一些现有的模板。你可以使用 print(tokenizer.chat_template) 查看任何聊天模型使用的模板。通常,支持工具使用的模型比其他模型有更复杂的模板——因此,当你刚开始时,它们可能不是一个好的学习例子!你还可以查看 Jinja 文档 以了解一般的 Jinja 格式和语法。
Jinja 模板在 transformers 中与在其他地方的 Jinja 模板相同。主要要知道的是,对话历史将在你的模板中作为名为 messages 的变量可访问。你可以在模板中像在 Python 中一样访问 messages,这意味着你可以使用 {% for message in messages %} 循环访问它,或者使用 {{ messages[0] }} 访问单个消息。
你还可以使用以下技巧编写干净、高效的 Jinja 模板:
修剪空白¶
默认情况下,Jinja 会打印块之前或之后的任何空白。这对于聊天模板来说可能是个问题,因为它们通常希望对空白非常精确!为了避免这种情况,我们强烈建议你像这样编写模板:
而不是像这样:
添加 - 将去除块之前的任何空白。第二个示例看起来无害,但换行符和缩进可能会出现在输出中,这可能不是你想要的!
特殊变量¶
在你的模板中,你将可以访问几个特殊变量。最重要的是 messages,它包含聊天历史记录作为消息字典的列表。然而,还有其他几个变量。并非每个变量都会在每个模板中使用。最常见的其他变量是:
tools包含一个 JSON 模式格式的工具列表。如果没有传递工具,则为None或未定义。documents包含一个文档列表,格式为{"title": "Title", "contents": "Contents"},用于检索增强生成。如果没有传递文档,则为None或未定义。add_generation_prompt是一个布尔值,如果用户请求生成提示,则为True,否则为False。如果设置了此变量,你的模板应该在对话末尾添加助手消息的头部。如果你的模型没有助手消息的特定头部,你可以忽略此标志。- 特殊标记 如
bos_token和eos_token。这些从tokenizer.special_tokens_map中提取。每个模板中可用的确切标记将根据父分词器而有所不同。
你可以将任何 kwarg 传递给 apply_chat_template,它将在模板中作为变量可访问。通常,我们建议尽量坚持使用上述核心变量,因为如果用户必须编写自定义代码来传递特定于模型的 kwargs,模型的使用将变得更加困难。然而,我们意识到这个领域发展迅速,因此如果你有一个新的用例不适合核心 API,请随意为它使用一个新的 kwarg!如果一个新的 kwarg 变得普遍,我们可能会将其提升到核心 API 中,并为其创建一个标准、文档化的格式。
可调用函数¶
在你的模板中,还有一小组可调用函数可用。这些是:
raise_exception(msg):引发TemplateException。这对于调试很有用,并且可以告诉用户他们正在做一些你的模板不支持的事情。strftime_now(format_str):等同于 Python 中的datetime.now().strftime(format_str)。这用于以特定格式获取当前日期/时间,通常包含在系统消息中。
与非 Python Jinja 的兼容性¶
有多种语言实现了 Jinja。它们通常具有相同的语法,但一个关键区别是,当你在 Python 中编写模板时,你可以使用 Python 方法,例如字符串上的 .lower() 或字典上的 .items()。如果有人在非 Python 实现中使用你的模板,这将中断。非 Python 实现在部署环境中特别常见,其中 JS 和 Rust 非常流行。
不要惊慌!有一些简单的更改可以确保你的模板在所有 Jinja 实现中兼容:
- 将 Python 方法替换为 Jinja 过滤器。它们通常具有相同的名称,例如
string.lower()变为string|lower,dict.items()变为dict|items。一个值得注意的变化是string.strip()变为string|trim。查看 Jinja 文档中的内置过滤器列表 了解更多。 - 将
True、False和None(这是 Python 特有的)替换为true、false和none。 - 直接渲染字典或列表可能会在其他实现中给出不同的结果(例如,字符串条目可能会从单引号变为双引号)。添加
tojson过滤器可以帮助确保一致性。
编写生成提示¶
我们在上面提到 add_generation_prompt 是一个特殊变量,将在你的模板中可访问,并由用户设置 add_generation_prompt 标志控制。如果你的模型期望助手消息的头部,那么你的模板必须支持在 add_generation_prompt 设置时添加头部。
以下是一个以 ChatML 风格格式化消息并支持生成提示的模板示例:
助手头部的确切内容将取决于你的特定模型,但它应该始终是表示助手消息开始的字符串,以便如果用户使用 add_generation_prompt=True 应用你的模板并生成文本,模型将写一个助手响应。还要注意,一些模型不需要生成提示,因为助手消息总是在用户消息之后立即开始。这在 LLaMA 和 Mistral 模型中特别常见,其中助手消息在结束用户消息的 [/INST] 标记之后立即开始。在这些情况下,模板可以忽略 add_generation_prompt 标志。
生成提示很重要!如果你的模型需要生成提示但在模板中未设置,那么模型生成可能会严重降级,或者模型可能会显示异常行为,如继续最后一条用户消息!
编写和调试更大的模板¶
当引入此功能时,大多数模板都很小,相当于 Jinja 的“单行脚本”。然而,随着新模型和功能(如工具使用和 RAG)的出现,一些模板可能长达 100 行或更多。在编写这些模板时,最好在文本编辑器中将它们写在一个单独的文件中。你可以轻松地将聊天模板提取到一个文件中:
open("template.jinja", "w").write(tokenizer.chat_template)
或者将编辑后的模板加载回分词器:
tokenizer.chat_template = open("template.jinja").read()
额外的奖励是,当你在单独的文件中编写长多行模板时,该文件中的行号将与模板解析或执行错误中的行号完全对应。这将更容易识别问题的来源。
为工具编写模板¶
尽管聊天模板没有为工具(或任何东西)强制执行特定的 API,但我们建议模板作者尽可能坚持标准 API。聊天模板的重点是允许代码在模型之间可转移,因此偏离标准工具 API 意味着用户将不得不编写自定义代码来使用你的模型的工具。有时这是不可避免的,但通常通过巧妙的模板编写,你可以使标准 API 工作!
下面,我们将列出标准 API 的元素,并给出编写与标准 API 良好配合的模板的技巧。
工具定义¶
你的模板应该期望 tools 变量要么为空(如果没有传递工具),要么是一个 JSON 模式字典的列表。我们的聊天模板方法允许用户将工具作为 JSON 模式或 Python 函数传递,但当传递函数时,我们会自动生成 JSON 模式并将其传递给你的模板。因此,你的模板接收到的 tools 变量将始终是一个 JSON 模式列表。以下是一个示例工具 JSON 模式:
{
"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"]
}
}
}
以下是一些在你的聊天模板中处理工具的示例代码。记住,这只是特定格式的示例——你的模型可能需要不同的格式!
你的模板渲染的特定标记和工具描述当然应该与你的模型训练时使用的格式匹配。没有要求你的模型理解 JSON 模式输入,只有你的模板可以将 JSON 模式转换为你的模型的格式。例如,Command-R 在训练时使用 Python 函数头定义工具,但 Command-R 工具模板接受 JSON 模式,内部转换类型并将输入工具渲染为 Python 头。你可以用模板做很多事情!
工具调用¶
如果存在,工具调用将是一个附加到具有“assistant”角色的消息的列表。注意 tool_calls 总是一个列表,尽管大多数工具调用模型一次只支持单个工具调用,这意味着列表通常只有一个元素。以下是一个包含工具调用的示例消息字典:
{
"role": "assistant",
"tool_calls": [
{
"type": "function",
"function": {
"name": "multiply",
"arguments": {
"a": 5,
"b": 6
}
}
}
]
}
处理它们的常见模式如下:
再次,你应该使用你的模型期望的格式和特殊标记渲染工具调用。
工具响应¶
工具响应有一个简单的格式:它们是一个包含“tool”角色的消息字典,一个给出调用函数名称的“name”键,以及一个包含工具调用结果的“content”键。以下是一个示例工具响应:
{
"role": "tool",
"name": "multiply",
"content": "30"
}
你不需要在工具响应中使用所有键。例如,如果你的模型不期望在工具响应中包含函数名称,那么渲染它可以像这样简单:
再次,记住实际的格式和特殊标记是特定于模型的——你应该非常小心,确保标记、空白和其他一切都与你的模型训练时使用的格式完全匹配!