Gestion d'erreurs et résilience pour applications LangChain : Production-ready error handling

tl;dr: Les applications LLM doivent gérer de nombreuses sources d'erreurs : timeouts API, rate limits, erreurs réseau, hallucinations. Ce guide couvre les patterns de résilience : retry avec exponential backoff et jitter, circuit breakers, fallback chains, graceful degradation, timeout management, et monitoring des erreurs. Essentiels pour la production.

Les applications LLM sont intrinsèquement fragiles : APIs externes, rate limits, timeouts réseau, erreurs de parsing, hallucinations. Sans gestion d’erreurs robuste, votre application crashera en production. Dans cet article, nous couvrons l’ensemble des patterns de résilience pour construire des systèmes LangChain production-ready.

Diagramme d’architecture LangChain illustrant la gestion des erreurs et la résilience pour le développement d’applications IA

Pourquoi la résilience est critique ?

Les sources d’erreurs multiples

# ❌ DANGER : Application sans gestion d'erreurs
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate

llm = ChatOpenAI()
prompt = ChatPromptTemplate.from_template("Réponds: {query}")
chain = prompt | llm

# En production, cela CRASHERA :
result = chain.invoke({"query": user_input})

# Erreurs possibles :
# - RateLimitError (429) → trop de requêtes
# - Timeout → API lente
# - APIConnectionError → réseau instable
# - AuthenticationError → clé API invalide
# - ServiceUnavailableError (503) → OpenAI down
# - OutputParserException → parsing échoué
# - ValueError → input invalide

Impact sans résilience

  • 🔴 Crash complet : Une erreur = application down
  • 💸 Perte de revenus : Downtime = clients mécontents
  • 😤 Expérience dégradée : Pas de réponse = frustration
  • 📉 Métriques : Taux d’erreur élevé
  • 🚨 Alertes : Oncall réveillé à 3h du matin

Classification des erreurs

Types d’erreurs et stratégies

# error_handling/error_classifier.py
from enum import Enum
from typing import Optional
import openai

class ErrorType(Enum):
    """Types d'erreurs avec stratégie associée"""
    RATE_LIMIT = "rate_limit"          # → Retry avec backoff
    TIMEOUT = "timeout"                 # → Retry immédiat
    API_CONNECTION = "api_connection"   # → Retry avec backoff
    AUTH = "authentication"             # → Fail fast (pas de retry)
    INVALID_INPUT = "invalid_input"     # → Fail fast
    PARSING = "parsing"                 # → Fallback ou fail
    SERVICE_UNAVAILABLE = "service_unavailable"  # → Retry avec backoff long
    UNKNOWN = "unknown"                 # → Retry avec limite

class ErrorClassifier:
    """Classifie les erreurs pour déterminer la stratégie"""

    @staticmethod
    def classify(error: Exception) -> ErrorType:
        """Classifie une erreur"""
        error_str = str(error).lower()

        # OpenAI errors
        if isinstance(error, openai.RateLimitError):
            return ErrorType.RATE_LIMIT
        elif isinstance(error, openai.Timeout):
            return ErrorType.TIMEOUT
        elif isinstance(error, openai.APIConnectionError):
            return ErrorType.API_CONNECTION
        elif isinstance(error, openai.AuthenticationError):
            return ErrorType.AUTH
        elif isinstance(error, openai.APIStatusError):
            if error.status_code == 503:
                return ErrorType.SERVICE_UNAVAILABLE
            return ErrorType.UNKNOWN

        # Parsing errors
        elif "parsing" in error_str or "outputparser" in error_str:
            return ErrorType.PARSING

        # Validation errors
        elif isinstance(error, (ValueError, TypeError)):
            return ErrorType.INVALID_INPUT

        return ErrorType.UNKNOWN

    @staticmethod
    def should_retry(error_type: ErrorType) -> bool:
        """Détermine si on doit retry"""
        NO_RETRY = {ErrorType.AUTH, ErrorType.INVALID_INPUT}
        return error_type not in NO_RETRY

    @staticmethod
    def get_max_retries(error_type: ErrorType) -> int:
        """Nombre maximum de retries selon le type"""
        return {
            ErrorType.RATE_LIMIT: 5,
            ErrorType.TIMEOUT: 3,
            ErrorType.API_CONNECTION: 4,
            ErrorType.SERVICE_UNAVAILABLE: 6,
            ErrorType.PARSING: 2,
            ErrorType.UNKNOWN: 3,
            ErrorType.AUTH: 0,
            ErrorType.INVALID_INPUT: 0,
        }.get(error_type, 3)

