Hugging Face Chat Template 정리하기

Hugging Face Chat Template 정리하기
Photo by Geronimo Giqueaux / Unsplash

대화형 언어 모델을 사용할 때 입력은 보통 다음과 같은 메시지 목록으로 표현합니다.

messages = [
    {"role": "user", "content": "Who are you?"},
]

사람에게는 이 구조가 충분히 직관적입니다. user는 사용자의 말이고, assistant는 모델의 답변이며, system은 모델이 따라야 할 상위 지시사항입니다. 하지만 모델이 실제로 처리하는 것은 이런 딕셔너리 목록이 아니라 하나의 토큰 시퀀스입니다.

여기서 필요한 것이 Chat Template입니다. Chat Template은 system, user, assistant, tool 같은 메시지 역할을 모델이 학습한 입력 형식에 맞게 문자열 또는 토큰 시퀀스로 변환하는 규칙입니다.

예전에는 Hugging Face 모델 저장소의 Files and versions 탭에서 tokenizer_config.json이나 관련 설정 파일을 직접 열어 챗 템플릿을 확인해야 했습니다. 모델마다 프롬프트 형식이 다르기 때문에, 실제로는 모델을 바꿀 때마다 템플릿을 확인하고 입력 포맷을 맞추는 과정이 필요했습니다.

최근에는 Hugging Face 모델 페이지에서 Use this model 영역을 통해 Transformers 코드 스니펫과 Chat Template 관련 사용 방식을 더 쉽게 확인할 수 있습니다. 이번 글에서는 Chat Template이 왜 필요한지, apply_chat_template()이 어떤 역할을 하는지, 실제 모델 템플릿은 어떻게 구성되는지, 그리고 보안과 커스터마이징 관점에서 무엇을 주의해야 하는지 정리합니다.

1. Chat Template이 해결하는 문제

대화형 모델은 “대화”를 이해하는 것처럼 보이지만, 내부적으로는 입력된 토큰의 다음 토큰을 예측합니다. 모델 입장에서는 다음 두 입력이 전혀 다르게 보일 수 있습니다.

User: 안녕하세요
Assistant: 안녕하세요. 무엇을 도와드릴까요?
<|im_start|>user
안녕하세요<|im_end|>
<|im_start|>assistant
안녕하세요. 무엇을 도와드릴까요?<|im_end|>

또 다른 모델은 아래와 같은 형식을 기대할 수도 있습니다.

<s>[INST] 안녕하세요 [/INST] 안녕하세요. 무엇을 도와드릴까요?</s>

세 입력은 사람에게는 모두 비슷한 대화처럼 보입니다. 하지만 모델은 학습 과정에서 특정 형식의 control token과 메시지 구분 방식을 보면서 미세 조정(fine-tuning)됩니다. 따라서 학습 시 사용한 형식과 다른 방식으로 입력을 넣으면, 모델이 역할 구분을 제대로 하지 못하거나 답변 품질이 떨어질 수 있습니다.

Chat Template은 이 문제를 줄이기 위한 장치입니다. 사용자는 rolecontent로 구성된 메시지 목록만 넘기고, 모델별 세부 포맷은 토크나이저가 가진 템플릿이 처리합니다.

2. Chat Template이란 무엇인가

Chat Template은 대화 메시지를 모델이 기대하는 입력 형식으로 바꾸는 템플릿입니다. Hugging Face Transformers에서는 보통 토크나이저에 chat_template 형태로 저장됩니다.

핵심 역할은 다음과 같습니다.

  • 메시지 역할을 모델별 control token으로 감쌉니다.
  • system, user, assistant, tool 메시지를 구분합니다.
  • 모델이 학습한 순서와 형식에 맞게 대화를 직렬화합니다.
  • 필요한 경우 BOS, EOS 같은 특수 토큰을 추가합니다.
  • 함수 호출(function calling), 도구 사용(tool use), RAG 같은 고급 입력 구조를 다룰 수 있게 합니다.

예를 들어 어떤 모델은 사용자 메시지를 [INST] ... [/INST]로 감싸도록 학습되었고, 다른 모델은 <|user|>, <|assistant|> 같은 토큰을 사용하도록 학습되었을 수 있습니다. 같은 계열의 모델이라도 instruction tuning 과정에서 사용한 템플릿이 다르면 입력 형식이 달라질 수 있습니다.

따라서 Chat Template은 단순한 문자열 포맷터가 아닙니다. 모델이 어떤 메시지가 사용자 요청인지, 어떤 메시지가 이전 assistant 응답인지, 어디서부터 새 답변을 생성해야 하는지를 판단하게 해주는 입력 계약에 가깝습니다.

