Aller au contenu principal

Conversation Multi-Tour

Mis à jour le 29 juillet 2026

Au-delà de l’appel unique

Dans une application réelle, l’utilisateur ne pose pas une question puis disparaît. Il enchaîne les demandes, revient sur un sujet, glisse un « et pour celui-là ? » que rien dans la phrase ne permet de résoudre. Le function calling doit tenir dans ce contexte de conversation multi-tour, où le modèle peut appeler des fonctions à chaque étape du dialogue sans jamais perdre le fil de ce qui précède.

Le principe : maintenir l’historique

La clé tient en une phrase : conservez tous les messages dans une liste qui grandit à chaque échange. Le modèle n’a pas de mémoire propre entre deux appels d’API ; le seul contexte dont il dispose est celui que vous lui renvoyez. C’est cet historique complet qui lui permettra de comprendre les références implicites.

import os
import json
from mistralai import Mistral
from functools import partial

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
model = "mistral-large-latest"

# Historique de la conversation
messages = [
    {
        "role": "system",
        "content": "Vous êtes un assistant de suivi de paiements. "
                   "Répondez en français de manière concise."
    }
]

Boucle de conversation complète

Le code ci-dessous se lit en deux temps. process_tool_calls exécute tous les appels demandés par le modèle et relance la conversation avec les résultats ; chat gère un tour complet, de la phrase de l’utilisateur jusqu’au texte final.

def process_tool_calls(response, messages, available_functions, tools):
    """Traite les tool_calls et retourne la réponse finale."""
    message = response.choices[0].message
    messages.append(message)

    # Exécuter chaque tool_call
    for tool_call in message.tool_calls:
        function_name = tool_call.function.name
        function_params = json.loads(tool_call.function.arguments)

        if function_name in available_functions:
            result = available_functions[function_name](**function_params)
        else:
            result = json.dumps({"error": f"Fonction {function_name} inconnue"})

        messages.append({
            "role": "tool",
            "name": function_name,
            "content": result,
            "tool_call_id": tool_call.id
        })

    # Rappeler le modèle avec les résultats
    return client.chat.complete(
        model=model, messages=messages, tools=tools
    )


def chat(user_input: str, messages: list, tools: list,
         available_functions: dict) -> str:
    """Gère un tour de conversation complet."""
    messages.append({"role": "user", "content": user_input})

    response = client.chat.complete(
        model=model, messages=messages, tools=tools
    )

    message = response.choices[0].message

    # Boucle tant que le modèle veut appeler des fonctions
    while message.tool_calls and len(message.tool_calls) > 0:
        response = process_tool_calls(
            response, messages, available_functions, tools
        )
        message = response.choices[0].message

    # Le modèle a fini — il répond en texte
    messages.append(message)
    return message.content

Le while est l’élément décisif. Après avoir reçu un résultat de fonction, le modèle peut fort bien en demander un autre : la boucle continue tant qu’il produit des tool_calls et ne s’arrête que lorsqu’il renvoie enfin du texte. Un simple if traiterait le premier appel puis rendrait une réponse vide dans tous les cas où deux appels étaient nécessaires.

Exemple de conversation multi-tour

Voyons ce que cette boucle donne sur quatre tours enchaînés.

# Tour 1
reponse = chat("Quel est le statut du paiement T1001 ?",
               messages, tools, available_functions)
print(f"Assistant : {reponse}")
# → "Le paiement T1001 a le statut : payé."

# Tour 2 — question de suivi
reponse = chat("Et quand a-t-il été effectué ?",
               messages, tools, available_functions)
print(f"Assistant : {reponse}")
# → "Le paiement T1001 a été effectué le 15 mars 2026."

# Tour 3 — nouvelle transaction
reponse = chat("Compare avec le paiement T1003.",
               messages, tools, available_functions)
print(f"Assistant : {reponse}")
# → "Le paiement T1003 est en attente, contrairement au T1001 qui est payé."

# Tour 4 — question sans fonction
reponse = chat("Merci, c'est tout pour aujourd'hui.",
               messages, tools, available_functions)
print(f"Assistant : {reponse}")
# → "Je vous en prie ! N'hésitez pas si vous avez d'autres questions."

Le tour 2 est celui qu’il faut examiner de près. « Et quand a-t-il été effectué ? » ne contient aucun identifiant ; c’est l’historique qui permet au modèle de comprendre qu’il s’agit toujours de T1001 et d’appeler retrieve_payment_date avec le bon argument. Supprimez les messages du tour 1 et cette phrase devient insoluble. Au tour 3, le modèle peut décider d’appeler plusieurs fonctions — statut et date de T1003 — pour construire la comparaison avec les résultats déjà obtenus sur T1001. Le tour 4, lui, ne déclenche aucun appel : le modèle sait aussi s’abstenir.

Chaîner les appels séquentiels

Certaines demandes exigent que le modèle attende un résultat avant de savoir quoi demander ensuite. La boucle while de chat() prend ce cas en charge sans une ligne de code supplémentaire.

# L'utilisateur demande quelque chose qui nécessite 2 appels séquentiels
reponse = chat(
    "Montre-moi le statut et la date du paiement T1002.",
    messages, tools, available_functions
)
# Le modèle peut :
# 1. Appeler retrieve_payment_status(T1002)
# 2. Recevoir le résultat
# 3. Appeler retrieve_payment_date(T1002)
# 4. Recevoir le résultat
# 5. Formuler la réponse finale avec les deux informations

Vu de l’historique de messages, le flux séquentiel prend cette allure :

user → assistant(tool_call_1) → tool(result_1) → assistant(tool_call_2) → tool(result_2) → assistant(réponse)

Comptez les tours d’assistant : trois, donc trois requêtes facturées à l’API. C’est le prix du séquentiel, et la raison pour laquelle la leçon suivante s’intéresse aux appels parallèles.

Gérer la taille de l’historique

En production, une conversation qui dure devient coûteuse : chaque tour renvoie l’intégralité des messages précédents, et la facture en tokens croît de façon quadratique. Une stratégie simple consiste à ne conserver que le message système et les derniers échanges.

def trim_messages(messages: list, max_messages: int = 20) -> list:
    """Conserve le message système et les N derniers messages."""
    system_messages = [m for m in messages if hasattr(m, "role")
                       and m.get("role") == "system"
                       or (isinstance(m, dict) and m.get("role") == "system")]

    non_system = [m for m in messages if m not in system_messages]

    if len(non_system) > max_messages:
        non_system = non_system[-max_messages:]

    return system_messages + non_system

Une précaution s’impose avant de brancher cette fonction : ne coupez jamais un tool_call sans son message tool correspondant. Si la troncature tombe entre les deux, l’API reçoit une demande d’appel restée sans réponse et rejette la requête. Ajustez la coupure pour préserver les paires complètes plutôt que de compter des messages à l’aveugle.

Points clés à retenir

  • Maintenez un historique complet des messages pour que le modèle comprenne le contexte
  • Utilisez une boucle while pour gérer les appels de fonctions séquentiels
  • Le modèle comprend les références implicites (« celui-là », « le précédent ») grâce au contexte
  • En production, limitez la taille de l’historique tout en gardant les paires tool_call/tool intactes
  • La fonction chat() présentée ici est un pattern réutilisable pour toute application de function calling