Retry avec exponential backoff

Pattern de base

# error_handling/retry.py
import time
import random
from typing import Callable, TypeVar, Optional
from functools import wraps

T = TypeVar('T')

def exponential_backoff(
    max_retries: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 60.0,
    exponential_base: float = 2.0,
    jitter: bool = True
):
    """
    Décorateur pour retry avec exponential backoff

    Args:
        max_retries: Nombre max de tentatives
        base_delay: Délai initial en secondes
        max_delay: Délai maximum
        exponential_base: Base pour calcul exponentiel
        jitter: Ajouter randomness pour éviter thundering herd
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        def wrapper(*args, **kwargs) -> T:
            last_exception = None

            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e

                    # Classifier l'erreur
                    from error_handling.error_classifier import ErrorClassifier
                    error_type = ErrorClassifier.classify(e)

                    # Ne pas retry si erreur non-retryable
                    if not ErrorClassifier.should_retry(error_type):
                        raise

                    # Dernier essai
                    if attempt == max_retries:
                        raise

                    # Calculer le délai
                    delay = min(
                        base_delay * (exponential_base ** attempt),
                        max_delay
                    )

                    # Ajouter jitter
                    if jitter:
                        delay = delay * (0.5 + random.random())

                    print(f"⚠️  Tentative {attempt + 1}/{max_retries} échouée: {e}")
                    print(f"   Retry dans {delay:.2f}s...")
                    time.sleep(delay)

            # Si on arrive ici, toutes les tentatives ont échoué
            raise last_exception

        return wrapper
    return decorator

Utilisation avec LangChain

# chains/resilient_chain.py
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from error_handling.retry import exponential_backoff

class ResilientChain:
    """Chain avec retry automatique"""

    def __init__(self):
        self.llm = ChatOpenAI(
            temperature=0,
            request_timeout=30  # Timeout de 30s
        )
        self.prompt = ChatPromptTemplate.from_template("Réponds: {query}")
        self.chain = self.prompt | self.llm

    @exponential_backoff(max_retries=3, base_delay=1.0, jitter=True)
    def invoke(self, query: str) -> str:
        """Invoke avec retry automatique"""
        result = self.chain.invoke({"query": query})
        return result.content
# Utilisation
chain = ResilientChain()

try:
    response = chain.invoke("Quelle est la capitale de la France ?")
    print(response)
except Exception as e:
    print(f"❌ Échec après tous les retries: {e}")

Retry adaptatif selon l’erreur

# error_handling/adaptive_retry.py
import time
import random
from error_handling.error_classifier import ErrorClassifier, ErrorType

class AdaptiveRetry:
    """Retry avec stratégie adaptée au type d'erreur"""

    @staticmethod
    def get_delay(error_type: ErrorType, attempt: int) -> float:
        """Calcule le délai selon le type d'erreur"""
        if error_type == ErrorType.RATE_LIMIT:
            # Backoff agressif pour rate limits
            return min(2 ** attempt, 120)  # Max 2 minutes

        elif error_type == ErrorType.TIMEOUT:
            # Retry rapide pour timeouts
            return 0.5 + (attempt * 0.5)

        elif error_type == ErrorType.SERVICE_UNAVAILABLE:
            # Backoff très long pour service down
            return min(5 ** attempt, 300)  # Max 5 minutes

        else:
            # Backoff standard
            return min(2 ** attempt, 60)

    @classmethod
    def retry_with_adaptive_backoff(cls, func, max_retries: int = 3):
        """Retry avec backoff adaptatif"""
        last_exception = None

        for attempt in range(max_retries + 1):
            try:
                return func()
            except Exception as e:
                last_exception = e
                error_type = ErrorClassifier.classify(e)

                if not ErrorClassifier.should_retry(error_type):
                    raise

                if attempt == max_retries:
                    raise

                # Délai adaptatif
                delay = cls.get_delay(error_type, attempt)

                # Jitter
                delay = delay * (0.5 + random.random())

                print(f"⚠️  {error_type.value}: Retry dans {delay:.1f}s")
                time.sleep(delay)

        raise last_exception

