May 20266 min de lectureJohan Bretonneau

Votre gateway LLM ne sait pas que vous faites tourner un agent
Comment le routing stateless casse silencieusement vos agents multi-turns

Chaque gateway LLM route chaque requête de façon indépendante. Pour une app one-shot, c'est parfait. Pour un agent multi-turn, c'est une catastrophe qui s'annonce. Voici comment le routing session-aware change tout.

Il existe une catégorie de bugs qui n'apparaissent qu'en production, avec de vraies charges, après que tu as déjà shipé. Pas d'exception levée. Pas de 500. L'agent devient juste silencieusement moins bon, et plus cher, au fil du temps.

Le bug, c'est ça : ta gateway LLM est stateless, et ton agent ne l'est pas.

Le problème du routing stateless

Quand une requête arrive sur une gateway LLM, la gateway prend une décision de routing : quel modèle, quel provider, sur la base du coût, de la charge et de la latence. Puis elle forwarde la requête et oublie. La requête suivante repart de zéro.

Ce modèle marche parfaitement pour 90 % des usages LLM. Un utilisateur envoie un message, tu reçois une réponse, c'est plié. Chaque appel est indépendant. Le routing stateless est rapide, simple, et correct.

Mais les agents ne sont pas des appels indépendants. Un agent, c'est une conversation, une séquence de turns qui se construisent les uns sur les autres. Le context window au turn 5 contient tout ce qui s'est passé du turn 1 au turn 4. Le comportement du modèle au turn 5 dépend de la façon dont il a interprété les turns 1 à 4.

Quand la gateway route le turn 1 vers claude-sonnet-3-7 puis route le turn 5 vers claude-haiku-3-5 parce que le cluster est sous charge, tu n'as pas juste changé de modèle. Tu as injecté une intelligence différente au milieu d'un processus de raisonnement en cours.

L'agent construisait un plan. Maintenant un modèle différent lit ce plan et décide quoi faire ensuite, un modèle avec des forces différentes, des tendances différentes, des modes de défaillance différents. Le contexte de conversation a été écrit par un modèle et est maintenant interprété par un autre.

Ce qui se passe concrètement

Voilà ce que ça donne. Imagine un agent de recherche : il fetch des données, raisonne dessus, appelle des outils, construit une réponse sur plusieurs turns.

Turn 1. La gateway voit une nouvelle requête, aucun historique de conversation. Elle route vers le meilleur modèle disponible pour la tâche, disons claude-sonnet-3-7.

Turn 2. Sonnet génère un plan. Il détaille 4 étapes, fait quelques appels d'outils intermédiaires, pose une question de clarification. La réponse est cohérente et dans les clous.

Turn 3. Une vague de trafic arrive sur ta gateway. Le load balancing se déclenche, et le tier moins cher devient le choix de routing optimal. Ton turn 3 part vers claude-haiku-3-5.

Turn 4. Haiku lit le contexte accumulé, une conversation de 12K tokens écrite par Sonnet, pleine de chaînes de raisonnement nuancées et d'étapes partiellement complétées. Haiku fait de son mieux, mais son interprétation est différente. Il rate une subtilité dans l'étape 3. Il donne une réponse plus courte, plus plate.

Turn 5. Sonnet est de retour (la charge a baissé). Il lit la réponse du turn 4 et essaie de continuer, mais le turn 4 a dévié. Sonnet tente de réconcilier. L'agent boucle. Les coûts s'envolent.

Pas d'erreur levée. Pas de 500. Juste un agent qui perd tranquillement sa cohérence à travers des changements de modèle dont il ne savait pas qu'ils avaient lieu.

Le fix : un seul header

Le fix le plus simple possible est aussi le bon : session locking via un conversation ID.

X-Conversation-ID: <ton-session-id>

Envoie ce header avec chaque requête de la conversation d'un agent. La gateway le reconnaît et se souvient : le turn 1 est allé au modèle X, donc les turns 2 à N vont aussi au modèle X. La session est épinglée.

Aucun changement dans la logique de ton agent. Pas de migration de schema. Pas de nouveau SDK à apprendre. Un header HTTP à chaque appel.

Dans HiWay2LLM, on a ajouté ça dans le Router Profile AGENT. Quand tu utilises le profile AGENT, la gateway enforce le session lock automatiquement, une fois qu'un modèle est sélectionné pour un conversation ID, il reste sélectionné pour toute la durée de cette conversation. Elle enforce aussi un floor de qualité, empêchant les downgrades cost-saving en pleine tâche, même sous charge.

Les Router Profiles : adapter le comportement au workload

Un insight qu'on a eu tôt : des workloads différents ont besoin de comportements de routing fondamentalement différents. Un routeur one-size-fits-all, c'est un compromis qui est faux pour tout le monde.

On a abouti à quatre profiles principaux :

CHAT. Interactif, latency-sensitive, léger. Un humain attend la réponse. Optimise pour la vitesse perçue, préfère les modèles avec une faible latence au premier token, accepte une certaine variance dans la profondeur des outputs. Pas de session locking nécessaire car chaque échange est court.

