Agents en pratique#

Les chapitres précédents ont introduit les agents LLM, les frameworks qui les structurent et les architectures multi-agents. Il reste une question décisive : comment ces systèmes se comportent-ils dans le monde réel ? Passer d’un prototype de démonstration à un agent fiable en production exige bien plus qu’un prompt astucieux et un appel à un modèle de langage. Il faut concevoir des boucles de rétroaction, instrumenter chaque étape, gérer les erreurs inévitables et, surtout, savoir quand l’humain doit reprendre la main.

Ce chapitre adopte une perspective résolument pratique. Nous examinons d’abord les cas d’usage où les agents LLM apportent une valeur démontrée, puis nous construisons pas à pas deux agents concrets — un agent de recherche d’information et un agent d’analyse de code. Nous formalisons ensuite le patron human-in-the-loop, indispensable pour toute application à enjeux, et les techniques de débogage et d’observabilité qui permettent de comprendre ce qu’un agent fait réellement.

La dernière section consolide les patterns de robustesse nécessaires en production : stratégies de reprise sur erreur, contrôle des coûts, garde-fous sur les actions autorisées. L’objectif est de fournir un guide opérationnel pour quiconque envisage de déployer un agent LLM au-delà du stade expérimental.

Cas d’usage réels#

L’enthousiasme autour des agents LLM ne doit pas masquer une réalité : tous les cas d’usage ne sont pas mûrs. Certains domaines bénéficient déjà d’agents déployés en production ; d’autres restent au stade de la recherche.

Assistants de recherche. Les agents de recherche documentaire sont parmi les plus aboutis. Ils combinent la formulation de requêtes, l’extraction d’information depuis des sources hétérogènes et la synthèse avec citations. Des systèmes comme Perplexity AI ou les deep research de GPT illustrent cette catégorie. La valeur ajoutée est claire : un humain passerait des heures à faire ce qu’un agent réalise en minutes, avec un suivi des sources.

Agents de code. Les agents comme Devin (Cognition), Cursor Agent ou Claude Code représentent une avancée significative. Ils lisent du code, identifient des problèmes, proposent des corrections et exécutent des tests. Leur force réside dans la boucle écrire-exécuter-corriger qui mime le workflow d’un développeur. Les limites actuelles concernent les bases de code très volumineuses et les décisions architecturales de haut niveau.

Agents d’analyse de données. Ces agents reçoivent une question en langage naturel, génèrent du code d’analyse (pandas, SQL), l’exécutent, interprètent les résultats et produisent des visualisations. Ils excellent sur les requêtes exploratoires bien délimitées, mais peinent sur les analyses nécessitant une compréhension métier profonde.

Service client. Les agents conversationnels de support technique gèrent les demandes de premier niveau : diagnostic de problème, navigation dans la documentation, escalade vers un humain si nécessaire. Le taux de résolution autonome atteint 40–60 % dans les déploiements matures, avec un gain de temps considérable pour les équipes humaines.

Remarque 81

Un agent LLM est pertinent lorsque trois conditions sont réunies : (1) la tâche nécessite plusieurs étapes avec des décisions intermédiaires, (2) l’espace d’action est contraint et bien défini (outils limités, format de sortie structuré), et (3) le coût d’une erreur est gérable (vérification possible, rollback, supervision humaine). Si la tâche est simple et déterministe, un programme classique suffit. Si le coût d’une erreur est catastrophique et qu’aucune vérification n’est possible, un agent autonome n’est pas le bon choix.

Hide code cell source

# Import des librairies Python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import time
import json
import logging
from collections import defaultdict

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)

Hide code cell source

# Maturité des cas d'usage agents (données illustratives)
cas_usage = [
    "Recherche\ndocumentaire", "Agents de\ncode", "Analyse de\ndonnées",
    "Service\nclient", "Automatisation\nworkflow", "Raisonnement\nscientifique"
]
maturite = [85, 70, 65, 55, 40, 25]
colors = ['#55A868', '#55A868', '#4C72B0', '#4C72B0', '#DD8452', '#DD8452']

fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.barh(cas_usage, maturite, color=colors, edgecolor='white', linewidth=1.2)
ax.set_xlabel("Maturité estimée en production (%)")
ax.set_title("Maturité des cas d'usage d'agents LLM (estimation 2025)")
ax.set_xlim(0, 100)
ax.bar_label(bars, fmt='%d %%', padding=5, fontsize=9)

legend_elements = [
    mpatches.Patch(color='#55A868', label='Déployé en production'),
    mpatches.Patch(color='#4C72B0', label='Adoption croissante'),
    mpatches.Patch(color='#DD8452', label='Expérimental'),
]
ax.legend(handles=legend_elements, loc='lower right', fontsize=9)
plt.show()
_images/d122706e266bda07c1188af15b58871b281a27c1ee3dabbd4c0d2d7fdd0a6caa.png

