Accueil / Articles

Construire un système de recherche multi-agents avec LangChain Deep Agents

Un aperçu pratique du pattern Deep Agents : un orchestrateur, des agents spécialisés, des outils sécurisés et une interface Streamlit, testé avec des LLM locaux et en cloud.

Éditeur : REACTAPP.TOP10 min de lecture2403 mots

Sources: Deep Agents in Action: Building a Multi-Agent Research System

Les boucles à instruction unique laissent place aux systèmes multi-agents. La nouvelle configuration divise le travail, conserve l’état entre les étapes et fournit à chaque agent un ensemble restreint d’outils. Une mise à l’épreuve d’un autre orchestrateur basé sur cette idée, Deep Agents de LangChain, constitue un bon cas d’étude ; voici donc ce qu’il fait et comment une petite application de recherche a été développée à partir de lui.

Ce que l’architecture Deep Agents résout

LangChain a récemment annoncé une plateforme gérée pour exécuter des topologies d’agents profonds. Ce modèle cible trois faiblesses propres à un agent unique travaillant sur une tâche longue : la fenêtre de contexte se détériore, l’agent hésite quant à l’outil à choisir, et les erreurs de raisonnement s’accumulent au fil des étapes.

Sa structure est hiérarchique :

  • Un orchestrateur de niveau supérieur divise une tâche complexe en parties et les confie à des spécialistes.
  • Chaque spécialiste exécute son propre cycle ReAct isolé, avec ses propres outils et mémoire.
  • Les spécialistes envoient des résultats structurés, que l’orchestrateur assemble ensuite.
  • Ces éléments sont imbriqués, de sorte que l’orchestrateur peut lui-même servir de sous-agent pour quelqu’un d’autre.

Par rapport à un seul agent qui fait tout, la différence se présente ainsi :

| Single Agent                       | Deep Agents                                       |
| ---------------------------------- | ------------------------------------------------- |
| One context window gets overloaded | Each agent has an isolated, focused context       |
| All reasoning in one prompt        | Specialised reasoning per domain                  |
| Hard to scale                      | Add specialists without changing the orchestrator |
| Hard to debug                      | Full delegation trace for auditability            |

La documentation officielle du framework le décrit comme le moyen le plus rapide pour créer des agents pilotés par des LLM, avec des systèmes de fichiers intégrés pour gérer le contexte, générer des sous-agents et conserver une mémoire à long terme. La planification des tâches et les compétences constituent des fonctionnalités optionnelles. Il énumère les capacités suivantes : agir dans un environnement grâce à des outils, des fichiers et l’exécution de code ; récupérer des souvenirs, des compétences et des connaissances sectorielles au moment opportun ; résumer l’historique et extraire de grands résultats du contexte ; déléguer des tâches à des sous-agents dans des fenêtres de contexte distinctes ; faire une pause pour obtenir l’approbation humaine aux points critiques ; et mettre à jour la mémoire, les compétences et les prompts en fonction de l’utilisation réelle.

Le dépôt du projet indique qu’il s’agit d’un framework open source fonctionnel dès l’installation, qui permet d’étendre, de modifier ou de remplacer n’importe quelle partie. Les paramètres par défaut sont optimisés pour des tâches longues et complexes, tout modèle d’appel d’outils est accepté (frontier, open-weight ou local), et il s’appuie sur LangGraph pour le streaming, la persistance et les points de contrôle, avec un suivi, une évaluation et un déploiement via LangSmith. La liste des fonctionnalités comprend des sous-agents disposant d’un contexte isolé, un système de fichiers basé sur des backends interchangeables, une gestion du contexte qui enregistre les résultats des outils sur disque, un accès à la shell dans un environnement sandbox, une mémoire persistante, une approbation humaine pour les appels d’outils, des compétences réutilisables, ainsi que des outils personnalisés ou n’importe quel serveur MCP.

