Architecture et composants fondamentaux des agents IA

tl;dr: Un agent IA repose sur 4 piliers : Perception (capteurs/APIs), Raisonnement (LLM décisionnel), Action (exécution d'outils), Mémoire (contexte + long-terme). Architecture modulaire avec orchestrateur central.

Les agents IA représentent une évolution majeure des modèles de langage : là où un LLM classique se contente de générer du texte, un agent IA agit de manière autonome pour accomplir des tâches complexes. Cette autonomie repose sur une architecture bien définie qui sépare clairement les responsabilités et permet une orchestration efficace.

Dans cet article, nous allons décortiquer l’architecture complète d’un agent IA production-ready, en explorant chaque composant en détail avec des implémentations concrètes en Python. Que vous construisiez un assistant personnel, un système d’automatisation ou un agent de recherche, cette architecture modulaire constitue la fondation nécessaire.

Schéma illustrant l’architecture des systèmes d’agents IA dans les systèmes d’agents d’intelligence artificielle

Pourquoi une architecture modulaire ?

Avant de plonger dans les détails techniques, comprenons pourquoi l’architecture modulaire est essentielle pour les agents IA :

1. Testabilité : Chaque module peut être testé indépendamment. Vous pouvez vérifier que votre module de perception parse correctement les entrées sans avoir à tester tout l’agent.

2. Maintenabilité : Modifier le système de mémoire n’impacte pas le raisonnement. Ajouter un nouvel outil n’affecte pas la perception. Cette séparation des préoccupations est cruciale pour des projets complexes.

3. Évolutivité : Vous pouvez commencer avec un agent simple (perception + raisonnement basique + 2 outils) puis enrichir progressivement chaque module sans tout réécrire.

4. Réutilisabilité : Un module d’action bien conçu peut être réutilisé dans plusieurs agents différents. Idem pour les stratégies de raisonnement.

5. Débogage : Quand un agent échoue, l’architecture modulaire permet d’identifier rapidement quel composant pose problème (perception mal configurée ? outil défaillant ? LLM qui hallucine ?).

💡 Production-ready : Les agents qui tournent en production chez OpenAI, Anthropic et Google suivent tous des architectures modulaires similaires. C’est un standard de l’industrie, pas une simple préférence académique.

Les 4 piliers des agents IA

Tout agent IA performant repose sur 4 composants fondamentaux :

💡 Architecture modulaire : Séparer ces 4 piliers permet de tester, déboguer et améliorer chaque composant indépendamment. C’est essentiel pour des agents production-ready.
┌─────────────────────────────────────────────────┐
│              🧠 AGENT IA                        │
├─────────────────────────────────────────────────┤
│                                                 │
│  👁️  PERCEPTION  →  🤔 RAISONNEMENT            │
│         ↑                    ↓                  │
│  💾  MÉMOIRE    ←    🛠️  ACTION                │
│                                                 │
└─────────────────────────────────────────────────┘

Perception (Input)

L’agent observe son environnement :

  • Requêtes utilisateur
  • Résultats d’outils
  • État du système
  • Données externes

Raisonnement (Brain)

L’agent réfléchit :

  • Analyse de la situation
  • Planification des étapes
  • Choix de la prochaine action
  • Évaluation des résultats

Action (Output)

L’agent agit :

  • Appels d’APIs
  • Exécution de code
  • Manipulation de données
  • Communication avec l’utilisateur

Mémoire (State)

L’agent se souvient :

  • Conversation en cours
  • Faits appris
  • Historique des actions
  • Résultats précédents

Architecture complète d’un agent

Schéma global

"""
┌──────────────────────────────────────────────────────────────┐
│                      UTILISATEUR                             │
└───────────────────────┬──────────────────────────────────────┘
                        ↓ Query
┌───────────────────────────────────────────────────────────────┐
│                   ORCHESTRATEUR                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  1. Parse la requête                                 │    │
│  │  2. Initialise la mémoire                            │    │
│  │  3. Lance la boucle agent                            │    │
│  │  4. Gère les erreurs et timeouts                     │    │
│  └──────────────────────────────────────────────────────┘    │
└───────┬───────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│                    PERCEPTION MODULE                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │ Input Parser│  │   Sensors   │  │ Tool Results│          │
│  └─────────────┘  └─────────────┘  └─────────────┘          │
└───────┬───────────────────────────────────────────────────────┘
        ↓ Observations
┌───────────────────────────────────────────────────────────────┐
│                   REASONING MODULE                            │
│  ┌─────────────────────────────────────────────────────┐     │
│  │              LLM (GPT-4, Claude)                    │     │
│  │  - Analyze observations                             │     │
│  │  - Plan next steps                                  │     │
│  │  - Decide on action                                 │     │
│  └─────────────────────────────────────────────────────┘     │
└───────┬───────────────────────────────────────────────────────┘
        ↓ Decision
┌───────────────────────────────────────────────────────────────┐
│                    ACTION MODULE                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │  API Calls  │  │Code Executor│  │  Database   │          │
│  └─────────────┘  └─────────────┘  └─────────────┘          │
└───────┬───────────────────────────────────────────────────────┘
        ↓ Results
┌───────────────────────────────────────────────────────────────┐
│                    MEMORY MODULE                              │
│  ┌─────────────────┐       ┌──────────────────┐             │
│  │  Short-term     │       │   Long-term      │             │
│  │  (Conversation) │       │  (Vector DB)     │             │
│  └─────────────────┘       └──────────────────┘             │
└───────────────────────────────────────────────────────────────┘

Implémentation modulaire

from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from abc import ABC, abstractmethod
import openai

# ============= 1. PERCEPTION =============

@dataclass
class Observation:
    """Une observation de l'environnement"""
    source: str  # "user", "tool", "system"
    content: str
    metadata: Dict[str, Any] = None

class PerceptionModule:
    """Collecte et structure les observations"""

    def __init__(self):
        self.sensors = []

    def observe(self, raw_input: Any) -> Observation:
        """Transforme un input brut en observation structurée"""
        if isinstance(raw_input, str):
            return Observation(source="user", content=raw_input)
        elif isinstance(raw_input, dict):
            return Observation(
                source=raw_input.get("source", "unknown"),
                content=raw_input.get("content", ""),
                metadata=raw_input.get("metadata", {})
            )
        else:
            return Observation(source="system", content=str(raw_input))

    def add_sensor(self, sensor_fn):
        """Ajoute un capteur (fonction qui retourne des observations)"""
        self.sensors.append(sensor_fn)

    def gather_context(self) -> List[Observation]:
        """Collecte toutes les observations des capteurs"""
        observations = []
        for sensor in self.sensors:
            try:
                obs = sensor()
                observations.append(obs)
            except Exception as e:
                observations.append(
                    Observation(source="system", content=f"Sensor error: {e}")
                )
        return observations


# ============= 2. RAISONNEMENT =============

class ReasoningStrategy(ABC):
    """Interface pour les stratégies de raisonnement"""

    @abstractmethod
    def think(self, observations: List[Observation], memory: 'Memory') -> Dict[str, Any]:
        """Analyse la situation et décide de la prochaine action"""
        pass

class ReActReasoning(ReasoningStrategy):
    """Stratégie ReAct : Thought → Action → Observation"""

    def __init__(self, llm_model="gpt-4"):
        self.model = llm_model

    def think(self, observations: List[Observation], memory: 'Memory') -> Dict[str, Any]:
        """
        Génère une pensée (thought) et décide d'une action
        """
        # Construire le contexte
        context = self._build_context(observations, memory)

        # Appel LLM pour raisonner
        response = openai.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": self._get_system_prompt()},
                {"role": "user", "content": context}
            ],
            functions=self._get_available_functions(),
            function_call="auto"
        )

        message = response.choices[0].message

        # Extraire la pensée et l'action
        thought = message.content or "Réflexion en cours..."

        action = None
        if message.function_call:
            action = {
                "name": message.function_call.name,
                "arguments": json.loads(message.function_call.arguments)
            }

        return {
            "thought": thought,
            "action": action,
            "finish": action is None  # Si pas d'action, c'est fini
        }

    def _build_context(self, observations: List[Observation], memory: 'Memory') -> str:
        """Construit le contexte pour le LLM"""
        context_parts = []

        # Historique de la conversation
        for msg in memory.short_term:
            context_parts.append(f"{msg['role']}: {msg['content']}")

        # Observations récentes
        context_parts.append("\n=== Observations récentes ===")
        for obs in observations[-5:]:  # Dernières 5 observations
            context_parts.append(f"[{obs.source}] {obs.content}")

        return "\n".join(context_parts)

    def _get_system_prompt(self) -> str:
        return """Tu es un agent IA qui résout des problèmes étape par étape.

Pour chaque requête :
1. PENSE : Analyse la situation
2. AGIS : Choisis un outil à utiliser (ou réponds directement)
3. OBSERVE : Analyse le résultat
4. Répète jusqu'à résolution

Sois concis et méthodique."""

    def _get_available_functions(self) -> List[Dict]:
        """Retourne la liste des fonctions disponibles pour le LLM"""
        return [
            {
                "name": "search_web",
                "description": "Recherche sur le web",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "Requête de recherche"}
                    },
                    "required": ["query"]
                }
            },
            {
                "name": "calculate",
                "description": "Effectue un calcul mathématique",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {"type": "string", "description": "Expression mathématique"}
                    },
                    "required": ["expression"]
                }
            }
        ]