3. Chat Template 예시: Qwen3-Next

Qwen3-Next 같은 최신 대화형 모델의 Chat Template을 보면 Jinja 문법을 사용해 메시지를 동적으로 조립합니다. 전체 템플릿은 길고 복잡하지만, 핵심 구조만 보면 다음과 같은 패턴을 확인할 수 있습니다.

{%- if tools %}
    {{- '<|im_start|>system\n' }}
    {%- if messages[0].role == 'system' %}
        {{- messages[0].content + '\n\n' }}
    {%- endif %}
    {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\n" }}
    {%- for tool in tools %}
        {{- "\n" }}
        {{- tool | tojson }}
    {%- endfor %}
    {{- '<|im_end|>\n' }}
{%- else %}
    {%- if messages[0].role == 'system' %}
        {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
    {%- endif %}
{%- endif %}

{%- for message in messages %}
    {%- if message.role == "user" %}
        {{- '<|im_start|>user\n' + message.content + '<|im_end|>\n' }}
    {%- elif message.role == "assistant" %}
        {{- '<|im_start|>assistant\n' + message.content }}
        {{- '<|im_end|>\n' }}
    {%- elif message.role == "tool" %}
        {{- '\n<tool_response>\n' }}
        {{- message.content }}
        {{- '\n</tool_response>' }}
    {%- endif %}
{%- endfor %}

{%- if add_generation_prompt %}
    {{- '<|im_start|>assistant\n' }}
{%- endif %}

이 템플릿에서 볼 수 있는 핵심은 네 가지입니다.

첫째, tools가 있는지에 따라 시스템 프롬프트의 구조가 달라집니다. 도구 사용이 필요한 경우에는 사용 가능한 함수의 서명과 호출 형식이 시스템 메시지 안에 포함됩니다.

둘째, 메시지 역할에 따라 문자열 조합 방식이 다릅니다. user, assistant, tool 메시지는 각각 다른 control token과 구조를 사용합니다.

셋째, tool call과 tool response를 별도의 XML 스타일 태그로 감쌉니다. 이는 모델이 일반 텍스트 응답과 도구 호출 결과를 구분하도록 돕습니다.

넷째, add_generation_prompt가 참이면 마지막에 assistant 응답이 시작될 위치를 추가합니다. 모델에게 “이제 assistant가 답변할 차례”라는 신호를 주는 역할입니다.

이처럼 Chat Template은 단순히 앞뒤에 문자열을 붙이는 수준이 아닙니다. 모델의 대화 형식, 도구 호출 방식, 응답 생성 시작 지점까지 함께 정의합니다.

4. Chat Template이 중요한 이유

Chat Template을 올바르게 사용하는 것은 모델 응답 품질과 직접 연결됩니다.

모델이 학습한 입력 형식을 맞춥니다

대화형 모델은 특정 메시지 형식으로 미세 조정됩니다. 이 형식과 다른 입력을 넣으면 모델이 사용자 메시지, assistant 메시지, 시스템 지시사항을 제대로 구분하지 못할 수 있습니다.

메시지 역할을 명확히 분리합니다

system, user, assistant, tool 메시지가 명확히 구분되어야 모델이 대화 맥락을 안정적으로 이해합니다. 특히 긴 대화나 멀티턴 대화에서는 역할 구분이 흐려질수록 응답 품질이 흔들릴 수 있습니다.

특수 토큰을 일관되게 처리합니다

BOS, EOS, 메시지 종료 토큰, assistant 시작 토큰 같은 특수 토큰은 모델 입력과 출력 생성에 영향을 줍니다. 이를 수동으로 조합하면 누락이나 중복이 발생하기 쉽습니다.

함수 호출과 도구 사용을 가능하게 합니다

도구 사용 모델은 단순한 대화보다 복잡한 입력 형식을 요구합니다. 사용할 수 있는 도구 목록, 함수 인자, tool call 출력 형식, tool response 형식이 모두 템플릿에 반영되어야 합니다.

모델 교체 시 유지보수가 쉬워집니다

Llama 계열 모델을 사용하다가 Qwen 계열 모델로 바꾼다고 가정해 보겠습니다. 프롬프트 문자열을 직접 조합하고 있었다면 모델별 형식 차이를 코드에서 모두 수정해야 합니다. 반면 apply_chat_template()을 사용하면 토크나이저가 해당 모델의 템플릿을 불러와 적용하므로, 입력 조립 로직을 크게 바꾸지 않아도 됩니다.

5. Hugging Face에서 Chat Template 확인하기

Hugging Face에서 템플릿을 확인하고 코드에 적용하는 방법은 매우 간단합니다.

Hugging Face 모델 페이지에서는 모델 사용 코드를 확인할 수 있습니다.

  1. 사용할 모델의 Hugging Face 페이지로 이동합니다.
  2. 모델 카드에서 Use this model 영역을 찾습니다.
  3. 라이브러리 목록에서 Transformers를 선택합니다.
  4. 제공되는 코드 스니펫에서 pipeline 방식 또는 직접 로드 방식을 확인합니다.

예를 들어 Qwen3-Next 계열 모델을 사용할 때는 다음과 같은 형태의 코드를 확인할 수 있습니다.

# pipeline을 사용한 고수준 실행
from transformers import pipeline

pipe = pipeline("text-generation", model="Qwen/Qwen3-Next-80B-A3B-Instruct")

messages = [
    {"role": "user", "content": "Who are you?"},
]

pipe(messages)

직접 모델과 토크나이저를 로드하는 방식은 다음과 같습니다.

# 모델과 토크나이저를 직접 로드
from transformers import AutoTokenizer, AutoModelForCausalLM

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-Next-80B-A3B-Instruct")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-Next-80B-A3B-Instruct")

messages = [
    {"role": "user", "content": "Who are you?"},
]

inputs = tokenizer.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(model.device)

outputs = model.generate(**inputs, max_new_tokens=40)

print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:]))

