Accueil / Articles

Les frameworks d’agents raisonnent : Temporal survit grâce à une exécution durable pour les agents.

LangGraph, CrewAI et AutoGen gèrent les appels aux LLM sans provoquer de pannes ni d’attentes prolongées. Comment l’exécution durable de Temporal comble cette lacune, et quand elle est superflue.

Éditeur : REACTAPP.TOP11 min de lecture2460 mots

Sources: Agentic AI Frameworks in 2026: The 3 Everyone’s Using and the 1 Nobody Talks About

Les ingénieurs en sondages qui développent des systèmes agents en 2026 évoquent leur framework, et presque tous mentionnent LangGraph, CrewAI, AutoGen ou une combinaison de ces outils. Aucun de ces outils n’est mauvais. Le problème, c’est que toute cette discussion a lieu au niveau incorrect de la pile technologique.

Les systèmes d’agents en production présentent un schéma récurrent. Ce qui réveille l’ingénieur de garde à 3 heures du matin n’est que rarement le framework qui détermine ce que pense l’agent. Il s’agit presque toujours d’un même type de panne : un processus s’arrête au milieu d’un flux de travail, une étape est exécutée deux fois car sa tentative de réessai n’était pas idempotente, une éviction de pod met fin à une tâche d’agent en cours depuis 40 minutes sans possibilité de la reprendre, ou encore l’état du flux de travail stocké dans une variable Python disparaît lorsque le conteneur est recyclé.

Aucun des systèmes LangGraph, CrewAI ou AutoGen ne gère cette famille de tâches de bout en bout, et c’est intentionnel, car ils ont pour but d’orchestrer les appels aux modèles de langage et l’utilisation d’outils. Les systèmes d’agents en production échouent principalement en raison d’un manque d’exécution fiable, un domaine bien plus ancien et moins dynamique qui précède d’environ une décennie l’apparition du terme « agent ». Temporal, le système qui le développe, est à peine mentionné dans les débats sur les frameworks d’agents, pourtant il est devenu silencieusement l’ossature de nombreux déploiements d’agents sérieux. Ce qui suit décrit ce que font bien ces trois frameworks populaires, où chaque abstraction présente des limites, et pourquoi la quatrième option, moins en vogue, mérite un investissement commercial.

LangGraph

La contribution de LangGraph consiste à transformer un agent en un graphe explicite défini par des états typés, plutôt qu’en une chaîne implicite de prompts. Cela a une véritable valeur, car le flux de contrôle devient inspectable et testable. Un workflow de tri, par exemple, peut ressembler à ceci :

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END

class TriageState(TypedDict):
    ticket_id: str
    category: Literal["billing", "technical", "abuse", "unknown"]
    priority: int
    assigned_queue: str | None

def classify_node(state: TriageState) -> TriageState:
    category = llm_classify(state["ticket_id"])  # the only probabilistic step
    return {**state, "category": category}

def route_node(state: TriageState) -> str:
    # deterministic, testable, no LLM involved
    return {
        "billing": "billing_queue",
        "technical": "tech_queue",
        "abuse": "trust_and_safety_queue",
        "unknown": "human_review",
    }[state["category"]]

graph = StateGraph(TriageState)
graph.add_node("classify", classify_node)
graph.set_entry_point("classify")
graph.add_conditional_edges("classify", route_node)
app = graph.compile()

C’est une bonne idée, bien formulée. Les problèmes commencent lorsque quelque chose doit persister plus longtemps qu’une seule appel à invoke() et son point de contrôle. Pensez à la coordination entre services, à des workflows s’étendant sur plusieurs jours, ou à des étapes exécutées où LangGraph n’a aucun contrôle : un job Spark, une file d’attente d’approbation qui passe par Slack et ne renvoie les résultats que des jours plus tard, ou un service déployé par une autre équipe selon son propre rythme. Le mécanisme de point de contrôle fonctionne, mais il ne couvre que le propre modèle d’exécution de LangGraph. Lorsqu’on transfère les données vers une infrastructure sans connexion aux modèles de langage, la durabilité devient quelque chose qu’il faut implémenter manuellement.

CrewAI

On peut déclarer une équipe multi-agents en quelques lignes :