Agent de recherche d’information#

Un agent de recherche suit un cycle multi-étapes : formulation de requête, recherche, extraction, synthèse et vérification. Chaque étape peut échouer ou nécessiter une itération, ce qui justifie l’architecture agentique plutôt qu’un simple appel de modèle.

Définition 77 (Agent de recherche)

Un agent de recherche d’information est un système qui, à partir d’une question en langage naturel, exécute de manière autonome un cycle itératif :

  1. Décomposition : transformer la question en sous-questions ciblées.

  2. Recherche : interroger une ou plusieurs sources (web, base de données, API).

  3. Extraction : identifier les passages pertinents dans les résultats.

  4. Synthèse : combiner les extraits en une réponse cohérente avec citations.

  5. Vérification : confronter la synthèse aux sources pour détecter les incohérences.

Le processus est itératif : si la vérification révèle des lacunes, l’agent reformule ses requêtes et recommence.

Exemple 57 (Simulation d’un agent de recherche)

L’exemple suivant simule un agent de recherche en Python pur. Les outils de recherche et d’extraction sont remplacés par des fonctions fictives, mais la structure de contrôle — boucle de décision, accumulation de contexte, vérification — est fidèle aux implémentations réelles.

Hide code cell source

# Simulation d'un agent de recherche d'information

def mock_search(query):
    """Simule un moteur de recherche renvoyant des résultats."""
    knowledge_base = {
        "transformers architecture": [
            {"title": "Attention Is All You Need", "snippet": "The Transformer architecture relies entirely on attention mechanisms, dispensing with recurrence.", "source": "Vaswani et al., 2017"},
            {"title": "BERT: Pre-training of Deep Bidirectional Transformers", "snippet": "BERT uses masked language modeling to learn bidirectional representations.", "source": "Devlin et al., 2019"},
        ],
        "LLM scaling laws": [
            {"title": "Scaling Laws for Neural Language Models", "snippet": "Performance improves predictably as a power law with model size, dataset size, and compute.", "source": "Kaplan et al., 2020"},
        ],
    }
    for key, results in knowledge_base.items():
        if any(word in query.lower() for word in key.split()):
            return results
    return [{"title": "Generic result", "snippet": "No specific information found.", "source": "N/A"}]

def mock_extract(results, question):
    """Extrait les passages pertinents des résultats."""
    return [
        {"text": r["snippet"], "source": r["source"], "relevance": np.random.uniform(0.6, 1.0)}
        for r in results
    ]

def mock_synthesize(extracts, question):
    """Synthétise les extraits en une réponse."""
    sources = [e["source"] for e in extracts]
    combined = " ".join(e["text"] for e in extracts)
    return {
        "answer": f"Synthèse pour '{question}': {combined}",
        "citations": sources,
        "confidence": np.mean([e["relevance"] for e in extracts])
    }

class ResearchAgent:
    """Agent de recherche multi-étapes."""

    def __init__(self, max_iterations=3, confidence_threshold=0.75):
        self.max_iterations = max_iterations
        self.confidence_threshold = confidence_threshold
        self.trace = []

    def run(self, question):
        all_extracts = []
        queries = [question]

        for i in range(self.max_iterations):
            step = {"iteration": i + 1, "queries": queries, "actions": []}

            # Recherche
            for q in queries:
                results = mock_search(q)
                step["actions"].append({"action": "search", "query": q, "n_results": len(results)})
                extracts = mock_extract(results, question)
                all_extracts.extend(extracts)

            # Synthèse
            synthesis = mock_synthesize(all_extracts, question)
            step["confidence"] = synthesis["confidence"]
            step["n_sources"] = len(synthesis["citations"])
            self.trace.append(step)

            # Vérification : confiance suffisante ?
            if synthesis["confidence"] >= self.confidence_threshold:
                return synthesis

            # Reformulation pour l'itération suivante
            queries = [f"{question} details", f"{question} recent advances"]

        return synthesis

# Exécution
agent = ResearchAgent(max_iterations=3, confidence_threshold=0.85)
result = agent.run("Transformer architecture and scaling laws")

print("=== Résultat de l'agent de recherche ===")
print(f"Réponse : {result['answer'][:120]}...")
print(f"Sources : {result['citations']}")
print(f"Confiance : {result['confidence']:.2f}")
print(f"\nTrace ({len(agent.trace)} itérations) :")
for step in agent.trace:
    print(f"  Itération {step['iteration']}{len(step['queries'])} requêtes, "
          f"confiance={step['confidence']:.2f}, sources={step['n_sources']}")
=== Résultat de l'agent de recherche ===
Réponse : Synthèse pour 'Transformer architecture and scaling laws': The Transformer architecture relies entirely on attention mec...
Sources : ['Vaswani et al., 2017', 'Devlin et al., 2019']
Confiance : 0.87