여기서 가장 중요한 부분은 tokenizer.apply_chat_template()입니다. 이 함수가 rolecontent로 구성된 메시지 목록을 모델의 Chat Template에 맞게 변환합니다.

6. apply_chat_template 살펴보기

apply_chat_template()은 단순히 문자열을 이어 붙이는 함수가 아닙니다. 모델이 요구하는 입력 형식을 토크나이저 수준에서 처리해 주는 인터페이스입니다.

messages

입력 대화입니다. 일반적으로 아래처럼 rolecontent를 가진 딕셔너리 목록으로 구성합니다.

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Explain chat templates simply."},
]

tokenize

tokenize=False로 설정하면 템플릿이 적용된 문자열을 확인할 수 있습니다. 디버깅할 때 유용합니다.

formatted_prompt = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
)

print(formatted_prompt)

모델에 바로 넣을 입력이 필요하다면 tokenize=True를 사용합니다.

add_generation_prompt

add_generation_prompt=True는 모델이 새 assistant 응답을 생성할 차례라는 신호를 추가합니다.

예를 들어 ChatML 계열 템플릿에서는 마지막에 다음과 유사한 문자열이 추가될 수 있습니다.

<|im_start|>assistant

이 신호가 없으면 모델이 사용자 메시지 뒤에 바로 이어 쓰거나, 대화가 끝났다고 판단하는 등 기대와 다른 동작을 할 수 있습니다.

return_dict, return_tensors

직접 model.generate()를 호출할 때는 return_dict=True, return_tensors="pt"를 함께 사용하는 경우가 많습니다. 이렇게 하면 input_ids, attention_mask 같은 텐서 입력을 모델에 전달하기 쉽습니다.

7. 국내 모델 예시 - KT Mi:dm

국내 모델의 Chat Template도 모델의 정체성과 동작 방식을 이해하는 데 도움이 됩니다. KT의 Mi:dm(믿:음) 모델 템플릿을 보면, 단순히 메시지 구분 형식만 정의하는 것이 아니라 모델의 정체성과 행동 지침을 시스템 프롬프트에 포함하고 있습니다.

일부 구조를 보면 다음과 같습니다.

{{- bos_token }}
{{- '<|start_header_id|>system<|end_header_id|>\n\n' }}
{{- 'Cutting Knowledge Date: December 2024\n' }}
{{- 'Today Date: ' + date_string + '\n\n' }}
{{- 'Mi:dm(믿:음)은 KT에서 개발한 AI 기반 어시스턴트이다. 너는 Mi:dm으로서 사용자에게 유용하고 안전한 응답을 제공해야 한다.\n\n' }}
{{- system_message }}
{{- '<|eot_id|>' }}

이 템플릿에서 주목할 부분은 다음과 같습니다.

지식 마감일과 현재 날짜를 명시합니다

Cutting Knowledge DateToday Date를 시스템 프롬프트에 포함해 모델이 자신의 지식 범위와 현재 날짜를 인식하도록 합니다.

모델의 정체성을 정의합니다