AGENT. Long-running, multi-turn, correction-critical. Une machine orchestre une tâche complexe. Session lock obligatoire. Floor de qualité enforced. Pas de downgrade de modèle en cours de session quelle que soit la charge.

BATCH. Traitement async, pas time-sensitive. Une job queue travaille à travers un gros backlog. La latence ne compte pas, le débit et le coût comptent. Maximise l'efficacité côté provider, répartit la charge.

SAVINGS. Routing cost-first. Tu as posé une contrainte budgétaire dure et tu veux chaque euro serré. Le routeur trouve le modèle le moins cher capable de gérer la tâche, pas de floor de qualité, pas de fioritures.

Le header pour setter un profile est tout aussi simple :

X-Router-Profile: AGENT

Tu choisis le profile une fois, au niveau du system prompt ou de l'initialisation de l'agent. La gateway gère tout le reste.

La détection de divergence : la couche en-dessous

Le session locking résout le problème du changement de modèle. Mais il y a un mode de défaillance lié que le locking seul ne règle pas : l'agent qui reste sur le même modèle mais commence à boucler quand même.

Ça arrive quand le modèle entre dans un état où il répète sans cesse des actions similaires, relancer un appel d'outil, reposer la même question, revenir à une étape déjà complétée. Le modèle est cohérent, mais l'agent est bloqué.

On appelle ça la divergence. Quand le profile AGENT la détecte, un pattern de conversation qui cycle sans progresser, il peut injecter un signal correctif. Pas un hard stop, pas un kill-switch, mais un petit recadrage : un message système que la gateway injecte dans le prochain turn pour réorienter le modèle.

On ne publie délibérément pas la logique de détection ici. Ce qui compte, c'est le comportement observable : les agents utilisant le profile AGENT divergent significativement moins souvent que les mêmes agents tournant sans lui. Quand ils divergent quand même, la récupération est automatique.

Pourquoi on a construit ça

La réponse honnête building-in-public : on en avait besoin pour nos propres agents chez Mytm.ai.

On fait tourner des agents pour plusieurs workflows internes, pipelines de recherche, process de contenu, analyse de documents. Au début, avant d'avoir une instrumentation correcte, on remarquait que certains de nos agents produisaient des outputs subtilement moins bons sur certains runs. Le debug était pénible. Les agents ne crashaient pas, ils... dérivaient.

Quand on a creusé dans les logs de requêtes, le pattern était évident : les conversations où la qualité baissait étaient celles où le modèle avait changé en cours de session. Pas toujours, mais assez régulièrement pour que ça compte.

Le fix, X-Conversation-ID et une politique de routing lockée, était la solution la plus simple possible. On l'a buildée pour nous-mêmes d'abord, shippée en interne, et regardé le problème de dérive disparaître. Puis on en a fait une feature first-class dans HiWay2LLM parce qu'on savait que chaque équipe qui fait tourner des agents frapperait le même mur.

Le header était la réponse évidente. La partie difficile, c'était de réaliser que le problème existait.

En pratique

Si tu fais tourner des agents aujourd'hui à travers une gateway LLM, vérifie si elle supporte une quelconque forme d'affinité de session au niveau conversation. La plupart ne le font pas. Elles ont été buildées pour le routing one-shot, et c'est un choix architectural légitime pour ce cas d'usage.

Mais si tes agents font quelque chose de complexe, raisonnement multi-turn, tool use, planification itérative, tu as besoin d'une conscience de session au niveau gateway, pas seulement dans le code de ton application.

Le workaround au niveau applicatif, c'est de toujours passer model=claude-sonnet-3-7 (ou ce que tu utilises) explicitement et de bypasser la logique de sélection de modèle du routeur. Ça marche, mais tu perds les bénéfices du routing intelligent : fallback automatique sur panne provider, optimisation de coûts entre les tâches, load balancing sur tes clés API.

La bonne solution, c'est une gateway qui comprend la différence entre une requête et une conversation, et qui route en conséquence.

// Avant : stateless, chaque turn re-route indépendamment
const response = await hiway.chat({
  messages: conversationHistory,
});

// Après : session-aware, modèle locké pour toute la durée de la conversation
const response = await hiway.chat({
  messages: conversationHistory,
  headers: {
    "X-Conversation-ID": sessionId,
    "X-Router-Profile": "AGENT",
  },
});

Deux headers. Aucun autre changement. Ton agent a maintenant une identité de modèle cohérente pour toute la conversation.

C'est tout le fix. Ce qui est surprenant, c'est pas la complexité. C'est que si peu de gateways le proposent.

Essayer HiWay2LLM →

Sans carte bancaire


Lectures liées : On a regardé un agent IA cramer 200 $ à 3 h du mat, 5 patterns de coûts LLM à l'échelle.

Partager

LinkedInXEmail

Cet article t'a servi ?

Commentaires

Sois le premier à commenter.