from crewai import Agent, Task, Crew

researcher = Agent(role="Research Analyst", goal="Find relevant market data", tools=[search_tool])
writer = Agent(role="Report Writer", goal="Turn research into a client-ready summary")

crew = Crew(agents=[researcher, writer], tasks=[
    Task(description="Research {company}'s Q2 earnings", agent=researcher),
    Task(description="Write a 3-paragraph summary for the client", agent=writer),
])
crew.kickoff(inputs={"company": "Acme Corp"})

Aucun outil ne permet de passer d’une idée vague à une démonstration fonctionnelle en vingt minutes plus rapidement que CrewAI. Le coût apparaît par la suite. Les fonctions kickoff() ne permettent pas de faire une pause de plusieurs heures ou jours, il n’y a pas de mécanisme de tentative automatique en dehors de celui intégré aux outils individuels, et il n’existe pas d’état persistant à moins que le développeur ne le sauvegarde lui-même. Il orchestre le raisonnement des LLM mais n’est pas un moteur de flux de travail, et il n’a jamais été présenté comme tel, bien que de nombreuses équipes finissent par l’utiliser de cette manière par hasard.

AutoGen

Une configuration minimale d’AutoGen associe un assistant à un proxy utilisateur :

import autogen

assistant = autogen.AssistantAgent(name="assistant", llm_config={"config_list": config_list})
user_proxy = autogen.UserProxyAgent(name="user_proxy", human_input_mode="NEVER")
user_proxy.initiate_chat(assistant, message="Summarize this quarter's incident reports.")

Le style conversationnel et émergent d’AutoGen convient aux travaux exploratoires. Sa version de 2026, dotée d’un environnement de travail plus structuré et de modèles basés sur des événements, tend même à séparer l’orchestration de la conversation. Au fond, sa base reste une transcription des messages plutôt qu’un registre durable et structuré des exécutions. Lorsque le processus qui gère la conversation s’arrête, ce qui reste est un journal de chat incomplet et non une computation pouvant reprendre.

Le manque commun

Tous les trois se distinguent dans ce qui est vraiment nouveau : faire en sorte qu’un LLM raisonne, utilise des outils et travaille avec d’autres agents pilotés par des LLM. La question plus ancienne et plus simple, à savoir s’assurer qu’un processus en plusieurs étapes se termine correctement quel que soit le plantage, la redémarrage, le temps d’attente ou les problèmes sous-jacents, n’est pas quelque chose qu’ils cherchent à résoudre. Ce n’est pas leur faute ; s’y attendre revient à commettre une erreur de catégorie.

Pensez à un flux de travail réel, et non à une démonstration : un agent qui concilie les factures des fournisseurs. Il extrait les lignes de détail d’un PDF, les compare avec une commande d’achat dans un système ERP, met en évidence les écarts dépassant un certain seuil pour qu’un humain les examine, puis peut attendre des jours une réponse avant d’enregistrer la transaction approuvée en comptabilité. Une étape échouée doit être réessayée sans double enregistrement. Un redéploiement au milieu du flux de travail ne doit pas entraîner un démarrage à zéro ni disparaître silencieusement.

┌────────────┐     ┌───────────────┐     ┌─────────────────┐     ┌───────────────┐
│  Extract   │ --> │ Match against │ --> │  Human approval │ --> │  Post to ERP  │
│  line items│     │ purchase order│     │  (can take days)│     │  (idempotent) │
└────────────┘     └───────────────┘     └─────────────────┘     └───────────────┘
      ▲                                          │
      └───────────── retry on failure ───────────┘

Nul n’a conçu LangGraph, CrewAI ou AutoGen pour garantir que ce diagramme survive à un redémarrage du conteneur au troisième jour, alors que quelqu’un est encore en train d’évaluer l’élément 14. Il ne s’agit pas d’orchestration de LLM ; il s’agit plutôt d’exécution fiable, et les systèmes qui y parviennent bien, avec Temporal en tête, assurent depuis des années des tâches backend peu spectaculaires : traitement des paiements, exécution des commandes et microservices selon le pattern saga, bien avant que le terme « agent » ne soit utilisé pour décrire de tels programmes.

Comment fonctionne Temporal

