Messages and Roles: user, assistant and system
LLM chat APIs model a conversation as a list of messages, each tagged with a role. The user role is the person (or your program) asking. The assistant role is the model's earlier replies. The system role carries standing instructions. Getting roles right is the foundation of every prompt.
The three roles
- user: input from the human or from your code, including documents and images you want analysed.
- assistant: text the model produced earlier. You include these to give the model its own past replies as context.
- system: instructions from you, the developer, about how to behave. It has more authority than ordinary user text.
Where the system prompt goes
This differs by provider, and it trips people up when they port code. With the Claude Messages API the system prompt is a separate system parameter, and messages holds only user and assistant turns. With OpenAI-style chat completions, the system (or developer) instructions are the first item in the messages list.
# Claude: system is its own field
claude_request = {
"model": "claude-sonnet-5-5",
"max_tokens": 500,
"system": "Answer in one short paragraph.",
"messages": [{"role": "user", "content": "What is a mutex?"}],
}
# OpenAI-style: system is the first message
openai_request = {
"messages": [
{"role": "system", "content": "Answer in one short paragraph."},
{"role": "user", "content": "What is a mutex?"},
],
}
Alternation rules
A conversation starts with a user turn and then alternates user, assistant, user, assistant. The last message you send is normally a user turn, because that is the thing the model answers. The Claude API merges consecutive messages of the same role into one turn, so two user messages in a row are treated as a single user turn. Other providers may reject that, so do not rely on it when you target several providers.
Building a list in code
import anthropic
client = anthropic.Anthropic()
messages = [
{"role": "user", "content": "I'm building a REST API in Flask."},
{"role": "assistant", "content": "Great. What would you like help with?"},
{"role": "user", "content": "How do I validate JSON input?"},
]
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=600,
messages=messages,
)
print(reply.content[0].text)
The model sees all three turns and answers the last question in the context of Flask. If you removed the first two messages, the answer would be generic.
Assistant turns are context, not memory
The model has no record of what it said unless you include it. That also means you control history. You can edit, shorten or fabricate assistant turns, for example to demonstrate the format you want in a few-shot prompt. Keep that power in mind when handling user-supplied history: if a client can send assistant turns to your server, it can put words in the model's mouth.
Roles in few-shot examples
Because assistant turns are just text you supply, you can use them to demonstrate the exact output you want. Write two or three user and assistant pairs that show the format, then add the real question as the final user message. The model tends to continue the established pattern. This works well for tone, labels and short structured formats. The later posts on few-shot prompting go deeper.
messages = [
{"role": "user", "content": "Review: The battery died in a day."},
{"role": "assistant", "content": "negative"},
{"role": "user", "content": "Review: Setup took two minutes and it just works."},
{"role": "assistant", "content": "positive"},
{"role": "user", "content": "Review: It is fine, nothing special."},
]
The model will most likely answer with a single word label such as neutral, because the pattern in the history shows one-word answers.
Roles across providers
Newer OpenAI models use a developer role for developer instructions in place of system, and some providers add roles for tool results. The idea is the same: one channel for instructions from the application, and one for the end user. When you write a wrapper that supports more than one provider, map your own role names to each provider's format in one place.
Content can be a string or blocks
A string is shorthand for a single text block. Use a list of blocks when you mix content types.
{"role": "user", "content": [
{"type": "text", "text": "Summarise the attached notes."},
{"type": "text", "text": "Notes: Q3 planning moved to Friday."},
]}
Common mistakes
- Putting a system-role item in the Claude
messageslist. It belongs in thesystemparameter. - Starting the conversation with an assistant message.
- Trusting a client to send honest history.
- Forgetting to append the assistant reply before the next user turn, which makes the model repeat itself.
Comments
No comments yet. Have a question or a tip? Be the first to share it.