Aller au contenu principal

Implémenter les tours multiples

Mis à jour le 30 juillet 2026

Jusqu’ici, nos échanges avec Claude suivaient un schéma simple : une requête, une réponse, terminé. L’utilisation d’outils casse ce schéma. Quand Claude décide d’appeler un outil, sa « réponse » n’est pas une fin — c’est une demande qui attend un résultat, et ce résultat appellera peut-être un nouvel outil, puis un autre. Votre code doit donc devenir une boucle : dialoguer avec le modèle jusqu’à ce qu’il n’ait plus besoin de rien.

La clé pour piloter cette boucle est le champ stop_reason de la réponse. Quand il vaut "tool_use", Claude s’est interrompu parce qu’il attend le résultat d’un outil ; quand il vaut autre chose (typiquement "end_turn"), la conversation est réellement terminée et vous pouvez rendre la main.

La boucle de conversation

def run_conversation(messages):
    while True:
        response = chat(messages, tools=[get_current_datetime_schema])
        add_assistant_message(messages, response)
        print(text_from_message(response))

        if response.stop_reason != "tool_use":
            break

        tool_results = run_tools(response)
        add_user_message(messages, tool_results)

    return messages

Lisez cette boucle comme un tour de table : à chaque itération, on envoie l’historique complet à Claude, on ajoute sa réponse à l’historique (c’est indispensable — le prochain appel doit inclure la demande d’outil pour que le résultat ait un sens), puis on regarde stop_reason. Tant que Claude demande des outils, on les exécute et on renvoie les résultats comme un message utilisateur ; dès qu’il conclut, on sort. Notez que les résultats d’outils voyagent bien dans un message de rôle user : du point de vue du protocole, c’est « vous » qui rapportez à Claude ce que l’outil a produit.

Cette boucle n’a pas de limite de tours ici, ce qui est acceptable pour un exercice ; en production, ajoutez un compteur de sécurité (par exemple 10 itérations maximum) pour qu’un enchaînement imprévu ne tourne pas indéfiniment à vos frais.

Gérer plusieurs appels d’outils

Un point que beaucoup découvrent en déboguant : une seule réponse de Claude peut contenir plusieurs demandes d’outils. Si l’utilisateur demande « quelle heure est-il à Paris et à Tokyo ? », Claude peut émettre deux appels dans le même tour. Votre code doit donc traiter la réponse comme une liste, jamais comme un appel unique :

def run_tools(message):
    tool_requests = [
        block for block in message.content if block.type == "tool_use"
    ]
    tool_result_blocks = []

    for tool_request in tool_requests:
        try:
            tool_output = run_tool(tool_request.name, tool_request.input)
            tool_result_blocks.append({
                "type": "tool_result",
                "tool_use_id": tool_request.id,
                "content": json.dumps(tool_output),
                "is_error": False
            })
        except Exception as e:
            tool_result_blocks.append({
                "type": "tool_result",
                "tool_use_id": tool_request.id,
                "content": f"Erreur : {e}",
                "is_error": True
            })

    return tool_result_blocks

Deux détails de cette fonction font la différence entre un prototype et un code robuste. D’abord, chaque résultat est relié à sa demande par tool_use_id : c’est ce qui permet à Claude d’apparier ses questions et vos réponses quand il y en a plusieurs — un identifiant manquant ou erroné provoque une erreur d’API. Ensuite, l’exception est capturée et renvoyée au modèle avec is_error: True, plutôt que de faire planter la boucle : Claude lit le message d’erreur et peut réagir intelligemment — reformuler ses arguments, essayer autre chose, ou expliquer le problème à l’utilisateur. Un outil qui échoue n’est pas une fin de conversation, c’est une information.

Routage des outils

Reste à relier le nom d’outil demandé par Claude à votre vraie fonction Python. Un simple aiguillage suffit :

def run_tool(tool_name, tool_input):
    if tool_name == "get_current_datetime":
        return get_current_datetime(**tool_input)
    elif tool_name == "other_tool":
        return other_tool_function(**tool_input)

L’intérêt de centraliser ce routage dans une fonction dédiée apparaît dès le deuxième outil : ajouter une capacité à votre application se résume à écrire la fonction, déclarer son schéma, et ajouter une branche ici — la boucle de conversation et la gestion des résultats, elles, ne changent jamais. C’est cette séparation qui rend l’architecture extensible : la mécanique du dialogue est écrite une fois, les outils s’y branchent.

Points clés à retenir

  • stop_reason == "tool_use" signale que Claude attend un résultat d’outil ; la conversation continue en boucle jusqu’à end_turn
  • La réponse de Claude doit être ajoutée à l’historique avant d’envoyer les résultats d’outils
  • Une réponse peut contenir plusieurs tool_use : traitez toujours une liste, chaque résultat apparié par tool_use_id
  • Renvoyez les erreurs d’exécution au modèle avec is_error: True au lieu de faire échouer la boucle
  • Centralisez le routage nom → fonction : les nouveaux outils s’ajoutent sans toucher à la logique de conversation