Idees fondamentales

  • Orchestration hiérarchique. Un agent superviseur reçoit un objectif de haut niveau et transmet des sous-tâches aux spécialistes, au lieu de traiter chaque étape dans une seule boucle.
  • Rôles spécialisés. Chaque agent dispose d’une instruction système précisément définie, de ses propres outils et d’une fenêtre de contexte distincte, pour des tâches telles que la récupération d’informations, les calculs ou la rédaction de documents.
  • Mémoire isolée. Chaque agent conserve sa propre mémoire de travail, ses faits et ses événements, afin que l’état d’une phase ne s’influence pas sur une autre.
  • Raisonnement ReAct. Les sous-agents répètent des cycles de Réflexion, Action et Observation à l’aide d’interfaces d’outils validées.

L’implémentation

L’application de démonstration est une version petite et pédagogique, construite à l’aide d’IBM Bob, qui fonctionne localement ou avec des modèles cloud. Elle suit le modèle des Deep Agents à une échelle modeste.

La tâche s’effectue à travers un petit arbre :

User Task
  └─ OrchestratorAgent (Planning & Synthesis)
      ├─ ResearcherAgent (web_search)
      ├─ AnalystAgent (calculator, code_executor)
      └─ WriterAgent (file_reader)

Quatre couches assurent son fonctionnement :

  1. L’orchestration (agents/orchestrator.py) prend en charge les entrées de l’utilisateur, évalue leur complexité, élabore un plan, délègue les sous-tâches séquentiellement et synthétise les résultats.
  2. Les spécialistes (agents/specialists.py) définissent un ResearcherAgent, un AnalystAgent et un WriterAgent.
  3. Les outils (tools/) fournissent des calculs arithmétiques exécutés en toute sécurité via l’AST, une exécution Python isolée, une abstraction pour les recherches sur le web ainsi que des opérations d’entrée/sortie de fichiers dans un environnement sandboxé.
  4. Le relais et l’interface utilisateur (app.py et main.py) proposent une interface Streamlit qui synchronise de manière asynchrone l’état entre les threads et suit en temps réel les différentes phases, ainsi qu’un outil en ligne de commande.

Le stack est constitué de Python pur, avec Streamlit pour l’interface. Pour l’inference locale, il a été testé avec Ollama, et llama.cpp est également pris en charge via des paramètres de configuration.

La structure des dossiers définie par les exigences :

deepagents-usecase/
├── agents/
│   ├── __init__.py         # Package exports
│   ├── base.py             # Abstract BaseAgent + ReAct loop
│   ├── llm_client.py       # LLM adapter (Ollama/llama.cpp/OpenAI/Anthropic)
│   ├── messages.py         # Typed message protocol
│   ├── orchestrator.py     # OrchestratorAgent (top-level)
│   ├── researcher.py       # ResearcherAgent specialist
│   ├── analyst.py          # AnalystAgent specialist
│   └── writer.py           # WriterAgent specialist
├── tools/
│   ├── __init__.py
│   ├── base.py             # BaseTool + ToolResult
│   ├── calculator.py       # Safe AST-based arithmetic evaluator
│   ├── code_executor.py    # Sandboxed Python execution (exec with allow-list)
│   ├── file_reader.py      # Sandboxed file reading (input/ only)
│   └── web_search.py       # Web search (stub + live Tavily)
├── memory/
│   ├── __init__.py
│   └── store.py            # AgentMemoryStore (short/long-term/episodic)
├── config/
│   ├── __init__.py
│   └── settings.py         # Centralised env-based configuration
├── tests/
│   ├── test_tools.py           # Tool unit tests (71 tests)
│   ├── test_memory.py          # Memory unit tests (24 tests)
│   ├── test_agents.py          # Agent integration tests (92 tests)
│   └── test_code_executor.py   # Code Executor tests (134 tests)
├── input/                  # Input documents (content gitignored)
├── output/                 # Generated reports (content gitignored)
├── memory/                 # Persistent agent memory (JSON files)
├── scripts/
│   ├── start.sh                  # Launch Streamlit in detached mode
│   ├── stop.sh                   # Gracefully stop the application
│   ├── cleanup.sh                # Remove .venv, __pycache__, etc.
│   └── check_code_executor.py    # Standalone Code Executor sanity-check
├── Docs/
│   ├── Architecture.md           # Mermaid architecture diagrams
│   ├── Quickstart.md             # Step-by-step getting started guide
│   ├── API.md                    # Full public API reference
│   └── CodeExecutorVerification.md  # Code Executor test & verification guide
├── app.py                  # Streamlit web UI
├── main.py                 # CLI entry point
├── requirements.txt
├── .env.example            # Environment variable template
└── README.md

