Skip to main content
Lesson 7 of 14 · Mistral API: Quick Start

Multi-Turn Conversations in Python

Lesson 7 of 14 Updated November 2, 2026 Developer
On this page
  1. Memory That Isn’t Really Memory
  2. The Three Roles That Structure History
  3. A Complete Conversation, Turn by Turn
  4. Keeping History from Becoming Prohibitively Expensive
  5. Crafting the System Message
  6. Key Takeaways

Memory That Isn’t Really Memory

Unlike a search engine, a language model follows the thread of a conversation: it considers what was said before and builds its responses on that accumulated context. This is called a multi-turn conversation, and it’s what makes elliptical questions like “and with arguments?” possible.

Technically, however, the model remembers nothing. No state is preserved on the server between calls. With every request, you must resend the entire history, which the model reads from start to finish before answering. This mechanism explains two things we’ll revisit later: you are entirely responsible for memory management, and each turn of the conversation costs more than the previous one.

The Three Roles That Structure History

Every message in the history carries a role that tells the model who is speaking. The system role defines the overall behavior: placed first, it influences every response that follows.

{"role": "system", "content": "You are an expert assistant in French cuisine. Always respond in French using precise culinary terms."}

The user role carries the user’s messages—that is, the question or instruction sent:

{"role": "user", "content": "How do I make perfect puff pastry?"}

Finally, the assistant role contains the model’s previous responses. These are the messages you reinject into the history to maintain coherence. Forgetting them is equivalent to erasing the conversation’s memory at every turn.

{"role": "assistant", "content": "To make perfect puff pastry, start by..."}

A Complete Conversation, Turn by Turn

The code below applies this principle: a function that adds the question to the history, calls the model, then stores the response in the right place before returning it.

import os
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

historique = [
    {
        "role": "system",
        "content": "You are a teaching assistant specialized in Python programming. Respond concisely and include code examples."
    }
]

def poser_question(question: str) -> str:
    """Sends a question and updates the history."""
    historique.append({"role": "user", "content": question})

    response = client.chat.complete(
        model="mistral-small-latest",
        messages=historique,
    )

    reponse_texte = response.choices[0].message.content
    historique.append({"role": "assistant", "content": reponse_texte})

    return reponse_texte

# Turn 1
print("Q: What is a decorator in Python?")
r1 = poser_question("What is a decorator in Python?")
print(f"R: {r1}\n")

# Turn 2 — the model knows the context
print("Q: Can you show me a concrete example?")
r2 = poser_question("Can you show me a concrete example?")
print(f"R: {r2}\n")

# Turn 3 — implicit reference to previous turns
print("Q: How do I use it with arguments?")
r3 = poser_question("How do I use it with arguments?")
print(f"R: {r3}\n")

By the third turn, the question never mentions the word “decorator” again. The model still understands that “use it” refers to that concept because it rereads the entire exchange. Remove the line that adds the response to the history and run it again: the model will ask what you’re talking about. This is the quickest proof that continuity doesn’t come from the model, but from your code.

Keeping History from Becoming Prohibitively Expensive

Every model has a limited context window, measured in tokens. As the history grows, every request becomes more expensive because everything is retransmitted. A thirty-turn conversation effectively bills its beginning thirty times over.

The simplest workaround is to truncate, keeping the system message and the last N exchanges:

MAX_MESSAGES = 20

def ajouter_message(historique, role, content):
    historique.append({"role": role, "content": content})

    # Keep the system message + the last N messages
    if len(historique) > MAX_MESSAGES + 1:  # +1 for the system message
        system_msg = historique[0]
        historique = [system_msg] + historique[-(MAX_MESSAGES):]

    return historique

This approach is cheap, but it’s brutal: what the user specified in the second turn disappears forever. When the conversation is long and that older context matters—a requirements gathering interview, for example—summarize it instead of throwing it away:

def resumer_historique(client, historique):
    """Summarizes old messages to free up context."""
    texte_complet = "\n".join(
        f"{m['role']}: {m['content']}"
        for m in historique[1:-4]  # Exclude system and the last 4 messages
    )

    resume = client.chat.complete(
        model="mistral-small-latest",
        messages=[
            {"role": "user", "content": f"Summarize this conversation in 3-4 sentences:\n{texte_complet}"}
        ]
    )

    return [
        historique[0],  # System message
        {"role": "assistant", "content": f"[Conversation Summary] {resume.choices[0].message.content}"},
    ] + historique[-4:]  # The last 4 messages

The idea is to compress the distant past into a few sentences while keeping the last four messages intact, since they carry the immediate context. The summary consumes one extra call, but it pays for itself many times over in subsequent turns.

Crafting the System Message

The system message is the most powerful and underused tool in this lesson. Many settle for a generic “You are a helpful assistant” that adds nothing. A good system message states verifiable constraints:

system_prompt = """You are a Corsen AI technical assistant.

Rules:
- Respond in French with a professional tone
- Provide code examples when relevant
- If you are unsure, say so rather than making things up
- Limit your responses to 200 words unless asked otherwise

Context: The user is working on a Python 3.12 project with FastAPI."""

Every line here produces a measurable effect: the length limit contains costs, the invitation to acknowledge uncertainty reduces hallucinations, and mentioning the stack avoids generic, off-topic responses. Concise, precise, unambiguous—that’s all it needs to be.

Key Takeaways

  • The complete history is resent with every request—the model “remembers” nothing
  • Three roles (system, user, assistant) structure the conversation
  • The system message defines overall behavior—always place it first
  • Manage context size by truncating or summarizing old messages
  • The longer the history, the more each request costs in tokens