Prompting That Works Locally: System Prompts, Chat Templates and Thinking Modes
A model that answers badly is usually being asked badly, and on a local machine “badly” often means “in a format it was never trained on”. By the end of this lesson you will be able to say exactly what reaches the model when you send a chat message, spot the three formatting faults that make a competent model look incompetent, switch a model’s reasoning mode deliberately in both directions, and set the sampling parameters its own publisher recommends instead of the ones your engine happened to default to.
Your messages are not what the model receives
Section titled “Your messages are not what the model receives”The Transformers documentation puts the mechanism plainly: “All causal LMs, whether
chat-trained or not, continue a sequence of tokens.” There is no messages array inside
the model. Somewhere between your API call and the forward pass, a list of role-and-content
dictionaries is flattened into one string of tokens, and the model continues it.
The flattening is done by the model’s chat template, a small program shipped with the
tokeniser. Different families flatten differently. Mistral-7B-Instruct wraps user turns in
[INST] and [/INST]; Zephyr uses <|user|> and <|assistant|>; the Qwen3 family uses
the ChatML tokens <|im_start|> and <|im_end|>. The documentation’s warning about mixing
them up is not subtle: “with the wrong control tokens, these models would have drastically
worse performance.”
What happens to a chat message on the way to the weights
- Your requestA list of {role, content} objects, plus sampling parameters.you write this
- Chat templateThe Jinja program in the model's tokeniser config, which turns the list into one string with the family's control tokens.ships with the model
- Generation promptThe tokens that say "an assistant turn starts here". Without them the model may continue your message instead of replying to it.
- TokeniserThe string becomes token ids. Special tokens added twice are a common and silent fault.
- EnginePrefill over those ids, then decode, under the sampler you configured.you configure this
Three things go wrong here often enough to be worth naming.
The wrong template. A GGUF file carries the template in its metadata, and
llama-server uses it when started with --jinja, which its README documents as
“whether to use jinja template engine for chat (default: enabled)”. A file converted or
re-packaged carelessly can carry a generic template instead of the model’s own, and the
symptom is a model that rambles, ignores the system prompt, or never stops. --chat-template
and --chat-template-file exist to override it when you have a better copy.
The missing generation prompt. apply_chat_template(..., add_generation_prompt=True)
appends the tokens that open an assistant turn. The documentation is explicit about what
happens without them: “the model may get confused and do something strange, like
continuing the user’s message instead of replying to it”. If you use an OpenAI-compatible
chat endpoint this is handled for you; if you build prompts by hand for the completion
endpoint, it is yours to get right.
Double special tokens. The template already inserts the beginning-of-sequence token.
The documentation warns that “adding additional special tokens is often incorrect or
duplicated, hurting model performance”, and recommends add_special_tokens=False if you
format with tokenize=False and tokenise later.
Seeing the prompt the server actually built
Section titled “Seeing the prompt the server actually built”You can stop guessing about all three. Two habits are enough.
The first is to render the template yourself, outside the engine, for any model whose
behaviour surprises you. apply_chat_template with tokenize=False returns the exact string
the model will be given, and reading it takes ten seconds. You are looking for three things:
the family’s control tokens where you expect them, an assistant turn opened at the end, and
no doubled beginning-of-sequence token.
Fragment — not complete on its own
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")messages = [ {"role": "system", "content": "Answer in one sentence."}, {"role": "user", "content": "Why is decode bandwidth-bound?"},]print(tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True))The second is to make the server tell you. Started with verbose logging, llama-server writes
what it received and what it built, and comparing that against the string above settles the
question of whether the engine is using the template you think it is. Do this once for each
new model you add to a service, not every time something looks odd; it takes a minute and it
removes a whole category of mystery.
System prompts: the part of the prompt you pay for every time
Section titled “System prompts: the part of the prompt you pay for every time”The system role holds directives about how the model should behave, and templates put it at the front. That position has two consequences.
The first is that it is read on every request. A three-hundred-token system prompt is three hundred tokens of prefill added to every question anybody asks, forever. That is cheap if the prefix is cached and it is not cheap otherwise, which is the subject of the last section of this lesson.
The second is that everything after it is conditioned on it. This is what makes system prompts worth writing carefully and worth keeping short. Put the invariants there: the role, the output format, the refusal rule, the language. Put the task in the user turn.
Smaller models tend to hold fewer simultaneous constraints than larger ones, but “tend to” is not a measurement, and the number of instructions your model will actually follow is something you can find out rather than guess. That is exactly what the lab at the end of this part is for: write the same task with three, six and ten constraints, and score the results.
Thinking modes, and how to drive them
Section titled “Thinking modes, and how to drive them”Several current open-weight families can produce a visible reasoning trace before their answer, and the switch is part of the prompt rather than a separate model.
Qwen3 generates its reasoning inside a <think> block. The card describes
enable_thinking=True, the default in apply_chat_template, as the setting where “the
model will generate think content wrapped in a <think>...</think> block”, and
enable_thinking=False as the setting where it “will not generate any think content and
will not include a <think>...</think> block”. On top of the hard switch there are soft
switches: with thinking enabled you can add /think or /no_think to a message, and the
card states that “the model will follow the most recent instruction in multi-turn
conversations”. The trap is that these are only soft switches: when enable_thinking=False
“the soft switches are not valid”, so a /think in your text does nothing and you will
spend twenty minutes wondering why.
gpt-oss exposes an effort level instead of a binary. The card documents setting
Reasoning: low, Reasoning: medium or Reasoning: high in the system prompt, described
respectively as “fast responses for general dialogue”, “balanced speed and detail” and
“deep and detailed analysis”. It also states that the models “were trained on our harmony
response format and should only be used with the harmony format as it will not work
correctly otherwise”, which is the same chat-template point in a stronger form.
Qwen3-Coder has no thinking mode at all. Its card says the model “supports only
non-thinking mode and does not generate <think></think> blocks in its output”. Asking it
to think step by step is fine as ordinary prompting; expecting a reasoning block is not.
The settings the publisher actually recommends
Section titled “The settings the publisher actually recommends”Engines ship with generic sampler defaults. Model cards ship with specific ones, and the gap between them is a common and invisible cause of poor output.
Qwen3’s card recommends, for thinking mode, Temperature=0.6, TopP=0.95, TopK=20 and
MinP=0, with an instruction in capital letters not to use greedy decoding, “as it can
lead to performance degradation and endless repetitions”. For non-thinking mode it
recommends Temperature=0.7, TopP=0.8, TopK=20 and MinP=0. It suggests a presence
penalty between 0 and 2 to reduce repetition, while noting that a higher value may cause
occasional language mixing and a slight drop in quality. It also recommends an output length
of 32,768 tokens for most queries and 38,912 for complex problems, which is a budget, not a
target. Qwen3-Coder’s card recommends temperature 0.7, top-p 0.8, top-k 20 and a repetition
penalty of 1.05.
Those are four different numbers for two models from the same publisher, which is the point: read the card for the model you are running.
RunnableAll tracks
llama-server \ --model ~/models/qwen3-8b/Qwen3-8B-Q4_K_M.gguf \ --alias qwen3-8b \ --jinja \ --ctx-size 8192 \ --temp 0.7 \ --top-p 0.8 \ --top-k 20 \ --min-p 0 \ --host 127.0.0.1 \ --port 8080Few-shot examples, priced
Section titled “Few-shot examples, priced”Showing the model two or three worked examples is the most reliable way to fix output format without touching the model. It is also the most reliable way to make every request slower, because the examples are prefill.
The arithmetic is easy and worth doing out loud. Eight examples of about two hundred tokens each is sixteen hundred tokens of prefix on every single request. Prefill is compute-bound, as Part 3 established, so on most machines that is a fixed cost added to the time before the first token appears. Whether it matters depends on whether that prefix is read again from scratch every time.
Where the tokens in one request go
- System promptInvariant. Same on every request.
- Few-shot examplesInvariant. Same on every request.
- Retrieved or pasted contextVaries per request. Put it after the invariant part.
- The questionShort. Varies per request.
- The answerDecoded one token at a time.
There is a point at which few-shot prompting stops being the right tool. If you need twenty examples to get the format right, you want either constrained decoding, which the next lesson covers and which makes the format structurally impossible to break, or a fine-tune, which Part 13 covers and which moves the examples out of the prompt and into the weights.
Prefix reuse: the reason the order of your prompt matters
Section titled “Prefix reuse: the reason the order of your prompt matters”Because a transformer’s key-value cache is built left to right, the cached state for a
prefix is still valid as long as the prefix has not changed. llama-server documents a
prompt cache that is enabled by default, an option to set the minimum chunk size it will
attempt to reuse from that cache by shifting the key-value entries, and a slot-save path for
persisting a slot’s cache to disk. Part 9’s engines generalise this: prefix caching shared
across requests is one of the main reasons a production server beats a single-user one when
many people ask similar questions.
The practical rule follows from the mechanism. Everything invariant goes first. System prompt, then examples, then documents, then the question. Change one character in the system prompt and every cached token after it is worthless. Put a timestamp at the top of your prompt and you have disabled prefix caching for your whole service without noticing.
Prefilling the answer
Section titled “Prefilling the answer”One more lever, useful and under-used. continue_final_message in apply_chat_template
removes the end-of-sequence tokens from a trailing assistant message so the model continues
it instead of starting a new one. The documentation describes it as “prefilling” a model
response and notes it “can be very useful for improving the accuracy of instruction
following when you know how to start its replies”. Ending the conversation with an assistant
message containing {"name": " is a blunt but effective way to get JSON. The documentation
also warns that add_generation_prompt and continue_final_message cannot be used together
and returns an error if you try.
For reasoning models there is a subtlety worth knowing: prefilling content closes the
reasoning block before generation starts, so a prefill has to target the reasoning field
itself if you want the model to continue thinking rather than answering.
Run a prompt ablation you can interpret
Section titled “Run a prompt ablation you can interpret”Choose one fixed task set and compare a plain instruction, the same instruction with a schema, and the schema plus a few examples. Keep the model, template, sampling and output budget unchanged. Save rendered prompts if the server exposes them, so an improvement can be attributed to the intended prompt change.
Examples should demonstrate edge cases as well as the easy path. For extraction, show an absent value and conflicting evidence; otherwise the model may learn from your prompt that every field should be filled. Keep untrusted document text clearly separated from application instructions, while enforcing permissions outside the prompt.
Report per-category changes and total tokens. A longer prompt that raises quality but doubles latency may be appropriate for offline analysis and poor for interactive completion. If a reasoning mode is changed at the same time, it is a separate treatment because it can change output length and computation. Prompting is an experiment with a measurable task contract, not a collection of phrases that improve every model.
The model receives one token string, not your messages, and the chat template is what
produces it. The wrong template, a missing generation prompt or a duplicated special token
each degrade a good model, and each is invisible unless you look at the prompt the server
built. System prompts are read on every request, so keep them invariant and put them first.
Thinking modes are prompt-level switches with per-family syntax: Qwen3’s enable_thinking
plus /think and /no_think, gpt-oss’s reasoning levels in the system prompt, and no
thinking mode at all in Qwen3-Coder. Use the sampling settings the model card recommends,
record them with every result, and remember that reasoning tokens cost decode time and
context. Few-shot examples fix format at a fixed prefill cost, prefix caching makes that
cost survivable, and both depend on putting the invariant part of the prompt first.
Check your understanding
Sources for this lesson
5 verified · checked 2026-09-08
- 01Hugging Face Transformers — Chat templates§ Using apply_chat_template; add_generation_prompt; continue_final_messagehuggingface.co/docs/transformers/chat_templating2026-09-08
- 02Qwen3-8B model card§ Switching Between Thinking and Non-Thinking Mode; Best Practiceshuggingface.co/Qwen/Qwen3-8B2026-09-08
- 03gpt-oss-20b model card§ Reasoning levels; harmony response formathuggingface.co/openai/gpt-oss-20b2026-09-08
- 04Qwen3-Coder-30B-A3B-Instruct model card§ Model overview; Best Practiceshuggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct2026-09-08
- 05llama.cpp — llama-server README§ Chat template options; prompt caching; /completion parametersgithub.com/ggml-org/llama.cpp/blob/master/tools/server/README.md2026-09-08
Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.