# Applied AI — Niveau Avancé
# Session 4 : Architecture multi-agents

**Programme :** Applied AI — Formation professionnelle en intelligence artificielle
**Instructeur :** Yann Isola
**Niveau :** Avancé — Architectes solutions préparant la certification *Claude Certified Architect*
**Durée recommandée :** 3 h 30 (2 h de cours magistral + 1 h 30 d'exercices pratiques)
**Prérequis :** Sessions 1 à 3 du niveau avancé (orchestration, outils/function calling, gestion de contexte)

---

## Objectifs pédagogiques

À l'issue de cette session, les participants seront capables de :

1. **Choisir et justifier** une stratégie de décomposition de tâches (verticale, horizontale, récursive) selon la complexité, le budget de tokens et le bénéfice de spécialisation.
2. **Concevoir** une politique d'escalade complète avec Human-in-the-Loop (HITL — humain dans la boucle) : déclencheurs, motifs d'escalade, intégration du feedback humain et gestion des SLA (*Service Level Agreement* — accord de niveau de service).
3. **Implémenter** les patrons de gestion d'erreurs propres aux systèmes multi-agents : retry idempotent, checkpointing d'état, circuit breaker (disjoncteur logiciel), dégradation gracieuse et files de lettres mortes (DLQ — *Dead Letter Queue*).
4. **Instrumenter** un système multi-agents avec du traçage distribué et des identifiants de corrélation.

---

## Plan de la session

| Bloc | Durée | Contenu |
|------|-------|---------|
| 1 | 40 min | Stratégies de décomposition de tâches (Chapitre 8) |
| 2 | 40 min | Escalade et Human-in-the-Loop (Chapitre 9) |
| 3 | 40 min | Gestion d'erreurs multi-agents (Chapitre 10) |
| 4 | 90 min | Exercices pratiques + démonstration interactive (page web) |
| 5 | 10 min | Quiz de validation et synthèse |

---

# Bloc 1 — Stratégies de décomposition de tâches

## 1.1 Pourquoi décomposer ?

Un agent unique confronté à une tâche complexe rencontre trois murs :

- **Le mur du contexte** : la fenêtre de contexte (quantité de texte que le modèle peut traiter en une fois) est finie. Une tâche massive sature le contexte et dégrade la qualité des réponses.
- **Le mur de la cohérence** : plus une tâche est longue, plus le risque de dérive (perte de fil, contradictions internes) augmente.
- **Le mur de la spécialisation** : un prompt système généraliste est moins performant qu'un prompt spécialisé sur chaque sous-domaine.

La décomposition transforme un problème monolithique en un graphe de sous-tâches confiées à des agents dédiés, coordonnés par un **orchestrateur**.

> **Point pédagogique :** insistez sur le fait que la décomposition n'est pas gratuite. Chaque agent supplémentaire ajoute de la latence, du coût en tokens et de la complexité opérationnelle. La question n'est jamais « peut-on décomposer ? » mais « le gain dépasse-t-il le coût ? ».

## 1.2 Décomposition verticale (par profondeur)

**Principe :** découper le flux de travail en *étapes séquentielles* de nature différente. Le motif canonique :

```
Planification → Exécution → Vérification
```

- **Agent planificateur** : analyse la demande, produit un plan structuré (liste ordonnée de sous-tâches, dépendances, critères de succès).
- **Agent exécutant** : réalise chaque étape du plan (appels d'outils, génération de contenu, transformations).
- **Agent vérificateur** : contrôle le résultat contre les critères de succès, détecte les incohérences, déclenche des corrections.

**Avantages :**
- Chaque agent a un prompt système court et ciblé.
- Le vérificateur, indépendant de l'exécutant, réduit les biais d'auto-évaluation (un agent qui vérifie son propre travail est notoirement indulgent avec lui-même).
- Le plan sert de contrat : il est auditable et modifiable par un humain avant exécution.

**Limites :**
- Latence cumulée : chaque étage attend le précédent.
- Le plan peut devenir obsolète en cours d'exécution si l'environnement change (nécessite une boucle de replanification).

**Exemple d'implémentation (pseudocode Python) :**

```python
class VerticalPipeline:
    def __init__(self, planner, executor, verifier, max_repairs=2):
        self.planner = planner        # Agent LLM avec prompt "planificateur"
        self.executor = executor      # Agent LLM avec accès aux outils
        self.verifier = verifier      # Agent LLM avec prompt "auditeur"
        self.max_repairs = max_repairs

    def run(self, task: str) -> Result:
        plan = self.planner.plan(task)          # → liste d'étapes + critères
        result = self.executor.execute(plan)    # → artefacts produits
        for attempt in range(self.max_repairs):
            report = self.verifier.check(result, plan.success_criteria)
            if report.ok:
                return Result(status="success", artifacts=result)
            # Le rapport d'audit devient une consigne de réparation
            result = self.executor.repair(result, report.issues)
        return Result(status="needs_human_review", artifacts=result)
```

Notez la sortie `needs_human_review` : la décomposition verticale s'articule naturellement avec l'escalade (Bloc 2).

## 1.3 Décomposition horizontale (par domaine)

**Principe :** découper par *domaine de compétence*. Chaque agent est propriétaire d'un domaine et le traite de bout en bout.

Exemple — analyse d'un dossier de conformité financière :

```
                    ┌──────────────────┐
                    │  Orchestrateur   │
                    └────────┬─────────┘
         ┌──────────────┬────┴─────────┬───────────────┐
         ▼              ▼              ▼               ▼
  Agent juridique  Agent fiscal   Agent risque   Agent rédaction
  (réglementation) (imposition)   (scoring)      (synthèse finale)
```

- Les agents juridique, fiscal et risque travaillent **en parallèle** (gain de latence majeur).
- L'agent de rédaction fusionne les analyses (étape de *fan-in*, convergence des résultats).

**Avantages :**
- Parallélisme natif → latence proche de celle de l'agent le plus lent, pas de la somme.
- Spécialisation maximale : chaque agent embarque le vocabulaire, les outils et les garde-fous de son domaine.
- Isolation des pannes : l'échec de l'agent fiscal n'empêche pas l'agent juridique de livrer.

**Limites :**
- Risque d'incohérences inter-domaines (deux agents tirent des conclusions contradictoires) → nécessite une étape de réconciliation.
- L'orchestrateur doit savoir *router* : mal découper les frontières de domaines crée des zones grises où aucun agent ne se sent responsable.

## 1.4 Décomposition récursive

**Principe :** chaque agent peut lui-même décomposer sa sous-tâche et déléguer à des sous-agents, formant un arbre de délégation.

```
Orchestrateur racine
├── Agent A (sous-tâche 1)
│   ├── Sous-agent A1
│   └── Sous-agent A2
│       └── Sous-sous-agent A2a
└── Agent B (sous-tâche 2)
```

**Règles de sécurité indispensables :**

1. **Profondeur maximale** (`max_depth`) : sans limite, un agent peut créer une récursion infinie de délégations. Valeur typique : 2 à 3 niveaux. ⚠ (valeur indicative, à calibrer selon votre charge)
2. **Budget de tokens hérité** : chaque niveau reçoit une fraction du budget parent. Si le parent a 100 000 tokens, il n'en délègue par exemple que 30 000 par enfant.
3. **Contrat de résultat** : chaque sous-agent retourne un format structuré (JSON avec statut, artefacts, coût consommé), jamais du texte libre non contraint.

**Quand la récursion brille :** les problèmes à structure fractale — revue de code d'un monorepo (chaque module se décompose en fichiers), due diligence documentaire (chaque dossier se décompose en pièces), génération de sites (chaque page se décompose en sections).

## 1.5 Quand décomposer ? Les trois critères de décision

| Critère | Question à poser | Seuil indicatif |
|---------|------------------|-----------------|
| **Seuil de complexité** | La tâche comporte-t-elle plus de N étapes hétérogènes ? Un seul prompt système peut-il couvrir tous les cas ? | > 5–7 étapes distinctes ⚠ |
| **Budget de tokens** | Le contexte nécessaire (documents + historique + outils) dépasse-t-il ~60–70 % de la fenêtre ? ⚠ | Saturation prévisible → décomposer |
| **Bénéfice de spécialisation** | Des prompts spécialisés amélioreraient-ils mesurablement la qualité par domaine ? | Gain qualité > surcoût orchestration |

**Arbre de décision à présenter au tableau :**

1. La tâche tient-elle confortablement dans un seul contexte avec une qualité acceptable ? → **Ne décomposez pas.** (L'architecture la plus robuste est celle qui n'existe pas.)
2. Les étapes sont-elles de *natures* différentes (planifier vs exécuter vs vérifier) ? → **Verticale.**
3. Les sous-problèmes relèvent-ils de *domaines* différents et parallélisables ? → **Horizontale.**
4. La structure est-elle imprévisible ou fractale ? → **Récursive**, avec garde-fous stricts.
5. Cas réels : souvent **hybride** — un pipeline vertical dont l'étape d'exécution est horizontale.

> **Piège de certification :** les questions d'examen opposent souvent verticale et horizontale sur un cas ambigu. Le discriminant fiable : *séquentialité de natures différentes* → verticale ; *parallélisme de domaines* → horizontale.

---

# Bloc 2 — Escalade et Human-in-the-Loop (HITL)

## 2.1 Pourquoi l'escalade est une exigence d'architecture, pas une option

Aucun système d'agents n'atteint 100 % d'autonomie fiable. L'architecte doit concevoir *dès le départ* les chemins par lesquels le système rend la main à l'humain. Un système sans escalade explicite escalade quand même — mais de façon chaotique : tickets support, incidents, perte de confiance.

**HITL (Human-in-the-Loop — humain dans la boucle)** désigne l'ensemble des mécanismes par lesquels un humain valide, corrige ou reprend le travail d'un agent.

## 2.2 Les trois déclencheurs d'escalade

### a) Confiance sous le seuil

L'agent (ou un modèle évaluateur) produit un score de confiance sur sa propre sortie. Sous un seuil défini, la tâche est escaladée.

```python
CONFIDENCE_THRESHOLD = 0.75  # ⚠ à calibrer sur données réelles, jamais au doigt mouillé

assessment = judge_agent.evaluate(answer, context)
if assessment.confidence < CONFIDENCE_THRESHOLD:
    escalate(task, reason="low_confidence", score=assessment.confidence)
```

**Point critique à enseigner :** les LLM (*Large Language Models* — grands modèles de langage) sont mal calibrés sur leur propre confiance auto-déclarée. Bonnes pratiques :
- Utiliser un **agent juge distinct** plutôt que l'auto-évaluation.
- Calibrer le seuil sur un jeu de validation étiqueté (courbe précision/taux d'escalade).
- Surveiller la dérive du taux d'escalade en production (un taux qui chute brutalement peut signaler un juge devenu complaisant, pas un système devenu meilleur).

### b) Détection hors périmètre (out-of-scope)

L'agent reconnaît que la demande sort de son domaine autorisé : sujet non couvert, langue non supportée, demande de conseil réglementé (juridique, médical, financier), tentative de contournement.

Implémentation typique : un **classifieur de périmètre** en amont (petit modèle rapide) + une consigne d'auto-détection dans le prompt système de l'agent (défense en profondeur — deux couches valent mieux qu'une).

### c) Niveau de risque

Certaines actions sont escaladées *par nature*, indépendamment de la confiance : virement au-delà d'un montant, suppression de données, communication externe engageante, modification de production. On définit une **matrice de risque** :

| Impact \ Réversibilité | Réversible | Irréversible |
|---|---|---|
| **Faible** | Autonome | Autonome + journalisation renforcée |
| **Moyen** | Autonome + revue a posteriori | Approbation humaine préalable |
| **Élevé** | Approbation humaine préalable | Approbation humaine + double validation |

## 2.3 Les trois patrons d'escalade

### Patron 1 — Pause-and-ask (pause et question)

L'agent **suspend** son exécution, pose une question précise à l'humain, attend la réponse, puis reprend. Synchrone du point de vue de la tâche.

- **Usage :** ambiguïté bloquante en cours de tâche, décision ponctuelle (choix entre deux options).
- **Exigence technique :** l'état de l'agent doit être **sérialisable** (sauvegardable) pour survivre à l'attente — l'humain peut répondre dans 4 heures.
- **Risque :** accumulation de tâches suspendues si les humains ne répondent pas → prévoir un timeout (voir SLA, §2.5).

### Patron 2 — Queue-for-review (file de revue)

L'agent **termine** son travail mais le résultat est placé dans une file de validation avant publication/exécution. Asynchrone, l'agent est libéré immédiatement.

- **Usage :** production de contenu à enjeu (réponses clients, documents contractuels), actions batch.
- **Avantage :** l'humain traite par lots, avec une interface de revue dédiée (diff, approuver/rejeter/corriger).
- **Métrique clé :** le taux d'approbation sans modification. S'il dépasse durablement ~98 % ⚠, envisager de passer certaines catégories en autonome (avec échantillonnage de contrôle).

### Patron 3 — Fallback-to-human (transfert à l'humain)

L'agent **abandonne** la tâche et la transfère intégralement à un humain, avec tout le contexte accumulé. C'est le filet de sécurité terminal.

- **Usage :** échec répété, situation hors périmètre, détresse utilisateur détectée, risque juridique.
- **Règle d'or du transfert :** l'humain ne doit **jamais repartir de zéro**. Le paquet de transfert contient : la demande initiale, l'historique des actions de l'agent, les hypothèses testées, la raison précise de l'escalade. Un transfert sans contexte détruit la valeur du travail agent.

## 2.4 Intégration du feedback humain

L'escalade n'est pas seulement un filet de sécurité — c'est un **capteur d'apprentissage**. Chaque correction humaine est une donnée.

**Boucle de feedback en quatre temps :**

1. **Capture** : chaque décision humaine (approbation, rejet, correction) est journalisée de façon structurée : `{task_id, sortie_agent, action_humaine, sortie_corrigée, motif}`.
2. **Agrégation** : analyse périodique des corrections par catégorie — quels types de tâches sont le plus souvent corrigés ? Quels motifs reviennent ?
3. **Injection** : les enseignements retournent dans le système par trois canaux, du plus léger au plus lourd :
   - **Prompt système** : ajout de règles et contre-exemples tirés des corrections récurrentes.
   - **Exemples few-shot / base de connaissances** : les paires (cas → correction validée) alimentent la récupération de contexte (RAG — *Retrieval-Augmented Generation*, génération augmentée par récupération).
   - **Fine-tuning** (ajustement fin du modèle) : uniquement si le volume de corrections est suffisant et le comportement cible stable. ⚠ Coûteux, à réévaluer à chaque évolution de modèle de base.
4. **Mesure** : le taux de correction par catégorie doit baisser après injection. Sinon, la boucle ne boucle pas — c'est du théâtre de feedback.

> **Point d'insistance pédagogique :** beaucoup d'organisations capturent le feedback mais ne l'injectent jamais. La question d'audit à poser : « montrez-moi la dernière modification de prompt motivée par une correction humaine, et sa date ».

## 2.5 Gestion des SLA d'escalade

Une escalade sans SLA (*Service Level Agreement* — accord de niveau de service) est une escalade vers un trou noir.

**Composants d'une politique de SLA :**

1. **Timeout d'escalade** : si aucun humain ne répond dans le délai, une action par défaut s'applique. Trois stratégies de timeout :
   - *Fail-safe* (sécurité par défaut) : la tâche est annulée ou mise en attente prolongée — pour les actions risquées.
   - *Fail-operational* : l'agent applique l'option conservatrice pré-approuvée — pour les tâches à faible enjeu.
   - *Ré-escalade* : la demande monte d'un niveau (n+1 hiérarchique, équipe d'astreinte).

2. **Files de priorité** : toutes les escalades ne se valent pas. File typique à trois niveaux :

| Priorité | Exemple | Délai cible ⚠ (indicatif) |
|---|---|---|
| P1 — Critique | Transaction bloquée, client en détresse, risque légal | < 15 min |
| P2 — Standard | Validation de contenu, ambiguïté non bloquante | < 4 h |
| P3 — Différable | Revue par échantillonnage, amélioration continue | < 48 h |

3. **Routage par compétence** : l'escalade juridique va au juriste, pas au support généraliste. Le paquet d'escalade porte des étiquettes (domaine, langue, urgence) exploitées par le routeur.

4. **Tableaux de bord** : taux d'escalade, temps de première réponse humaine, taux de dépassement de SLA, répartition par motif. Ce sont des métriques d'architecture, pas seulement d'exploitation.

---

# Bloc 3 — Gestion d'erreurs dans les systèmes multi-agents

## 3.1 La propagation d'erreurs : l'effet domino

Dans un système multi-agents, une erreur locale devient systémique par **cascade à travers l'orchestrateur** :

```
Sous-agent C échoue
      ↓
Agent B attend C → timeout → B échoue
      ↓
L'orchestrateur reçoit deux échecs corrélés → abandonne la mission entière
      ↓
Résultat : 45 minutes de travail des agents A, D, E jetées à la poubelle
```

**Trois principes anti-cascade :**

1. **Isolation (bulkheads — cloisons étanches)** : l'échec d'une branche ne doit pas invalider les branches indépendantes. L'orchestrateur agrège les résultats partiels : « 4 analyses sur 5 livrées, l'analyse fiscale a échoué ».
2. **Timeouts systématiques** : tout appel inter-agents porte un délai maximal. Un agent qui attend indéfiniment est une panne silencieuse.
3. **Typologie d'erreurs** : distinguer les erreurs *réessayables* (timeout réseau, rate limit — limite de débit d'API, surcharge temporaire) des erreurs *fatales* (entrée invalide, permission refusée, contrainte métier violée). Réessayer une erreur fatale gaspille du budget et retarde l'escalade.

```python
class AgentError(Exception):
    retryable: bool

class RateLimitError(AgentError):   retryable = True
class ToolTimeoutError(AgentError): retryable = True
class InvalidInputError(AgentError): retryable = False
class PolicyViolationError(AgentError): retryable = False
```

## 3.2 Stratégies de retry (nouvelle tentative)

### a) La condition préalable : l'idempotence des outils

Un outil est **idempotent** si l'exécuter deux fois produit le même effet qu'une fois. Sans idempotence, le retry est dangereux : rejouer « créer une facture » après un timeout ambigu peut créer un doublon (le timeout ne dit pas si la première tentative a abouti côté serveur).

**Technique standard : la clé d'idempotence** — le client génère un identifiant unique par *intention* d'opération ; le serveur déduplique.

```python
import uuid

def create_invoice(client_id: str, amount: float) -> Invoice:
    idem_key = f"inv-{uuid.uuid4()}"          # générée UNE fois par intention
    for attempt in range(3):
        try:
            return api.post("/invoices",
                            json={"client_id": client_id, "amount": amount},
                            headers={"Idempotency-Key": idem_key})
        except TransientError:
            sleep(backoff(attempt))            # backoff exponentiel + jitter
    raise ToolFailedError("create_invoice", idem_key)

def backoff(attempt: int) -> float:
    """Backoff exponentiel avec jitter (gigue aléatoire) pour éviter
    que tous les agents ne réessaient au même instant (thundering herd)."""
    import random
    return min(30, (2 ** attempt)) * random.uniform(0.5, 1.5)
```

> **Règle d'architecte :** dans la spécification de chaque outil exposé aux agents, documenter explicitement `idempotent: true/false`. L'orchestrateur n'autorise le retry automatique que sur les outils marqués idempotents.

### b) Checkpointing d'état (points de sauvegarde)

Pour les tâches longues, sauvegarder l'état à chaque jalon permet de reprendre après échec **sans tout rejouer** :

```python
class CheckpointedMission:
    def run(self, mission_id: str):
        state = self.store.load(mission_id) or MissionState.initial()
        for step in self.plan.steps:
            if step.id in state.completed_steps:
                continue                       # déjà fait : on saute
            result = self.execute(step, state)
            state.record(step.id, result)
            self.store.save(mission_id, state)  # checkpoint après CHAQUE jalon
```

Le checkpoint contient : étapes accomplies, artefacts produits, budget consommé, et le **contexte minimal** nécessaire à la reprise (pas tout l'historique de conversation — le résumer).

### c) Rollback partiel (annulation partielle)

Quand une étape échoue au milieu d'une séquence avec effets de bord, on annule les effets de la séquence *en cours* sans détruire le travail des séquences validées. Patron : **compensation** (chaque action possède une action inverse enregistrée). Inspiré du patron *Saga* des architectures microservices :

```
Étape 1 : réserver stock       → compensation : libérer stock
Étape 2 : débiter compte       → compensation : recréditer compte
Étape 3 : créer expédition ✗ ÉCHEC
→ Rejouer les compensations 2 puis 1 (ordre inverse), état cohérent restauré.
```

## 3.3 Le patron Circuit Breaker (disjoncteur logiciel)

**Problème :** un service externe (API d'outil, sous-agent) tombe en panne. Sans protection, chaque tâche continue de l'appeler, subit le timeout complet, gaspille tokens et latence, et surcharge le service déjà agonisant.

**Solution :** un disjoncteur avec trois états, comme un disjoncteur électrique :

```
                 N échecs consécutifs
   ┌─────────┐ ─────────────────────► ┌─────────┐
   │  FERMÉ  │                        │ OUVERT  │
   │(closed) │ ◄──── succès ────┐     │ (open)  │
   └─────────┘                  │     └────┬────┘
   appels passent               │          │ après délai de refroidissement
   normalement                  │          ▼ (cooldown)
                          ┌─────┴───────────┐
                          │   SEMI-OUVERT   │──── échec ────► retour à OUVERT
                          │   (half-open)   │
                          └─────────────────┘
                          laisse passer UN appel d'essai
```

- **FERMÉ (closed)** : fonctionnement normal ; on compte les échecs consécutifs.
- **OUVERT (open)** : après N échecs (ex. : 5 ⚠, à calibrer), les appels sont **rejetés immédiatement** sans être tentés. Le système échoue vite (*fail fast*) et le service en panne souffle.
- **SEMI-OUVERT (half-open)** : après un délai de refroidissement (ex. : 30 s ⚠), un unique appel d'essai passe. Succès → retour à FERMÉ. Échec → retour à OUVERT.

```python
import time

class CircuitBreaker:
    def __init__(self, failure_threshold=5, cooldown_s=30):
        self.failure_threshold = failure_threshold
        self.cooldown_s = cooldown_s
        self.failures = 0
        self.state = "closed"
        self.opened_at = None

    def call(self, fn, *args, **kwargs):
        if self.state == "open":
            if time.time() - self.opened_at >= self.cooldown_s:
                self.state = "half_open"       # tentative d'essai autorisée
            else:
                raise CircuitOpenError("appel rejeté : circuit ouvert")
        try:
            result = fn(*args, **kwargs)
        except TransientError:
            self._on_failure()
            raise
        self._on_success()
        return result

    def _on_failure(self):
        self.failures += 1
        if self.state == "half_open" or self.failures >= self.failure_threshold:
            self.state = "open"
            self.opened_at = time.time()

    def _on_success(self):
        self.failures = 0
        self.state = "closed"
```

**Spécificité multi-agents :** placer un disjoncteur *par dépendance* (un par outil externe, un par sous-agent), jamais un disjoncteur global. Quand un disjoncteur s'ouvre, l'orchestrateur doit le savoir pour activer la dégradation gracieuse (§3.4) plutôt que de laisser les tâches s'écraser contre lui.

## 3.4 Dégradation gracieuse

**Principe :** face à une panne partielle, **réduire la capacité plutôt qu'échouer complètement**. Le système livre un service diminué mais honnête.

**Échelle de dégradation (à concevoir a priori, pas dans la panique) :**

| Niveau | Situation | Comportement |
|---|---|---|
| 0 — Nominal | Tout fonctionne | Service complet |
| 1 — Dégradé léger | Un outil d'enrichissement en panne | Réponse sans l'enrichissement, mention explicite du manque |
| 2 — Dégradé fort | Sous-agent spécialisé indisponible | Réponse générique par l'agent principal + avertissement + proposition d'escalade |
| 3 — Minimal | Modèle principal indisponible | Bascule sur modèle de secours plus petit, périmètre restreint ⚠ |
| 4 — Refus honnête | Rien ne fonctionne | Message d'indisponibilité + capture de la demande pour traitement différé |

**Deux règles absolues :**
1. **Transparence** : ne jamais livrer une réponse dégradée en la présentant comme complète. « L'analyse fiscale n'a pas pu être réalisée (service indisponible) ; voici les 4 autres volets » — c'est acceptable. Le silence sur le manque ne l'est pas.
2. **Dégradation ≠ baisse de sécurité** : les garde-fous (filtres, validations, escalades) ne se dégradent jamais. On dégrade la *capacité*, pas le *contrôle*.

## 3.5 Dead Letter Queue (DLQ — file de lettres mortes)

**Principe :** une tâche qui a épuisé ses retries et ne peut être traitée n'est **jamais silencieusement supprimée**. Elle est déposée dans une file dédiée — la file de lettres mortes — avec tout son contexte de diagnostic.

**Contenu d'une entrée DLQ bien formée :**

```json
{
  "task_id": "task-8842",
  "correlation_id": "corr-a1b9f3",
  "deposited_at": "2026-07-02T14:31:07Z",
  "attempts": 3,
  "last_error": {"type": "ToolTimeoutError", "tool": "search_registry", "timeout_s": 60},
  "agent_chain": ["orchestrator", "agent-juridique", "sub-agent-registre"],
  "payload": { "...la tâche originale intacte..." },
  "state_checkpoint_ref": "ckpt://missions/8842/step-3"
}
```

**Exploitation de la DLQ :**
- **Alerte** : un dépôt en DLQ déclenche une notification (la DLQ qui grossit en silence est un anti-patron classique).
- **Rejeu** : après correction de la cause racine, les entrées sont rejouées — d'où l'importance des clés d'idempotence et des checkpoints.
- **Analyse de tendance** : la composition de la DLQ est un miroir des faiblesses du système. 80 % de `ToolTimeoutError` sur le même outil = problème d'infrastructure, pas d'agents.

## 3.6 Observabilité : traçage distribué et identifiants de corrélation

Dans un système où une demande traverse un orchestrateur, trois agents et sept appels d'outils, répondre à « pourquoi cette réponse est-elle fausse ? » sans traçage relève de l'archéologie.

### a) Identifiant de corrélation (correlation ID)

Un identifiant unique est généré à l'entrée de la demande et **propagé à travers chaque agent, chaque appel d'outil, chaque message de file**. Toute ligne de journal le porte.

```python
import contextvars, uuid

correlation_id = contextvars.ContextVar("correlation_id")

def handle_request(request):
    cid = request.headers.get("X-Correlation-ID") or f"corr-{uuid.uuid4().hex[:8]}"
    correlation_id.set(cid)
    log.info("request.received", cid=cid, task=request.task)
    return orchestrator.run(request, cid=cid)   # propagation explicite aux sous-agents
```

Règle : `grep corr-a1b9f3 logs/*` doit reconstituer **l'intégralité** du parcours d'une demande. Si un composant « perd » l'identifiant, l'observabilité est cassée à cet endroit précis.

### b) Traçage distribué (distributed tracing)

Au-delà des logs : chaque opération devient un **span** (segment chronométré) rattaché à une **trace** (l'arbre complet de la demande). Standard de facto : **OpenTelemetry** (spécification ouverte d'observabilité).

```
Trace corr-a1b9f3 (durée totale : 42,3 s)
└── span orchestrator.run                    [42,3 s]
    ├── span planner.plan                    [3,1 s]  ✓
    ├── span agent-juridique.analyze         [18,2 s] ✓
    │   ├── span tool.search_registry        [12,0 s] ✓  ← goulot identifié
    │   └── span llm.generate                [5,9 s]  ✓
    ├── span agent-fiscal.analyze            [17,8 s] ✗ TIMEOUT
    │   └── span tool.tax_api                [15,0 s] ✗  ← cause racine
    └── span redactor.merge                  [2,4 s]  ✓ (mode dégradé niveau 1)
```

**Attributs de span spécifiques aux agents LLM à capturer :** modèle utilisé, tokens entrée/sortie, coût estimé, nom de l'outil, décision d'escalade, état du disjoncteur, version du prompt système. Ces attributs transforment le traçage en outil d'optimisation de coûts et de qualité, pas seulement de débogage.

> **Question d'audit d'architecture (à poser telle quelle en entretien de certification) :** « Montrez-moi la trace complète d'une demande qui a échoué la semaine dernière, du point d'entrée jusqu'à la DLQ. » Si l'équipe ne peut pas, l'observabilité est déclarative, pas réelle.

---

# Synthèse : la grille de l'architecte

| Domaine | Question de conception | Artefact livrable |
|---|---|---|
| Décomposition | Verticale, horizontale, récursive — et pourquoi ? | Diagramme d'architecture + justification des 3 critères |
| Escalade | Quels déclencheurs, quels patrons, quels SLA ? | Politique d'escalade + matrice de risque |
| Erreurs | Que se passe-t-il quand X tombe ? (pour chaque X) | Carte des modes de panne + échelle de dégradation |
| Retry | Quels outils sont idempotents ? | Spécification d'outils annotée |
| Observabilité | Peut-on rejouer l'histoire d'une demande ? | Schéma de propagation du correlation ID |

**Message final aux futurs certifiés :** un système multi-agents n'est pas jugé sur son comportement quand tout va bien — n'importe quelle démo y arrive. Il est jugé sur son comportement quand un outil timeout, quand un agent hallucine, quand un humain ne répond pas. La certification évalue précisément cela : **l'architecture des mauvais jours**.

---

## Notes pour l'instructeur

- **Démo interactive (webpage/index.html)** : utilisez le simulateur de circuit breaker en direct — basculez les échecs et faites constater les transitions d'états par la classe. C'est le concept le plus abstrait de la session ; la simulation le rend tangible.
- **Erreur fréquente des participants** : confondre queue-for-review (l'agent finit, l'humain valide après) et pause-and-ask (l'agent s'arrête, l'humain débloque pendant). Insistez avec un exemple chronologique.
- **Lien certification** : les chapitres 8–10 pèsent lourd dans l'examen *Claude Certified Architect* ⚠ (pondération susceptible d'évoluer — vérifier le syllabus officiel en vigueur). Les questions croisent souvent deux chapitres (ex. : « un sous-agent échoue 5 fois : circuit breaker ou escalade ? » — réponse : les deux, dans cet ordre).
- **Timing** : si le temps manque, sacrifiez le §3.2c (rollback partiel/Saga) plutôt que l'observabilité — le traçage est systématiquement évalué.