Trace (1 itérations) :
  Itération 1 — 1 requêtes, confiance=0.87, sources=2

Remarque 82

Un anti-pattern fréquent dans les agents de recherche est la boucle de reformulation sans fin : l’agent reformule indéfiniment ses requêtes sans jamais atteindre le seuil de confiance. Trois garde-fous sont essentiels : (1) un nombre maximal d’itérations, (2) une détection de requêtes redondantes (si la reformulation est identique à une requête précédente, s’arrêter), et (3) une réponse dégradée mais honnête lorsque le seuil n’est pas atteint (« voici ce que j’ai trouvé, mais ma confiance est limitée »).

Agent d’analyse de code#

L’analyse automatisée de code est l’un des domaines où les agents LLM montrent le plus de potentiel. Un agent de code review suit un cycle structuré : lire le code, identifier les problèmes, proposer des corrections et, si possible, valider les corrections par exécution de tests.

Exemple 58 (Agent de revue de code)

L’exemple ci-dessous simule un agent d’analyse de code qui examine une fonction Python, identifie des problèmes potentiels et propose des corrections. En production, le LLM remplacerait les règles codées en dur, mais le patron d’orchestration reste le même.

Hide code cell source

# Simulation d'un agent d'analyse de code

class CodeAnalysisAgent:
    """Agent d'analyse de code avec détection de problèmes."""

    RULES = {
        "bare_except": {
            "pattern": "except:",
            "severity": "warning",
            "message": "Clause except nue — capturer Exception explicitement.",
            "fix": "except Exception as e:"
        },
        "mutable_default": {
            "pattern": "def ",
            "check": lambda line: "=[]" in line.replace(" ", "") or "={}" in line.replace(" ", ""),
            "severity": "error",
            "message": "Argument mutable par défaut — utiliser None.",
        },
        "no_docstring": {
            "pattern": "def ",
            "severity": "info",
            "message": "Fonction sans docstring.",
        },
    }

    def __init__(self):
        self.findings = []
        self.trace = []

    def analyze(self, code):
        lines = code.strip().split("\n")
        self.findings = []

        # Étape 1 : analyse ligne par ligne
        for i, line in enumerate(lines, 1):
            stripped = line.strip()
            if stripped == "except:":
                self.findings.append({
                    "line": i, "severity": "warning",
                    "message": "Clause except nue — capturer Exception explicitement.",
                    "original": stripped, "suggestion": "except Exception as e:"
                })
            if "=[]" in stripped.replace(" ", "") and "def " in stripped:
                self.findings.append({
                    "line": i, "severity": "error",
                    "message": "Argument mutable par défaut — risque de partage d'état.",
                    "original": stripped, "suggestion": "Utiliser None comme défaut, initialiser dans le corps."
                })

        # Étape 2 : vérification des docstrings
        in_function = False
        func_line = 0
        for i, line in enumerate(lines, 1):
            stripped = line.strip()
            if stripped.startswith("def "):
                in_function = True
                func_line = i
            elif in_function:
                if not stripped.startswith('"""') and not stripped.startswith("'''"):
                    self.findings.append({
                        "line": func_line, "severity": "info",
                        "message": "Fonction sans docstring.",
                        "original": lines[func_line - 1].strip(), "suggestion": "Ajouter une docstring."
                    })
                in_function = False

        self.trace.append({"action": "analyze", "n_findings": len(self.findings)})
        return self.findings

    def report(self):
        severity_icons = {"error": "[ERREUR]", "warning": "[AVERT]", "info": "[INFO]"}
        print(f"{'='*60}")
        print(f"Rapport d'analyse — {len(self.findings)} problème(s) détecté(s)")
        print(f"{'='*60}")
        for f in sorted(self.findings, key=lambda x: ("error", "warning", "info").index(x["severity"])):
            icon = severity_icons[f["severity"]]
            print(f"  L{f['line']:>3d} {icon:>8s}  {f['message']}")
            print(f"           Original  : {f['original']}")
            print(f"           Suggestion: {f['suggestion']}")
            print()

# Code à analyser
sample_code = """
def process_data(items, cache=[]):
    for item in items:
        try:
            result = transform(item)
            cache.append(result)
        except:
            pass
    return cache

def transform(x):
    return x * 2 + 1
"""

agent = CodeAnalysisAgent()
agent.analyze(sample_code)
agent.report()
============================================================
Rapport d'analyse — 4 problème(s) détecté(s)
============================================================
  L  1 [ERREUR]  Argument mutable par défaut — risque de partage d'état.
           Original  : def process_data(items, cache=[]):
           Suggestion: Utiliser None comme défaut, initialiser dans le corps.

  L  6  [AVERT]  Clause except nue — capturer Exception explicitement.
           Original  : except:
           Suggestion: except Exception as e:

  L  1   [INFO]  Fonction sans docstring.
           Original  : def process_data(items, cache=[]):
           Suggestion: Ajouter une docstring.

  L 10   [INFO]  Fonction sans docstring.
           Original  : def transform(x):
           Suggestion: Ajouter une docstring.