# ============= 3. ACTION =============

class Tool(ABC):
    """Interface pour un outil"""

    @abstractmethod
    def execute(self, **kwargs) -> Any:
        """Exécute l'outil avec les arguments fournis"""
        pass

    @property
    @abstractmethod
    def name(self) -> str:
        """Nom de l'outil"""
        pass

class CalculatorTool(Tool):
    """Outil de calcul"""

    @property
    def name(self) -> str:
        return "calculate"

    def execute(self, expression: str) -> str:
        try:
            result = eval(expression)  # ⚠️ Dangereux en prod
            return f"Résultat : {result}"
        except Exception as e:
            return f"Erreur de calcul : {str(e)}"

class WebSearchTool(Tool):
    """Outil de recherche web (simulé)"""

    @property
    def name(self) -> str:
        return "search_web"

    def execute(self, query: str) -> str:
        # En production, utiliser une vraie API (Tavily, Serper, etc.)
        return f"Résultats de recherche pour '{query}': [Simulé] Top 3 résultats..."

class ActionModule:
    """Gère l'exécution des actions"""

    def __init__(self):
        self.tools: Dict[str, Tool] = {}

    def register_tool(self, tool: Tool):
        """Enregistre un nouvel outil"""
        self.tools[tool.name] = tool

    def execute(self, action: Dict[str, Any]) -> Observation:
        """Exécute une action et retourne l'observation résultante"""
        tool_name = action["name"]
        arguments = action["arguments"]

        if tool_name not in self.tools:
            return Observation(
                source="system",
                content=f"Outil inconnu : {tool_name}"
            )

        try:
            result = self.tools[tool_name].execute(**arguments)
            return Observation(
                source="tool",
                content=result,
                metadata={"tool": tool_name, "arguments": arguments}
            )
        except Exception as e:
            return Observation(
                source="system",
                content=f"Erreur lors de l'exécution de {tool_name} : {str(e)}"
            )