La configuration est flexible, ce qui permet au code de s’adapter aux configurations courantes. Le backend peut communiquer avec des environnements locaux tels que llama.cpp et Ollama, ou avec des fournisseurs de services cloud accessibles via des clés API. Un fichier d’environnement commenté décrit toutes les paramètres nécessaires :

# =============================================================================
# Deep Agents System — Environment Configuration
# =============================================================================
# Copy this file to .env and fill in your values.
# NEVER commit the .env file to version control.
#
# Usage:
#   cp .env.example .env
#   # edit .env with your actual keys
# =============================================================================

# ─── LLM Provider ──────────────────────────────────────────────────────────
# Select ONE provider. Comment out the others.

# Option A: Ollama (local, default — no API key needed)
LLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2

# Option B: llama.cpp (local)
# LLM_PROVIDER=llamacpp
# LLAMACPP_BASE_URL=http://localhost:9931/v1
# LLAMACPP_MODEL=local-model

# Option C: OpenAI
# LLM_PROVIDER=openai
# OPENAI_API_KEY=sk-...
# OPENAI_MODEL=gpt-4o

# Option D: Anthropic
# LLM_PROVIDER=anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# ANTHROPIC_MODEL=claude-3-5-sonnet-20241022

# ─── Application Settings ──────────────────────────────────────────────────
APP_PORT=8501
APP_HOST=0.0.0.0
LOG_LEVEL=INFO

# ─── Agent Configuration ───────────────────────────────────────────────────
# Maximum reasoning steps per agent (lower = faster on slow local LLMs)
MAX_AGENT_STEPS=8

# Maximum tokens per LLM call (1024 is enough for ReAct; raise for longer reports)
MAX_TOKENS=1024

# Temperature for LLM responses (0.0 = deterministic, 1.0 = creative)
TEMPERATURE=0.1

# ─── Memory & Storage ──────────────────────────────────────────────────────
# Directory for persistent agent memory (relative to project root)
MEMORY_DIR=./memory

# Maximum number of memories to retain per agent
MAX_MEMORY_ENTRIES=100

# ─── Tool Configuration ────────────────────────────────────────────────────
# Enable or disable specific tools (true/false)
TOOL_WEB_SEARCH_ENABLED=true
TOOL_CALCULATOR_ENABLED=true
TOOL_FILE_READER_ENABLED=true
TOOL_CODE_EXECUTOR_ENABLED=true

# Web search stub — set to real Tavily/SerpAPI key for live search
# TAVILY_API_KEY=tvly-...

# ─── Output ────────────────────────────────────────────────────────────────
OUTPUT_DIR=./output

Code digne d’attention

L’application a pour but de montrer comment les tâches sont divisées et confiées à des ensembles d’outils distincts. Elle est conçue pour être étendue, afin que de nouveaux outils et des spécialistes supplémentaires puissent s’y intégrer facilement.

Évaluateur arithmétique sécurisé

Permettre à un modèle d’exécuter du code arbitraire pour des calculs mathématiques créerait une faille de sécurité. C’est pourquoi la calculette analyse les expressions à l’aide du module ast de Python et n’accepte que des types de nœuds et des fonctions spécifiquement autorisés, jamais une appel direct à eval():

# Whitelisted AST operators and math functions
_SAFE_OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.Pow: operator.pow,
    ast.USub: operator.neg,
}

_SAFE_FUNCTIONS = {
    "abs": abs, "round": round, "sqrt": math.sqrt,
    "sin": math.sin, "cos": math.cos, "log": math.log,
    "pi": math.pi, "e": math.e,
}

