Messages, roles and the system instruction
Working with LLM APIs: advanced · Lesson 2 / 21
There is no conversation, there is an array
The model does not store your conversation. On every call it receives the full list of messages again and remembers nothing from the previous request. The feeling of memory is produced by your code, which stitches the history together and sends all of it. One practical consequence follows: dialogue length converts directly into money and latency, and it grows quadratically if you send the whole history on every step.
Roles and what they mean
- system — behavior rules for the whole call. It usually comes first and carries extra weight when it conflicts with the rest.
- user — whatever came from a human. This is always untrusted data.
- assistant — past model answers, including its requests to call tools.
- tool — results your code returned for a model request. Each such result must reference the call identifier.
messages: system rules, format, boundaries user first user request assistant tool call request (call id) tool call result (same id) assistant final text user next request
What goes into the system instruction
The system message sets the role, the answer format, the boundaries and the refusal rules. It does not change between calls, which makes it the first candidate for prompt caching of the prefix — if the provider supports it and the system part is long and constant, you pay for it at a reduced rate. Changing user data must not go there: it breaks the cache and mixes trusted content with untrusted content.
Separating roles is not cosmetic, it is your first line of defense. The model is trained to treat system rules as higher priority. If you glue your instruction and the user text into one string under the user role, you give that advantage away for free: the line "ignore previous instructions" inside a customer review gets exactly the same standing as your rules.
How to truncate history
- Window of recent messages. Cheap and predictable, but it loses facts from the start of the conversation.
- Compression into a summary. Old messages are replaced by a short state text. It costs one extra call but keeps length constant.
- Structured state. Store parsed fields — the order, the step, the confirmed parameters — instead of dialogue text, and build the prompt from them.
The third option is almost always better when the task is not free-form chat. It gives constant context length, reproducibility, and the ability to check state with ordinary tests.
Message order matters too
The model treats the beginning and the end of the array differently. Instructions placed at the very end sit closer to the moment of generation and are followed more willingly than the same words at the start of a long context. A practical trick: keep general behavior rules in the system message, and repeat the format requirement for this specific answer as the last line of the user message. That duplication costs a dozen tokens and noticeably lowers the share of wrong-format answers on long prompts.
Cheat sheet
- Conversation memory is created by your code; the model stores nothing.
- Roles separate trusted from untrusted — do not glue them together.
- The system part is constant: it is a candidate for prefix caching.
- For scripted tasks store state as a structure, not as conversation text.
Take one real model call from your code or project. Write out its messages array in full, marking the role of each message and the token count of each. Separately note which parts change from call to call and which stay constant. Then estimate what this array becomes after ten conversation turns, and describe which truncation strategy you would choose and why exactly that one.