vllm 聊天模板

打印 上一主题 下一主题

主题 869|帖子 869|积分 2607

配景

近来在使用vllm来运行大模型,使用了文档提供的代码如下所示,发现模型只是在补全我的话,像一个base的大模型一样,而我使用的是颠末指令微调的有聊天本领的大模型。回过头看huggingface提供的使用大模型的代码,发现有一个方法是apply_apply_chat_template,并且对话还通常有着脚色,比方"user"或"system",这让我意识到使用大模型的聊天功能并不是直接将输入提供给模型即可。因此需要对大模型聊天本领背后的细节举行一些相识。实现将prompt转为对话信息的代码见:https://github.com/JinFish/EasyChatTemplating
  1. from vllm import LLM, SamplingParams
  2. prompts = [
  3.     "Hello, my name is",
  4.     "The president of the United States is",
  5.     "The capital of France is",
  6.     "The future of AI is",
  7. ]
  8. sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
  9. llm = LLM(model="../../pretrained_models/llama3-chat")
  10. outputs = llm.generate(prompts, sampling_params)
  11. # Print the outputs.
  12. for output in outputs:
  13.     prompt = output.prompt
  14.     generated_text = output.outputs[0].text
  15.     print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
  16.    
  17. >>> Prompt: 'Hello, my name is', Generated text: ' Helen and I am a 35 year old mother of two. I am a'
  18. >>> Prompt: 'The president of the United States is', Generated text: ' the head of the executive branch of the federal government, and is the highest-ranking'
  19. >>> Prompt: 'The capital of France is', Generated text: ' Paris, and it is also the largest city in the country. It is situated'
  20. >>> Prompt: 'The future of AI is', Generated text: ' full of endless possibilities, but it also poses significant challenges and risks. As AI'
复制代码
当前的大模型通常是decoder-only的模型,无论是单轮对话还是多轮对话都是一股脑地丢进模型,而区分对话中的脚色和对话需要一些特殊的标记。比方:在用户输入的时间,格式是user:我今早上吃了炒米粉。assistant:炒米粉在广东是蛮常见的早餐,但是油太多,可以偶尔吃吃。而输入给模型的则是:<s><intp>我今早上吃了炒米粉。</intp> [ASST] 炒米粉在广东是蛮常见的早餐,但是油太多,可以偶尔吃吃。[/ASST] eos_token。其中<intp>和</intp>用来表现用户的输入,[ASST]和[/ASST]表现模型的复兴。eos_token表现会话的结束。
别的,目前大模型最常见的应用便是“对话”,在对话的上下文中,往往语言模型不是像往常那样延续一个单独的文本字符串,而是要延续由一个或多个**“messages”(消息)组成的会话**,并且每个消息都会包含一个**“role”(脚色)**,比方"user"或者"assistant",以及对应的消息内容。
就像差异的模型有差异的分词方式、特殊标记和情势一样,差异的大模型也有差异的chat template,这是tokenizer的一部分,其主要指定了如何将以消息列表呈现的会话转换成模型所期望的单个token化的字符串格式。以mistralai/Mistral-7B-Instruct-v0.1为例,其会使用<s>表现一个会话的开始,</s>表现回合的结束,即用来表现回合的边界,其会使用[INST]以及[/INST]来表现用户输入的信息:
  1. from transformers import AutoTokenizer
  2. tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1")
  3. chat = [
  4.   {"role": "user", "content": "Hello, how are you?"},
  5.   {"role": "assistant", "content": "I'm doing great. How can I help you today?"},
  6.   {"role": "user", "content": "I'd like to show off how chat templating works!"},
  7. ]
  8. tokenizer.apply_chat_template(chat, tokenize=False)
  9. "<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]"
复制代码
如何使用chat template

从上个例子上来看,使用chat template是比较简朴的,首先就是定义一个带有”role“和,”content“为键的消息列表,然后将该列表传入给tokenizerapply_chat_template方法即可。
  1. from transformers import AutoModelForCausalLM, AutoTokenizer
  2. checkpoint = "HuggingFaceH4/zephyr-7b-beta"
  3. tokenizer = AutoTokenizer.from_pretrained(checkpoint)
  4. model = AutoModelForCausalLM.from_pretrained(checkpoint)  # You may want to use bfloat16 and/or move to GPU here
  5. messages = [
  6.     {
  7.         "role": "system",
  8.         "content": "You are a friendly chatbot who always responds in the style of a pirate",
  9.     },
  10.     {"role": "user", "content": "How many helicopters can a human eat in one sitting?"},
  11. ]
  12. tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt")
  13. print(tokenizer.decode(tokenized_chat[0]))
