Chapter 7 · AI-Curious Developer

System prompts and conversation memory

A real conversation with a model isn't one string — it's a list of message dicts, each with a "role" and "content". The model has no memory of its own; it only ever sees the list you hand it. "Memory" is just you keeping the list and appending to it.

Three roles cover everything:

  • "system" — the standing instructions ("You are a concise assistant"). Conventionally the first message, and there's usually exactly one.
  • "user" — what the person said.
  • "assistant" — what the model said back.

Multi-turn memory is mechanical: start with the system message, then append a user dict each time the person speaks and an assistant dict each time the model replies. Send the whole growing list every turn and the model sees the full history. Drop a turn from the list and, as far as the model is concerned, it never happened.

This is plain list-and-dict work — list.append, dict literals, a for loop. No framework required.

Syntax

messages = [
    {"role": "system", "content": "You are concise."},
    {"role": "user", "content": "What is 2+2?"},
    {"role": "assistant", "content": "4"},
]

# Add a turn to keep memory:
messages.append({"role": "user", "content": "And times 3?"})

Worked examples

A conversation is a list of role/content dicts

messages = [
    {"role": "system", "content": "You are concise."},
    {"role": "user", "content": "What is 2+2?"},
    {"role": "assistant", "content": "4"},
]
for m in messages:
    print(f"{m['role']}: {m['content']}")
# Output:
# system: You are concise.
# user: What is 2+2?
# assistant: 4

Appending turns is how memory works

def add_turn(history, role, content):
    history.append({"role": role, "content": content})
    return history

convo = [{"role": "system", "content": "Be helpful."}]
add_turn(convo, "user", "Hi")
add_turn(convo, "assistant", "Hello!")
add_turn(convo, "user", "Bye")
print(len(convo), [m["role"] for m in convo])
# Output:
# 4 ['system', 'user', 'assistant', 'user']

Build a multi-turn conversation from exchanges

def build_conversation(system_prompt, exchanges):
    messages = [{"role": "system", "content": system_prompt}]
    for user_text, assistant_text in exchanges:
        messages.append({"role": "user", "content": user_text})
        messages.append({"role": "assistant", "content": assistant_text})
    return messages

conv = build_conversation("Be helpful.", [("hi", "hello"), ("2+2?", "4")])
print([m["role"] for m in conv])
# Output:
# ['system', 'user', 'assistant', 'user', 'assistant']

Common mistakes & gotchas

The model has no memory of its own

If you don't append the previous turns to the list, the model genuinely doesn't know they happened — each call is stateless. "It forgot what I said" almost always means a turn never made it into the messages list you sent.

System message placement and count

The system message is conventionally first and singular. Burying instructions in a user message, or sending several conflicting system messages, gives muddier results. Keep the standing instructions in one system entry at the top.

History grows without bound

Every turn you append makes the list longer, and you send the whole thing each time — eventually it gets large (and, with a real model, costly / over a length limit). Real systems trim or summarise old turns. The mechanism is still just editing a list.

Why it matters

Every chatbot, agent and copilot is, underneath, a list of message dicts you grow and re-send. Once you see conversation memory as ordinary list management, multi-turn AI stops being magic and becomes code you can read, test and debug.