聊天模板
简介
大型语言模型 (LLM) 的一个越来越常见的用例是 **聊天**。在聊天环境中,模型不会像标准语言模型那样继续单个文本字符串,而是继续由一个或多个 **消息** 组成的对话,每个消息都包含一个 **角色**,例如“用户”或“助手”,以及消息文本。
与分词类似,不同的模型对聊天的输入格式有不同的期望。这就是我们添加 **聊天模板** 作为功能的原因。聊天模板是分词器的一部分。它们指定如何将表示为消息列表的对话转换为模型期望格式的单个可分词字符串。
让我们使用 `mistralai/Mistral-7B-Instruct-v0.1` 模型的快速示例来具体说明这一点。
>>> 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
模型,你应该得到
<|user|> Hello, how are you?</s> <|assistant|> I'm doing great. How can I help you today?</s> <|user|> I'd like to show off how chat templating works!</s>
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) # You may want to use bfloat16 and/or move to GPU here
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 期望的输入格式的字符串。
<|system|> You are a friendly chatbot who always responds in the style of a pirate</s> <|user|> How many helicopters can a human eat in one sitting?</s> <|assistant|>
现在我们的输入已正确格式化为 Zephyr 可以接受的格式,我们可以使用该模型为用户的提问生成回复
outputs = model.generate(tokenized_chat, max_new_tokens=128)
print(tokenizer.decode(outputs[0]))
这将生成
<|system|> You are a friendly chatbot who always responds in the style of a pirate</s> <|user|> How many helicopters can a human eat in one sitting?</s> <|assistant|> Matey, I'm afraid I must inform ye that humans cannot eat helicopters. Helicopters are not food, they are flying machines. Food is meant to be eaten, like a hearty plate o' grog, a savory bowl o' stew, or a delicious loaf o' bread. But helicopters, they be for transportin' and movin' around, not for eatin'. So, I'd say none, me hearties. None at all.
啊哈,原来如此简单!
是否有用于聊天的自动化管道?
是的,有!我们的文本生成管道支持聊天输入,这使得使用聊天模型变得容易。过去,我们使用一个专门的“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]) # Print the assistant's response
{'role': 'assistant', 'content': "Matey, I'm afraid I must inform ye that humans cannot eat helicopters. Helicopters are not food, they are flying machines. Food is meant to be eaten, like a hearty plate o' grog, a savory bowl o' stew, or a delicious loaf o' bread. But helicopters, they be for transportin' and movin' around, not for eatin'. So, I'd say none, me hearties. None at all."}
管道将为你处理分词和调用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)
"""<|im_start|>user
Hi there!<|im_end|>
<|im_start|>assistant
Nice to meet you!<|im_end|>
<|im_start|>user
Can I ask a question?<|im_end|>
"""
以下是**使用**生成提示时的样子
tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
"""<|im_start|>user
Hi there!<|im_end|>
<|im_start|>assistant
Nice to meet you!<|im_end|>
<|im_start|>user
Can I ask a question?<|im_end|>
<|im_start|>assistant
"""
注意,这次我们添加了指示机器人响应开始的标记。这确保了当模型生成文本时,它将编写一个机器人响应,而不是做一些意想不到的事情,比如继续用户的消息。请记住,聊天模型仍然只是语言模型 - 它们被训练来继续文本,而聊天对它们来说只是特殊类型的文本!你需要用适当的控制标记引导它们,以便它们知道应该做什么。
并非所有模型都需要生成提示。一些模型,如 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
,以便它开始一条新消息。但是,如果输入聊天中的最后一条消息具有“助手”角色,它将假设此消息是预填充消息并切换到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])
我们会得到
<|user|> Which is bigger, the moon or the sun?</s> <|assistant|> The sun.</s>
从这里开始,只需像使用标准语言建模任务一样继续训练,使用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
,它将在模板内部可用。这让你有很大的自由度将聊天模板用于许多事情。对这些参数的名称或格式没有限制 - 你可以传递字符串、列表、字典或任何你想要的其他内容。
话虽如此,这些额外参数有一些常见的用例,例如传递用于函数调用的工具,或传递用于检索增强生成的文件。在这些常见情况下,我们对这些参数的名称和格式有一些建议,这些建议在下面的部分中进行了描述。我们鼓励模型作者使他们的聊天模板与这种格式兼容,以便于在模型之间轻松传递工具调用代码。
高级:工具使用/函数调用
“工具使用”LLM可以在生成答案之前选择调用函数作为外部工具。当将工具传递给工具使用模型时,您可以简单地将函数列表传递给tools
参数。
import datetime
def current_time():
"""Get the current local time as a string."""
return str(datetime.now())
def multiply(a: float, b: float):
"""
A function that multiplies two numbers
Args:
a: The first number to multiply
b: The second number to multiply
"""
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:
"""
Get the current temperature at a location.
Args:
location: The location to get the temperature for, in the format "City, Country"
unit: The unit to return the temperature in. (choices: ["celsius", "fahrenheit"])
Returns:
The current temperature at the specified location in the specified units, as a float.
"""
return 22. # A real function should probably actually get the temperature!
def get_current_wind_speed(location: str) -> float:
"""
Get the current wind speed in km/h at a given location.
Args:
location: The location to get the temperature for, in the format "City, Country"
Returns:
The current wind speed at the given location in km/h, as a float.
"""
return 6. # A real function should probably actually get the wind speed!
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]):]))
我们会得到
<tool_call> {"arguments": {"location": "Paris, France", "unit": "celsius"}, "name": "get_current_temperature"} </tool_call><|im_end|>
该模型已使用有效的参数调用了函数,格式与函数文档字符串中请求的格式相同。它推断出我们很可能指的是法国的巴黎,并且它还记得,作为 SI 单位的故乡,法国的温度当然应该以摄氏度显示。
上述输出格式特定于我们在此示例中使用的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" # Random ID, 9 alphanumeric characters
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]):]))
我们会得到
The current temperature in Paris, France is 22.0 ° Celsius.<|im_end|>
尽管这是一个使用虚拟工具和单个调用的简单演示,但同样的技术适用于多个真实工具和更长的对话。这可以成为一种强大的方法,可以利用实时信息、计算器等计算工具或访问大型数据库来扩展对话代理的功能。
理解工具模式
您传递给apply_chat_template
的tools
参数的每个函数都会转换为一个JSON 模式。然后将这些模式传递给模型聊天模板。换句话说,工具使用模型不会直接看到您的函数,并且它们永远不会看到函数内部的实际代码。它们关心的是函数的**定义**和传递给它们的参数——它们关心工具的功能和使用方法,而不是它们的工作原理!您需要读取它们的输出,检测它们是否请求使用工具,将它们的参数传递给工具函数,并在聊天中返回响应。
只要您的函数遵循上述规范,生成传递给模板的 JSON 模式应该是自动且不可见的,但如果您遇到问题,或者您只是想要更多地控制转换,您可以手动处理转换。这是一个手动模式转换的示例。
from transformers.utils import get_json_schema
def multiply(a: float, b: float):
"""
A function that multiplies two numbers
Args:
a: The first number to multiply
b: The second number to multiply
"""
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
的示例。
# A simple function that takes no arguments
current_time = {
"type": "function",
"function": {
"name": "current_time",
"description": "Get the current local time as a string.",
"parameters": {
'type': 'object',
'properties': {}
}
}
}
# A more complete function that takes two numerical arguments
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”LLM可以在响应查询之前搜索文档语料库以获取信息。这使得模型能够将其知识库远远扩展到其有限的上下文大小之外。我们对 RAG 模型的建议是,它们的模板应该接受一个documents
参数。这应该是一个文档列表,其中每个“文档”都是一个包含title
和contents
键的单个字典,这两个键都是字符串。由于此格式比用于工具的 JSON 模式简单得多,因此不需要任何辅助函数。
这是一个 RAG 模板运行的示例。
from transformers import AutoTokenizer, AutoModelForCausalLM
# Load the model and tokenizer
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 # Get the device the model is loaded on
# Define conversation input
conversation = [
{"role": "user", "content": "What has Man always dreamed of?"}
]
# Define documents for retrieval-based generation
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..."
}
]
# Tokenize conversation and documents using a RAG template, returning PyTorch tensors.
input_ids = tokenizer.apply_chat_template(
conversation=conversation,
documents=documents,
chat_template="rag",
tokenize=True,
add_generation_prompt=True,
return_tensors="pt").to(device)
# Generate a response
gen_tokens = model.generate(
input_ids,
max_new_tokens=100,
do_sample=True,
temperature=0.3,
)
# Decode and print the generated text along with generation prompt
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
聊天模板,但请注意,此模板与实际模板略有简化!
{%- for message in messages %}
{{- '<|' + message['role'] + |>\n' }}
{{- message['content'] + eos_token }}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|assistant|>\n' }}
{%- endif %}
如果您以前从未见过其中任何一个,这是一个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 为您提供了很大的灵活性来执行更复杂的操作!让我们看看一个可以像 LLaMA 格式化输入的方式类似地格式化输入的 Jinja 模板(请注意,真实的 LLaMA 模板包括对默认系统消息的处理以及通常略有不同的系统消息处理 - 请不要在您的实际代码中使用此模板!)
{%- for message in messages %}
{%- if message['role'] == 'user' %}
{{- bos_token + '[INST] ' + message['content'] + ' [/INST]' }}
{%- elif message['role'] == 'system' %}
{{- '<<SYS>>\\n' + message['content'] + '\\n<</SYS>>\\n\\n' }}
{%- elif message['role'] == 'assistant' %}
{{- ' ' + message['content'] + ' ' + eos_token }}
{%- endif %}
{%- endfor %}
希望如果您仔细观察一下,您就能看出此模板的作用 - 它根据每条消息的角色添加了特定的标记,如[INST]
和[/INST]
。由于它们包含的标记,用户、助手和系统消息对模型来说是清晰可区分的。
高级:添加和编辑聊天模板
如何创建聊天模板?
很简单,只需编写一个 Jinja 模板并设置tokenizer.chat_template
即可。您可能会发现,从另一个模型的现有模板开始并根据您的需要进行编辑更容易!例如,我们可以采用上面的 LLaMA 模板,并在助手消息中添加“ [ASST]”和“ [/ASST]”
{%- for message in messages %}
{%- if message['role'] == 'user' %}
{{- bos_token + '[INST] ' + message['content'].strip() + ' [/INST]' }}
{%- elif message['role'] == 'system' %}
{{- '<<SYS>>\\n' + message['content'].strip() + '\\n<</SYS>>\\n\\n' }}
{%- elif message['role'] == 'assistant' %}
{{- '[ASST] ' + message['content'] + ' [/ASST]' + eos_token }}
{%- endif %}
{%- endfor %}
现在,只需设置tokenizer.chat_template
属性。下次您使用apply_chat_template()时,它将使用您的新模板!此属性将保存在tokenizer_config.json
文件中,因此您可以使用push_to_hub()将您的新模板上传到 Hub,并确保每个人都使用适合您的模型的正确模板!
template = tokenizer.chat_template
template = template.replace("SYS", "SYSTEM") # Change the system token
tokenizer.chat_template = template # Set the new template
tokenizer.push_to_hub("model_name") # Upload your new template to the 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
参数。
我们发现这可能会让用户感到困惑,因此,如果您自己正在编写模板,我们建议您尽可能地尝试将其全部放入一个模板中!
我应该使用哪个模板?
在设置已经过聊天训练的模型的模板时,您应该确保模板与模型在训练期间看到的邮件格式完全匹配,否则您可能会遇到性能下降。即使您进一步训练模型,情况也是如此 - 如果您保持聊天标记不变,您可能会获得最佳性能。这与标记化非常类似 - 当您精确匹配训练期间使用的标记化时,您通常会获得推理或微调的最佳性能。
另一方面,如果您是从头开始训练模型,或者对用于聊天的基础语言模型进行微调,则您有很大的自由选择合适的模板!LLM 足够智能,可以学习处理许多不同的输入格式。一个流行的选择是ChatML
格式,这对许多用例来说都是一个不错且灵活的选择。它看起来像这样
{%- for message in messages %}
{{- '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n' }}
{%- endfor %}
如果您喜欢这个,这里有一行形式,准备复制到您的代码中。单行代码还包括对生成提示的便捷支持,但请注意它不会添加 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|>
标记中,并简单地将角色写为字符串,这允许您灵活地选择训练的角色。输出如下所示
<|im_start|>system You are a helpful chatbot that will do its best not to say anything so stupid that people tweet about it.<|im_end|> <|im_start|>user How are you?<|im_end|> <|im_start|>assistant I'm doing great!<|im_end|>
“用户”、“系统”和“助手”角色是聊天的标准,我们建议在有意义的情况下使用它们,尤其是在您希望您的模型与TextGenerationPipeline良好配合时。但是,您并不限于这些角色 - 模板非常灵活,任何字符串都可以是角色。
我想添加一些聊天模板!我应该如何开始?
如果您有任何聊天模型,您应该设置其tokenizer.chat_template
属性并使用apply_chat_template()进行测试,然后将更新后的分词器推送到Hub。即使您不是模型所有者,这也适用 - 如果您使用的是具有空聊天模板的模型,或仍在使用默认类模板的模型,请向模型存储库提交拉取请求,以便正确设置此属性!
一旦设置了属性,就完成了!tokenizer.apply_chat_template
现在将对该模型正常工作,这意味着它也自动支持TextGenerationPipeline
等地方!
通过确保模型具有此属性,我们可以确保整个社区都能使用开源模型的全部功能。格式不匹配一直困扰着该领域,并长期以来一直默默地损害着性能——是时候结束这一切了!
高级:模板编写技巧
开始编写Jinja模板最简单的方法是查看一些现有的模板。您可以对任何聊天模型使用print(tokenizer.chat_template)
来查看它正在使用哪个模板。通常,支持工具使用的模型比其他模型具有更复杂的模板——因此,当您刚开始学习时,它们可能不是一个好的学习示例!您还可以查看Jinja文档,了解常规Jinja格式和语法的详细信息。
transformers
中的Jinja模板与其他地方的Jinja模板相同。主要需要注意的是,对话历史记录将在您的模板内部作为一个名为messages
的变量访问。
您可以在模板中像在Python中一样访问messages
,这意味着您可以使用{% for message in messages %}
循环遍历它,或者例如使用{{ messages[0] }}
访问单个消息。
您还可以使用以下技巧编写简洁高效的Jinja模板
修剪空白字符
默认情况下,Jinja将打印块之前或之后出现的任何空白字符。这对于通常希望对空白字符非常精确的聊天模板来说可能是一个问题!为了避免这种情况,我们强烈建议您像这样编写模板
{%- for message in messages %}
{{- message['role'] + message['content'] }}
{%- endfor %}
而不是像这样
{% for message in messages %}
{{ message['role'] + message['content'] }}
{% endfor %}
添加-
将删除块之前出现的任何空白字符。第二个示例看起来无害,但换行符和缩进最终可能会包含在输出中,这可能不是您想要的!
特殊变量
在您的模板内部,您可以访问几个特殊变量。其中最重要的是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实现的Jinja上使用您的模板,这将导致错误。非Python实现在部署环境中特别常见,其中JS和Rust非常流行。
不过,不要惊慌!您可以对模板进行一些简单的更改,以确保它们与所有Jinja实现兼容
- 将Python方法替换为Jinja过滤器。这些通常具有相同的名称,例如
string.lower()
变为string|lower
,dict.items()
变为dict|items
。一个值得注意的变化是string.strip()
变为string|trim
。请参阅Jinja文档中内置过滤器列表了解更多信息。 - 替换Python特有的
True
、False
和None
,使用true
、false
和none
。 - 直接呈现字典或列表可能会在其他实现中产生不同的结果(例如,字符串条目可能会从单引号更改为双引号)。添加
tojson
过滤器可以帮助确保在此处的一致性。
编写和调试较大的模板
引入此功能时,大多数模板都非常小,相当于Jinja中的“单行”脚本。但是,随着新模型和诸如工具使用和RAG等功能的出现,一些模板可能长达100行甚至更多。在编写此类模板时,最好使用文本编辑器将它们写入单独的文件中。您可以轻松地将聊天模板提取到文件中
open("template.jinja", "w").write(tokenizer.chat_template)
或将编辑后的模板加载回分词器中
tokenizer.chat_template = open("template.jinja").read()
此外,当您在单独的文件中编写一个较长、多行的模板时,该文件中的行号将与模板解析或执行错误中的行号完全对应。这将使您更容易识别问题根源。
< > 在 GitHub 上更新