Monitoring IA en Production : Observabilité et Alertes Essentielles

Monitoring IA en Production : Observabilité et Alertes Essentielles

Votre modèle d’IA fonctionne parfaitement en développement, mais comment savez-vous qu’il tient ses promesses en production — aujourd’hui, demain, et dans six mois ? Selon le rapport State of AI de McKinsey 2024, seulement 35 % des entreprises qui déploient des modèles IA ont mis en place un système de surveillance efficace. Le résultat : des dégradations silencieuses qui s’accumulent pendant des semaines avant d’être détectées, souvent à la suite d’un incident visible. Le monitoring IA n’est pas optionnel — c’est la condition sine qua non d’un déploiement responsable.

Pourquoi le monitoring IA est différent du monitoring applicatif classique

Un système IA présente des défis spécifiques que les outils de monitoring traditionnels ne couvrent pas :

Le data drift : la distribution des données en entrée peut changer progressivement. Un modèle de détection de fraude entraîné sur 2023 peut se dégrader silencieusement si les patterns de fraude évoluent en 2025.

Le model drift (ou concept drift) : la relation entre les entrées et les sorties correctes peut changer, même si les données d’entrée restent stables. Exemple : un modèle de scoring crédit calibré avant une crise économique.

La non-déterminisme des LLM : contrairement à une fonction déterministe, un LLM peut produire des réponses très différentes pour des entrées similaires. Comment détecter qu’un LLM “hallucine” davantage qu’avant ?

Le coût variable : chaque appel à un LLM a un coût en tokens. Un bug ou un changement de comportement utilisateur peut faire exploser la facture en quelques heures.

Les métriques essentielles à surveiller

Métriques système (couche infrastructure)

Ces métriques sont similaires à tout système distribué :

MétriqueSeuil d’alerte typiqueOutil
Latence P50/P95/P99P95 > 2sPrometheus + Grafana
Taux d’erreur> 1 % sur 5 minAlertmanager
Disponibilité< 99.5 % sur 24hUptime Kuma
File d’attente> 100 requêtes en attentePrometheus
CPU/RAM> 80 % pendant > 10 minNode Exporter

Métriques LLM (couche modèle)

# Exemple d'instrumentation avec Prometheus
from prometheus_client import Counter, Histogram, Gauge
import time

# Compteurs
llm_requests_total = Counter(
    'llm_requests_total',
    'Nombre total de requêtes LLM',
    ['model', 'status', 'use_case']
)

# Histogramme de latence
llm_latency_seconds = Histogram(
    'llm_latency_seconds',
    'Latence des requêtes LLM en secondes',
    ['model', 'use_case'],
    buckets=[0.5, 1.0, 2.0, 5.0, 10.0, 30.0]
)

# Consommation de tokens
llm_tokens_total = Counter(
    'llm_tokens_total',
    'Tokens consommés',
    ['model', 'token_type']  # token_type: input ou output
)

# Coût estimé
llm_cost_usd = Counter(
    'llm_cost_usd_total',
    'Coût estimé en USD',
    ['model']
)

def call_llm_with_metrics(prompt: str, model: str, use_case: str) -> str:
    start = time.time()
    status = "success"
    
    try:
        response = call_llm(prompt, model)
        
        # Enregistrement des tokens
        llm_tokens_total.labels(model=model, token_type="input").inc(
            response.usage.input_tokens
        )
        llm_tokens_total.labels(model=model, token_type="output").inc(
            response.usage.output_tokens
        )
        
        # Coût (exemple pour Claude Sonnet)
        cost = (response.usage.input_tokens * 3 + response.usage.output_tokens * 15) / 1_000_000
        llm_cost_usd.labels(model=model).inc(cost)
        
        return response.content[0].text
        
    except Exception as e:
        status = "error"
        raise
    finally:
        llm_latency_seconds.labels(model=model, use_case=use_case).observe(
            time.time() - start
        )
        llm_requests_total.labels(
            model=model, status=status, use_case=use_case
        ).inc()

Métriques qualité des réponses

C’est la couche la plus difficile à monitorer car elle nécessite une définition de “bonne réponse” :

import hashlib
from datetime import datetime
import sqlite3

class LLMQualityMonitor:
    def __init__(self, db_path: str = "llm_quality.db"):
        self.conn = sqlite3.connect(db_path)
        self._init_db()
    
    def _init_db(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS llm_responses (
                id INTEGER PRIMARY KEY,
                timestamp TEXT,
                use_case TEXT,
                prompt_hash TEXT,
                response_length INTEGER,
                contains_refusal INTEGER,
                contains_hallucination_marker INTEGER,
                user_feedback INTEGER,  -- -1: négatif, 0: neutre, 1: positif
                latency_ms INTEGER
            )
        """)
        self.conn.commit()
    
    def log_response(self, use_case: str, prompt: str, response: str, latency_ms: int):
        prompt_hash = hashlib.md5(prompt.encode()).hexdigest()
        
        # Détection heuristique d'un refus ou d'une incertitude
        refusal_markers = [
            "je ne peux pas", "il m'est impossible", 
            "je n'ai pas accès", "en tant qu'IA"
        ]
        contains_refusal = any(m in response.lower() for m in refusal_markers)
        
        # Marqueurs d'hallucination potentielle
        hallucination_markers = [
            "selon mes informations", "il me semble que",
            "je crois que", "si je me souviens bien"
        ]
        contains_hallucination = any(m in response.lower() for m in hallucination_markers)
        
        self.conn.execute("""
            INSERT INTO llm_responses 
            (timestamp, use_case, prompt_hash, response_length, 
             contains_refusal, contains_hallucination_marker, latency_ms)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (
            datetime.now().isoformat(), use_case, prompt_hash,
            len(response), int(contains_refusal), int(contains_hallucination), latency_ms
        ))
        self.conn.commit()