# ============= 4. MÉMOIRE =============

class Memory:
    """Gère la mémoire court-terme et long-terme"""

    def __init__(self):
        self.short_term: List[Dict[str, str]] = []  # Conversation actuelle
        self.long_term: Dict[str, Any] = {}  # Faits persistants
        self.episodic: List[Dict[str, Any]] = []  # Historique des actions

    def add_message(self, role: str, content: str):
        """Ajoute un message à la mémoire court-terme"""
        self.short_term.append({"role": role, "content": content})

    def remember_fact(self, key: str, value: Any):
        """Stocke un fait en mémoire long-terme"""
        self.long_term[key] = value

    def recall_fact(self, key: str) -> Optional[Any]:
        """Récupère un fait de la mémoire long-terme"""
        return self.long_term.get(key)

    def add_episode(self, action: str, result: str, success: bool):
        """Ajoute un épisode à la mémoire épisodique"""
        self.episodic.append({
            "action": action,
            "result": result,
            "success": success,
            "timestamp": datetime.now().isoformat()
        })

    def get_recent_episodes(self, n: int = 5) -> List[Dict]:
        """Récupère les N derniers épisodes"""
        return self.episodic[-n:]

    def clear_short_term(self):
        """Réinitialise la mémoire court-terme"""
        self.short_term = []


# ============= 5. ORCHESTRATEUR =============

