Aller au contenu principal

Architecture de production : monitoring, alerting, scaling

Votre application en production : les trois piliers

Déployer une application LLM en production ne s’arrête pas au code. Trois piliers garantissent la fiabilité : le monitoring (observer), l’alerting (réagir) et le scaling (absorber la charge). Cette leçon finale rassemble tous les concepts du cours dans une architecture de production complète.

Monitoring : observer tout

Métriques essentielles

import time
from dataclasses import dataclass, field
from collections import defaultdict

@dataclass
class MetriquesProduction:
    """Collecte les métriques essentielles en production."""

    compteurs: dict = field(default_factory=lambda: defaultdict(int))
    latences: dict = field(default_factory=lambda: defaultdict(list))
    erreurs: dict = field(default_factory=lambda: defaultdict(int))
    couts: dict = field(default_factory=lambda: defaultdict(float))

    def enregistrer_appel(
        self,
        modele: str,
        latence: float,
        tokens_entree: int,
        tokens_sortie: int,
        cout: float,
        succes: bool,
    ):
        self.compteurs[modele] += 1
        self.latences[modele].append(latence)
        self.couts[modele] += cout

        if not succes:
            self.erreurs[modele] += 1

        # Garder les 10 000 dernières latences
        if len(self.latences[modele]) > 10_000:
            self.latences[modele] = self.latences[modele][-10_000:]

    def percentile(self, modele: str, p: float) -> float:
        """Calcule un percentile de latence."""
        valeurs = sorted(self.latences.get(modele, []))
        if not valeurs:
            return 0.0
        idx = int(len(valeurs) * p / 100)
        return valeurs[min(idx, len(valeurs) - 1)]

    def taux_erreur(self, modele: str) -> float:
        total = self.compteurs.get(modele, 0)
        if total == 0:
            return 0.0
        return self.erreurs.get(modele, 0) / total

    def tableau_de_bord(self) -> dict:
        """Génère un tableau de bord complet."""
        dashboard = {}
        for modele in self.compteurs:
            dashboard[modele] = {
                "appels": self.compteurs[modele],
                "latence_p50": f"{self.percentile(modele, 50):.2f}s",
                "latence_p99": f"{self.percentile(modele, 99):.2f}s",
                "taux_erreur": f"{self.taux_erreur(modele):.2%}",
                "cout_total": f"${self.couts[modele]:.2f}",
            }
        return dashboard

Middleware de monitoring

import openai
import functools

metriques = MetriquesProduction()

def monitorer(func):
    """Décorateur qui monitore chaque appel API."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        modele = kwargs.get("model", args[0] if args else "inconnu")
        debut = time.perf_counter()
        succes = True

        try:
            resultat = func(*args, **kwargs)
            return resultat
        except Exception as e:
            succes = False
            raise
        finally:
            latence = time.perf_counter() - debut
            metriques.enregistrer_appel(
                modele=modele,
                latence=latence,
                tokens_entree=0,
                tokens_sortie=0,
                cout=0,
                succes=succes,
            )

    return wrapper

Alerting : réagir vite

Système d’alertes

from enum import Enum

class NiveauAlerte(Enum):
    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"

class SystemeAlertes:
    """Déclenche des alertes basées sur les métriques."""

    def __init__(self):
        self.seuils = {
            "taux_erreur": {"warning": 0.05, "critical": 0.15},
            "latence_p99": {"warning": 10.0, "critical": 30.0},
            "cout_horaire": {"warning": 50.0, "critical": 200.0},
        }
        self.alertes_envoyees: list[dict] = []

    def verifier(self, metriques: MetriquesProduction) -> list[dict]:
        """Vérifie les métriques et génère des alertes."""
        alertes = []

        for modele in metriques.compteurs:
            # Taux d'erreur
            taux = metriques.taux_erreur(modele)
            if taux >= self.seuils["taux_erreur"]["critical"]:
                alertes.append({
                    "niveau": NiveauAlerte.CRITICAL,
                    "message": (
                        f"Taux d'erreur CRITIQUE pour {modele}: "
                        f"{taux:.1%}"
                    ),
                    "modele": modele,
                })
            elif taux >= self.seuils["taux_erreur"]["warning"]:
                alertes.append({
                    "niveau": NiveauAlerte.WARNING,
                    "message": (
                        f"Taux d'erreur élevé pour {modele}: "
                        f"{taux:.1%}"
                    ),
                    "modele": modele,
                })

            # Latence
            p99 = metriques.percentile(modele, 99)
            if p99 >= self.seuils["latence_p99"]["critical"]:
                alertes.append({
                    "niveau": NiveauAlerte.CRITICAL,
                    "message": (
                        f"Latence p99 CRITIQUE pour {modele}: "
                        f"{p99:.1f}s"
                    ),
                    "modele": modele,
                })

        self.alertes_envoyees.extend(alertes)
        return alertes

    async def notifier(self, alertes: list[dict]):
        """Envoie les notifications (webhook, email, Slack)."""
        import httpx

        for alerte in alertes:
            if alerte["niveau"] == NiveauAlerte.CRITICAL:
                # Notification immédiate
                async with httpx.AsyncClient() as http:
                    await http.post(
                        "https://votre-webhook.example.com/alertes",
                        json={
                            "niveau": alerte["niveau"].value,
                            "message": alerte["message"],
                        },
                    )

Scaling : absorber la charge

Circuit breaker

Protégez votre application quand l’API est en panne :

class CircuitBreaker:
    """Coupe les appels quand trop d'erreurs se produisent."""

    def __init__(
        self,
        seuil_ouverture: int = 5,
        delai_reset: float = 60.0,
    ):
        self.seuil = seuil_ouverture
        self.delai_reset = delai_reset
        self.echecs_consecutifs = 0
        self.ouvert_depuis: float | None = None

    @property
    def est_ouvert(self) -> bool:
        if self.ouvert_depuis is None:
            return False
        if time.time() - self.ouvert_depuis > self.delai_reset:
            # Tenter de fermer le circuit
            self.ouvert_depuis = None
            self.echecs_consecutifs = 0
            return False
        return True

    def enregistrer_succes(self):
        self.echecs_consecutifs = 0
        self.ouvert_depuis = None

    def enregistrer_echec(self):
        self.echecs_consecutifs += 1
        if self.echecs_consecutifs >= self.seuil:
            self.ouvert_depuis = time.time()

    def appeler(self, func, *args, **kwargs):
        """Exécute la fonction si le circuit est fermé."""
        if self.est_ouvert:
            raise RuntimeError(
                "Circuit ouvert — API indisponible. "
                f"Retry dans {self.delai_reset}s."
            )
        try:
            resultat = func(*args, **kwargs)
            self.enregistrer_succes()
            return resultat
        except Exception as e:
            self.enregistrer_echec()
            raise