Circuit breaker pattern

Le circuit breaker empêche les appels répétés à un service défaillant.

Implémentation

# error_handling/circuit_breaker.py
from enum import Enum
from datetime import datetime, timedelta
from typing import Callable, Optional
import threading

class CircuitState(Enum):
    CLOSED = "closed"      # Fonctionne normalement
    OPEN = "open"          # Service down, rejette les appels
    HALF_OPEN = "half_open"  # Test si service revenu

class CircuitBreaker:
    """
    Circuit breaker pour protéger contre services défaillants

    États:
    - CLOSED: Normal, laisse passer les appels
    - OPEN: Trop d'erreurs, rejette les appels immédiatement
    - HALF_OPEN: Test après timeout pour voir si service revenu
    """

    def __init__(
        self,
        failure_threshold: int = 5,
        recovery_timeout: float = 60.0,
        expected_exception: type = Exception
    ):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.expected_exception = expected_exception

        self.failure_count = 0
        self.last_failure_time: Optional[datetime] = None
        self.state = CircuitState.CLOSED
        self.lock = threading.Lock()

    def call(self, func: Callable, *args, **kwargs):
        """Appelle la fonction via le circuit breaker"""
        with self.lock:
            # Si circuit OPEN, vérifier timeout
            if self.state == CircuitState.OPEN:
                if self._should_attempt_reset():
                    self.state = CircuitState.HALF_OPEN
                    print("🔶 Circuit HALF_OPEN: Test de récupération...")
                else:
                    raise CircuitBreakerOpenError(
                        f"Circuit breaker OPEN. Retry dans "
                        f"{self._time_until_retry():.0f}s"
                    )

        # Tenter l'appel
        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except self.expected_exception as e:
            self._on_failure()
            raise

    def _should_attempt_reset(self) -> bool:
        """Détermine si on doit tenter de reset le circuit"""
        return (
            self.last_failure_time is not None
            and datetime.now() - self.last_failure_time
            >= timedelta(seconds=self.recovery_timeout)
        )

    def _time_until_retry(self) -> float:
        """Temps restant avant retry"""
        if self.last_failure_time is None:
            return 0
        elapsed = (datetime.now() - self.last_failure_time).total_seconds()
        return max(0, self.recovery_timeout - elapsed)

    def _on_success(self):
        """Succès: reset le compteur"""
        with self.lock:
            self.failure_count = 0
            if self.state == CircuitState.HALF_OPEN:
                self.state = CircuitState.CLOSED
                print("✅ Circuit CLOSED: Service récupéré")

    def _on_failure(self):
        """Échec: incrémenter le compteur"""
        with self.lock:
            self.failure_count += 1
            self.last_failure_time = datetime.now()

            if self.state == CircuitState.HALF_OPEN:
                # Échec pendant test → retour à OPEN
                self.state = CircuitState.OPEN
                print("❌ Circuit OPEN: Service toujours down")

            elif self.failure_count >= self.failure_threshold:
                # Trop d'échecs → OPEN
                self.state = CircuitState.OPEN
                print(f"🔴 Circuit OPEN: {self.failure_count} échecs consécutifs")

class CircuitBreakerOpenError(Exception):
    """Levée quand le circuit breaker est ouvert"""
    pass

Utilisation avec LangChain