class Agent:
    """Orchestrateur principal de l'agent"""

    def __init__(
        self,
        reasoning_strategy: ReasoningStrategy,
        max_iterations: int = 10
    ):
        self.perception = PerceptionModule()
        self.reasoning = reasoning_strategy
        self.action = ActionModule()
        self.memory = Memory()
        self.max_iterations = max_iterations

    def run(self, query: str) -> str:
        """
        Exécute l'agent sur une requête
        """
        # 1. Perception : Observer la requête
        initial_observation = self.perception.observe(query)
        self.memory.add_message("user", query)

        observations = [initial_observation]
        iteration = 0

        while iteration < self.max_iterations:
            print(f"\n{'='*60}")
            print(f"🔄 Itération {iteration + 1}/{self.max_iterations}")
            print(f"{'='*60}")

            # 2. Raisonnement : Analyser et décider
            decision = self.reasoning.think(observations, self.memory)

            thought = decision["thought"]
            action = decision["action"]
            finish = decision["finish"]

            print(f"\n💭 Pensée : {thought}")

            # Sauvegarder la pensée
            self.memory.add_message("assistant", f"[THOUGHT] {thought}")

            # 3. Action : Exécuter si nécessaire
            if finish:
                print(f"\n✅ Réponse finale : {thought}")
                return thought

            if action:
                print(f"\n🔧 Action : {action['name']}({action['arguments']})")

                # Exécuter l'action
                result_observation = self.action.execute(action)
                observations.append(result_observation)

                print(f"📊 Résultat : {result_observation.content}")

                # Sauvegarder dans la mémoire
                self.memory.add_message("tool", result_observation.content)
                self.memory.add_episode(
                    action=action["name"],
                    result=result_observation.content,
                    success="Erreur" not in result_observation.content
                )

            iteration += 1

        return "⚠️ Limite d'itérations atteinte"


# ============= EXEMPLE D'UTILISATION =============

if __name__ == "__main__":
    # Créer l'agent
    agent = Agent(
        reasoning_strategy=ReActReasoning(llm_model="gpt-4"),
        max_iterations=10
    )

    # Enregistrer les outils
    agent.action.register_tool(CalculatorTool())
    agent.action.register_tool(WebSearchTool())

    # Exécuter
    result = agent.run("""
    Cherche sur le web le PIB de la France en 2023,
    puis calcule combien ça fait par habitant (67 millions d'habitants)
    """)

    print(f"\n{'='*60}")
    print(f"📝 Résultat final : {result}")
    print(f"{'='*60}")

Sortie exemple

============================================================
🔄 Itération 1/10
============================================================

💭 Pensée : Je dois d'abord rechercher le PIB de la France en 2023

🔧 Action : search_web({'query': 'PIB France 2023'})
📊 Résultat : Résultats de recherche pour 'PIB France 2023': Le PIB de la France en 2023 est estimé à 2 950 milliards d'euros...

============================================================
🔄 Itération 2/10
============================================================

💭 Pensée : Maintenant je dois calculer le PIB par habitant : 2950000000000 / 67000000

🔧 Action : calculate({'expression': '2950000000000 / 67000000'})
📊 Résultat : Résultat : 44029.85

============================================================
🔄 Itération 3/10
============================================================

✅ Réponse finale : Le PIB de la France en 2023 est d'environ 2 950 milliards d'euros. Avec 67 millions d'habitants, cela fait environ 44 030 € par habitant.

Patterns d’architecture avancés

Au-delà de l’architecture de base, plusieurs patterns avancés permettent de gérer des cas d’usage complexes. Chaque pattern répond à des besoins spécifiques et présente des trade-offs différents en termes de performance, coût et complexité.

Comparaison des patterns architecturaux

PatternCas d’usage idéalAvantagesInconvénientsCoût LLM
Simple (ReAct)Tâches linéaires, recherche web, Q&ASimple, rapide, facile à déboguerLimité pour tâches complexesFaible
HiérarchiqueTâches multi-domaines (recherche + analyse + synthèse)Spécialisation, parallélisation possibleComplexité de coordinationMoyen-Élevé
Plan-and-ExecuteTâches avec étapes claires (analyses, rapports)Prévisible, optimiséRigide, difficulté avec imprévuMoyen
Self-ReflectionTâches critiques nécessitant qualité maximaleHaute qualité, auto-correctionLent, coûteux en appels LLMTrès Élevé
Collaborative Multi-AgentProblèmes nécessitant expertises multiplesCréativité, robustesseTrès complexe, difficile à contrôlerTrès Élevé
🔎 Tip
Pattern recommandé : L’architecture hiérarchique avec un superviseur est idéale pour des tâches complexes. Le superviseur décompose le problème et délègue à des agents spécialisés. Utilisé par AutoGPT, BabyAGI et les systèmes multi-agents de LangChain.

Agent hiérarchique