Exemple 59 (Agent d’analyse de données)

Un agent d’analyse de données suit le cycle : comprendre la question en langage naturel, générer du code d’analyse (pandas, matplotlib), exécuter le code, interpréter les résultats. En production, le code généré est exécuté dans un bac à sable isolé (sandbox) pour des raisons de sécurité. L’agent peut itérer si le code échoue ou si les résultats semblent incohérents.

Human-in-the-loop#

L’autonomie totale d’un agent est rarement souhaitable. Dans la plupart des déploiements réels, l’humain reste dans la boucle — soit comme validateur à des points de contrôle, soit comme recours en cas d’incertitude. Le patron human-in-the-loop (HITL) formalise ces interactions.

Définition 78 (Human-in-the-loop)

Le patron human-in-the-loop (HITL) désigne une architecture dans laquelle un agent autonome soumet certaines décisions à la validation d’un opérateur humain avant de les exécuter. Les points de contrôle (checkpoints) sont définis selon trois critères :

  • Irréversibilité : toute action non annulable (envoi d’email, modification de base de données, paiement) requiert une approbation.

  • Incertitude : lorsque la confiance du modèle est inférieure à un seuil, l’agent demande confirmation.

  • Politique d’escalade : certaines catégories d’actions sont systématiquement soumises à validation indépendamment de la confiance.

Le HITL n’est pas un aveu de faiblesse de l’agent ; c’est un choix d’ingénierie qui optimise le compromis entre vitesse d’exécution et fiabilité.

Exemple 60 (Workflow HITL avec points de contrôle)

L’exemple suivant implémente une machine à états simple avec des points de contrôle HITL. Chaque transition peut être automatique ou nécessiter une approbation, selon la politique configurée.

Hide code cell source

# Machine à états HITL

class HITLWorkflow:
    """Workflow avec points de contrôle human-in-the-loop."""

    STATES = ["init", "research", "draft", "review", "execute", "done"]

    def __init__(self, approval_policy=None):
        self.state = "init"
        self.history = []
        # Par défaut : approbation requise avant exécution
        self.approval_policy = approval_policy or {
            "research": False,   # Automatique
            "draft": False,      # Automatique
            "review": True,      # Approbation requise
            "execute": True,     # Approbation requise
            "done": False,       # Automatique
        }

    def transition(self, next_state, context=""):
        requires_approval = self.approval_policy.get(next_state, False)

        if requires_approval:
            # Simuler une approbation humaine (en production : attente réelle)
            approved = self._simulate_human_review(next_state, context)
            status = "approuvé" if approved else "rejeté"
            self.history.append({
                "from": self.state, "to": next_state,
                "checkpoint": True, "status": status, "context": context
            })
            if not approved:
                return False
        else:
            self.history.append({
                "from": self.state, "to": next_state,
                "checkpoint": False, "status": "auto", "context": context
            })

        self.state = next_state
        return True

    def _simulate_human_review(self, state, context):
        """Simule une décision humaine (90 % d'approbation)."""
        return np.random.random() < 0.9

    def run_full_pipeline(self):
        steps = [
            ("research", "Recherche d'information sur le sujet"),
            ("draft", "Rédaction du brouillon"),
            ("review", "Contenu prêt pour validation humaine"),
            ("execute", "Publication du contenu"),
            ("done", "Pipeline terminé"),
        ]
        for next_state, context in steps:
            success = self.transition(next_state, context)
            if not success:
                print(f"  Pipeline arrêté : '{next_state}' rejeté par l'humain.")
                return self.history
        return self.history

# Exécution
np.random.seed(42)
workflow = HITLWorkflow()
history = workflow.run_full_pipeline()

print("=== Trace du workflow HITL ===")
for step in history:
    checkpoint = "CHECKPOINT" if step["checkpoint"] else "auto"
    print(f"  {step['from']:>10s} -> {step['to']:<10s}  [{checkpoint:>10s}]  {step['status']:>8s}  | {step['context']}")
  Pipeline arrêté : 'execute' rejeté par l'humain.
=== Trace du workflow HITL ===
        init -> research    [      auto]      auto  | Recherche d'information sur le sujet
    research -> draft       [      auto]      auto  | Rédaction du brouillon
       draft -> review      [CHECKPOINT]  approuvé  | Contenu prêt pour validation humaine
      review -> execute     [CHECKPOINT]    rejeté  | Publication du contenu

Hide code cell source

# Visualisation : diagramme de flux avec portes HITL
fig, ax = plt.subplots(figsize=(12, 5))
ax.set_xlim(-0.5, 10.5)
ax.set_ylim(-1.5, 3)
ax.set_aspect('equal')
ax.axis('off')

