Ajouter un LLM à un backend en production est un problème d'intégration avant d'être un problème de machine learning. Traitez le modèle comme une dépendance tierce peu fiable : timeout, tentatives plafonnées, disjoncteur (circuit breaker) et chemin de repli. Sortez-le du chemin de requête synchrone, validez chaque sortie contre un schéma, et mesurez chaque changement de prompt avec un jeu de fixtures. La suite de cet article transforme ces règles en pratique.
Pourquoi traiter un appel LLM comme une dépendance tierce peu fiable ?
Parce que c'est exactement ce qu'elle est : une API distante avec une latence variable, des pannes occasionnelles, des limites de débit et une facture à la clé. Toute la discipline que vous appliquez déjà à une passerelle de paiement ou à un fournisseur d'e-mails s'applique ici : timeouts stricts, tentatives plafonnées, disjoncteur, et un repli qui garde la fonctionnalité utile quand le modèle ne l'est pas.
En production, les API de modèles affichent couramment des latences p95 qui se mesurent en secondes, pas en millisecondes, et elles se dégradent sous charge comme n'importe quel service partagé. Fixez un timeout explicite bien en dessous de la patience de vos utilisateurs, plafonnez les tentatives (chaque essai coûte des tokens : une tempête de retries est aussi un incident de facturation), et ouvrez le disjoncteur après plusieurs échecs consécutifs pour qu'une panne du fournisseur n'empile pas des milliers d'appels condamnés. Surtout, concevez le repli en premier : une réponse précédente en cache, une heuristique plus simple, ou un « indisponible pour le moment » honnête gardent le produit utilisable pendant que le modèle ne l'est pas.
const TIMEOUT_MS = 8000;
const MAX_ATTEMPTS = 3;
async function callModel(payload) {
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(MODEL_API_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
if (res.ok) return await res.json();
if (res.status < 500) break; // un 4xx n'aboutira pas mieux au prochain essai
} catch {
// timeout ou erreur réseau : on retente s'il reste des essais
} finally {
clearTimeout(timer);
}
}
return null; // l'appelant bascule sur le repli sans IA
}
Pourquoi sortir le modèle du chemin de requête synchrone ?
Parce qu'un appel qui prend de deux à vingt secondes n'a rien à faire entre le clic d'un utilisateur et une réponse HTTP. Quand le produit le permet, acceptez la requête, poussez un job dans une file d'attente, traitez-le en asynchrone et notifiez le client quand le résultat arrive. L'utilisateur attend devant un indicateur de chargement, pas sur votre pool de connexions.
Une file entre votre API et le modèle vous offre des retries sans latence visible, de la contre-pression quand le fournisseur vous limite, et une file de rebut pour les entrées qui échouent en boucle. Amazon SQS et un worker couvrent la plupart des cas ; nous avons détaillé ce motif dans notre article sur l'architecture événementielle avec Lambda et SQS. L'appel synchrone reste défendable pour des fonctions de type autocomplétion, mais uniquement avec un budget de latence serré et un repli sans IA instantané quand ce budget est dépassé.
Comment empêcher une sortie non déterministe de corrompre vos données ?
En validant chaque réponse du modèle contre un schéma avant qu'elle ne touche quoi que ce soit de durable. Du texte libre ne doit jamais atteindre une écriture en base, un flux de paiement ou une API en aval sans contrôle. Demandez une sortie structurée, parsez-la, validez-la, et dirigez tout ce qui échoue vers un repli ou un humain, jamais vers la suite.
import { z } from 'zod';
// La réponse du modèle doit respecter ce contrat avant toute persistance
const TicketTriage = z.object({
category: z.enum(['billing', 'technical', 'account']),
urgency: z.enum(['low', 'normal', 'high']),
summary: z.string().max(300),
});
const parsed = TicketTriage.safeParse(JSON.parse(raw));
if (!parsed.success) {
// Sortie invalide : repli sur les règles et signalement pour revue
return triageWithRules(ticket);
}
saveTriage(parsed.data);
Définissez le contrat avec une bibliothèque de schémas et rejetez tout ce qui ne parse pas. Le Top 10 OWASP pour les applications LLM classe la mauvaise gestion des sorties parmi les failles les plus courantes, et c'est la plus simple à prévenir. Pour tout ce qui est visible des clients, ajoutez deux couches : un feature flag pour couper la fonctionnalité sans déploiement, et une revue humaine d'un échantillon de sorties (voire de toutes, au début) avant de laisser la boucle tourner seule.
Vous prévoyez une fonctionnalité IA sur un backend qui a déjà des utilisateurs payants ? Décrivez votre système : diagnostic d'une page sous 48 h.
Recevoir mon diagnostic →Comment savoir qu'un changement de prompt n'a rien dégradé ?
Avec un petit jeu d'évaluation exécuté avant et après chaque changement de prompt ou de modèle. Vingt à cinquante entrées réelles avec leurs sorties attendues, notées automatiquement, attrapent la plupart des régressions qu'un survol de trois exemples laisse passer. Traitez les prompts comme du code : versionnés, relus, et bloquants pour le déploiement quand le score baisse.
Conservez ces entrées anonymisées dans le dépôt, à côté du prompt, chacune avec sa sortie structurée attendue. Notez-les automatiquement : correspondance exacte sur les champs du schéma, présence du contenu requis, taux de refus. Exécutez le jeu en CI à chaque modification du prompt, et relancez-le quand le fournisseur publie une nouvelle version du modèle, car ces mises à jour changent le comportement sans prévenir. Décidez de ce qu'est un score acceptable avant de toucher quoi que ce soit, sinon chaque résultat paraîtra correct après coup.
Comment garder une facture prévisible ?
Trois leviers : budgets de tokens, cache, et étagement des modèles. Plafonnez les tokens d'entrée et de sortie par requête, mettez en cache les appels identiques ou très proches, et orientez les cas simples vers un petit modèle économique en réservant le grand aux cas difficiles. Sans ces plafonds, un utilisateur enthousiaste ou une boucle de retry décide de votre facture mensuelle.
Plafonnez les tokens de sortie sur chaque appel et rejetez les entrées trop volumineuses avant qu'elles n'atteignent l'API. Utilisez le cache de prompts côté fournisseur pour les prompts système répétés et le contexte partagé : Anthropic comme OpenAI documentent, à l'heure où nous écrivons, le prompt caching et des traitements par lots à tarif réduit. Routez par difficulté : un petit modèle classe et extrait très bien, et n'escalader que la minorité ambiguë vers un grand modèle coûte bien moins cher que d'y envoyer tout le trafic. Mesurez ensuite la dépense par fonctionnalité et posez une alerte dessus, comme pour n'importe quelle autre ligne d'infrastructure.
Qu'est-ce qui quitte réellement votre infrastructure ?
Chaque prompt que vous envoyez, y compris toutes les données utilisateur que vous y avez interpolées. Au sens du RGPD, le fournisseur est un sous-traitant : il vous faut un accord de traitement des données (DPA), une base légale documentée, et la suppression des données personnelles dont le modèle n'a pas besoin. Partez du principe que les prompts sont journalisés quelque part, sauf mention contraire du contrat.
Avant chaque appel, retirez ce dont le modèle n'a pas besoin : remplacez noms, e-mails, numéros de téléphone et IBAN par des jetons de substitution, puis réinjectez-les après la réponse si la fonctionnalité l'exige. Signez le DPA du fournisseur, vérifiez si les prompts sont conservés et pour combien de temps, et confirmez qu'ils ne servent pas à l'entraînement dans le cadre de votre offre. Certains fournisseurs proposent un traitement dans l'UE ou des options de résidence des données : vérifiez ce que votre contrat couvre réellement, pas ce que la page marketing laisse entendre. Appliquez la même rigueur à vos propres journaux : un prompt dans un fichier de log est aussi une donnée personnelle.
Quand l'IA est-elle le mauvais outil ?
Dès qu'une solution déterministe fait déjà le travail. Une regex qui extrait un numéro de commande, une requête SQL qui classe des clients, une table de règles qui oriente des tickets : tout cela est plus rapide, moins cher, testable et explicable. Ne sortez un modèle que lorsque les entrées sont réellement non structurées et que les règles échouent en continu.
| Tâche | Commencez par | Un modèle se justifie quand |
|---|---|---|
| Extraire un numéro de commande d'un objet d'e-mail | Une regex | Les formats sont réellement libres et multilingues |
| « Meilleurs comptes par chiffre d'affaires ce trimestre » | Une requête SQL | Les utilisateurs posent la question en langage naturel |
| Orienter les tickets par mot-clé | Une table de règles | Les formulations varient trop pour des règles |
| Valider un e-mail ou un numéro de TVA | Une bibliothèque de validation | Jamais |
Ce tri est la première étape de notre travail d'IA et d'automatisation : un modèle qui remplace une regex fonctionnelle ajoute de la latence, un coût et un nouveau mode de défaillance, et supprime un test auquel vous pouviez faire confiance.
La check-list avant mise en production
Avant d'ouvrir la fonctionnalité à de vrais utilisateurs, vérifiez chaque point :
- Timeout et tentatives plafonnées sur chaque appel au modèle, plus un disjoncteur
- Un chemin de repli qui garde la fonctionnalité utilisable, en mode dégradé, quand le fournisseur est en panne
- Les appels au modèle hors du chemin synchrone, ou un budget de latence strict s'ils doivent y rester
- Validation par schéma de chaque sortie : rien de non validé n'atteint le stockage ou l'argent
- Un jeu d'evals branché sur la CI, exécuté à chaque changement de prompt ou de modèle
- Plafonds de tokens, cache, étagement des modèles et alerte de dépense
- DPA signé, données personnelles retirées des prompts, rétention comprise
- Feature flag et coupe-circuit sur tout ce qui est visible des clients