class HierarchicalAgent(Agent):
    """Agent avec délégation à des sous-agents"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.sub_agents = {}

    def register_sub_agent(self, name: str, agent: Agent):
        """Enregistre un sous-agent spécialisé"""
        self.sub_agents[name] = agent

    def delegate(self, task: str, sub_agent_name: str) -> str:
        """Délègue une tâche à un sous-agent"""
        if sub_agent_name not in self.sub_agents:
            return f"Sous-agent {sub_agent_name} introuvable"

        return self.sub_agents[sub_agent_name].run(task)

# Exemple
supervisor = HierarchicalAgent(reasoning_strategy=ReActReasoning())

# Agents spécialisés
data_agent = Agent(reasoning_strategy=ReActReasoning())
data_agent.action.register_tool(SQLTool())

research_agent = Agent(reasoning_strategy=ReActReasoning())
research_agent.action.register_tool(WebSearchTool())

# Enregistrement
supervisor.register_sub_agent("data", data_agent)
supervisor.register_sub_agent("research", research_agent)

Agent avec Plan-and-Execute

class PlanAndExecuteAgent(Agent):
    """Agent qui planifie d'abord, puis exécute"""

    def run(self, query: str) -> str:
        # 1. Planification : Décomposer en sous-tâches
        plan = self._create_plan(query)
        print(f"📋 Plan créé : {len(plan)} étapes")

        results = []

        # 2. Exécution : Traiter chaque étape
        for i, step in enumerate(plan):
            print(f"\n▶️ Étape {i+1}/{len(plan)}: {step}")
            step_result = super().run(step)
            results.append(step_result)

        # 3. Synthèse finale
        return self._synthesize(query, results)

    def _create_plan(self, query: str) -> List[str]:
        """Utilise le LLM pour créer un plan"""
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{
                "role": "user",
                "content": f"""Décompose cette tâche en étapes simples et séquentielles :

{query}

Réponds uniquement avec une liste numérotée."""
            }]
        )

        plan_text = response.choices[0].message.content
        # Parser la liste
        steps = [line.strip() for line in plan_text.split('\n') if line.strip()]
        return steps

    def _synthesize(self, original_query: str, step_results: List[str]) -> str:
        """Synthétise les résultats de toutes les étapes"""
        synthesis_prompt = f"""Requête originale : {original_query}

Résultats des étapes :
{chr(10).join(f'{i+1}. {r}' for i, r in enumerate(step_results))}

Fournis une réponse finale synthétique."""

        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": synthesis_prompt}]
        )

        return response.choices[0].message.content

Agent avec self-reflection

class ReflectiveAgent(Agent):
    """Agent qui critique et améliore ses propres résultats"""

    def run(self, query: str, max_reflections: int = 3) -> str:
        current_answer = super().run(query)

        for i in range(max_reflections):
            # Auto-critique
            critique = self._critique(query, current_answer)

            if critique["is_satisfactory"]:
                print(f"✅ Réponse satisfaisante après {i} réflexions")
                break

            print(f"\n🔍 Réflexion {i+1} : {critique['issues']}")

            # Amélioration
            improved_answer = self._improve(query, current_answer, critique)
            current_answer = improved_answer

        return current_answer

    def _critique(self, query: str, answer: str) -> Dict:
        """Critique la réponse actuelle"""
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{
                "role": "user",
                "content": f"""Évalue cette réponse :

Question : {query}
Réponse : {answer}

La réponse est-elle :
1. Correcte factuellement ?
2. Complète ?
3. Bien structurée ?

Réponds en JSON : {{"is_satisfactory": bool, "issues": str}}"""
            }],
            response_format={"type": "json_object"}
        )

        return json.loads(response.choices[0].message.content)

    def _improve(self, query: str, current: str, critique: Dict) -> str:
        """Améliore la réponse basée sur la critique"""
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{
                "role": "user",
                "content": f"""Améliore cette réponse :

Question : {query}
Réponse actuelle : {current}
Problèmes identifiés : {critique['issues']}

Fournis une réponse améliorée."""
            }]
        )

        return response.choices[0].message.content

Optimisations de performance

Caching des décisions

from functools import lru_cache
import hashlib

class CachedAgent(Agent):
    """Agent avec cache des décisions récurrentes"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.decision_cache = {}

    def run(self, query: str) -> str:
        # Créer une clé de cache
        cache_key = hashlib.md5(query.encode()).hexdigest()

        if cache_key in self.decision_cache:
            print("⚡ Réponse en cache")
            return self.decision_cache[cache_key]

        result = super().run(query)
        self.decision_cache[cache_key] = result

        return result

Exécution parallèle d’outils

import asyncio
from typing import List

class AsyncActionModule(ActionModule):
    """Module d'action avec exécution parallèle"""

    async def execute_parallel(self, actions: List[Dict]) -> List[Observation]:
        """Exécute plusieurs actions en parallèle"""
        tasks = [self._execute_async(action) for action in actions]
        return await asyncio.gather(*tasks)

    async def _execute_async(self, action: Dict) -> Observation:
        """Version asynchrone de execute()"""
        # Simuler une opération async
        await asyncio.sleep(0.1)
        return self.execute(action)

