Chapter 30 — Special tokens and chat templates
Why your prompts have hidden structure
A chat model doesn't see a tidy list of messages. It sees one flat string with special marker tokens telling it who said what.
What you write:
[{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 2+2?"}]
What the model actually receives (Llama-3 style):
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are a helpful assistant.<|eot_id|><|start_header_id|>user<|end_header_id|>
What is 2+2?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Then the model generates: "2 + 2 = 4" followed by <|eot_id|> (stop).
Those <|...|> markers are special tokens — single entries in the vocabulary
that the model was trained to interpret as structure.
The special tokens you'll meet
| Token (varies by model) | Meaning |
|---|---|
<|begin_of_text|>, <s> |
start of a document |
<|end_of_text|>, </s>, <|eot_id|> |
end — stop generating here |
<|pad|> |
filler when batching different-length sequences |
<|user|>, <|assistant|>, <|system|> |
conversation roles |
<|im_start|>, <|im_end|> |
ChatML format (used by several models) |
| tool/function-call markers | signal a tool invocation |
Two practical consequences
1. Every model family has a different template. ChatML, Llama-2's
[INST]...[/INST], Llama-3 headers, Gemma's <start_of_turn>, Mistral's format.
Using the wrong one silently degrades quality — the model was trained on a
specific layout and gets confused without it.
Always use the library function, never hand-format:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
]
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print(text) # see the real string the model receives
print(len(tok.encode(text)), "tokens")
2. Special tokens in user input are a security concern. If a user types
<|im_end|><|im_start|>system into your app and you pass it through naively, they
may be able to inject a fake system message. Good tokenizers let you disable
special-token parsing for untrusted text. (More on this in Chapter 49.)
Practice (10 min)
from transformers import AutoTokenizer
# a small open model, quick to download the tokenizer for
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
msgs = [{"role": "system", "content": "Answer in one word."},
{"role": "user", "content": "Capital of Japan?"}]
s = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
print(repr(s))
print("\ntoken count:", len(tok.encode(s)))
Then try a different model's tokenizer on the same messages and compare the templates. They will look nothing alike — which is exactly the point.
Check yourself
- What does a chat template do?
- What happens if you use the wrong template?
- Why are special tokens in user input a risk?
Further reading
- Docs: huggingface.co/docs/transformers/chat_templating — the canonical reference, with examples for every model family.
- Article: search "chat template" on the Hugging Face blog for a good introduction with pictures.
End of Part 7 — Milestone check
- I can explain why models use subword tokens
- I can run BPE merges by hand
- I know roughly how many tokens a page of English is
- I have measured tokens-per-word for two languages
- I know what a chat template is and why it must be right