states = ["Init", "Recherche", "Rédaction", "Revue", "Exécution", "Terminé"]
x_positions = [0, 2, 4, 6, 8, 10]
is_checkpoint = [False, False, False, True, True, False]

for x, label, cp in zip(x_positions, states, is_checkpoint):
    color = '#E24A33' if cp else '#4C72B0'
    shape = 'D' if cp else 'o'
    ax.plot(x, 1, shape, markersize=28, color=color, zorder=5)
    ax.text(x, 1, label, ha='center', va='center', fontsize=7,
            color='white', fontweight='bold')
    if cp:
        ax.text(x, -0.3, "HITL", ha='center', va='center', fontsize=8,
                color='#E24A33', fontweight='bold',
                bbox=dict(boxstyle='round,pad=0.3', facecolor='#FDECEA', edgecolor='#E24A33'))

for i in range(len(x_positions) - 1):
    ax.annotate('', xy=(x_positions[i+1] - 0.5, 1), xytext=(x_positions[i] + 0.5, 1),
                arrowprops=dict(arrowstyle='->', color='gray', lw=1.5))

legend_elements = [
    plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='#4C72B0', markersize=12, label='Étape automatique'),
    plt.Line2D([0], [0], marker='D', color='w', markerfacecolor='#E24A33', markersize=12, label='Checkpoint HITL'),
]
ax.legend(handles=legend_elements, loc='upper left', fontsize=9)
ax.set_title("Pipeline agentique avec portes human-in-the-loop", fontsize=12, pad=15)
plt.show()
_images/1c1e652e75c15d8da99bafdf9e6b343acb34a3670a7d22d7a203da83b682cc0e.png

Propriété 19 (Compromis fiabilité-autonomie)

Il existe un compromis fondamental entre fiabilité et autonomie dans un système agentique. Soit \(A\) le degré d’autonomie (fraction de décisions prises sans validation humaine) et \(R\) la fiabilité (probabilité qu’une action soit correcte). En général :

\[R(A) = R_0 - \alpha \cdot A^\beta\]

\(R_0\) est la fiabilité avec supervision totale, et \(\alpha, \beta > 0\) sont des paramètres dépendant de la qualité du modèle et de la complexité de la tâche. L’objectif est de trouver le point \(A^*\) qui maximise l’utilité :

\[U(A) = \gamma \cdot A - (1 - R(A)) \cdot C_{\text{erreur}}\]

\(\gamma\) est la valeur de la vitesse d’exécution et \(C_{\text{erreur}}\) le coût d’une erreur. Plus le coût d’erreur est élevé, plus \(A^*\) est faible (davantage de supervision humaine).

Debugging et observabilité#

Un agent LLM est un système opaque par nature : ses décisions dépendent du modèle, du contexte accumulé et d’interactions stochastiques avec des outils externes. Sans instrumentation, diagnostiquer un comportement incorrect est quasiment impossible.

Définition 79 (Observabilité d’un agent)

L”observabilité d’un agent est la capacité à reconstituer, a posteriori, la séquence complète de ses décisions, actions et résultats. Elle repose sur trois piliers :

  1. Logs structurés : chaque étape est enregistrée avec un horodatage, l’action choisie, les arguments, le résultat et les métriques (tokens consommés, latence).

  2. Traces : les logs sont chaînés en une trace identifiable par un identifiant unique de session, permettant de suivre le raisonnement de bout en bout.

  3. Métriques agrégées : taux de succès, nombre moyen d’itérations, coût par requête, distribution des types d’erreurs.

Des outils comme LangSmith, Arize Phoenix ou Weights & Biases Traces fournissent des interfaces visuelles pour explorer ces données. Le principe reste le même : capturer chaque décision de l’agent dans un format structuré et requêtable.

Exemple 61 (Logger de traces structuré)

Le logger ci-dessous capture chaque action de l’agent dans un format JSON structuré. En production, ces traces sont envoyées vers un système de stockage centralisé (Elasticsearch, BigQuery, etc.) pour analyse ultérieure.

Hide code cell source

# Logger de traces structuré pour agents

class AgentTraceLogger:
    """Logger structuré capturant chaque étape d'un agent."""

    def __init__(self, session_id="session_001"):
        self.session_id = session_id
        self.steps = []
        self.start_time = time.time()

    def log_step(self, step_number, action, arguments, result, tokens_used=0, status="success"):
        entry = {
            "session_id": self.session_id,
            "step": step_number,
            "timestamp": round(time.time() - self.start_time, 4),
            "action": action,
            "arguments": arguments,
            "result_preview": str(result)[:200],
            "tokens_used": tokens_used,
            "status": status,
        }
        self.steps.append(entry)
        return entry

    def summary(self):
        total_tokens = sum(s["tokens_used"] for s in self.steps)
        total_time = self.steps[-1]["timestamp"] if self.steps else 0
        errors = sum(1 for s in self.steps if s["status"] != "success")
        return {
            "session_id": self.session_id,
            "total_steps": len(self.steps),
            "total_tokens": total_tokens,
            "total_time_s": total_time,
            "errors": errors,
        }