Fallback entre modèles

class FallbackModeles:
    """Bascule vers un modèle de secours en cas de panne."""

    def __init__(self):
        self.modeles = ["gpt-5.3", "gpt-5.4", "o4-mini"]
        self.circuits = {
            m: CircuitBreaker() for m in self.modeles
        }

    def appeler(self, prompt: str) -> str:
        """Essaie chaque modèle jusqu'à obtenir une réponse."""
        client = openai.OpenAI()

        for modele in self.modeles:
            circuit = self.circuits[modele]
            if circuit.est_ouvert:
                continue

            try:
                response = circuit.appeler(
                    client.responses.create,
                    model=modele,
                    input=prompt,
                )
                return response.output_text
            except Exception:
                continue

        raise RuntimeError("Tous les modèles sont indisponibles.")

Auto-scaling basé sur la file d’attente

class AutoScaler:
    """Ajuste le nombre de workers selon la charge."""

    def __init__(
        self,
        workers_min: int = 2,
        workers_max: int = 50,
        seuil_scale_up: int = 100,
        seuil_scale_down: int = 10,
    ):
        self.workers_min = workers_min
        self.workers_max = workers_max
        self.seuil_up = seuil_scale_up
        self.seuil_down = seuil_scale_down
        self.workers_actifs = workers_min

    def ajuster(self, taille_file: int) -> int:
        """Retourne le nombre de workers recommandé."""
        if taille_file > self.seuil_up:
            self.workers_actifs = min(
                self.workers_actifs * 2,
                self.workers_max,
            )
        elif taille_file < self.seuil_down:
            self.workers_actifs = max(
                self.workers_actifs // 2,
                self.workers_min,
            )

        return self.workers_actifs

Checklist de mise en production

Avant de déployer votre application LLM, vérifiez ces points :

  • Monitoring des latences (p50, p95, p99) et des taux d’erreur
  • Alertes configurées sur les seuils critiques
  • Circuit breaker pour protéger contre les pannes API
  • Fallback entre modèles configuré
  • Modération des entrées et sorties activée
  • Rate limiting côté client implémenté
  • Logs structurés pour le debugging
  • Évals automatisées dans la CI/CD
  • Budget de coût avec alertes
  • Documentation des prompts versionnée

Points clés à retenir

  • Le monitoring couvre latence, erreurs, coûts et qualité des réponses
  • Les alertes doivent être graduées : warning puis critical
  • Le circuit breaker protège contre les cascades de pannes
  • Le fallback entre modèles assure la disponibilité
  • Une checklist de production évite les oublis critiques