# Créer un circuit breaker pour OpenAI
openai_breaker = CircuitBreaker(
    failure_threshold=5,      # 5 échecs → OPEN
    recovery_timeout=60.0,    # Test après 60s
    expected_exception=Exception
)

def call_llm_with_breaker(query: str) -> str:
    """Appel LLM protégé par circuit breaker"""
    def _call():
        chain = prompt | llm
        result = chain.invoke({"query": query})
        return result.content

    return openai_breaker.call(_call)

# Utilisation
try:
    response = call_llm_with_breaker("Test")
    print(response)
except CircuitBreakerOpenError as e:
    print(f"⚠️  Service temporairement indisponible: {e}")
    # Utiliser un fallback

Fallback chains

Si la chain principale échoue, basculer sur une alternative.

Fallback simple

# chains/fallback_chain.py
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain.prompts import ChatPromptTemplate
from typing import Optional

class FallbackChain:
    """Chain avec fallback automatique"""

    def __init__(self):
        # Primary: OpenAI GPT-4
        self.primary_llm = ChatOpenAI(model="gpt-4", temperature=0)

        # Fallback 1: OpenAI GPT-3.5
        self.fallback1_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

        # Fallback 2: Claude
        self.fallback2_llm = ChatAnthropic(model="claude-3-sonnet-20240229")

        self.prompt = ChatPromptTemplate.from_template("Réponds: {query}")

    def invoke(self, query: str) -> dict:
        """Invoke avec fallback automatique"""
        llms = [
            ("GPT-4", self.primary_llm),
            ("GPT-3.5", self.fallback1_llm),
            ("Claude", self.fallback2_llm)
        ]

        errors = []

        for name, llm in llms:
            try:
                chain = self.prompt | llm
                result = chain.invoke({"query": query})
                return {
                    "response": result.content,
                    "model_used": name,
                    "fallback_used": name != "GPT-4"
                }
            except Exception as e:
                print(f"⚠️  {name} échoué: {e}")
                errors.append(f"{name}: {e}")
                continue

        # Tous les fallbacks ont échoué
        raise Exception(f"Tous les modèles ont échoué: {errors}")

Fallback avec dégradation de qualité

# chains/graceful_degradation.py
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate

class GracefulDegradationChain:
    """Chain avec dégradation gracieuse de qualité"""

    def __init__(self):
        # Niveaux de qualité décroissants
        self.tiers = [
            {
                "name": "Premium",
                "model": "gpt-4",
                "temperature": 0,
                "max_tokens": 2000
            },
            {
                "name": "Standard",
                "model": "gpt-3.5-turbo",
                "temperature": 0,
                "max_tokens": 1000
            },
            {
                "name": "Basic",
                "model": "gpt-3.5-turbo",
                "temperature": 0.3,
                "max_tokens": 500
            },
        ]

        self.prompt = ChatPromptTemplate.from_template(
            "Réponds de manière concise: {query}"
        )

    def invoke(self, query: str) -> dict:
        """Invoke avec dégradation gracieuse"""
        for tier in self.tiers:
            try:
                llm = ChatOpenAI(
                    model=tier["model"],
                    temperature=tier["temperature"],
                    max_tokens=tier["max_tokens"]
                )
                chain = self.prompt | llm
                result = chain.invoke({"query": query})

                return {
                    "response": result.content,
                    "quality_tier": tier["name"],
                    "degraded": tier["name"] != "Premium"
                }
            except Exception as e:
                print(f"⚠️  Tier {tier['name']} échoué, dégradation...")
                continue

        # Fallback ultime: réponse statique
        return {
            "response": "Service temporairement indisponible. Veuillez réessayer.",
            "quality_tier": "Emergency",
            "degraded": True
        }

Timeout management

Timeouts à tous les niveaux

# chains/timeout_chain.py
import signal
from contextlib import contextmanager
from typing import Optional

class TimeoutError(Exception):
    """Levée quand un timeout est atteint"""
    pass