# Exemple d'utilisation
async def main():
    agent = Agent(reasoning_strategy=ReActReasoning())
    agent.action = AsyncActionModule()

    actions = [
        {"name": "search_web", "arguments": {"query": "météo Paris"}},
        {"name": "search_web", "arguments": {"query": "météo Lyon"}},
        {"name": "search_web", "arguments": {"query": "météo Marseille"}}
    ]

    # Exécution parallèle = 3x plus rapide
    results = await agent.action.execute_parallel(actions)

Streaming des résultats

class StreamingAgent(Agent):
    """Agent qui stream ses résultats en temps réel"""

    def run_stream(self, query: str):
        """Version streaming de run()"""
        observations = [self.perception.observe(query)]

        for iteration in range(self.max_iterations):
            decision = self.reasoning.think(observations, self.memory)

            # Stream la pensée
            yield {"type": "thought", "content": decision["thought"]}

            if decision["finish"]:
                yield {"type": "final", "content": decision["thought"]}
                break

            if decision["action"]:
                # Stream l'action
                yield {"type": "action", "content": decision["action"]}

                result = self.action.execute(decision["action"])
                observations.append(result)

                # Stream le résultat
                yield {"type": "observation", "content": result.content}

# Utilisation
agent = StreamingAgent(reasoning_strategy=ReActReasoning())

for event in agent.run_stream("Quelle est la météo à Paris ?"):
    if event["type"] == "thought":
        print(f"💭 {event['content']}")
    elif event["type"] == "action":
        print(f"🔧 {event['content']}")
    elif event["type"] == "observation":
        print(f"📊 {event['content']}")
    elif event["type"] == "final":
        print(f"✅ {event['content']}")

Gestion des erreurs et robustesse

⚠️ Warning
Production-ready : Un agent production doit gérer les erreurs gracieusement avec retry automatique, fallbacks et timeouts. Sans cela, une API down peut bloquer tout votre système.
class RobustAgent(Agent):
    """Agent avec gestion avancée des erreurs"""

    def __init__(self, *args, retry_on_error=True, **kwargs):
        super().__init__(*args, **kwargs)
        self.retry_on_error = retry_on_error
        self.error_count = 0
        self.max_errors = 3

    def run(self, query: str) -> str:
        try:
            return self._run_with_fallback(query)
        except Exception as e:
            return self._handle_critical_error(e)

    def _run_with_fallback(self, query: str) -> str:
        """Exécution avec fallback sur erreur"""
        try:
            return super().run(query)
        except ToolExecutionError as e:
            if self.retry_on_error and self.error_count < self.max_errors:
                self.error_count += 1
                print(f"⚠️ Erreur outil, retry {self.error_count}/{self.max_errors}")
                return self._run_with_fallback(query)
            else:
                # Fallback : réponse sans outils
                return self._generate_fallback_response(query)

    def _handle_critical_error(self, error: Exception) -> str:
        """Gestion des erreurs critiques"""
        return f"Désolé, une erreur s'est produite : {str(error)}. Veuillez réessayer."

    def _generate_fallback_response(self, query: str) -> str:
        """Génère une réponse sans utiliser d'outils"""
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{
                "role": "user",
                "content": f"{query}\n\nRéponds sans utiliser d'outils externes."
            }]
        )
        return response.choices[0].message.content

Métriques et observabilité

from dataclasses import dataclass
from datetime import datetime
import time

@dataclass
class AgentMetrics:
    """Métriques d'exécution d'un agent"""
    iterations: int
    tools_called: int
    total_duration_s: float
    llm_calls: int
    tokens_used: int
    success: bool
    error_message: Optional[str] = None

