채팅 템플릿

채팅 템플릿 (Chat templates)

chat basics 가이드는 [TextGenerationPipeline]을 사용해 채팅 기록을 저장하고 채팅 모델에서 텍스트를 생성하는 방법을 다뤄요.

이 가이드는 더 고급 사용자를 위한 것으로, 기반이 되는 클래스·메서드와 모델과 채팅할 때 실제로 어떤 일이 일어나는지 이해하는 데 핵심적인 개념을 다뤄요.

채팅 모델을 이해하는 데 결정적인 통찰은 이거예요. 모든 인과 LM은 채팅용으로 훈련됐든 아니든 토큰 시퀀스를 이어갑니다. 인과 LM을 훈련할 때 보통 방대한 텍스트 코퍼스에 대한 "사전 학습(pre-training)"으로 시작해 "베이스" 모델을 만들죠. 이 베이스 모델들은 채팅에 맞게 "미세조정"되는 경우가 많은데, 메시지 시퀀스로 형식화된 데이터로 훈련하는 걸 말해요. 그래도 채팅은 여전히 토큰 시퀀스일 뿐이에요! 채팅 모델에 넘기는 rolecontent 딕셔너리 리스트는 토큰 시퀀스로 변환되는데, 보통 <|user|><|assistant|>, <|end_of_message|> 같은 제어 토큰이 붙어 모델이 채팅 구조를 볼 수 있게 해요. 채팅 형식은 매우 많으며, 심지어 같은 베이스 모델에서 미세조정됐더라도 모델마다 다른 형식이나 제어 토큰을 쓸 수 있어요!

그래도 당황하지 마세요. 채팅 모델을 쓰기 위해 모든 가능한 채팅 형식을 외울 필요는 없어요. 채팅 모델에는 채팅이 어떻게 형식화되길 기대하는지 알려주는 채팅 템플릿이 딸려 있거든요. 이 템플릿은 [apply_chat_template] 메서드로 접근할 수 있어요. 두 예시를 볼게요. 두 모델 모두 같은 Mistral-7B 베이스 모델에서 미세조정됐어요.

Mistral:

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]

Zephyr:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta")
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)
<|user|>\nHello, how are you?</s>\n<|assistant|>\nI'm doing great. How can I help you today?</s>\n<|user|>\nI'd like to show off how chat templating works!</s>\n

Mistral-7B-Instruct는 사용자 메시지의 시작·끝을 나타내는 [INST][/INST] 토큰을 쓰는 반면, Zephyr-7B는 발언자 역할을 나타내는 <|user|><|assistant|> 토큰을 써요. 이것이 채팅 템플릿이 중요한 이유예요. 제어 토큰이 틀리면 이 모델들의 성능이 크게 떨어지거든요.

apply_chat_template 사용하기

apply_chat_template의 입력은 rolecontent 키를 가진 딕셔너리 리스트로 구조화돼야 해요. role 키는 발언자를, content 키는 메시지를 지정해요. 흔한 역할은 다음과 같아요.

  • user: 사용자의 메시지
  • assistant: 모델의 메시지
  • system: 모델이 어떻게 행동해야 하는지에 대한 지시(보통 채팅 시작 부분에 위치)