템플릿 안에는 Mi:dm이 어떤 모델인지, 어떤 방식으로 응답해야 하는지에 대한 지시가 포함됩니다. 이는 모델의 페르소나와 응답 스타일을 일관되게 유지하는 데 영향을 줄 수 있습니다.

한국어 사용 환경을 고려한 지침이 포함됩니다

경어체 사용, 안전한 응답, 특정 관점에 대한 안내 등 한국어 사용자 환경을 고려한 지시사항이 시스템 프롬프트에 포함될 수 있습니다.

Llama 3 계열 토큰 형식을 사용합니다

<|start_header_id|>, <|eot_id|> 같은 토큰은 Llama 3 계열에서 볼 수 있는 형식입니다. 템플릿을 보면 모델이 어떤 계열의 구조를 따르는지 어느 정도 추정할 수 있습니다.

이처럼 Chat Template은 모델 입력 형식을 정리하는 도구이면서, 모델의 정체성·정책·응답 스타일을 시스템 수준에서 반영하는 장치로도 사용될 수 있습니다.

8. Chat Template과 보안

Chat Template은 보안 관점에서도 주의가 필요합니다. 특히 Jinja 템플릿은 조건문, 반복문, 필터를 사용할 수 있기 때문에 단순한 문자열 치환보다 표현력이 높습니다. 이 유연성은 강점이지만, 잘못 다루면 위험 요소가 될 수 있습니다.

Server-Side Template Injection

Server-Side Template Injection, 즉 SSTI는 검증되지 않은 입력이 템플릿 소스 자체로 해석될 때 발생할 수 있는 취약점입니다. 일반적인 사용처럼 사용자 메시지를 message.content 값으로 넘기는 것과, 사용자 입력을 템플릿 문자열 자체에 섞어 렌더링하는 것은 구분해야 합니다.

위험한 방식은 다음과 같습니다.

# 위험한 예시: 사용자 입력을 템플릿 소스에 직접 결합
template = "{{ '" + user_input + "' }}"

반대로 안전한 방향은 템플릿 구조를 고정하고, 사용자 입력은 데이터로만 전달하는 것입니다.

# 권장 방향: 템플릿 구조와 사용자 데이터를 분리
messages = [
    {"role": "user", "content": user_input}
]

prompt = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
)

핵심은 사용자의 입력이 템플릿 코드가 되지 않도록 분리하는 것입니다.

특수 토큰 주입

또 다른 위험은 사용자가 모델의 특수 토큰을 입력에 포함해 템플릿 구조를 깨뜨리려는 경우입니다. 예를 들어 사용자가 <|im_end|> 같은 토큰을 의도적으로 입력하면, 일부 모델에서는 메시지 경계가 흐려질 수 있습니다.

이런 공격은 모델이 사용하는 대화 형식을 역이용합니다. 사용자 메시지가 원래는 user 역할 안에만 있어야 하는데, 특수 토큰을 통해 모델이 이를 다른 역할이나 메시지 경계로 오해하도록 유도할 수 있습니다.

안전하게 다루기 위한 기준

Chat Template을 사용할 때는 다음 기준을 두는 것이 좋습니다.

  • 신뢰할 수 있는 모델과 토크나이저의 템플릿을 사용합니다.
  • 사용자 입력을 템플릿 소스 문자열에 직접 결합하지 않습니다.
  • 사용자 입력은 messagescontent 값으로 전달합니다.
  • 서비스 환경에서는 모델별 특수 토큰이 사용자 입력에 들어왔을 때의 동작을 테스트합니다.
  • 도구 호출 모델에서는 tool call과 tool response 경계가 깨지지 않는지 확인합니다.
  • 커스텀 템플릿을 만들 때는 학습 데이터 형식과 추론 입력 형식이 일치하는지 검증합니다.

Chat Template은 편의 기능이지만, 실제 서비스에서는 입력 경계와 역할 구분을 다루는 보안 표면이기도 합니다.


9. 나만의 Chat Template 만들기

Transformers에서는 기존 템플릿을 수정하거나 새로운 Chat Template을 설정할 수 있습니다. 가장 직접적인 방법은 tokenizer.chat_template에 Jinja 템플릿 문자열을 할당하는 것입니다.

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct")