def _safe_eval(node: ast.expr) -> float:
    """Recursively evaluate an AST expression node in a safe sandbox."""
    if isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float)):
            return float(node.value)
        raise ValueError(f"Unsupported constant type: {type(node.value).__name__}")

    if isinstance(node, ast.BinOp):
        op_type = type(node.op)
        if op_type not in _SAFE_OPERATORS:
            raise ValueError(f"Unsupported binary operator: {op_type.__name__}")
        left = _safe_eval(node.left)
        right = _safe_eval(node.right)
        return _SAFE_OPERATORS[op_type](left, right)

    if isinstance(node, ast.Call):
        func_name = node.func.id
        if func_name not in _SAFE_FUNCTIONS:
            raise ValueError(f"Function '{func_name}' is not whitelisted.")
        args = [_safe_eval(a) for a in node.args]
        return _SAFE_FUNCTIONS[func_name](*args)

    raise ValueError(f"Unsupported AST node type: {type(node).__name__}")

Exécuteur de code en environnement isolé

Pour des traitements de données plus lourds, cet exécuteur vous offre un espace de noms isolé avec des fonctions intégrées restreintes, quelques modules scientifiques déjà importés, une redirection des sorties et un délai d’expiration basé sur les threads :

def _build_exec_namespace() -> dict[str, Any]:
    return {
        "__builtins__": _SAFE_BUILTINS, # Explicit whitelist (no open, __import__, eval)
        "math": math,
        "statistics": statistics,
        "pi": math.pi,
        "e": math.e,
    }

def _execute_code(code: str, timeout: int = 5) -> ToolResult:
    exec_result = _ExecResult()
    captured_io = io.StringIO()

    def _worker():
        namespace = _build_exec_namespace()
        namespace["__builtins__"]["print"] = lambda *args, **kw: print(*args, **{**kw, "file": captured_io})
        try:
            exec(code, namespace)
            exec_result.stdout = captured_io.getvalue()
        except Exception as exc:
            exec_result.error = f"{type(exc).__name__}: {exc}"

    thread = threading.Thread(target=_worker, daemon=True)
    thread.start()
    thread.join(timeout=timeout)
    if thread.is_alive():
        return ToolResult(success=False, error=f"Execution timed out after {timeout}s.")

Un relais sécurisé pour l’interface Streamlit

Les threads de travail de Streamlit ne peuvent pas écrire directement dans st.session_state, car ils ne disposent pas de ScriptRunContext. L’application contourne ce problème en utilisant un relais singleton mémorisé via @st.cache_resource et protégé par un verrou réentrant (threading.RLock), ce qui permet aux étapes de l’agent d’être transmises au-delà des frontières des threads vers la page :

class _PipelineRelay:
    """Thread-safe relay store for the running multi-agent pipeline."""
    def __init__(self) -> None:
        self.lock = threading.RLock()
        self.status_messages: list[dict[str, str]] = []
        self.agent_cards: dict[str, list[str]] = {}
        self.running: bool = False

    def append_status(self, msg_dict: dict[str, str]) -> None:
        with self.lock:
            self.status_messages.append(msg_dict)
            agent_id = msg_dict.get("agent_id", "orchestrator")
            self.agent_cards.setdefault(agent_id, []).append(msg_dict.get("detail", ""))

@st.cache_resource
def _get_relay() -> _PipelineRelay:
    return _PipelineRelay()

Un exemple concret : le changement climatique

La tâche à accomplir consistait en une analyse de l’impact des concentrations croissantes de CO2 sur les écosystèmes dans le monde entier, y compris les niveaux actuels de CO2, la hausse en pourcentage par rapport à l’époque préindustrielle, ainsi qu’un ensemble de recommandations politiques en conclusion.

Ce qui se passe ensuite :

  1. L’orchestrateur divise le travail en recherche, analyse et rédaction.
  2. Le chercheur effectue trois recherches sur Internet portant sur les tendances du CO2, l’impact sur les écosystèmes et les cadres politiques.
  3. L’analyste utilise l’outil de calcul : (421 - 280) / 280 * 100, ce qui donne une augmentation d’environ 50,36 %.
  4. Le rédacteur élabore un document Markdown structuré comprenant un résumé exécutif, les conclusions, l’analyse des données et des recommandations.
  5. L’orchestrateur intègre tout cela dans le rapport final.