[apply_chat_template]은 이 리스트를 받아 형식화된 시퀀스를 반환해요. 시퀀스를 토큰화하고 싶으면 tokenize=True를 설정해요.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta")
model = AutoModelForCausalLM.from_pretrained("HuggingFaceH4/zephyr-7b-beta", device_map="auto", dtype=torch.bfloat16)

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").to(model.device)
print(tokenizer.decode(tokenized_chat["input_ids"][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|>

토큰화된 채팅을 [~GenerationMixin.generate]에 넘겨 응답을 생성해요.

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.

[!WARNING] 어떤 토크나이저는 특수 <bos>·<eos> 토큰을 추가해요. 채팅 템플릿은 필요한 모든 특수 토큰을 이미 포함해야 하며, 추가 특수 토큰을 더하면 종종 틀리거나 중복돼 모델 성능을 해쳐요. apply_chat_template(tokenize=False)로 텍스트를 형식화했다면, 나중에 토큰화할 때 add_special_tokens=False를 설정해 토큰 중복을 피해야 해요. apply_chat_template(tokenize=True)를 쓰면 이 문제가 없으니 대개 더 안전한 선택이에요!

add_generation_prompt

위 예시에서 [~PreTrainedTokenizerBase.apply_chat_template#add_generation_prompt] 인자를 눈치챘을 거예요. 이 인자는 채팅 끝에 assistant 응답의 시작을 알리는 토큰을 추가해요. 기억하세요. 모든 채팅 추상화 아래에서 채팅 모델은 여전히 토큰 시퀀스를 이어가는 그냥 언어 모델이에요! 이제 assistant 응답에 있다고 알리는 토큰을 포함하면 올바르게 응답을 쓰지만, 이 토큰이 없으면 모델이 혼란스러워져 사용자 메시지를 이어가는 등 이상한 일을 할 수 있어요!

add_generation_prompt이 실제로 뭘 하는지 이해하기 위해 예시를 볼게요. 먼저 add_generation_prompt 없이 채팅을 형식화해요.

tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
tokenized_chat
<|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|>

이제 같은 채팅을 add_generation_prompt=True로 형식화해요.

tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
tokenized_chat
<|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

add_generation_prompt=True일 때 assistant 메시지의 시작을 나타내기 위해 끝에 <|im_start|>assistant가 추가돼요. 이렇게 하면 모델이 assistant 응답이 다음에 온다는 걸 알 수 있어요.

모든 모델이 생성 프롬프트를 요구하는 건 아니며, Llama 같은 일부 모델은 assistant 응답 앞에 특수 토큰이 없어요. 이런 경우 [~PreTrainedTokenizerBase.apply_chat_template#add_generation_prompt]는 효과가 없어요.

continue_final_message

[~PreTrainedTokenizerBase.apply_chat_template#continue_final_message] 파라미터는 채팅의 마지막 메시지를 새로 시작하는 대신 계속할지 제어해요. 시퀀스 종료 토큰을 제거해서 모델이 마지막 메시지에서 생성을 이어가게 해요.

이는 모델 응답을 "프리필링(prefilling)"할 때 유용해요. 아래 예시에서 모델은 새 메시지를 시작하는 대신 JSON 문자열을 이어가는 텍스트를 생성해요. 응답을 어떻게 시작할지 알 때 지시사항 따르기 정확도를 높이는 데 매우 유용할 수 있어요.

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)

[!WARNING] [~PreTrainedTokenizerBase.apply_chat_template#add_generation_prompt]과 [~PreTrainedTokenizerBase.apply_chat_template#continue_final_message]을 함께 쓰면 안 돼요. 전자는 새 메시지를 시작하는 토큰을 추가하고, 후자는 시퀀스 종료 토큰을 제거하기 때문이에요. 같이 쓰면 오류가 반환돼요.

content 대신 필드 이름을 문자열로 넘겨 그 필드를 프리필해요. Reasoning 모델은 Qwen에서는 reasoning_content, Gemma에서는 thinking처럼 별도 필드를 자주 노출해요. content를 프리필하면 생성이 시작되기 전에 reasoning 블록을 닫아, 모델이 그 안에서 이어갈 수 없게 돼요. Reasoning 필드를 직접 프리필하면 블록이 열린 채로 남아요.

chat = [
    {"role": "user", "content": "Explain 1+1"},
    {"role": "assistant", "reasoning_content": "The user wants a simple addition. ", "content": ""},
]

formatted_chat = tokenizer.apply_chat_template(chat, tokenize=False, continue_final_message="reasoning_content")

이름이 붙은 필드는 마지막 메시지에 존재해야 하고 채팅 템플릿이 참조해야 해요. 두 검사 중 하나라도 실패하면 오류가 발생해요.

[TextGenerationPipeline]은 새 메시지를 시작하기 위해 기본적으로 [~PreTrainedTokenizerBase.apply_chat_template#add_generation_prompt]을 True로 설정해요. 하지만 채팅의 마지막 메시지가 assistant 역할이면 이를 프리필로 가정하고 continue_final_message=True로 전환해요. 대부분의 모델이 연속으로 여러 assistant 메시지를 지원하지 않기 때문이에요. 이 동작을 덮어쓰려면 파이프라인에 [~PreTrainedTokenizerBase.apply_chat_template#continue_final_message] 인자를 명시적으로 넘겨요.

모델 훈련 (Model training)

채팅 템플릿으로 모델을 훈련하는 건 템플릿이 모델이 훈련된 토큰과 일치하도록 보장하는 좋은 방법이에요. 데이터셋 전처리 단계로 채팅 템플릿을 적용해요. add_generation_prompt=False로 설정해요. assistant 응답을 촉구할 추가 토큰은 훈련 중에 도움이 되지 않기 때문이에요.

채팅 템플릿으로 데이터셋을 전처리하는 예시는 아래와 같아요.

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>

이 단계 후에는 인과 언어 모델의 training recipeformatted_chat 컬럼으로 계속 따라가면 돼요.

출처: Hugging Face Transformers — Chat templates

더 알아보기 (Learn more)