Le modèle Temporal semble étonnamment simple. Le flux de travail est écrit sous forme de fonction dans du code ordinaire, avec des boucles, des conditions et des appels de fonctions, et Temporal assure la persistance de l’historique d’exécution de cette fonction. Si le processus plante à un moment donné, Temporal rejoue la fonction à partir de son historique d’événements persisté jusqu’au point de panne et continue de manière déterministe, sans réexécuter les effets secondaires. La magie réside dans le fait que les effets secondaires, tels que l’appel à un LLM, la consultation d’une API ou l’écriture dans une base de données, se trouvent dans des fonctions distinctes appelées activités. Temporal réessaie ces dernières de manière indépendante avec un délai d’attente configurable, tandis que la fonction de flux de travail ne fait qu’ coordonner leurs exécutions.

L’agent de rapprochement des factures ci-dessous considère l’appel au LLM comme une activité parmi plusieurs autres. Tout d’abord, voici les activités, qui sont les parties en contact avec le monde extérieur :

# activities.py — the parts that touch the outside world (LLMs, APIs, DBs)
from temporalio import activity
from dataclasses import dataclass

@dataclass
class Invoice:
    invoice_id: str
    vendor: str
    line_items: list[dict]

@activity.defn
async def extract_line_items(pdf_url: str) -> list[dict]:
    # calls a vision-capable LLM, or a dedicated OCR model — doesn't matter to the workflow
    text = await download_and_ocr(pdf_url)
    return await llm_extract_structured(text, schema=LINE_ITEM_SCHEMA)

@activity.defn
async def match_purchase_order(invoice: Invoice) -> dict:
    po = await erp_client.get_purchase_order(invoice.vendor)
    discrepancies = diff_line_items(invoice.line_items, po["line_items"])
    return {"po_id": po["id"], "discrepancies": discrepancies}
@activity.defn
async def post_to_erp(invoice_id: str, po_id: str) -> dict:
    # idempotency is enforced with a deterministic key derived from workflow inputs,
    # not a random UUID generated per attempt — this is the same lesson as always
    return await erp_client.post_journal_entry(
        idempotency_key=f"invoice-post-{invoice_id}",
        invoice_id=invoice_id,
        po_id=po_id,
    )

@activity.defn
async def notify_human_reviewer(invoice_id: str, discrepancies: list[dict]) -> None:
    await slack_client.post_message(
        channel="#ap-review",
        text=f"Invoice {invoice_id} has {len(discrepancies)} discrepancies needing review.",
    )

Vient ensuite le flux de travail, qui prend en charge le rôle d’orchestration que LangGraph ou CrewAI assument généralement, à ceci près que la durabilité y est intégrée et ne dépend pas de l’activation d’un point de contrôle :

# workflows.py
from temporalio import workflow
from datetime import timedelta

@workflow.defn
class InvoiceReconciliationWorkflow:
    def __init__(self) -> None:
        self._approval_received = False
        self._approved = False

    @workflow.signal
    async def submit_human_decision(self, approved: bool) -> None:
        # A signal is how the outside world talks to a running, possibly-days-old workflow
        self._approval_received = True
        self._approved = approved

    @workflow.run
    async def run(self, pdf_url: str, invoice_id: str, vendor: str) -> dict:
        line_items = await workflow.execute_activity(
            extract_line_items, pdf_url,
            start_to_close_timeout=timedelta(minutes=2),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )

        invoice = Invoice(invoice_id=invoice_id, vendor=vendor, line_items=line_items)
        match_result = await workflow.execute_activity(
            match_purchase_order, invoice,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=5),
        )

        if match_result["discrepancies"]:
            await workflow.execute_activity(
                notify_human_reviewer, invoice_id, match_result["discrepancies"],
                start_to_close_timeout=timedelta(seconds=10),
            )
            # This is the line that would be genuinely hard to write safely in the
            # other three frameworks: wait, durably, for up to 7 days, for a signal
            # that might arrive from a Slack button, a web form, anything —
            # while the workflow's entire state sits safely in Temporal's history,
            # costing zero compute while it waits.
            await workflow.wait_condition(
                lambda: self._approval_received,
                timeout=timedelta(days=7),
            )
            if not self._approved:
                return {"status": "rejected", "invoice_id": invoice_id}

        result = await workflow.execute_activity(
            post_to_erp, invoice_id, match_result["po_id"],
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=5),
        )
        return {"status": "posted", "invoice_id": invoice_id, "erp_result": result}

