Przejdź do treści

Co to jest chat template? Ten sam model, dwa narzędzia, inne odpowiedzi

Okładka artykułu o chat template: ta sama rozmowa sklejona w dwa różne ciągi tokenów przez Mistral i Zephyr, szablon w Ollamie i GGUF

Ten sam plik modelu, ta sama wiadomość, a w jednym programie odpowiedź zwięzła i po polsku, w drugim bełkot albo ciąg dalszy pytania zamiast odpowiedzi: to nie losowość, tylko szablon rozmowy. Chat template decyduje o tym, jak lista wiadomości system, user i assistant zamienia się w jeden ciąg tokenów, który model faktycznie czyta. Poniżej tłumaczę to od zera, z testem na żywo: przepuściłem tę samą polską rozmowę przez dwa tokenizery modeli, które wyrastają z tej samej bazy, i różnica widać gołym okiem. Na końcu pokazuję, gdzie ten szablon siedzi w Ollamie i w plikach GGUF oraz po czym poznać, że jest zły.

Najważniejsze informacje

CoSzczegóły
Definicjaszablon tekstowy, który zamienia listę wiadomości (system, user, assistant) w ciąg tokenów dla modelu
Dlaczego ważnymodele czatu uczyły się na konkretnych tokenach kontrolnych; złe tokeny to wyraźnie gorsza jakość odpowiedzi wg dokumentacji Hugging Face
SkładnieHugging Face: Jinja, metoda apply_chat_template; Ollama: składnia Go w sekcji TEMPLATE Modelfile
Gdzie siedziw plikach GGUF pod metadaną tokenizer.chat_template; u HF w tokenizatorze modelu
Podgląd w Ollamieollama show --modelfile pokazuje sekcję TEMPLATE danego modelu
Mój test (26.10)ta sama rozmowa po polsku: Mistral-7B-Instruct skleja system z pytaniem w [INST], Zephyr-7b-beta robi osobne bloki system i user
Kosztzero, to pole tekstowe w konfiguracji, nie dodatkowy komponent

Model nie widzi rozmowy, tylko strumień tokenów

Dokumentacja Hugging Face zaczyna ten temat od zdania, które warto zapamiętać w wersji angielskiej: „All causal LMs, whether chat-trained or not, continue a sequence of tokens”. Model językowy zawsze dopisuje tekst do tekstu, nie prowadzi żadnej wewnętrznej listy wiadomości. Dlatego model bazowy, o którym pisałem szerzej w tekście o instruct i base, kontynuuje twoje pytanie, zamiast na nie odpowiadać: dostaje strumień i dokleja kolejny fragment.

Model czatu przeszedł dostrajanie na danych zapisanych jako sekwencja wiadomości, więc nauczył się rozpoznawać znaczniki, które tę sekwencję wyróżniają. Dokumentacja HF wymienia po imieniu tokeny kontrolne typu <|user|>, <|assistant|> albo <|end_of_message|>, a zaraz za tym ostrzega: „There are many possible chat formats, and different models may use different formats or control tokens, even if they were fine-tuned from the same base model”. Każda rodzina ma własny dialekt. Chat template to właśnie ten dialekt zapisany w formie szablonu.

Trzy role: system, user i assistant

Wiadomości w rozmowie mają przypisaną rolę. system to dyrektywa, jak model ma się zachowywać: styl, język, zakazy, zwykle umieszczana na początku. user to wpisy człowieka, assistant to odpowiedzi modelu, również te z wcześniejszych tur, bo przy każdym zapytaniu narzędzie ponownie skleja całą historię i wysyła ją od zera. Model nie pamięta poprzedniej tury w sposób techniczny, pamięta ją, bo widzi ją w strumieniu.

Drugi subtelny element to zachęta do odpowiedzi. Szablon może dokleić na końcu znacznik otwierający wypowiedź asystenta i tu dokumentacja HF podaje konsekwencję wprost: jeśli tych tokenów zabraknie, „the model may get confused and do something strange, like **continuing** the user’s message instead of replying to it”. Stąd klasyczny objaw modelu lokalnego, który zamiast odpowiedzi dopisuje dalszy ciąg pytania albo listę wariantów twojego własnego zdania. To nie model się zepsuł, to szablon nie domknął rozmowy.

Dokumentacja pokazuje to na małym dialogu. Te same wiadomości zapisane bez zachęty kończą się zamkniętym pytaniem użytkownika, a po włączeniu parametru add_generation_prompt dokładają na końcu nagłówek asystenta z pustym miejscem na odpowiedź:

bez zachęty:
<|im_start|>user
Can I ask a question?<|im_end|>

z zachętą (add_generation_prompt=True):
<|im_start|>user
Can I ask a question?<|im_end|>
<|im_start|>assistant

Cała różnica to jeden wiersz na końcu strumienia, tylko że model czyta go jako sygnał: teraz twoja kolej.

Test na żywo: ta sama rozmowa, dwa różne wyniki