复制代码
运行上述代码则可以得到对应的chat template格式:
  1. <|system|>
  2. You are a friendly chatbot who always responds in the style of a pirate</s>
  3. <|user|>
  4. How many helicopters can a human eat in one sitting?</s>
  5. <|assistant|>
复制代码
generation prompt & add_generation_prompt

在上述例子中的apply_chat_template方法中有一个参数为add_generation_prompt,其值为True或False,如果设置为True,那么模型就会自动天生一些固定的prompt,即generation prompt。比方,有以下对话:
  1. messages = [
  2.     {"role": "user", "content": "Hi there!"},
  3.     {"role": "assistant", "content": "Nice to meet you!"},
  4.     {"role": "user", "content": "Can I ask a question?"}
  5. ]
复制代码
如果将参数设置为False,则会得到以下的chat template格式的输出:
  1. tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
  2. """<|im_start|>user
  3. Hi there!<|im_end|>
  4. <|im_start|>assistant
  5. Nice to meet you!<|im_end|>
  6. <|im_start|>user
  7. Can I ask a question?<|im_end|>
  8. """
复制代码
如果将参数设置为True,则会得到以下格式的输出:
  1. tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
  2. """<|im_start|>user
  3. Hi there!<|im_end|>
  4. <|im_start|>assistant
  5. Nice to meet you!<|im_end|>
  6. <|im_start|>user
  7. Can I ask a question?<|im_end|>
  8. <|im_start|>assistant
  9. """
复制代码
可以看到比上个例子多了<|im_start|>assistant,这是模型回复之前的标识。这样可以确保模型天生文本时会输出助手对应的复兴,而不是做一些意想不到的事变,比如继续用户的信息。 留意:**聊天模型仍然只是语言模型,它们被练习来续写文本,而聊天对它们来说也只是一种特殊的文本。我们需要用得当的控制标记来引导它们,让它们知道自己应该做什么。**提前为模型写好<|im_start|>assistant可以让模型知道自己输出的应该是复兴,而不是续写用户的输入。
但是并不是所有的模型都支持该特性,因为有一些模型其“助手复兴”之前没有任何的特殊标识,因此在这些模型上使用add_generation_prompt是没有结果的。
chat templates的额外输入

通常来说,只需要给apply_chat_template方法传入messages即可,但也可以传入一些额外的参数供模板访问。比方将tools传入可以实现对工具的使用,或者将文档传入实现RAG。
工具使用 / 函数调用

“Tool use”即LLM会在天生答案之前能够选择调用的方法作为额外的工具,将工具传递给可以使用工具的模型时,可以简朴地将函数列表传递给工具参数:
  1. import datetime
  2. def current_time():
  3.     """Get the current local time as a string."""
  4.     return str(datetime.now())
  5. def multiply(a: float, b: float):
  6.     """
  7.     A function that multiplies two numbers
  8.    
  9.     Args:
  10.         a: The first number to multiply
  11.         b: The second number to multiply
  12.     """
  13.     return a * b
  14. tools = [current_time, multiply]
  15. model_input = tokenizer.apply_chat_template(
  16.     messages,
  17.     tools=tools
  18. )
复制代码
为了使工具能够正常工作,应该按照上述格式编写函数,以便将其作为工具正确剖析。 具体来说,应遵照以下规则:


  • 函数应有一个描述性的名称
  • 每个参数都必须有范例提示
  • 函数必须具有标准 Google 风格的 docstring(换句话说,在初始函数描述之后,必须有一个 Args: 块来描述参数,除非函数没有任何参数)
  • 不要在 Args: 代码块中包含范例。 换句话说,应该写 a: 第一个要相乘的数字,而不是 a (int): 第一个要相乘的数字。 范例提示应放在函数头中
  • 函数可以在 docstring 中包含返回范例和 Returns: 块。 不外,这些都是可选的,因为大多数工具使用模型都会忽略它们
当模型具体调用一个方法时发生了什么呢:


  • 当模型天生响应时,它的输出可能包含调用特定工具的信息。这通常是以模型预定义的格式或特殊标记来表达的,比如模型可能会天生类似call tool <tool_name> with arg1=value1, arg2=value2的语句。需要剖析这些参数,从模型的输出中提取出工具的名字以及调用该工具所需的参数。
  • 将模型调用工具的请求添加到对话历史中,这是为了保持对话上下文的连贯性。这一步确保了工具调用是对话的一部分,且在将来的模型推理中可以参考这次调用。
  • 使用从模型输出中剖析得到的工具名和参数,实际调用相应的工具函数。这可能是实行一个外部API调用、运行一段代码、查询数据库或其他任何范例的操纵。
  • 将工具实行后返回的结果添加回对话历史中。这样,模型在后续的推理过程中可以访问这些结果,从而基于工具提供的信息天生更丰富、更具体的响应。
