채팅 템플릿 작성하기
채팅 템플릿 작성하기 (Writing a chat template)
채팅 템플릿은 tokenizer의 ~PreTrainedTokenizer.chat_template 속성에 저장된 Jinja 템플릿이에요. Jinja는 Python 같은 코드와 구문을 작성할 수 있게 해주는 템플릿 언어예요.
출처: 문서
본문
{%- for message in messages %}
{{- '<|' + message['role'] + '|>\n' }}
{{- message['content'] + eos_token }}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|assistant|>\n' }}
{%- endif %}
이걸 한동안 들여다보면, 이상한 {%- 구문이 있긴 하지만 사실 Python과 아주 비슷하다는 걸 알아차릴 거예요. 이 템플릿은 메시지 리스트를 순회하며, 각 메시지에 대해 역할(role)과 내용(content)을 출력하고 끝에 end-of-sequence 토큰을 붙여요. add_generation_prompt=True면 대화 끝에 assistant 메시지의 시작 헤더를 추가해요.
작성한 템플릿을 문자열로 로드해서 tokenizer의 chat_template 속성에 할당해요. 설정하면 apply_chat_template()을 호출할 때마다 그 템플릿이 사용돼요. save_pretrained()이나 push_to_hub()가 호출되면 tokenizer와 함께 저장되기도 해요. 템플릿은 tokenizer 디렉터리의 chat_template.jinja 파일에 저장돼요. 템플릿을 바꾸려면 이 파일을 직접 편집할 수 있는데, 템플릿 문자열을 조작하는 것보다 종종 더 쉬워요. Transformers가 지원하는 다른 온디스크 형태는 아래 채팅 템플릿 저장·로드를 참고해 주세요.
템플릿 작성 팁 (Template writing tips)
Jinja 템플릿 작성의 가장 쉬운 시작 방법은 기존 템플릿을 참고하는 거예요. 어떤 채팅 모델이든 print(tokenizer.chat_template)으로 그 모델이 사용하는 템플릿을 볼 수 있어요. 도구를 호출하지 않고 RAG도 지원하지 않는 단순한 모델부터 시작해 보세요. tool-use 모델은 매우 복잡한 템플릿을 가질 수 있으니까요. 마지막으로 Jinja 문서에서 형식과 구문에 대한 더 자세한 내용을 확인해 보세요.
특히 채팅 템플릿을 작성할 때 마주칠 수 있는 몇 가지 팁과 함정이 있는데, 이 섹션에서 그중 일부를 자세히 다룰게요.
멀티모달 채팅 템플릿 작성하기
멀티모달 템플릿에서는 chat_template 속성이 tokenizer가 아니라 processor에 설정돼요. 메시지의 content 키는 단일 문자열이 아니라 콘텐츠 dict들의 리스트인 경우가 많아요. 리스트의 각 콘텐츠 항목 타입을 확인하고 그에 맞게 처리하고 싶을 거예요.
일반적으로 템플릿이 이미지나 비디오 데이터에 직접 접근하면 안 돼요. 이는 보통 템플릿 렌더링이 끝난 후 processor가 처리해요. 대신 템플릿은 이미지나 비디오 콘텐츠를 만났을 때 <|image|>나 <|video|> 같은 단일 특수 토큰을 내보내야 해요. processor가 나중에 그 단일 특수 토큰을 이미지·비디오 토큰 시퀀스로 확장해요. 내보낼 정확한 토큰은 여러분이 다루는 모델에 따라 달라요. 데이터를 어떻게 처리하는지 보려면 기존 멀티모달 processor를 로드해 보는 것을 강력히 권장해요.
아래 예시 템플릿은 혼합된 이미지와 텍스트 콘텐츠를 처리해요.
{%- for message in messages %}
{%- if loop.index0 == 0 %}
{{- bos_token }}
{%- endif %}
{{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }}
{%- if message['content'] is string %}
{{- message['content'] }}
{%- else %}
{%- for content in message['content'] %}
{%- if content['type'] == 'image' %}
{{- '<|image|>' }}
{%- elif content['type'] == 'text' %}
{{- content['text'] }}
{%- endif %}
{%- endfor %}
{%- endif %}
{{- '<|eot_id|>' }}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{%- endif %}
이 멀티모달 템플릿은 위의 더 단순한 템플릿과 매우 비슷하지만, content 리스트를 확인하고 그것들을 순회해서 필요한 곳에 <|image|> 토큰을 렌더링해요. 이렇게 하면 이미지를 사용자 텍스트의 "흐름 안에" 삽입할 수 있어요.
모든 모델이 이렇게 동작하는 건 아니에요. 어떤 모델은 모든 이미지를 사용자 메시지 끝으로 옮길 수도 있어요. 채팅 템플릿은 항상 모델이 학습된 형식과 일치해야 해요.
공백 다듬기 (Trimming whitespace)
Jinja는 텍스트 블록 앞이나 뒤의 공백을 그대로 출력해요. 이는 채팅 템플릿에서 문제가 될 수 있어요. 모델 학습 때 없었던 추가 공백을 넣으면 성능이 떨어질 수 있으니까요. 공백을 제거하려면 Jinja 줄 구문에 -을 추가해요. 이렇게 하면 렌더링된 출력에 들여쓰기가 실수로 출력되지 않으면서 Python 스타일의 들여쓰기와 줄바꿈으로 템플릿을 작성할 수 있어요.
아래 예시 템플릿은 -을 사용하지 않아서 출력에 추가 공백이 생겨요.
{% for message in messages %}
{{ message['role'] + message['content'] }}
{% endfor %}
의도한 내용만 출력되도록 -을 사용하는 것을 강력히 권장해요.
{%- for message in messages %}
{{- message['role'] + message['content'] }}
{%- endfor %}
특수 변수와 호출 가능 함수 (Special variables and callables)
템플릿의 유일한 상수는 messages 변수와 add_generation_prompt boolean이에요. 하지만 apply_chat_template() 메서드에 전달되는 다른 모든 키워드 인자에 접근할 수 있어요.
이것은 유연성을 제공하고 스펙을 설계할 때 생각하지 못했을 수 있는 사용 사례를 지원하게 해줘요. 가장 흔한 추가 변수는 tools로, JSON schema 형식의 도구 리스트를 담고 있어요. 원하는 변수 이름을 써도 되지만, 관례를 따르고 이 용도로 tools를 사용하는 것을 강력히 권장해요. 그러면 템플릿이 표준 API와 더 호환돼요.
tokenizer.special_tokens_map에 담긴 어떤 토큰에도 접근할 수 있어요. 여기에는 bos_token과 eos_token 같은 특수 토큰이 자주 포함돼요. {{- bos_token }}처럼 이름으로 직접 접근하면 돼요.
사용 가능한 호출 가능 함수가 두 가지 있어요. 호출하려면 {{- function_name(argument) }}을 사용해요.
raise_exception(msg)는TemplateException을 발생시켜요. 디버깅이나 잘못된 템플릿 사용을 사용자에게 경고할 때 유용해요.strftime_now(format_str)는 특정 형식의 현재 날짜·시간을 가져와요. 시스템 메시지에서 자주 필요하지요. Python의 datetime.now().strftime(format_str)과 동등해요.
비-Python Jinja와의 호환성
Jinja는 여러 언어로 구현되며 일반적으로 같은 구문을 가져요. Python으로 템플릿을 작성하면 문자열에 lower 같은 Python 메서드나 dict에 items 같은 메서드를 사용할 수 있어요. 하지만 템플릿이 비-Python 구현, 예를 들어 JavaScript나 Rust로 배포될 때는 이게 동작하지 않아요.
모든 Jinja 구현에서 호환성을 보장하려면 아래 변경을 하세요.
- Python 메서드를 Jinja 필터로 바꿔요. 예를 들어
string.lower()을string|lower로,dict.items()을dict|dictitems로 바꿔요. 대부분의 변경은 같은 패턴을 따르지만,string.strip()은string|trim으로 바뀌는 것만 제외해요. 필터 전체 목록은 built-in filters 목록을 참고해 주세요. True,False,None(이것들은 Python 전용이에요)을 각각true,false,none으로 바꿔요.- dict나 list를 직접 렌더링하면 다른 구현에서 결과가 달라질 수 있어요. 예를 들어 문자열 항목이 작은따옴표에서 큰따옴표로 바뀔 수 있지요. 이를 피하려면 일관성을 유지하기 위해 tojson 필터를 추가해요.
큰 템플릿 (Big templates)
최신 모델이나 도구 호출, RAG 같은 기능을 가진 모델은 100줄이 넘는 더 큰 템플릿이 필요해요. 더 큰 템플릿은 별도 파일로 작성하는 게 더 쉬울 수 있어요. 별도 파일의 줄 번호는 템플릿 파싱·실행 오류의 줄 번호와 정확히 일치해서, 잠재적 문제를 디버깅하기 쉬워요.
템플릿을 별도 파일로 작성하고 채팅 템플릿으로 추출해요.
open("template.jinja", "w").write(tokenizer.chat_template)
편집한 템플릿을 다시 tokenizer로 로드할 수도 있어요.
tokenizer.chat_template = open("template.jinja").read()
채팅 템플릿 저장·로드 (Storing and loading chat templates)
채팅 템플릿은 여러 다른 형식으로 디스크에 저장돼요. 최신 checkpoint는 템플릿을 독립형 .jinja 파일로 저장하지만, 오래된 checkpoint는 tokenizer나 processor config에 임베드해요.
저장 형식 (Storage formats)
템플릿은 다음 형식 중 하나로 저장될 수 있어요.
chat_template.jinja(권장). 저장소 루트의 독립형 Jinja 파일로, 단일 채팅 템플릿을 담고 있어요. 이것이 save_pretrained()이 기본으로 쓰는 형식이에요. 템플릿을 별도 파일에 저장하면 검사, 편집, diff가 쉬워져요. tokenizer와 processor 둘 다chat_template.jinja를 같은 방식으로 로드해요.additional_chat_templates/<name>.jinja. 모델이 여러 이름 붙은 템플릿을 제공할 때 사용되는 독립형 Jinja 파일 디렉터리(예:default템플릿과 별도의tool_use템플릿).default템플릿은 여전히 저장소 루트의chat_template.jinja에 들어가지만, 그 외의 모든 이름 붙은 템플릿은additional_chat_templates/<name>.jinja에 들어가요. 여기서 파일 이름 stem이 템플릿 이름이에요.
[!WARNING] 아래 레거시 형식은 역호환 로딩용으로만 유지돼요. 두 형식 중 하나에 채팅 템플릿을 쓰지 마세요.
-
tokenizer_config.json의chat_template필드. 독립형.jinja파일 이전에 사용된 로드 전용 레거시 형식이에요. 템플릿은tokenizer_config.json안에 JSON 문자열로 임베드돼요. 모델에 여러 이름 붙은 템플릿이 있으면 필드는 단일 문자열 대신{"name": ..., "template": ...}dict 리스트예요. 이 형식을 사용하는 기존 저장소는 계속 로드되지만, save_pretrained()은 대신 현대적인.jinja형식을 작성해요. -
chat_template.json. 오래된 멀티모달 processor checkpoint가 사용하는 로드 전용 레거시 형식이에요.{"chat_template": "<template string>"}형태의 JSON 파일이에요. 이 형식을 사용하는 기존 저장소는 계속 로드되지만, save_pretrained()은 대신 현대적인.jinja형식을 작성해요. 레거시chat_template.json과 현대적인.jinja파일을 섞은 processor 저장소는 로드 시 오류를 발생시켜요.
로드 우선순위 (Loading precedence)
from_pretrained()을 호출하면 Transformers는 고정된 우선순위로 저장 형식을 해석해요. 독립형 .jinja 파일이 config에 임베드된 템플릿보다 우선해요. 로더는:
tokenizer_config.json(processor라면 레거시chat_template.json)에 있는chat_template필드를 읽어요.- 저장소 루트에
chat_template.jinja가 있으면 읽고default템플릿으로 사용해 1단계를 오버라이드해요. additional_chat_templates/의 모든.jinja파일을 파일 이름 stem을 키로 읽고 병합해요.
결과에 default 템플릿이 하나만 있으면 ~PreTrainedTokenizer.chat_template이 그 문자열로 설정돼요. 이름 붙은 템플릿이 여러 개면 chat_template은 {name: template_string} dict가 돼요. 그 경우 apply_chat_template()은 도구가 전달되면 tool_use 항목을, 그렇지 않으면 default를 선택해요.
저장 (Saving)
save_pretrained()과 push_to_hub()은 기본적으로 .jinja 형식을 작성해요. 단일 문자열 템플릿은 chat_template.jinja가 돼요. 이름 붙은 템플릿 dict는 default 항목을 chat_template.jinja에 쓰고 나머지 각 항목은 additional_chat_templates/ 아래에 파일 하나씩 써요. 중복을 피하기 위해 chat_template 필드는 tokenizer_config.json에서 제거돼요.
템플릿을 레거시 형식 중 하나로 저장하는 지원되는 방법은 없어요. 레거시 형식은 오래된 저장소를 로드하기 위해서만 유지돼요.
오래된 저장소 업데이트하기 (Updating an older repository)
tokenizer_config.json이나 chat_template.json에 임베드된 템플릿을 권장 .jinja 형식으로 마이그레이션하려면 다시 로드하고 저장하면 돼요.
로드 단계가 저장소가 사용하는 레거시 형식을 chat_template에서 정규화하고, ~PushToHubMixin.push_to_hub이 chat_template.jinja 파일을 반환해요.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("your-org/your-model")
tokenizer.push_to_hub("your-org/your-model")
도구용 템플릿 (Templates for tools)
도구용 템플릿을 작성하는 특정 형식은 없지만 표준 API를 따르는 게 가장 좋아요. 그러면 사용자가 여러분 모델에서 도구를 쓰기 위해 커스텀 코드를 작성할 필요 없이, 템플릿이 여러 모델에서 널리 접근 가능해져요.
[!WARNING] 공백과 특수 토큰 같은 형식은 모델별로 달라요. 모든 것이 모델이 학습된 형식과 정확히 일치하는지 확인하세요.
다음 섹션은 도구용 템플릿 작성의 표준 API 요소를 나열해요.
도구 정의 (Tool definitions)
도구는 Python 함수나 JSON schema로 전달돼요. 함수가 전달되면 JSON schema가 자동 생성되어 템플릿에 전달돼요. 템플릿이 tools 변수에 접근할 때는 항상 JSON schema 리스트예요.
템플릿이 항상 도구를 JSON schema로 받긴 하지만, 이를 모델이 학습된 형식과 일치하도록 렌더링할 때는 형식을 크게 바꿔야 할 수도 있어요. 예를 들어 Command-R은 Python 함수 헤더로 정의된 도구로 학습됐어요. 템플릿은 내부적으로 JSON schema 타입을 변환하고 입력 도구를 Python 헤더로 렌더링해요.
아래 예시는 JSON 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"]
}
}
}
채팅 템플릿에서 도구 정의를 처리하는 예시는 아래와 같아요. 특정 토큰과 레이아웃은 모델이 학습된 것과 일치하도록 바꿔야 해요.
{%- if tools %}
{%- for tool in tools %}
{{- '<tool>' + tool['function']['name'] + '\n' }}
{%- for argument in tool['function']['parameters']['properties'] %}
{{- argument + ': ' + tool['function']['parameters']['properties'][argument]['description'] + '\n' }}
{%- endfor %}
{{- '\n</tool>' }}
{%- endfor %}
{%- endif %}
도구 호출 (Tool calls)
도구 정의를 렌더링하는 것 외에도 템플릿에서 **도구 호출(tool calls)**과 **도구 응답(tool responses)**을 렌더링해야 해요.
도구 호출은 일반적으로 "assistant" 메시지의 tool_calls 키로 전달돼요. 대부분의 도구 호출 모델이 단일 도구 호출만 지원하지만 이는 항상 리스트예요. 즉 리스트에 보통 한 요소만 들어 있어요.
{
"role": "assistant",
"tool_calls": [
{
"type": "function",
"function": {
"name": "multiply",
"arguments": {
"a": 5,
"b": 6
}
}
}
]
}
도구 호출을 처리하는 흔한 패턴은 아래와 같아요. 출발점으로 사용해도 되지만, 템플릿이 실제로 모델이 학습된 형식과 일치하는지 반드시 확인하세요!
{%- if message['role'] == 'assistant' and 'tool_calls' in message %}
{%- for tool_call in message['tool_calls'] %}
{{- '<tool_call>' + tool_call['function']['name'] + '\n' + tool_call['function']['arguments']|tojson + '\n</tool_call>' }}
{%- endfor %}
{%- endif %}
도구 응답 (Tool responses)
도구 응답은 tool 역할을 가진 메시지 dict예요. 도구 호출보다 훨씬 단순하고, 보통 role, name, content 키만 담고 있어요.
{
"role": "tool",
"name": "multiply",
"content": "30"
}
일부 템플릿은 name 키조차 필요 없을 수 있어요. 그런 경우 content 키만 읽도록 템플릿을 작성할 수 있어요.
{%- if message['role'] == 'tool' %}
{{- "<tool_result>" + message['content'] + "</tool_result>" }}
{%- endif %}
기여하기 (Contribute)
템플릿이 준비되면 tokenizer의 chat_template 속성에 설정하고 apply_chat_template()로 테스트해요. 기대대로 동작한다면 push_to_hub()으로 Hub에 업로드해요.
모델 소유자가 아니더라도, 채팅 템플릿이 비어 있거나 잘못된 모델에 템플릿을 추가하는 것은 여전히 도움이 돼요. 모델 저장소에 pull request를 열어 템플릿을 추가해 주세요!
tokenizer.chat_template = template
tokenizer.push_to_hub("amazing_company/cool_model", commit_message="Add chat template", create_pr=True)