26 października odpaliłem bibliotekę transformers 5.20.2 i metodę apply_chat_template na dwóch tokenizatorach: mistralai/Mistral-7B-Instruct-v0.1 oraz HuggingFaceH4/zephyr-7b-beta. Oba, jak podaje dokumentacja HF, wyrastają z tej samej bazy Mistral-7B, więc różnica pochodzi wyłącznie z dostrajania i szablonu. Wiadomości były dwie: system „Odpowiadasz zawsze po polsku.” i user „Wymień trzy kolory.”, z włączoną zachętą do odpowiedzi.

Mistral-7B-Instruct:
<s> [INST] Odpowiadasz zawsze po polsku.

Wymień trzy kolory. [/INST]

Zephyr-7b-beta:
<|system|>
Odpowiadasz zawsze po polsku.</s>
<|user|>
Wymień trzy kolory.</s>
<|assistant|>

To jest dokładnie ten sam dialog. Mistral w ogóle nie ma bloku na wiadomość systemową, więc szablon skleja ją z pytaniem użytkownika i rozdziela pustym wierszem, całość zamknięta w [INST]. Zephyr rozdziela role pełnymi blokami z własnymi znacznikami i zamyka każdą wiadomość tokenem </s>. Żaden z zapisów nie jest lepszy: każdy model uczył się swoich znaczników i tylko ich rozumie. Gdyby narzędzie wysłało Mistralowi bloki Zephyra, odpowiedź spadłaby jakościowo w sposób, który nawet nie wymaga benchmarka, żeby go zauważyć.

Doprecyzuję zakres: test był tokenizerowy, sprawdziłem, jak rozmowa zostaje zapisana w tokenach, bez odpalania samych wag, więc oceniam zapis, a nie płynność odpowiedzi. O jakości przy złych znacznikach mówi dokumentacja HF, którą cytuję niżej.

Jinja u Hugging Face, Go w Ollamie

Ten sam szablon ma różne implementacje, zależnie od narzędzia. W ekosystemie Hugging Face to szablon Jinja przypięty do tokenizatora, wywoływany metodą apply_chat_template; to właśnie tą ścieżką poszedł mój test powyżej. Frazy typu „jinja chat template” czy „chat template huggingface” trafiają więc w to samo miejsce: w pole tekstowe w konfiguracji tokenizatora, nie w kod biblioteki.

Ollama trzyma szablon w Modelfile, w sekcji TEMPLATE, i używa składni Go templates, z trzema podstawowymi zmiennymi: .System, .Prompt i .Response. Dokumentacja zastrzega, że „syntax may be model specific”, czyli szablon odwzorowuje dialekt konkretnego modelu. Przykład z dokumentacji wygląda tak:

TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}{{ if .Prompt }}<|im_start|>user
{{ .Prompt }}<|im_end|>
{{ end }}<|im_start|>assistant
{{ .Response }}<|im_end|>"""

Zdasz sobie sprawę, że to ten sam zapis <|im_start|>, który pojawił się u Zephyra, tylko dopisany ręcznie w warunkach Go. Zmianę systemówki robi się przy tym bez ruszania szablonu: w tym samym Modelfile siedzi instrukcja SYSTEM (w przykładzie z dokumentacji: „You are Mario from super mario bros, acting as an assistant”), a własny wariant tworzy się poleceniem ollama create nazwa -f ./Modelfile i odpala przez ollama run nazwa. Szablon zostaje wtedy dokładnie taki, jak zaprojektował go autor modelu, zmienia się tylko to, co wpina się w zmienną .System.

Miałem przy okazji sprawdzić współczesny przypadek z myśleniem: na tokenizatorze Qwen/Qwen3-0.6B ten sam dialog zwrócił domyślnie sekwencję kończącą się na <|im_start|>assistant, a po ustawieniu enable_thinking=False szablon dokleił jeszcze pusty blok <think> zaraz za nagłówkiem asystenta. To ciekawy mostek do poprzedniego wpisu o instruct i thinking: tryb myślenia ma swoje odbicie już w szablonie, zanim model wypowie pierwsze słowo.

Szablon jedzie razem z modelem

W formacie GGUF, czyli w tym, co ściąga Ollama i LM Studio, szablon nie jest osobnym plikiem, tylko metadaną modelu. Hugging Face opisuje GGUF jako format, który „encodes both the tensors and a standardized set of metadata”, a w kodzie llama.cpp ten zestaw ma nazwę wprost: stała CHAT_TEMPLATE przyjmuje wartość tokenizer.chat_template (plik constants.py, odczyt 26 października). Są też warianty tokenizer.chat_template.{name}, więc jeden plik może wozić kilka szablonów naraz.

Z tego wynikają dwie praktyczne rzeczy. Po pierwsze, konwersja modelu do GGUF, na przykład narzędziem gguf-my-repo, przenosi szablon razem z wagami, więc dobrze skonwertowany plik odpowiada tak samo w każdym narzędziu, które go czyta. Po drugie, jeśli ktoś wrzucił zły szablon albo w ogóle go nie skonwertował, obie niedogodności widzisz samodzielnie: podglądarka metadanych GGUF na Hubie pokazuje metadane każdego pliku, a w Ollamie pełny szablon danego modelu wypisuje ollama show --modelfile. Samo polecenie cytuję za dokumentacją, bo nie trzymam Ollamy na maszynie roboczej; testy tokenizatorów powyżej zrobiłem lokalnie.

Kiedy szablon jest zły, model daje to znać

Trzy objawy z dokumentacji, nie z plotek. Pierwszy: zły dialekt znaczników, czyli „with the wrong control tokens, these models would have drastically worse performance”, więc odpowiedzi robią się chaotyczne albo przerywają się w pół zdania. Drugi: brak zachęty do odpowiedzi, o którym pisałem wyżej, model kontynuuje twoje pytanie zamiast odpowiadać. Trzeci: zdublowane tokeny specjalne, dokumentacja HF ostrzega, że szablony zawierają komplet tokenów początku i końca, a dokładanie kolejnych „is often incorrect or duplicated, hurting model performance”.

Kolejność naprawy, która chroni przed częstymi wpadkami: najpierw obejrzyj szablon, który naprawdę dostaje model, w Ollamie przez ollama show --modelfile, dla pliku GGUF w podglądarce metadanych na Hubie. Potem porównaj go z kartą modelu u wydawcy i na końcu sprawdź, czy twoje narzędzie nie dokłada własnych tokenów na wierzch tego, co przychodzi z pliku. W większości przypadków problem znika po powrocie do niezmodyfikowanego modelu, bo to szablon z karty był jedynym zapisem, na którym model widział rozmowy podczas dostrajania. Własne modyfikacje zostawiam na moment, gdy budujesz własnego asystenta w Modelfile i świadomie zmieniasz systemówkę; wtedy ruszasz tylko warstwę .System, nie strukturę znaczników.

Ściąga: gdzie szukać szablonu

GdzieJak podejrzeć
Ollamaollama show --modelfile wypisuje sekcję TEMPLATE modelu
Plik GGUFmetadana tokenizer.chat_template, widoczna w podglądarce metadanych na Hubie
Hugging Facemetoda apply_chat_template na tokenizatorze modelu
Modelfile własnysekcja TEMPLATE ze zmiennymi .System, .Prompt, .Response

Co z tego dla kogoś pracującego po polsku

Po pierwsze, spokój: systemówka po polsku to zwykły tekst, który szablon wstawia między znaczniki w takiej formie, w jakiej ją wpiszesz. W moim teście polskie zdanie przeszło przez oba tokenizatory bez żadnych przekształceń, różniło się tylko otoczenie. Szablon nie tłumaczy i nie poprawia treści, on ją pakuje. Po drugie, nazwy nie przetłumaczysz: podpowiedzi Google PL nie zna frazy „czat szablon”, przy odczycie 26 października lista była pusta, więc szukaj po angielsku, najlepiej z nazwą narzędzia, „chat template ollama” daje żywe wyniki.

Trzeci punkt to wybór narzędzia i modelu, który robisz raz, a męczysz potem miesiącami: ścieżki opisane wyżej prowadzą przez Ollamę po polsku, a dobór wariantu modelu do sprzętu rozkłada na czynniki pierwsze poradnik o wyborze modelu w Ollamie. Szeroki kontekst lokalnych modeli, od sprzętu po kwantyzację, trzyma hub Lokalne AI.

Źródła

  • Hugging Face, dokumentacja „Chat templates” (cytaty o tokenach kontrolnych, rolach, add_generation_prompt i zdublowanych tokenach specjalnych; przykłady Mistral-7B-Instruct i Zephyr-7b-beta), odczyt 26.10.2026: https://huggingface.co/docs/transformers/chat_templating
  • Ollama, dokumentacja Modelfile, sekcja TEMPLATE (składnia Go templates, zmienne .System/.Prompt/.Response, przykład z im_start, ollama show --modelfile), odczyt 26.10.2026: https://docs.ollama.com/modelfile
  • llama.cpp, plik gguf-py/gguf/constants.py (stała CHAT_TEMPLATE = „tokenizer.chat_template”, warianty z sufiksem .{name}), odczyt 26.10.2026: https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/gguf/constants.py
  • Hugging Face, dokumentacja „GGUF” (format zapisuje tensory i standaryzowany zestaw metadanych; podglądarka metadanych na Hubie; narzędzie gguf-my-repo), odczyt 26.10.2026: https://huggingface.co/docs/hub/gguf
  • Test własny (26.10.2026, transformers 5.20.2, tokenizer-only): apply_chat_template dla mistralai/Mistral-7B-Instruct-v0.1, HuggingFaceH4/zephyr-7b-beta i Qwen/Qwen3-0.6B na rozmowie system „Odpowiadasz zawsze po polsku.” / user „Wymień trzy kolory.”; wyniki verbatim w treści

Powiązane artykuły