class ObservableAgent(Agent):
    """Agent avec collecte de métriques"""

    def run(self, query: str) -> str:
        start_time = time.time()
        metrics = AgentMetrics(
            iterations=0,
            tools_called=0,
            total_duration_s=0.0,
            llm_calls=0,
            tokens_used=0,
            success=False
        )

        try:
            result = super().run(query)
            metrics.success = True
            return result
        except Exception as e:
            metrics.error_message = str(e)
            raise
        finally:
            metrics.total_duration_s = time.time() - start_time
            self._log_metrics(metrics)

    def _log_metrics(self, metrics: AgentMetrics):
        """Log les métriques (vers un système de monitoring)"""
        print(f"\n📊 Métriques d'exécution:")
        print(f"  - Itérations : {metrics.iterations}")
        print(f"  - Outils appelés : {metrics.tools_called}")
        print(f"  - Durée : {metrics.total_duration_s:.2f}s")
        print(f"  - Appels LLM : {metrics.llm_calls}")
        print(f"  - Succès : {metrics.success}")

Exercices pratiques

Implémenter un nouvel outil

Créez un outil TranslationTool qui utilise l’API de traduction de votre choix :

class TranslationTool(Tool):
    @property
    def name(self) -> str:
        return "translate"

    def execute(self, text: str, target_language: str) -> str:
        # TODO: Implémenter
        pass

Mémoire vectorielle

Ajoutez une mémoire vectorielle à la classe Memory pour stocker et récupérer des faits par similarité sémantique.

Agent multi-modal

Créez un agent capable de traiter des images en ajoutant un ImagePerceptionModule.

Best practices de production

Construire un agent qui fonctionne en démo est une chose. Le faire tourner en production de manière fiable en est une autre. Voici les principes essentiels :

Gestion des timeouts et limites

Problème : Un outil bugué peut bloquer indéfiniment votre agent.

Solution : Imposez des timeouts stricts à tous les niveaux :

# Timeout par outil (3 secondes max)
@timeout(3)
def execute_tool(tool, args):
    return tool.execute(**args)

# Timeout global de l'agent (30 secondes max)
@timeout(30)
def agent.run(query):
    # ...

Limites recommandées :

  • Timeout par outil : 2-5 secondes
  • Timeout global agent : 30-60 secondes
  • Max itérations : 10-15
  • Max appels LLM consécutifs : 5

Validation des entrées/sorties

Tous les outils doivent valider leurs entrées ET leurs sorties :

class SafeTool(Tool):
    def execute(self, **kwargs):
        # 1. Valider entrées
        self._validate_inputs(kwargs)

        # 2. Exécuter
        result = self._execute(**kwargs)

        # 3. Valider sortie
        self._validate_output(result)

        return result

Rate limiting et gestion des coûts

Problème : Un agent qui boucle peut générer des coûts LLM astronomiques.

Solutions :

  • Circuit breaker : Arrête l’agent si >X appels/minute
  • Budget par conversation : Limite le coût total d’une session
  • Retry exponentiel : Attend de plus en plus longtemps entre retries
  • Caching intelligent : Stocke les résultats d’appels identiques
