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
| Co | Szczegóły |
|---|---|
| Definicja | szablon tekstowy, który zamienia listę wiadomości (system, user, assistant) w ciąg tokenów dla modelu |
| Dlaczego ważny | modele czatu uczyły się na konkretnych tokenach kontrolnych; złe tokeny to wyraźnie gorsza jakość odpowiedzi wg dokumentacji Hugging Face |
| Składnie | Hugging Face: Jinja, metoda apply_chat_template; Ollama: składnia Go w sekcji TEMPLATE Modelfile |
| Gdzie siedzi | w plikach GGUF pod metadaną tokenizer.chat_template; u HF w tokenizatorze modelu |
| Podgląd w Ollamie | ollama 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 |
| Koszt | zero, 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.
- Najważniejsze informacje
- Model nie widzi rozmowy, tylko strumień tokenów
- Trzy role: system, user i assistant
- Test na żywo: ta sama rozmowa, dwa różne wyniki
- Jinja u Hugging Face, Go w Ollamie
- Szablon jedzie razem z modelem
- Kiedy szablon jest zły, model daje to znać
- Ściąga: gdzie szukać szablonu
- Co z tego dla kogoś pracującego po polsku
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
| Gdzie | Jak podejrzeć |
|---|---|
| Ollama | ollama show --modelfile wypisuje sekcję TEMPLATE modelu |
| Plik GGUF | metadana tokenizer.chat_template, widoczna w podglądarce metadanych na Hubie |
| Hugging Face | metoda apply_chat_template na tokenizatorze modelu |
| Modelfile własny | sekcja 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