# 시스템 프롬프트를 포함한 새 템플릿을 정의합니다
new_template = (
    "{% for message in messages %}"
        "{% if message['role'] == 'system' %}"
            "{{ bos_token + 'You are a helpful pirate assistant who speaks in a pirate accent.\\n' }}"
        "{% elif message['role'] == 'user' %}"
            "{{ '<|start_header_id|>user<|end_header_id|>\\n\\n' + message['content'] | trim + '<|eot_id|>' }}"
        "{% elif message['role'] == 'assistant' %}"
            "{{ '<|start_header_id|>assistant<|end_header_id|>\\n\\n' + message['content'] | trim + '<|eot_id|>' }}"
        "{% endif %}"
    "{% endfor %}"
    "{% if add_generation_prompt %}"
        "{{ '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}"
    "{% endif %}"
)

# 토크나이저의 Chat Template을 교체합니다
tokenizer.chat_template = new_template

chat = [
    {"role": "user", "content": "Who are you?"}
]

prompt = tokenizer.apply_chat_template(
    chat,
    tokenize=False,
    add_generation_prompt=True,
)

print(prompt)

이 방식은 특정 서비스에서 항상 같은 시스템 프롬프트를 넣고 싶을 때 유용할 수 있습니다. 매번 API 호출마다 긴 시스템 프롬프트를 반복해서 넘기지 않고, 템플릿 수준에서 기본 지침을 포함할 수 있기 때문입니다.

다만 커스텀 템플릿을 사용할 때는 두 가지를 확인해야 합니다.

첫째, 모델이 해당 형식을 이해할 수 있어야 합니다. 모델이 학습하지 않은 control token이나 메시지 구조를 넣으면 응답 품질이 떨어질 수 있습니다.

둘째, 학습과 추론의 형식이 맞아야 합니다. 특정 목적의 챗봇을 미세 조정할 때는 학습 데이터셋의 메시지 형식과 실제 추론 시 사용하는 Chat Template이 일관되어야 합니다.

커스텀 템플릿은 강력한 도구이지만, 모델의 학습 형식과 서비스 요구사항을 함께 고려해야 안정적으로 사용할 수 있습니다.

10. 체크리스트

Chat Template을 사용할 때는 아래 항목을 확인하면 좋습니다.

모델 사용 전

  • 모델 카드에 Chat Template 관련 설명이 있는지 확인합니다.
  • tokenizer.chat_template이 설정되어 있는지 확인합니다.
  • tokenize=False로 실제 렌더링된 프롬프트를 출력해 봅니다.
  • 모델이 기대하는 system, user, assistant, tool 역할을 확인합니다.
  • 도구 사용 모델이라면 tools, tool_calls, tool_response 형식을 확인합니다.

추론 코드 작성 시

  • 직접 문자열을 조합하기보다 apply_chat_template()을 우선 사용합니다.
  • model.generate()에 넘길 때는 add_generation_prompt=True를 검토합니다.
  • 이미 템플릿이 특수 토큰을 추가한다면, 별도로 특수 토큰을 중복 추가하지 않습니다.
  • 모델 교체 시 프롬프트 문자열이 아니라 모델 ID와 토크나이저 템플릿을 함께 확인합니다.

서비스 적용 시

  • 사용자 입력이 템플릿 소스가 되지 않도록 분리합니다.
  • 모델별 특수 토큰이 사용자 입력에 들어왔을 때의 동작을 테스트합니다.
  • 시스템 프롬프트를 템플릿에 내장할 경우, 서비스 정책 변경 시 배포 절차를 명확히 합니다.
  • 학습 데이터 형식과 추론 템플릿 형식이 일치하는지 확인합니다.

결론

Chat Template은 대화형 모델을 사용할 때 입력 형식을 맞추기 위한 핵심 구성요소입니다. 사용자는 rolecontent로 구성된 메시지를 넘기지만, 모델은 결국 특정 control token과 문자열 구조로 변환된 토큰 시퀀스를 입력으로 받습니다.

이 변환이 모델이 학습한 형식과 맞지 않으면, 모델 성능이 나빠 보이거나 지시사항을 제대로 따르지 못할 수 있습니다. 반대로 apply_chat_template()을 사용하면 모델별 입력 형식을 토크나이저가 처리해 주기 때문에, 모델 교체와 유지보수가 훨씬 쉬워집니다.

Chat Template은 대화 포맷을 맞추는 도구에 그치지 않습니다. 시스템 프롬프트, 도구 호출, 응답 생성 시작 지점, 모델 정체성, 보안 경계까지 함께 다루는 입력 설계의 핵심 레이어입니다.

대화형 언어 모델을 안정적으로 사용하려면 모델 자체만 볼 것이 아니라, 그 모델이 어떤 Chat Template을 기대하는지도 함께 확인해야 합니다.