@contextmanager
def time_limit(seconds: int):
    """Context manager pour timeout"""
    def signal_handler(signum, frame):
        raise TimeoutError(f"Timeout après {seconds}s")

    # Configurer l'alarme
    signal.signal(signal.SIGALRM, signal_handler)
    signal.alarm(seconds)

    try:
        yield
    finally:
        # Désactiver l'alarme
        signal.alarm(0)

class TimeoutChain:
    """Chain avec timeouts configurables"""

    def __init__(
        self,
        llm_timeout: int = 30,
        total_timeout: int = 60
    ):
        self.llm = ChatOpenAI(
            request_timeout=llm_timeout  # Timeout pour chaque appel LLM
        )
        self.total_timeout = total_timeout
        self.prompt = ChatPromptTemplate.from_template("Réponds: {query}")

    def invoke(self, query: str, timeout: Optional[int] = None) -> str:
        """Invoke avec timeout global"""
        timeout = timeout or self.total_timeout

        try:
            with time_limit(timeout):
                chain = self.prompt | self.llm
                result = chain.invoke({"query": query})
                return result.content
        except TimeoutError as e:
            raise TimeoutError(
                f"Chain timeout après {timeout}s. "
                f"Augmentez le timeout ou simplifiez la requête."
            )

Timeouts avec asyncio

# chains/async_timeout_chain.py
import asyncio
from langchain_openai import ChatOpenAI

class AsyncTimeoutChain:
    """Chain asynchrone avec timeout"""

    def __init__(self, timeout: float = 30.0):
        self.llm = ChatOpenAI(temperature=0)
        self.timeout = timeout
        self.prompt = ChatPromptTemplate.from_template("Réponds: {query}")

    async def ainvoke(self, query: str) -> str:
        """Invoke async avec timeout"""
        chain = self.prompt | self.llm

        try:
            result = await asyncio.wait_for(
                chain.ainvoke({"query": query}),
                timeout=self.timeout
            )
            return result.content
        except asyncio.TimeoutError:
            raise TimeoutError(f"Timeout après {self.timeout}s")

# Utilisation
async def main():
    chain = AsyncTimeoutChain(timeout=10.0)
    try:
        response = await chain.ainvoke("Question complexe...")
        print(response)
    except TimeoutError:
        print("⏱️  Timeout")

# asyncio.run(main())

Recovery automatique

Auto-healing chain

# chains/self_healing_chain.py
from typing import Optional, Dict
import time

class SelfHealingChain:
    """Chain avec auto-correction"""

    def __init__(self):
        self.llm = ChatOpenAI(temperature=0)
        self.prompt = ChatPromptTemplate.from_template("Réponds: {query}")
        self.chain = self.prompt | self.llm

        # Statistiques pour auto-healing
        self.error_count = 0
        self.success_count = 0
        self.last_errors = []

    def invoke(self, query: str, max_attempts: int = 3) -> Dict:
        """Invoke avec auto-correction"""
        for attempt in range(max_attempts):
            try:
                result = self._invoke_with_monitoring(query)
                self._on_success()
                return result

            except OutputParserException as e:
                # Erreur de parsing → ajuster le prompt
                print(f"⚠️  Parsing error, ajustement du prompt...")
                self._adjust_prompt_for_parsing()
                continue

            except Exception as e:
                self._on_error(e)

                # Si trop d'erreurs, mode dégradé
                if self.error_count > 10:
                    return self._degraded_response(query)

                raise

    def _invoke_with_monitoring(self, query: str) -> Dict:
        """Invoke avec monitoring"""
        start = time.time()
        result = self.chain.invoke({"query": query})
        latency = time.time() - start

        return {
            "response": result.content,
            "latency_ms": latency * 1000,
            "model": "gpt-4"
        }

    def _on_success(self):
        """Succès → reset compteur d'erreurs"""
        self.success_count += 1
        if self.success_count > 5:
            self.error_count = max(0, self.error_count - 1)

    def _on_error(self, error: Exception):
        """Erreur → enregistrer"""
        self.error_count += 1
        self.last_errors.append({
            "timestamp": time.time(),
            "error": str(error)
        })

        # Garder seulement les 10 dernières
        self.last_errors = self.last_errors[-10:]

    def _adjust_prompt_for_parsing(self):
        """Ajuster le prompt pour améliorer le parsing"""
        # Ajouter des instructions de format
        self.prompt = ChatPromptTemplate.from_template(
            "Réponds à cette question de manière structurée.\n"
            "Format: Réponse simple et claire.\n\n"
            "Question: {query}"
        )
        self.chain = self.prompt | self.llm

    def _degraded_response(self, query: str) -> Dict:
        """Réponse dégradée si trop d'erreurs"""
        return {
            "response": "Service en mode dégradé. Veuillez réessayer plus tard.",
            "degraded": True,
            "error_count": self.error_count
        }