# Simuler une exécution d'agent avec logging
logger = AgentTraceLogger(session_id="agent-research-042")

simulated_steps = [
    ("decompose_query", {"question": "Impact des LLM sur la recherche"}, "3 sous-questions générées", 150),
    ("search", {"query": "LLM research applications 2024"}, "8 résultats trouvés", 0),
    ("search", {"query": "LLM limitations scientific research"}, "5 résultats trouvés", 0),
    ("extract", {"n_documents": 13}, "7 passages pertinents extraits", 800),
    ("synthesize", {"n_extracts": 7}, "Synthèse de 350 mots avec 5 citations", 1200),
    ("verify", {"synthesis_length": 350}, "Confiance 0.88 — 1 incohérence mineure", 600),
    ("refine", {"issue": "date incorrecte"}, "Correction appliquée, confiance 0.93", 400),
]

for i, (action, args, result, tokens) in enumerate(simulated_steps, 1):
    time.sleep(0.01)
    logger.log_step(i, action, args, result, tokens_used=tokens)

print("=== Trace de l'agent ===")
for step in logger.steps:
    print(f"  Étape {step['step']:>2d} | {step['action']:<20s} | "
          f"tokens={step['tokens_used']:>5d} | {step['status']}")

print(f"\n=== Résumé ===")
summary = logger.summary()
for k, v in summary.items():
    print(f"  {k}: {v}")
=== Trace de l'agent ===
  Étape  1 | decompose_query      | tokens=  150 | success
  Étape  2 | search               | tokens=    0 | success
  Étape  3 | search               | tokens=    0 | success
  Étape  4 | extract              | tokens=  800 | success
  Étape  5 | synthesize           | tokens= 1200 | success
  Étape  6 | verify               | tokens=  600 | success
  Étape  7 | refine               | tokens=  400 | success

=== Résumé ===
  session_id: agent-research-042
  total_steps: 7
  total_tokens: 3150
  total_time_s: 0.0709
  errors: 0

Hide code cell source

# Visualisation : décomposition de la latence par étape (waterfall)
steps_labels = [s["action"] for s in logger.steps]
# Simuler des latences réalistes (ms)
latencies = np.array([120, 850, 720, 350, 1500, 800, 600])
cumulative = np.cumsum(latencies) - latencies

colors_waterfall = ['#4C72B0', '#55A868', '#55A868', '#DD8452', '#E24A33', '#8172B2', '#8172B2']

fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.barh(steps_labels, latencies, left=cumulative, color=colors_waterfall,
               edgecolor='white', linewidth=1.2)
ax.set_xlabel("Temps cumulé (ms)")
ax.set_title("Décomposition de la latence — Agent de recherche")
ax.bar_label(bars, labels=[f"{l} ms" for l in latencies], padding=3, fontsize=8)
ax.invert_yaxis()
plt.show()
_images/a641d3fdf5040971828c2103d8d1e916aba56dbed6e3051783b0118ef522bd54.png

Hide code cell source

# Visualisation : distribution des types d'erreurs (données simulées)
error_types = ["Timeout\noutil", "Parsing\nréponse", "Hallucination\ndétectée",
               "Outil\nindisponible", "Boucle\ninfinie", "Token\nlimit"]
error_counts = [45, 32, 28, 18, 12, 8]
colors_err = ['#4C72B0', '#DD8452', '#E24A33', '#55A868', '#8172B2', '#CCB974']

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# Diagramme en barres
axes[0].bar(error_types, error_counts, color=colors_err, edgecolor='white', linewidth=1.2)
axes[0].set_ylabel("Nombre d'occurrences")
axes[0].set_title("Distribution des erreurs d'agent\n(sur 500 exécutions simulées)")
for i, v in enumerate(error_counts):
    axes[0].text(i, v + 1, str(v), ha='center', fontsize=9)

# Diagramme circulaire
axes[1].pie(error_counts, labels=error_types, colors=colors_err, autopct='%1.0f%%',
            startangle=90, textprops={'fontsize': 8})
axes[1].set_title("Répartition relative des erreurs")

plt.show()
_images/4845bd345add05300178933c72e96a44020ee56fff80da97364447347ad33ae7.png

Robustesse et patterns de production#

Un agent déployé en production fait face à des pannes réseau, des timeouts d’API, des réponses malformées et des boucles non terminantes. Les patterns de robustesse sont empruntés à l’ingénierie logicielle distribuée et adaptés au contexte agentique.

Remarque 83