Plusieurs détails méritent attention, car ce sont précisément les modes de défaillance qui rendent les systèmes d’agents instables, et il s’agit ici de propriétés du temps de exécution plutôt que de fonctionnalités qu’il faudrait se rappeler d’ajouter.

Une attente de sept jours est gratuite et résiste à tout. workflow.wait_condition ne maintient aucun thread, processus ou conteneur ouvert pendant une semaine. Temporal stocke l’état du workflow dans son propre système de stockage durable, de sorte que le travailleur peut être réaffecté, réduit à zéro ou même planter entièrement entre-temps. Lorsque le signal arrive enfin, Temporal rejoue l’historique et reprend exactement à ce await. Faire de même avec un checkpointer LangGraph ou une équipe CrewAI reviendrait essentiellement à reconstruire le modèle de génération d’événements de Temporal.

Les tentatives de réexécution sont automatiques, restreintes dans leur portée et ne répètent jamais un travail déjà terminé. Supposons que post_to_erp échoue après que match_purchase_order ait réussi. Temporal ne réexécutera pas match_purchase_order, car son résultat se trouve déjà dans l’historique persistant. Comparez cela à l’approche naïve de la même idée :

# BEFORE — naive retry loop, the kind you write at 11pm under deadline pressure
async def reconcile_invoice_naive(pdf_url, invoice_id, vendor):
    for attempt in range(3):
        try:
            line_items = await extract_line_items(pdf_url)
            match_result = await match_purchase_order(Invoice(invoice_id, vendor, line_items))
            if match_result["discrepancies"]:
                await notify_human_reviewer(invoice_id, match_result["discrepancies"])
                approved = await wait_for_approval_somehow(invoice_id)  # good luck
                if not approved:
                    return {"status": "rejected"}
            return await post_to_erp(invoice_id, match_result["po_id"])
        except Exception:
            continue  # retries EVERYTHING from the top, including re-extracting the PDF

Cette version naïve réitère l’extraction par le LLM à chaque tentative, gaspillant des tokens et de l’argent pour une étape qui a déjà fonctionné, et wait_for_approval_somehow ne propose aucune solution valable si elle doit survivre à un redéploiement. La version de Temporal ne traite pas ces problèmes grâce à du code supplémentaire ; dans cette forme, les problèmes ne surviennent jamais, grâce à la structure de l’exécution.

Le Python pur signifie qu’il est testable. Temporal intègre un cadre de tests qui permet de sauter une attente de timedelta(days=7) en quelques millisecondes au sein d’un test unitaire, et les activités peuvent être simulées sans avoir recours au réseau :

import pytest
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker

@pytest.mark.asyncio
async def test_rejected_invoice_does_not_post_to_erp():
    async with await WorkflowEnvironment.start_time_skipping() as env:
        async with Worker(
            env.client, task_queue="test-queue",
            workflows=[InvoiceReconciliationWorkflow],
            activities=[extract_line_items, match_purchase_order, notify_human_reviewer, post_to_erp],
        ):
            handle = await env.client.start_workflow(
                InvoiceReconciliationWorkflow.run,
                args=["fake.pdf", "INV-001", "Acme"],
                id="test-invoice-001",
                task_queue="test-queue",
            )
            await handle.signal(InvoiceReconciliationWorkflow.submit_human_decision, False)
            result = await handle.result()
            assert result["status"] == "rejected"

Remarquez les absences : pas de sommeil réel sur sept jours, pas d’inspection approfondie des mécanismes internes du point de contrôle, pas de raisonnement manuel concernant les redémarrages. La durabilité constitue une infrastructure essentielle, et ce n’est pas quelque chose que chaque test doit prouver à nouveau.

Utilisation des deux couches

La réponse équilibrée n’est pas d’abandonner LangGraph au profit de Temporal. Ils fonctionnent à des niveaux différents, et les meilleures configurations en production pour 2026 les combinent : LangGraph ou CrewAI pour le raisonnement au sein d’une étape, Temporal pour la couche de flux de travail qui relie le processus sur plusieurs jours et services. Une activité peut englober une telle étape de raisonnement délimitée comme ceci :

