Aller au contenu principal

Exécution serveur vs client

Mis à jour le 28 juillet 2026

Deux façons de gérer les handoffs

Quand un agent transfère le contrôle à un autre agent, l’API Mistral propose deux modes d’exécution : serveur (automatique) et client (manuel). Le paramètre tient en un mot, mais il détermine qui pilote réellement le workflow. En mode serveur, vous déléguez la chorégraphie complète à Mistral et vous recevez le résultat. En mode client, vous reprenez la main entre chaque étape. Tout le reste — les agents, leurs outils, leurs cibles de handoff — est rigoureusement identique.

Mode serveur, celui par défaut

response = client.beta.conversations.start(
    agent_id=router_agent.id,
    inputs="Analyse et calcule les tendances du marché.",
    handoff_execution="server"  # Défaut — peut être omis
)

Le déroulement est entièrement pris en charge : le routeur reçoit la requête, décide de transférer à l’agent recherche et Mistral exécute le transfert ; l’agent recherche fait son travail puis transfère au calculateur, et Mistral exécute là encore ; le calculateur produit le résultat. Vous recevez la réponse finale, accompagnée de tous les outputs intermédiaires — vous voyez donc ce qui s’est passé, mais après coup.

Ce mode a trois qualités qui expliquent qu’il soit le défaut. Il est simple, puisqu’il n’y a aucune logique de contrôle à écrire de votre côté. Il est rapide, parce que la chaîne entière s’exécute chez Mistral sans aller-retour réseau à chaque relais. Il est fiable, la gestion des erreurs et des timeouts entre agents étant assurée pour vous.

Ses limites sont l’exacte contrepartie. Vous n’avez aucun contrôle intermédiaire : impossible de valider ou de modifier un handoff pendant qu’il se produit. Le coût est opaque, au sens où tous les agents de la chaîne s’exécutent sans que vous puissiez interrompre en cours de route une requête qui part dans une direction inutile. Et vous ne pouvez insérer aucune logique maison entre les étapes — ni journalisation fine, ni validation, ni décision métier.

Mode client, quand vous voulez la main

response = client.beta.conversations.start(
    agent_id=router_agent.id,
    inputs="Analyse le marché du cloud.",
    handoff_execution="client"
)

Le routeur reçoit la requête et décide de transférer, exactement comme avant. Mais au lieu que le transfert s’exécute, vous recevez un événement agent.handoff et la chaîne s’arrête là. C’est à vous de la relancer en appelant l’agent cible — ce qui vous laisse la latitude de valider, de journaliser, d’enrichir le contexte, ou tout simplement de refuser.

response = client.beta.conversations.start(
    agent_id=router_agent.id,
    inputs="Cherche les dernières actualités tech.",
    handoff_execution="client"
)

for entry in response.outputs:
    if entry.type == "agent.handoff":
        target_agent_id = entry.to_agent
        print(f"Handoff demandé vers : {target_agent_id}")

        # Valider et continuer avec l'agent cible
        next_response = client.beta.conversations.append(
            conversation_id=response.conversation_id,
            agent_id=target_agent_id,
            inputs="Continue avec le contexte précédent."
        )

        # Traiter la réponse de l'agent cible
        for next_entry in next_response.outputs:
            print(f"{next_entry.type}: {next_entry.content}")

Remarquez que la reprise passe par conversations.append sur la même conversation_id : c’est ce qui garantit que l’agent cible hérite de tout l’historique. Repartir sur une nouvelle conversation reviendrait à lui demander de travailler sans savoir ce qui a été fait avant.

Le fragment ci-dessus ne traite qu’un seul relais. Une chaîne réelle en enchaîne plusieurs, ce qui appelle une boucle.

def execute_with_control(router_id, query):
    """Exécute un workflow multi-agents avec contrôle à chaque étape."""
    response = client.beta.conversations.start(
        agent_id=router_id,
        inputs=query,
        handoff_execution="client"
    )

    conversation_id = response.conversation_id
    results = []

    while True:
        handoff_found = False

        for entry in response.outputs:
            if entry.type == "agent.handoff":
                handoff_found = True
                target = entry.to_agent
                print(f"[LOG] Handoff vers {target}")

                # Ici vous pouvez ajouter : validation, logging, métriques
                response = client.beta.conversations.append(
                    conversation_id=conversation_id,
                    agent_id=target,
                    inputs="Poursuis le traitement."
                )
                conversation_id = response.conversation_id
                break

            elif entry.type == "message.output":
                results.append(entry.content)

        if not handoff_found:
            break

    return results

La structure est celle d’une pompe : tant qu’un handoff apparaît dans les outputs, on le traite et on relance ; dès qu’un tour de boucle n’en trouve plus, la chaîne est terminée et l’on sort avec les messages accumulés. Le commentaire au milieu marque l’emplacement qui justifie tout ce mode : c’est là, et seulement là, que votre code s’intercale entre deux agents.

Choisir entre les deux

Le mode serveur couvre la production standard. Si votre workflow est prédictible, si vous faites confiance aux agents que vous avez écrits, ou si vous prototypez et voulez voir le résultat rapidement, l’automatisme complet est le bon choix — et écrire une boucle de contrôle serait du travail perdu.

Le mode client se justifie dès qu’une exigence extérieure s’invite dans la chaîne. Une validation humaine, quand un opérateur doit approuver un transfert vers un agent qui déclenche des actions sensibles. Un besoin de journalisation et d’audit, quand la conformité impose de tracer chaque étape avec vos propres identifiants et vos propres horodatages. Un routage conditionnel, quand la décision dépend de règles métier que le modèle ne peut pas connaître — le statut du client, l’heure, un quota. Un plafonnement, ou rate limiting, quand vous voulez borner le nombre d’agents exécutés pour une requête. Une stratégie de repli, enfin, quand l’échec d’un agent doit déclencher autre chose qu’une erreur remontée à l’utilisateur.

Voici à quoi ressemble la première de ces situations, réduite à son squelette.

def workflow_avec_validation(router_id, query):
    response = client.beta.conversations.start(
        agent_id=router_id,
        inputs=query,
        handoff_execution="client"
    )

    for entry in response.outputs:
        if entry.type == "agent.handoff":
            target = entry.to_agent
            approval = input(f"Autoriser le transfert vers {target} ? (o/n) ")

            if approval.lower() == "o":
                response = client.beta.conversations.append(
                    conversation_id=response.conversation_id,
                    agent_id=target,
                    inputs="Continue."
                )
            else:
                print("Transfert refusé.")
                break

L’input en console tient lieu de démonstration ; dans une application, il sera remplacé par une notification à un opérateur et par la reprise du workflow quand la réponse arrive. Le principe reste le même : le transfert n’a lieu que si votre code le déclenche.

La bonne démarche consiste à démarrer en mode serveur et à ne basculer que le jour où une contrainte précise l’exige. Passer en mode client « au cas où » vous fait payer immédiatement la complexité d’une boucle et les allers-retours réseau, pour un contrôle dont vous ne vous servez pas encore.

Points clés à retenir

  • Mode serveur = tout automatique, simple et rapide, pas de contrôle intermédiaire
  • Mode client = vous recevez les événements de handoff et décidez quoi faire
  • Le mode client permet validation, logging, routage conditionnel et gestion d’erreurs
  • Pour la plupart des cas, le mode serveur suffit — passez au mode client quand vous avez besoin de contrôle
  • Les deux modes supportent les mêmes chaînes d’agents, seul le contrôle diffère