Monitoring des erreurs

Error tracker

# monitoring/error_tracker.py
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Dict, List
import json

class ErrorTracker:
    """Track et analyse les erreurs"""

    def __init__(self):
        self.errors: List[Dict] = []
        self.error_counts = defaultdict(int)

    def track_error(
        self,
        error: Exception,
        context: Dict = None
    ):
        """Enregistre une erreur"""
        from error_handling.error_classifier import ErrorClassifier

        error_type = ErrorClassifier.classify(error)

        error_entry = {
            "timestamp": datetime.now().isoformat(),
            "type": error_type.value,
            "message": str(error),
            "context": context or {}
        }

        self.errors.append(error_entry)
        self.error_counts[error_type.value] += 1

        # Log en JSON
        print(json.dumps(error_entry))

    def get_error_rate(self, window_minutes: int = 60) -> float:
        """Calcule le taux d'erreur sur une période"""
        cutoff = datetime.now() - timedelta(minutes=window_minutes)

        recent_errors = [
            e for e in self.errors
            if datetime.fromisoformat(e["timestamp"]) > cutoff
        ]

        return len(recent_errors) / max(window_minutes, 1)

    def get_top_errors(self, limit: int = 5) -> List[tuple]:
        """Top N erreurs les plus fréquentes"""
        sorted_errors = sorted(
            self.error_counts.items(),
            key=lambda x: x[1],
            reverse=True
        )
        return sorted_errors[:limit]

    def should_alert(self, threshold_per_minute: float = 1.0) -> bool:
        """Détermine si on doit alerter"""
        error_rate = self.get_error_rate(window_minutes=5)
        return error_rate > threshold_per_minute

Intégration complète

# chains/production_chain.py
from error_handling.retry import exponential_backoff
from error_handling.circuit_breaker import CircuitBreaker
from monitoring.error_tracker import ErrorTracker

class ProductionChain:
    """Chain production-ready avec toutes les protections"""

    def __init__(self):
        self.llm = ChatOpenAI(
            temperature=0,
            request_timeout=30
        )
        self.prompt = ChatPromptTemplate.from_template("Réponds: {query}")

        # Protection
        self.circuit_breaker = CircuitBreaker(
            failure_threshold=5,
            recovery_timeout=60.0
        )

        # Monitoring
        self.error_tracker = ErrorTracker()

        # Fallback
        self.fallback_llm = ChatOpenAI(model="gpt-3.5-turbo")

    @exponential_backoff(max_retries=3, base_delay=1.0, jitter=True)
    def invoke(self, query: str) -> Dict:
        """Invoke avec toutes les protections"""
        try:
            # Via circuit breaker
            def _call():
                chain = self.prompt | self.llm
                return chain.invoke({"query": query})

            result = self.circuit_breaker.call(_call)

            return {
                "response": result.content,
                "model": "gpt-4",
                "fallback_used": False
            }

        except Exception as e:
            # Tracker l'erreur
            self.error_tracker.track_error(e, context={"query": query})

            # Alerter si trop d'erreurs
            if self.error_tracker.should_alert():
                print("🚨 ALERTE: Taux d'erreur élevé!")

            # Fallback
            try:
                print("⚠️  Utilisation du fallback (GPT-3.5)...")
                chain = self.prompt | self.fallback_llm
                result = chain.invoke({"query": query})

                return {
                    "response": result.content,
                    "model": "gpt-3.5-turbo",
                    "fallback_used": True
                }
            except Exception as fallback_error:
                # Fallback ultime
                return {
                    "response": "Service temporairement indisponible.",
                    "model": "emergency",
                    "fallback_used": True,
                    "error": str(fallback_error)
                }