class RateLimitedAgent(Agent):
    def __init__(self, *args, max_cost_usd=0.50, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_cost = max_cost_usd
        self.current_cost = 0.0

    def run(self, query):
        if self.current_cost >= self.max_cost:
            raise BudgetExceededError(
                f"Budget dépassé : ${self.current_cost:.2f}"
            )
        return super().run(query)

Logging et traçabilité

Chaque action doit être loggée avec un ID de trace unique pour faciliter le débogage :

import uuid
import logging

class TracedAgent(Agent):
    def run(self, query):
        trace_id = str(uuid.uuid4())
        logger = logging.getLogger(f"agent.{trace_id}")

        logger.info(f"[START] Query: {query}")

        try:
            result = super().run(query)
            logger.info(f"[SUCCESS] Result: {result}")
            return result
        except Exception as e:
            logger.error(f"[ERROR] {str(e)}", exc_info=True)
            raise

Fallbacks et dégradation gracieuse

Définissez toujours un comportement de secours :

Niveau 1 : Outil principal échoue → Essayer outil alternatif Niveau 2 : Tous les outils échouent → Réponse LLM sans outils Niveau 3 : LLM échoue → Réponse pré-définie d’erreur

⚠️ Warning
Erreur courante : Ne jamais retourner d’erreur brute à l’utilisateur. Un agent production doit TOUJOURS retourner une réponse formatée, même en cas d’échec total. Message recommandé : ‘Je rencontre des difficultés techniques. Veuillez réessayer dans quelques instants.’

Monitoring et alertes

Surveillez ces métriques en temps réel :

  • Taux de succès : % de requêtes complétées sans erreur (cible : >95%)
  • Latence P50/P95/P99 : Temps de réponse (cible P95 : <5s)
  • Coût moyen par requête : Budget LLM ($0.01-$0.10 selon complexité)
  • Taux d’erreur par outil : Identifier outils défaillants
  • Distribution des itérations : Détecter agents qui bouclent

Alertes critiques :

  • Taux d’erreur >10% pendant 5 minutes → Page l’équipe
  • Latence P95 >10s → Investigation requise
  • Coût moyen >$0.50 → Agent potentiellement en boucle

Cas d’usage réels et architectures adaptées

Customer support agent (Zendesk, Intercom)

Architecture : Simple ReAct avec 4-6 outils

Outils :

  • search_knowledge_base : Recherche dans la doc
  • get_user_info : Récupère infos client (CRM)
  • check_order_status : Statut commande
  • create_ticket : Escalade vers humain

Métriques de succès :

  • Résolution sans escalade : 60-70%
  • Temps de réponse : <3s
  • Satisfaction utilisateur : >4.2/5

Coût : $0.02-0.05 par conversation

Data analysis agent (Business Intelligence)

Architecture : Plan-and-Execute + Self-Reflection

Outils :

  • execute_sql : Requêtes sur data warehouse
  • generate_chart : Visualisations (matplotlib, plotly)
  • statistical_test : Tests statistiques

Workflow :

  1. Planification : Décomposer la question analytique
  2. Exécution : Requêtes SQL + calculs
  3. Réflexion : Vérifier cohérence des résultats
  4. Génération : Rapport avec graphiques

Métriques :

  • Exactitude des analyses : 85-90%
  • Temps moyen : 15-30s
  • Coût : $0.10-0.30 par analyse

Code review agent (GitHub, GitLab)

Architecture : Hiérarchique avec 3 sous-agents

Sous-agents :

  • Security Agent : Détecte vulnérabilités (SQL injection, XSS)
  • Quality Agent : Code smells, complexité, duplication
  • Performance Agent : Optimisations possibles

Workflow :

  1. Superviseur répartit le code entre sous-agents
  2. Chaque agent analyse sa partie
  3. Superviseur agrège et priorise les feedbacks

Métriques :

  • Vrais positifs : 70-80% (vs 40-50% pour linters)
  • Faux positifs : <15%
  • Temps review : 2-5 min pour 500 lignes

Research assistant (Perplexity-style)

Architecture : ReAct avec mémoire vectorielle longue

Outils :

  • web_search : Recherche multi-sources (Google, Bing, Scholar)
  • fetch_url : Récupère contenu page
  • extract_citations : Identifie sources citables
  • vector_store : Stocke/récupère faits par similarité

Particularités :

  • Mémoire vectorielle pour éviter recherches redondantes
  • Citations systématiques avec URLs
  • Fact-checking multi-sources

Métriques :

  • Exactitude factuelle : 85-92% (validé manuellement)
  • Citations pertinentes : >90%
  • Coût : $0.08-0.15 par recherche approfondie
💡 Tendance 2025 : Les agents hybrides (LLM + recherche + code + multimodal) deviennent la norme. GPT-4o, Claude 3.5 Sonnet et Gemini 1.5 Pro intègrent nativement vision, function calling et browsing pour des agents plus performants.

Prochaines étapes

Vous maîtrisez maintenant l’architecture fondamentale des agents IA. Dans le prochain article, nous plongerons en profondeur dans le pattern ReAct (Reasoning + Acting) qui alimente les agents les plus performants actuels.

Ce que vous allez apprendre :

  • Implémentation détaillée de ReAct avec prompts optimisés
  • Comparaison avec Chain-of-Thought, Plan-and-Execute et Reflexion
  • Techniques d’optimisation pour réduire coûts et latence
  • Gestion avancée des boucles et des erreurs
  • Intégration avec LangChain et frameworks modernes

Points clés à retenir de cet article :

Architecture modulaire : Séparation Perception/Reasoning/Action/Memory pour testabilité et maintenabilité ✅ Orchestrateur central : Gère la boucle principale et coordonne les modules ✅ Stratégies interchangeables : Différents patterns de raisonnement selon le cas d’usage ✅ Robustesse production : Timeouts, validation, rate limiting, monitoring indispensables ✅ Patterns avancés : Hiérarchique, Plan-and-Execute, Self-Reflection pour tâches complexes ✅ Observabilité critique : Logs, traces, métriques pour debug et optimisation


Ressources et liens

Articles connexes de la série

Fondamentaux :

Frameworks et implémentation :

Production :

Concepts IA connexes

Frameworks et outils

Python :

JavaScript/TypeScript :

Documentation officielle


Retour à la série Agents IA | Module suivant : Pattern ReAct →