Détection du drift

Data drift : surveiller la distribution des entrées

import numpy as np
from scipy import stats
from collections import deque

class DataDriftDetector:
    """
    Implémentation simplifiée du test de Kolmogorov-Smirnov
    pour détecter les changements de distribution.
    """
    
    def __init__(self, reference_window: int = 1000, detection_window: int = 200):
        self.reference_data = deque(maxlen=reference_window)
        self.current_data = deque(maxlen=detection_window)
        self.drift_threshold = 0.05  # p-value KS test
    
    def add_reference(self, value: float):
        self.reference_data.append(value)
    
    def check_drift(self, value: float) -> dict:
        self.current_data.append(value)
        
        if len(self.reference_data) < 100 or len(self.current_data) < 50:
            return {"drift_detected": False, "reason": "insufficient_data"}
        
        # Test KS : compare les deux distributions
        ks_stat, p_value = stats.ks_2samp(
            list(self.reference_data),
            list(self.current_data)
        )
        
        drift_detected = p_value < self.drift_threshold
        
        return {
            "drift_detected": drift_detected,
            "ks_statistic": float(ks_stat),
            "p_value": float(p_value),
            "severity": "high" if p_value < 0.01 else "medium" if drift_detected else "none"
        }

# Utilisation : surveiller la longueur des prompts entrants
prompt_length_monitor = DataDriftDetector()

def process_with_drift_check(prompt: str) -> str:
    drift_result = prompt_length_monitor.check_drift(len(prompt))
    
    if drift_result["drift_detected"]:
        send_alert(
            f"⚠️ Data drift détecté — Longueur des prompts",
            f"KS stat: {drift_result['ks_statistic']:.3f}, "
            f"p-value: {drift_result['p_value']:.4f}"
        )
    
    return call_llm(prompt)

Stack de monitoring recommandée

Pour une PME (budget limité)

# docker-compose.yml — stack monitoring légère
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
  
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret
    volumes:
      - grafana-data:/var/lib/grafana
  
  alertmanager:
    image: prom/alertmanager:latest
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
    ports:
      - "9093:9093"

volumes:
  grafana-data:

Configuration Alertmanager pour les notifications Slack :

# alertmanager.yml
global:
  slack_api_url: 'https://hooks.slack.com/services/T.../B.../xxx'

route:
  receiver: 'slack-alerts'
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

receivers:
  - name: 'slack-alerts'
    slack_configs:
      - channel: '#ia-monitoring'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'

Règles d’alerte Prometheus

# alerts.yml
groups:
  - name: llm_alerts
    rules:
      - alert: LLMHighLatency
        expr: histogram_quantile(0.95, rate(llm_latency_seconds_bucket[5m])) > 5
        for: 2m
        labels:
          severity: warning
        annotations:
          description: "Latence P95 LLM > 5s depuis 2 minutes"
      
      - alert: LLMHighErrorRate
        expr: rate(llm_requests_total{status="error"}[5m]) / rate(llm_requests_total[5m]) > 0.05
        for: 1m
        labels:
          severity: critical
        annotations:
          description: "Taux d'erreur LLM > 5% — intervention requise"
      
      - alert: LLMCostSpike
        expr: rate(llm_cost_usd_total[1h]) * 3600 > 50
        for: 5m
        labels:
          severity: warning
        annotations:
          description: "Projection coût LLM > 50 USD/heure — vérifier une boucle infinie"

Monitoring du coût : éviter les mauvaises surprises

Un budget cloud IA peut exploser en quelques heures en cas de bug (boucle infinie, prompt trop long, traffic inhabituel). Mettez en place des gardes-fous :

from functools import wraps
import threading

class CostGuard:
    def __init__(self, max_cost_per_hour_usd: float = 20.0):
        self.max_cost_per_hour = max_cost_per_hour_usd
        self.hourly_cost = 0.0
        self.lock = threading.Lock()
        self._reset_hourly_cost()
    
    def _reset_hourly_cost(self):
        self.hourly_cost = 0.0
        # Reset toutes les heures
        threading.Timer(3600, self._reset_hourly_cost).start()
    
    def check_and_add_cost(self, cost: float) -> bool:
        with self.lock:
            if self.hourly_cost + cost > self.max_cost_per_hour:
                send_critical_alert(
                    "🚨 Plafond de coût LLM atteint",
                    f"Coût horaire: {self.hourly_cost:.2f} USD"
                )
                return False  # Bloquer la requête
            self.hourly_cost += cost
            return True

cost_guard = CostGuard(max_cost_per_hour_usd=20.0)

Ce qu’il faut retenir

Le monitoring d’un système IA en production se déroule sur trois niveaux complémentaires : l’infrastructure (latence, disponibilité), la qualité des réponses (taux de refus, cohérence), et la dérive des données (drift). Chaque niveau nécessite des outils et des métriques spécifiques.

La bonne nouvelle : avec une stack open source (Prometheus + Grafana + Alertmanager), vous pouvez avoir une observabilité de niveau production pour moins de 50 €/mois d’infrastructure. L’investissement initial en instrumentation du code est amorti dès le premier incident évité.

La règle d’or : si vous ne pouvez pas le mesurer, vous ne pouvez pas le faire confiance en production.

Vous déployez un système IA et souhaitez mettre en place un monitoring robuste ? Notre équipe vous accompagne dans la conception de votre stratégie d’observabilité. Contactez-nous à contact@brio-novia.eu pour un audit de votre système actuel.