Le contrôle des coûts est un enjeu critique. Un agent mal configuré peut consommer des milliers de tokens par requête, avec un coût qui croît linéairement (voire super-linéairement si le contexte s’allonge à chaque itération). Trois leviers de contrôle sont essentiels :

  1. Limite d’itérations : fixer un nombre maximal de boucles agent (typiquement 5–15).

  2. Budget de tokens : fixer un plafond total de tokens par session (entrée + sortie).

  3. Coût marginal décroissant : si les dernières itérations n’améliorent plus significativement le résultat, s’arrêter tôt (early stopping).

Remarque 84

Les garde-fous (guardrails) limitent l’espace d’action de l’agent pour prévenir les comportements indésirables. Ils opèrent à plusieurs niveaux :

  • Filtrage des actions : liste blanche d’outils autorisés, validation des arguments avant exécution.

  • Filtrage des sorties : détection de contenu toxique, d’informations personnelles ou de code malveillant.

  • Sandboxing : exécution du code généré dans un environnement isolé (conteneur, VM, WebAssembly).

Exemple de garde-fou en pseudo-code :

ALLOWED_TOOLS = {"search", "calculate", "read_file"}

def validate_action(action, args):
    if action not in ALLOWED_TOOLS:
        raise ForbiddenActionError(f"Outil {action} non autorisé")
    if action == "read_file" and ".." in args["path"]:
        raise ForbiddenActionError("Traversée de répertoire interdite")
    return True

En production, ces garde-fous sont implémentés comme des middleware dans le pipeline d’exécution de l’agent.

Hide code cell source

# Visualisation : coût vs nombre d'itérations (rendements décroissants)
iterations = np.arange(1, 16)
# Qualité : gain logarithmique
quality = 0.5 + 0.4 * np.log(iterations) / np.log(15)
quality = np.clip(quality, 0, 0.95)
# Coût : croissance linéaire à super-linéaire
cost_per_iter = 0.02 * (1 + 0.1 * iterations)  # coût croissant par itération
cumulative_cost = np.cumsum(cost_per_iter)
# Utilité : qualité - lambda * coût
utility = quality - 0.8 * cumulative_cost

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# Panneau gauche : qualité et coût
ax1 = axes[0]
color_q = '#4C72B0'
color_c = '#E24A33'
ax1.plot(iterations, quality, 'o-', color=color_q, label="Qualité de la réponse", linewidth=2)
ax1.set_xlabel("Nombre d'itérations")
ax1.set_ylabel("Qualité (score)", color=color_q)
ax1.tick_params(axis='y', labelcolor=color_q)
ax1.set_ylim(0.4, 1.0)

ax2 = ax1.twinx()
ax2.plot(iterations, cumulative_cost, 's--', color=color_c, label="Coût cumulé ($)", linewidth=2)
ax2.set_ylabel("Coût cumulé ($)", color=color_c)
ax2.tick_params(axis='y', labelcolor=color_c)

lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='center right', fontsize=9)
axes[0].set_title("Qualité vs coût selon le nombre d'itérations")

# Panneau droit : utilité marginale
axes[1].plot(iterations, utility, 'o-', color='#55A868', linewidth=2, label="Utilité nette")
axes[1].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
optimal_idx = np.argmax(utility)
axes[1].axvline(x=iterations[optimal_idx], color='#E24A33', linestyle='--', alpha=0.7,
                label=f"Optimum : {iterations[optimal_idx]} itérations")
axes[1].plot(iterations[optimal_idx], utility[optimal_idx], '*', color='#E24A33',
             markersize=15, zorder=5)
axes[1].set_xlabel("Nombre d'itérations")
axes[1].set_ylabel("Utilité nette")
axes[1].set_title("Utilité nette — rendements décroissants")
axes[1].legend(fontsize=9)

plt.show()
_images/8f99398d49c82448329a34cc1a0cbedaedab12589dff4db6bec474c0b452ef19.png

Définition 80 (Stratégie de reprise (fallback))

Une stratégie de reprise (fallback strategy) définit le comportement de l’agent lorsqu’une action échoue. Les patterns classiques sont :

  1. Retry avec backoff exponentiel : retenter l’action après un délai croissant (\(\Delta t_k = \Delta t_0 \cdot 2^k\) pour la \(k\)-ième tentative).

  2. Fallback vers un modèle alternatif : si le modèle principal est indisponible, basculer vers un modèle plus petit ou plus rapide.

  3. Dégradation gracieuse : retourner un résultat partiel plutôt qu’aucun résultat.

  4. Circuit breaker : après \(n\) échecs consécutifs, désactiver temporairement l’outil défaillant.

En pseudo-code :

def execute_with_fallback(action, max_retries=3):
    for attempt in range(max_retries):
        try:
            return action()
        except TransientError:
            time.sleep(BASE_DELAY * 2 ** attempt)
        except PermanentError:
            return fallback_action()
    return graceful_degradation()