Patterns avancés

Bulkhead pattern

# error_handling/bulkhead.py
import threading
from queue import Queue
from typing import Callable

class Bulkhead:
    """Isolation des ressources pour éviter cascade failure"""

    def __init__(self, max_concurrent: int = 10, queue_size: int = 100):
        self.semaphore = threading.Semaphore(max_concurrent)
        self.queue = Queue(maxsize=queue_size)
        self.max_concurrent = max_concurrent

    def execute(self, func: Callable, *args, **kwargs):
        """Exécute avec limitation de concurrence"""
        if not self.semaphore.acquire(blocking=False):
            raise BulkheadFullError(
                f"Bulkhead plein ({self.max_concurrent} requêtes en cours)"
            )

        try:
            return func(*args, **kwargs)
        finally:
            self.semaphore.release()

class BulkheadFullError(Exception):
    pass

Retry budgets

# error_handling/retry_budget.py
from datetime import datetime, timedelta

class RetryBudget:
    """Limite le nombre de retries sur une période"""

    def __init__(self, max_retries_per_minute: int = 100):
        self.max_retries = max_retries_per_minute
        self.retry_history = []

    def can_retry(self) -> bool:
        """Vérifie si on a du budget retry"""
        # Nettoyer l'historique (> 1 minute)
        cutoff = datetime.now() - timedelta(minutes=1)
        self.retry_history = [
            t for t in self.retry_history if t > cutoff
        ]

        return len(self.retry_history) < self.max_retries

    def consume(self):
        """Consomme 1 retry du budget"""
        if not self.can_retry():
            raise RetryBudgetExceededError(
                "Budget de retry épuisé (trop de retries par minute)"
            )

        self.retry_history.append(datetime.now())

class RetryBudgetExceededError(Exception):
    pass

Checklist de résilience

Avant le déploiement

  • Classification des erreurs : Toutes les erreurs sont classifiées
  • Retry strategy : Exponential backoff avec jitter
  • Max retries : Limites configurées selon type d’erreur
  • Circuit breakers : Activés pour services externes
  • Fallbacks : Au moins 1 fallback par chain critique
  • Timeouts : Définis à tous les niveaux (LLM, chain, global)
  • Graceful degradation : Mode dégradé implémenté
  • Error tracking : Toutes les erreurs sont loggées
  • Alerting : Alertes sur taux d’erreur élevé
  • Monitoring : Dashboards pour erreurs par type

En production

  • Error rate : < 1% d’erreurs
  • Circuit breaker trips : < 5 par jour
  • Fallback usage : < 10% des requêtes
  • Timeout rate : < 0.5%
  • Recovery time : < 5 minutes après incident
  • Alerts : Pas de faux positifs
  • Runbooks : Procédures pour chaque type d’erreur

Ressources complémentaires

Documentation

Articles connexes


Conclusion

La résilience n’est pas optionnelle en production. Les applications LLM sont fragiles par nature, et sans gestion d’erreurs robuste, vous aurez des crashs fréquents.

Les patterns essentiels :

  1. Retry avec exponential backoff et jitter
  2. Circuit breakers pour protéger contre services down
  3. Fallbacks pour dégradation gracieuse
  4. Timeouts à tous les niveaux
  5. Monitoring et alerting sur erreurs

Avec ces patterns, vous construisez des systèmes production-ready qui résistent aux pannes et offrent une expérience utilisateur fiable.

Prochaine étape recommandée : Implémenter patterns async avancés pour optimiser les performances !


Retour à la Formation LangChain | Module suivant : Patterns Async Avancés →