Le résultat est enregistré dans output/deep_agents_report_<timestamp>.md, et un exemple ressemble à ceci :

Benefits of Renewable Energy and Calculation of 280 Times 42

Benefits of Renewable Energy

Renewable energy reduces greenhouse gas emissions, contributing to climate change mitigation (Source: [National Renewable Energy Laboratory](https://www.nrel.gov/renewables/energy-benefits.html)). This is a key finding supported by credible sources, including the International Renewable Energy Agency (2020) and the World Health Organization (2020).

Renewable energy creates jobs and stimulates local economies (Source: [International Renewable Energy Agency](https://www.irena.org/publications/2020/Jun/Global-Status-Report-2020)). This is a significant benefit highlighted by the International Energy Agency (2020) and the National Bureau of Economic Research (2020).

Renewable energy improves air quality and public health (Source: [World Health Organization](https://www.who.int/news-room/fact-sheets/detail/air-pollution)). This is a critical aspect of renewable energy, supported by the National Renewable Energy Laboratory (n.d.).

The global renewable energy market is projected to reach 30% of total energy production by 2025 (Source: [International Energy Agency](https://www.iea.org/news/pressrelease/2020/june/global-renewables-report-2020/)). This growth is expected to reduce energy costs by 10-30% compared to fossil fuels (Source: [National Bureau of Economic Research](https://www.nber.org/papers/w28822)).

Calculation of 280 Times 42

The calculation of 280 times 42 yields 11,840. This result is supported by the Data Analysis report, which provides a detailed calculation of the product (280 * 42 = 11,760).

Trend Analysis

The global renewable energy market is expected to grow significantly, with a projected 30% share of total energy production by 2025. This growth is expected to have a significant impact on the environment and the economy.

Key Insights

1. Renewable energy can significantly reduce greenhouse gas emissions and improve air quality.
2. Renewable energy can create jobs and stimulate local economies.

Limitations

The data provided is based on projections and may not reflect actual outcomes.

Sources

1. National Renewable Energy Laboratory. (n.d.). Energy Benefits of Renewable Energy. Retrieved from <https://www.nrel.gov/renewables/energy-benefits.html>
2. International Renewable Energy Agency. (2020). Global Status Report 2020. Retrieved from <https://www.irena.org/publications/2020/Jun/Global-Status-Report-2020>
3. World Health Organization. (2020). Air pollution. Retrieved from <https://www.who.int/news-room/fact-sheets/detail/air-pollution>
4. International Energy Agency. (2020). Global Renewables Report 2020. Retrieved from <https://www.iea.org/news/pressrelease/2020/june/global-renewables-report-2020/>
5. National Bureau of Economic Research. (2020). The Economics of Renewable Energy. Retrieved from <https://www.nber.org/papers/w28822>

Ajouter un autre spécialiste

De nouveaux outils et agents s’intègrent sans grande difficulté. Pour ajouter un spécialiste :

  1. Créez agents/my_specialist.py en tant que sous-classe de BaseAgent.
  2. Mettez en œuvre la propriété role_description.
  3. Enregistrez l’agent dans OrchestratorAgent.__init__.
  4. Ajoutez la logique de délégation dans OrchestratorAgent.run_task.

Un squelette à partir duquel commencer :

from agents.base import BaseAgent

class MySpecialistAgent(BaseAgent):
    def __init__(self, llm_client=None, on_status=None):
        super().__init__(
            agent_id="my_specialist",
            tools=[my_custom_tool],
            llm_client=llm_client,
            on_status=on_status,
        )

    @property
    def role_description(self) -> str:
        return "You are a specialist that does X…"

En résumé

Le projet démontre le fonctionnement concret du modèle Deep Agents : la délégation à travers une hiérarchie, des outils sécurisés grâce à un environnement sandbox AST, ainsi qu’une interface utilisateur qui diffuse des mises à jour sans enfreindre les règles de multithreading de Streamlit. Des pipelines d’agents complexes peuvent s’exécuter de manière fiable tant avec des endpoints cloud que avec des environnements LLM locaux tels qu’Ollama et llama.cpp.