整个流程形成了一个闭环,模型可以提出需求(调用工具),实行工具,然后将工具的结果反馈给模型,使模型能够在对话中使用这些结果继续举行故意义的交互。这在构建复杂的应用场景,如查询数据库、实行盘算或调用天气API时非常有效。
下面是一个具体的一个模型调用工具的例子,使用了8B Hermes-2-Pro模型:
  1. import torch
  2. from transformers import AutoModelForCausalLM, AutoTokenizer
  3. checkpoint = "NousResearch/Hermes-2-Pro-Llama-3-8B"
  4. tokenizer = AutoTokenizer.from_pretrained(checkpoint, revision="pr/13")
  5. model = AutoModelForCausalLM.from_pretrained(checkpoint, torch_dtype=torch.bfloat16, device_map="auto")
  6. def get_current_temperature(location: str, unit: str) -> float:
  7.     """
  8.     Get the current temperature at a location.
  9.    
  10.     Args:
  11.         location: The location to get the temperature for, in the format "City, Country"
  12.         unit: The unit to return the temperature in. (choices: ["celsius", "fahrenheit"])
  13.     Returns:
  14.         The current temperature at the specified location in the specified units, as a float.
  15.     """
  16.     return 22.  # A real function should probably actually get the temperature!
  17. def get_current_wind_speed(location: str) -> float:
  18.     """
  19.     Get the current wind speed in km/h at a given location.
  20.    
  21.     Args:
  22.         location: The location to get the temperature for, in the format "City, Country"
  23.     Returns:
  24.         The current wind speed at the given location in km/h, as a float.
  25.     """
  26.     return 6.  # A real function should probably actually get the wind speed!
  27. tools = [get_current_temperature, get_current_wind_speed]
  28. messages = [
  29.   {"role": "system", "content": "You are a bot that responds to weather queries. You should reply with the unit used in the queried location."},
  30.   {"role": "user", "content": "Hey, what's the temperature in Paris right now?"}
  31. ]
  32. inputs = tokenizer.apply_chat_template(messages, chat_template="tool_use", tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt")
  33. inputs = {k: v.to(model.device) for k, v in inputs.items()}
  34. out = model.generate(**inputs, max_new_tokens=128)
  35. print(tokenizer.decode(out[0][len(inputs["input_ids"][0]):]))
复制代码
将用户的对话和函数传给通过tokenizer处置惩罚后,将处置惩罚结果输入给模型得到:
  1. <tool_call>
  2. {"arguments": {"location": "Paris, France", "unit": "celsius"}, "name": "get_current_temperature"}
  3. </tool_call><|im_end|>
复制代码
可以看出模型的输出是一个工具调用指令,它遵照了一种结构化的格式,用于指示系统调用一个特定的工具或API:


  • <tool_call> 和 </tool_call> 是标签,它们标记了模型输出中工具调用的开始和结束。
  • {...} 中的内容是一个JSON格式的对象,包含了调用工具所需的信息。
  • "name": "get_current_temperature" 指出了模型想要调用的工具或函数的名称,这里是获取当前温度的功能。
  • "arguments" 字段包含了传递给该工具的参数,它本身也是一个JSON对象,其中包含:

    • "location": "aris, France",指定要查询的所在为法国巴黎。
    • "unit": "celsius",表明温度单位应该是摄氏度。
    接下来还需要将工具的调用附加在会话上,让模型得知自己调用了什么工具,以及结果是什么:
    首先是随机化了一个工具id,用来唯一地表现在某个会话中调用的工具,并且记录工具的范例和工具的名称和参数:

  1. tool_call_id = "vAHdf3"  # Random ID, should be unique for each tool call
  2. tool_call = {"name": "get_current_temperature", "arguments": {"location": "Paris, France", "unit": "celsius"}}
  3. messages.append({"role": "assistant", "tool_calls": [{"id": tool_call_id, "type": "function", "function": tool_call}]})
复制代码
然后将工具的返回结果添加入会话中:
  1. messages.append({"role": "tool", "tool_call_id": tool_call_id, "name": "get_current_temperature", "content": "22.0"})
复制代码
末了,让模型根据读取对应的信息继续天生输出给用户:
  1. inputs = tokenizer.apply_chat_template(messages, chat_template="tool_use", tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt")
  2. inputs = {k: v.to(model.device) for k, v in inputs.items()}
  3. out = model.generate(**inputs, max_new_tokens=128)
  4. print(tokenizer.decode(out[0][len(inputs["input_ids"][0]):]))
  5. "The current temperature in Paris, France is 22.0 ° Celsius.<|im_end|>""
复制代码
实际上,模型并没有完全地阅读函数中的代码。模型真正关心的是函数定义和需要传递给函数的参数以及参数的描述,模型关心的是工具的作用和使用方法,而不是工具的工作原理。因此,函数会被处置惩罚成JSON的格式供模型阅读:
  1. from transformers.utils import get_json_schema
  2. def multiply(a: float, b: float):
  3.     """
  4.     A function that multiplies two numbers
  5.    
  6.     Args:
  7.         a: The first number to multiply
  8.         b: The second number to multiply
  9.     """
  10.     return a * b
  11. schema = get_json_schema(multiply)
  12. print(schema)
  13. # 可以看到,返回的结果只包含了函数的说明性的信息,并没有对应原理或源码的信息
  14. {
  15.   "type": "function",
  16.   "function": {
  17.     "name": "multiply",
  18.     "description": "A function that multiplies two numbers",
  19.     "parameters": {
  20.       "type": "object",
  21.       "properties": {
  22.         "a": {
  23.           "type": "number",
  24.           "description": "The first number to multiply"
  25.         },
  26.         "b": {
  27.           "type": "number",
  28.           "description": "The second number to multiply"
  29.         }
  30.       },
  31.       "required": ["a", "b"]
  32.     }
  33.   }
  34. }
复制代码
Chat Template的工作机制

将“messages”转换为模型可以理解的格式需要通过Chat Template,这些聊天模板通常存储在模型的tokenizer_config.json中的chat_template的键中,其在tokenizer加载的过程中会将其赋给tokenizer的chat_template属性,其中blenderbot-400M-distill的聊天模板如下所示:
  1. from transformers import AutoTokenizer
  2. tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill")
  3. tokenizer.default_chat_template
  4. "{% for message in messages %}{% if message['role'] == 'user' %}{{ ' ' }}{% endif %}{{ message['content'] }}{% if not loop.last %}{{ '  ' }}{% endif %}{% endfor %}{{ eos_token }}"
复制代码
这些聊天模板通常以Jinja2的情势存在,以下是其更加直观的样式:
  1. {%- for message in messages %}
  2.     {%- if message['role'] == 'user' %}
  3.         {{- ' ' }}
  4.     {%- endif %}
  5.     {{- message['content'] }}
  6.     {%- if not loop.last %}
  7.         {{- '  ' }}
  8.     {%- endif %}
  9. {%- endfor %}
  10. {{- eos_token }}
复制代码
由于Jinja2会保留模板中标签自带的缩进和换行,所以会在控制语句和表达式中添加-,用来消除其前面的空白。将上述代码转为python的代码的话,如下所示:
  1. for idx, message in enumerate(messages):
  2.     if message['role'] == 'user':
  3.         print(' ')
  4.     print(message['content'])
  5.     if not idx == len(messages) - 1:  # Check for the last message in the conversation
  6.         print('  ')
  7. print(eos_token)
复制代码
所以,总的来说,上述模板做的事变有:


  • 遍历每条信息,如果信息的脚色是用户,那么会先添加一个空格。
  • 添加每条信息内容。
  • 如果不是末了一条信息,那么就添加两个空格。
  • 在末了一条信息后添加eos_token。
多个模板的情况

一些模型在差异的情况下使用差异的模板,它们可能会使用一个模板用于聊天,另一个模板用于调用工具或者使用RAG。在这种情况下,给tokenizer中的chat_template属性是一个字典,包含了多个模板,其中每个键是模板的名称,其中可能会有一个键为default,用于大部分的样例。这可能会导致一些歧义或狐疑,在可能的情况下,最好是使用一个模板用于所有的情况。
在引入更加机动的聊天模板(chat templates)之前,模型的聊天本领受限于其内部实现。这意味着差异模型在处置惩罚聊天输入时可能有差异的行为,这主要由模型的架构和练习数据决定。为了保持向后兼容性,当引入新的聊天模板机制时,旧的、基于模型类的处置惩罚方式被保留下来,作为“default”模板。
如果一个模型没有显式地设置聊天模板(chat template),但它的模型类中定义了一个“default”模板,那么TextGenerationPipeline和其他相干的方法会使用这个类级别的模板来处置惩罚聊天对话。这保证了即使开辟者没有自动设置聊天模板,模型也能以某种预设的方式举行对话处置惩罚。
要相识一个特定模型的默认模板是什么,可以检查tokenizer的default_chat_template属性。这提供了模型默认的聊天处置惩罚方式的信息。
尽管“default”模板为向后兼容提供了便利,但官方文档强烈建议开辟者不要依靠这些默认模板,而是应该显式地设置chat_template属性。这样做有几个利益:


  • 明白地表明模型已经为聊天场景举行了正确的设置。
  • 提供了更大的机动性和控制,可以根据具体的应用场景调解模板。
  • 淘汰了将来更新中移除默认模板可能带来的风险。
本文参考自https://huggingface.co/docs/transformers/chat_templating

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

西河刘卡车医

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表