Conversations server-side : pourquoi c'est la clé du multi-provider
Le routing multi-provider resout un problème mais en créé un autre : si chaque message peut aller sur un provider different, comment maintient-on le contexte de la conversation ?
Le problème du contexte stateless
L'approche classique : le client envoie tout l'historique a chaque requête.
messages = [
{"role": "user", "content": "Bonjour"},
{"role": "assistant", "content": "Bonjour !"},
{"role": "user", "content": "Comment vas-tu ?"},
... # 20 messages plus tard
]
Ca marche mais ca a 4 gros defauts :
- Coût qui grossit : à chaque question, on repaie tout l'historique déjà envoyé
- Cache sementique cassee : chaque prompt a un embedding unique (il contient tout l'historique)
- Context window dépassé : sur de longues conversations, certains modèles finissent par rejeter la requête
- Risque de perte : rafraichir la page = perdre la conversation
La solution : persistance server-side
On stocke les conversations côté serveur, dans une base de données. Le client envoie juste l'ID + le nouveau message. Le serveur reconstruit le contexte.
# Le client envoie uniquement le nouveau message
headers = {
"Authorization": "Bearer sk-...",
"X-Conversation-Id": "conv_abc123", # en-tête d'exemple
}
body = {
"model": "auto",
"messages": [{"role": "user", "content": "Continue..."}],
}
Le serveur :
- Charge les N derniers messages depuis la DB
- Ajoute le nouveau message
- Envoie au LLM
- Persiste la réponse
La fenetre glissante (sliding window)
Garder TOUS les messages en contexte finit par depasser le context window. La solution classique : garder seulement les N derniers (20 dans l'exemple ci-dessous).
def build_llm_context(conversation_id, new_message, window=20):
messages = get_messages(conversation_id, limit=window)
messages.append({"role": "user", "content": new_message})
return messages
La bonne valeur de N dépend de la longueur de vos messages et de la fenêtre de contexte des modèles visés.
L'étape suivante : summarization
Pour pallier la perte de contexte, on peut resumer automatiquement les messages au-dela de la fenetre. Ce resume devient un "message système virtuel" prepend a chaque requête.
def build_llm_context_v2(conversation_id, new_message, window=20):
# Les N derniers messages tels quels
recent = get_messages(conversation_id, limit=window)
# Resume des messages plus anciens
summary = get_conversation_summary(conversation_id)
messages = []
if summary:
messages.append({"role": "system", "content": f"Contexte: {summary}"})
messages.extend(recent)
messages.append({"role": "user", "content": new_message})
return messages
Le résumé peut être généré par un petit modèle économique quand la fenêtre dépasse la limite. Le coût est faible par rapport au bénéfice sur les longues conversations, mais le résumé reste une perte d'information : à tester sur vos cas réels.
Les pieges du changement de provider
1. System message vs user message
Tous les providers ne gèrent pas les system messages pareil :
- OpenAI, Anthropic, Mistral :
systemen premier message - Gemini : champ
system_instructionsepare - Cohere :
preambledédié
Une passerelle compatible OpenAI et Anthropic doit convertir le format pour chaque provider. Sans ca, un system envoye a Gemini
est traite comme un user et degrade les réponses.
2. Format des messages
OpenAI accepte {role, content}. Anthropic aussi mais exige max_tokens.
Gemini utilise contents avec parts. Encore du mapping a faire.
3. Les tokens de delimitation
Si un utilisateur met [INST] dans son prompt (Llama) ou Human: (Claude),
le provider peut interpreter ca comme une delimitation de message et s'emmeler. Un filtre contre l'injection de délimiteurs
devient essentiel en mode multi-provider (voir pourquoi 396 patterns anti-injection).
L'UX : sidebar comme ChatGPT
Côté frontend, la persistance server-side permet une UX type ChatGPT : liste des conversations, historique accessible, reprise d'une discussion sur n'importe quel appareil.
Le client envoie juste l'ID, le serveur fait le reste. Aucune logique de gestion d'historique cote client.
Voir routtx en démonstration
Lors de la démonstration, vous voyez comment routtx choisit le modèle avec model="auto", bascule d'un fournisseur à l'autre et consigne chaque échange dans son journal.