@activity.defn
async def run_agent_reasoning_step(ticket_text: str) -> dict:
    # LangGraph handles the bounded, single-turn reasoning —
    # classify, decide, draft — inside an activity that Temporal
    # retries and times out like any other side effect.
    result = triage_graph.invoke({"ticket_id": ticket_text, "audit_log": []})
    return {"category": result["category"], "priority": result["priority"]}

@workflow.defn
class SupportTicketWorkflow:
    @workflow.run
    async def run(self, ticket_id: str, ticket_text: str) -> dict:
        triage = await workflow.execute_activity(
            run_agent_reasoning_step, ticket_text,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )
        if triage["priority"] >= 8:
            await workflow.execute_activity(page_oncall_engineer, ticket_id, ...)
            await workflow.wait_condition(lambda: self._acknowledged, timeout=timedelta(minutes=15))
        # ... routing, SLA timers, escalation chains — all durable, all testable

C’est l’architecture que je recommande pour une équipe dont le système d’agents doit fonctionner en production. Le framework conçu spécifiquement pour les LLM gère ce pour quoi il est optimisé, c’est-à-dire le raisonnement au sein d’étapes définies. Un moteur d’exécution fiable s’occupe de sa mission principale, garantissant que l’ensemble du processus, qu’il dure des minutes ou des semaines et qu’il implique des appels à des LLM ou des approbations humaines, se termine correctement ou échoue de manière visible. Aucun des deux n’a besoin de remplir le rôle de l’autre.

Lorsque les coûts temporels sont trop élevés

Temporal présente des inconvénients qui en font un choix peu adapté comme solution par défaut pour de nombreuses tâches automatisées. Il s’agit d’un système distribué supplémentaire à gérer, que ce soit un serveur auto-hébergé ou une facture Temporal Cloud, sans oublier les travailleurs et les files d’attente de tâches : une infrastructure bien plus importante que celle nécessitée par un processus Python exécutant LangGraph. Le code des workflows doit également respecter des règles de déterminisme strictes. Appeler directement datetime.now(), utiliser de la randomisation non contrôlée ou effectuer des opérations I/O brutes directement dans la fonction du workflow est strictement interdit. Les équipes ont besoin de temps pour s’y faire, et en cas de violation des règles, des erreurs de non-déterminisme difficiles à comprendre apparaissent lors de la réexécution. Pour une tâche courte et à un seul processus, comme répondre à une question ou résumer un document en une seule appel à un LLM, une exécution fiable représente simplement un surcoût inutile, puisqu’il n’existe aucun processus à plusieurs étapes qui pourrait être endommagé en cas de panne.

Règle de décision

Choisissez une exécution durable comme pilier chaque fois qu’un flux de travail peut durer plus longtemps qu’un processus unique, doit coordonner plusieurs services ou attendre une intervention humaine, ou gère des éléments qui ne doivent pas être traités deux fois, tels que de l’argent, des stocks ou un e-mail qui ne doit pas être envoyé à plusieurs reprises. Laissez le framework LLM de votre choix fonctionner en tant que composant au sein d’une de ses étapes. Lorsque tout commence et se termine dans un seul cycle demande-réponse, passez outre, car son ajout ne résoudrait qu’un problème qui n’existe pas encore.

De nombreuses discussions sur le choix du framework d’agents négligent une question plus importante : quel niveau du système nécessite réellement un renforcement de sécurité ? LangGraph, CrewAI et AutoGen mènent un débat utile sur la manière de structurer le raisonnement des LLM et la coordination entre plusieurs agents. Cependant, la question de savoir si un système d’agents survit à une mauvaise mise en œuvre, à une partition du réseau ou au fait qu’une personne mette trois jours à cliquer sur « approuver » est une question distincte, et les moteurs de flux de travail l’ont en grande partie résolue discrètement, ayant déjà géré des exécutions durables bien avant que l’on ne parle d’IA agentielle. Les frameworks populaires ajustent ce qui est visible dans une démonstration ; celui qui reste ignoré décide plutôt si quelque chose tient encore debout un mois plus tard.