Aller au contenu principal

Conversations Multi-Tour en Python

Mis à jour le 29 juillet 2026

Une mémoire qui n’en est pas une

Contrairement à un moteur de recherche, un modèle de langage tient le fil d’une conversation : il tient compte de ce qui a été dit avant et construit ses réponses sur ce contexte accumulé. C’est ce qu’on appelle une conversation multi-tour, et c’est ce qui rend possible une question aussi elliptique que « et avec des arguments ? ».

Techniquement, pourtant, le modèle ne se souvient de rien. Entre deux appels, il n’existe aucun état conservé côté serveur. À chaque requête, c’est vous qui lui renvoyez l’intégralité de l’historique, qu’il relit de bout en bout avant de répondre. Cette mécanique explique deux choses que nous retrouverons plus loin : la responsabilité de la mémoire vous incombe entièrement, et chaque tour de conversation coûte plus cher que le précédent.

Les trois rôles qui structurent l’historique

Chaque message de l’historique porte un rôle qui indique au modèle qui parle. Le rôle system définit le comportement global : placé en premier, il influence toutes les réponses qui suivent.

{"role": "system", "content": "Vous êtes un assistant expert en cuisine française. Répondez toujours en français avec des termes culinaires précis."}

Le rôle user porte les messages de l’utilisateur, c’est-à-dire la question ou l’instruction envoyée :

{"role": "user", "content": "Comment réussir une pâte feuilletée ?"}

Le rôle assistant, enfin, contient les réponses précédentes du modèle. Ce sont ces messages que vous réinjectez dans l’historique pour maintenir la cohérence — les oublier revient à effacer la mémoire de la conversation à chaque tour.

{"role": "assistant", "content": "Pour réussir une pâte feuilletée, commencez par..."}

Une conversation complète, tour par tour

Le code ci-dessous applique le principe : une fonction qui ajoute la question à l’historique, appelle le modèle, puis range la réponse au bon endroit avant de la retourner.

import os
from mistralai import Mistral

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

historique = [
    {
        "role": "system",
        "content": "Vous êtes un assistant pédagogique spécialisé en programmation Python. Répondez de façon concise et avec des exemples de code."
    }
]

def poser_question(question: str) -> str:
    """Envoie une question et met à jour l'historique."""
    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

# Tour 1
print("Q: Qu'est-ce qu'un décorateur en Python ?")
r1 = poser_question("Qu'est-ce qu'un décorateur en Python ?")
print(f"R: {r1}\n")

# Tour 2 — le modèle connaît le contexte
print("Q: Pouvez-vous me montrer un exemple concret ?")
r2 = poser_question("Pouvez-vous me montrer un exemple concret ?")
print(f"R: {r2}\n")

# Tour 3 — référence implicite aux tours précédents
print("Q: Comment l'utiliser avec des arguments ?")
r3 = poser_question("Comment l'utiliser avec des arguments ?")
print(f"R: {r3}\n")

Au troisième tour, la question ne mentionne plus jamais le mot « décorateur ». Le modèle comprend malgré tout que « l’utiliser » y fait référence, parce qu’il relit l’intégralité de l’échange. Retirez la ligne qui ajoute la réponse à l’historique et exécutez de nouveau : le modèle vous demandera de quoi vous parlez. C’est la démonstration la plus rapide du fait que la continuité ne vient pas de lui, mais de votre code.

Empêcher l’historique de devenir hors de prix

Chaque modèle dispose d’une fenêtre de contexte limitée, mesurée en tokens. Plus l’historique s’allonge, plus chaque requête coûte cher, puisque tout est retransmis. Une conversation de trente tours facture donc son début trente fois.

La parade la plus simple consiste à tronquer, en conservant le message système et les N derniers échanges :

MAX_MESSAGES = 20

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

    # Garder le message system + les N derniers messages
    if len(historique) > MAX_MESSAGES + 1:  # +1 pour le system
        system_msg = historique[0]
        historique = [system_msg] + historique[-(MAX_MESSAGES):]

    return historique

Cette approche coûte peu, mais elle est brutale : ce que l’utilisateur a précisé au deuxième tour disparaît définitivement. Quand la conversation est longue et que ce contexte ancien compte — un entretien de recueil de besoin, par exemple —, résumez-le plutôt que de le jeter :

def resumer_historique(client, historique):
    """Résume les anciens messages pour libérer du contexte."""
    texte_complet = "\n".join(
        f"{m['role']}: {m['content']}"
        for m in historique[1:-4]  # Exclure system et les 4 derniers
    )

    resume = client.chat.complete(
        model="mistral-small-latest",
        messages=[
            {"role": "user", "content": f"Résumez cette conversation en 3-4 phrases :\n{texte_complet}"}
        ]
    )

    return [
        historique[0],  # System message
        {"role": "assistant", "content": f"[Résumé de la conversation] {resume.choices[0].message.content}"},
    ] + historique[-4:]  # Les 4 derniers messages

Le principe est de compresser le passé lointain en quelques phrases tout en gardant les quatre derniers messages intacts, puisque ce sont eux qui portent le contexte immédiat. Le résumé consomme un appel supplémentaire, largement rentabilisé sur les tours suivants.

Soigner le message système

Le message système est l’outil le plus puissant et le plus sous-exploité de cette leçon. Beaucoup se contentent d’un « Vous êtes un assistant utile » qui n’apporte rien. Un bon message système énonce des contraintes vérifiables :

system_prompt = """Vous êtes un assistant technique Corsen AI.

Règles :
- Répondez en français avec un ton professionnel
- Fournissez des exemples de code quand c'est pertinent
- Si vous n'êtes pas sûr, dites-le plutôt que d'inventer
- Limitez vos réponses à 200 mots sauf demande contraire

Contexte : l'utilisateur travaille sur un projet Python 3.12 avec FastAPI."""

Chaque ligne y produit un effet mesurable : la limite de longueur contient les coûts, l’invitation à reconnaître l’incertitude réduit les affirmations inventées, et la mention de la stack évite des réponses génériques hors sujet. Concis, précis, sans ambiguïté — c’est tout ce qu’on lui demande.

Points clés à retenir

  • L’historique complet est renvoyé à chaque requête — le modèle ne « mémorise » rien
  • Les trois rôles (system, user, assistant) structurent la conversation
  • Le message system définit le comportement global — placez-le toujours en premier
  • Gérez la taille du contexte en tronquant ou en résumant les anciens messages
  • Plus l’historique est long, plus chaque requête coûte cher en tokens