Le choix de la stratégie dépend du type d’erreur : les erreurs transitoires (timeout, rate limit) justifient un retry ; les erreurs permanentes (outil supprimé, permission refusée) nécessitent un fallback immédiat.

Hide code cell source

# Simulation de retry avec backoff exponentiel

def simulate_unreliable_api(failure_rate=0.6):
    """Simule une API qui échoue avec une certaine probabilité."""
    if np.random.random() < failure_rate:
        raise ConnectionError("API timeout")
    return {"status": "success", "data": "résultat valide"}

def execute_with_retry(func, max_retries=5, base_delay=0.1):
    """Exécution avec retry et backoff exponentiel."""
    attempts = []
    for attempt in range(max_retries):
        delay = base_delay * (2 ** attempt)
        try:
            result = func()
            attempts.append({"attempt": attempt + 1, "delay": delay, "status": "success"})
            return result, attempts
        except ConnectionError as e:
            attempts.append({"attempt": attempt + 1, "delay": delay, "status": "failure", "error": str(e)})
            time.sleep(0.001)  # Délai symbolique pour la simulation
    return None, attempts

# Exécution de plusieurs sessions pour visualiser
np.random.seed(42)
all_sessions = []
for session_id in range(20):
    result, attempts = execute_with_retry(
        lambda: simulate_unreliable_api(failure_rate=0.5),
        max_retries=5
    )
    all_sessions.append({
        "session": session_id,
        "n_attempts": len(attempts),
        "final_status": attempts[-1]["status"],
        "attempts": attempts,
    })

# Résumé
successes = sum(1 for s in all_sessions if s["final_status"] == "success")
avg_attempts = np.mean([s["n_attempts"] for s in all_sessions])
print(f"Sessions réussies : {successes}/20 ({successes/20*100:.0f}%)")
print(f"Nombre moyen de tentatives : {avg_attempts:.1f}")
print(f"\nDétail des 5 premières sessions :")
for s in all_sessions[:5]:
    statuses = " -> ".join(a["status"][0].upper() for a in s["attempts"])
    print(f"  Session {s['session']:>2d} : {statuses:>20s}  ({s['n_attempts']} tentatives)")
Sessions réussies : 20/20 (100%)
Nombre moyen de tentatives : 2.3

Détail des 5 premières sessions :
  Session  0 :               F -> S  (2 tentatives)
  Session  1 :                    S  (1 tentatives)
  Session  2 :                    S  (1 tentatives)
  Session  3 :     F -> F -> F -> S  (4 tentatives)
  Session  4 :                    S  (1 tentatives)

Résumé#

Ce chapitre a abordé les aspects pratiques du déploiement d’agents LLM, depuis les cas d’usage concrets jusqu’aux patterns de robustesse en production.

  1. Les cas d’usage mûrs pour les agents LLM incluent la recherche documentaire, l’analyse de code, l’analyse de données et le service client. Le critère de pertinence repose sur trois conditions : tâche multi-étapes, espace d’action contraint et coût d’erreur gérable.

  2. Un agent de recherche d’information suit un cycle itératif — formulation de requête, recherche, extraction, synthèse, vérification — avec des critères d’arrêt explicites (seuil de confiance, nombre maximal d’itérations) et une attribution systématique des sources.

  3. Un agent d’analyse de code combine la lecture de code, la détection de problèmes par règles et par LLM, la proposition de corrections et la validation par exécution de tests. Le patron d’orchestration est similaire à celui de la recherche.

  4. Le patron human-in-the-loop insère des points de contrôle humains aux transitions critiques : actions irréversibles, décisions à faible confiance, catégories soumises à une politique d’escalade. Il optimise le compromis entre autonomie et fiabilité.

  5. L”observabilité repose sur des logs structurés, des traces chaînées par session et des métriques agrégées. Elle est indispensable pour le diagnostic des comportements incorrects et l’amélioration continue du système.

  6. Les patterns de robustesse empruntés à l’ingénierie distribuée — retry avec backoff, fallback vers des modèles alternatifs, dégradation gracieuse, circuit breaker — assurent la résilience face aux pannes inévitables des composants externes.

  7. Le contrôle des coûts passe par des limites d’itérations, des budgets de tokens et une détection des rendements décroissants. Un agent non borné est un risque financier autant qu’un risque technique.

Remarque 85

L’erreur la plus fréquente dans le déploiement d’agents LLM est de sous-estimer la complexité opérationnelle. Un agent qui fonctionne dans un notebook de démonstration n’est pas un agent prêt pour la production. La différence réside dans l’instrumentation (traces, métriques), la gestion des erreurs (retry, fallback), le contrôle des coûts (budgets, limites) et la supervision humaine (HITL, escalade). Ces quatre piliers transforment un prototype fragile en un système fiable.