Un agent sans persistance est un agent jetable. Dès que le processus s’arrête — crash, redéploiement, simple fin de requête HTTP — tout son état disparaît. Pour un démonstrateur, peu importe. Pour un système qui doit reprendre une conversation, attendre la validation d’un humain pendant trois heures, ou se remettre d’une panne sans rejouer dix appels LLM facturés, c’est rédhibitoire.

Le checkpointer de LangGraph répond à tout cela avec une seule idée : après chaque étape du graphe, on sauvegarde l’état complet. Cette sauvegarde débloque quatre capacités qui semblent distinctes mais partagent le même mécanisme — mémoire conversationnelle, reprise sur panne, retour en arrière (time-travel) et human-in-the-loop.

Le modèle mental : super-steps et checkpoints

LangGraph exécute le graphe par super-steps : à chaque tour, l’ensemble des nœuds prêts s’exécute, l’état est mis à jour via les reducers, puis un checkpoint est écrit. Un checkpoint contient l’état du graphe, le prochain nœud à exécuter, et les éventuelles tâches en attente.

Ces checkpoints sont regroupés par thread (thread_id). Un thread est une ligne de vie : une suite ordonnée de checkpoints qui constitue l’historique complet d’une exécution. Deux conversations distinctes = deux threads = deux historiques isolés.

Fig.01 · Super-steps & checkpoints
exécution super-step n nœuds prêts
écriture checkpoint état persisté
exécution super-step n+1 nœuds suivants
écriture checkpoint état persisté
Après chaque super-step, l'état complet est écrit dans un checkpoint. Cette suite de points de sauvegarde EST la mémoire durable de l'agent.
from langgraph.checkpoint.memory import InMemorySaver

agent = graphe.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "conversation-42"}}
agent.invoke({"messages": [msg_utilisateur]}, config)

Le thread_id passé dans config est la clé de voûte : c’est lui qui dit au checkpointer ranger et relire l’état. L’oublier, c’est repartir d’une ardoise vierge à chaque appel.

Mémoire court-terme : la conversation qui persiste

La première conséquence, presque gratuite, est la mémoire conversationnelle. Relancez l’agent avec le même thread_id : le checkpointer recharge l’historique avant d’exécuter le moindre nœud.

agent.invoke({"messages": [{"role": "user", "content": "Je m'appelle Léa."}]}, config)
# Plus tard, même thread_id :
rep = agent.invoke({"messages": [{"role": "user", "content": "Quel est mon prénom ?"}]}, config)
# → "Léa" : l'historique a été rechargé automatiquement.

On ne renvoie que le nouveau message : le reducer add_messages le fusionne avec l’historique persisté. C’est la mémoire court-terme — celle d’un thread. La mémoire long-terme, qui traverse les threads, relève du Store et fera l’objet d’un autre deep-dive.

Passer à un backend durable

En production, on remplace InMemorySaver par un checkpointer Postgres. L’API du graphe ne change pas d’une ligne — seule l’instanciation diffère.

from langgraph.checkpoint.postgres import PostgresSaver

DB_URI = "postgresql://user:pass@localhost:5432/agents?sslmode=disable"

with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup()  # crée les tables au premier lancement
    agent = graphe.compile(checkpointer=checkpointer)
    agent.invoke({"messages": [msg]}, config)

Human-in-the-loop : suspendre avant d’agir

Voici la capacité qui justifie à elle seule la persistance. Un agent qui peut envoyer un e-mail, émettre un remboursement ou supprimer des données ne doit pas le faire sans garde-fou. On veut suspendre l’exécution, montrer à un humain ce que l’agent s’apprête à faire, et ne reprendre qu’après son feu vert.

Fig.02 · Human-in-the-loop
run exécution jusqu'au nœud
pause interrupt() gel + persistance
hors-ligne humain valide / corrige
Command resume reprise exacte
interrupt() gèle l'exécution et persiste l'état. L'attente humaine peut durer une seconde ou un jour ; Command(resume=…) reprend exactement là où le graphe s'était arrêté.

La fonction interrupt() fait exactement cela : appelée dans un nœud, elle gèle le graphe, persiste l’état et rend la main à l’appelant.

from langgraph.types import interrupt, Command

def validation_humaine(etat: Etat) -> dict:
    action = etat["action_proposee"]
    # Gèle l'exécution et expose la proposition à l'humain.
    decision = interrupt({
        "type": "validation",
        "action": action,
        "question": "Confirmer cette action ?",
    })
    if decision == "approuver":
        return {"messages": [executer(action)]}
    return {"messages": [{"role": "assistant", "content": "Action annulée."}]}

Côté appelant, l’interruption se détecte dans le résultat, puis se lève avec un Command(resume=...) :

resultat = agent.invoke({"messages": [msg]}, config)

if "__interrupt__" in resultat:
    # … on présente resultat["__interrupt__"] à un humain, on attend sa réponse …
    agent.invoke(Command(resume="approuver"), config)  # reprise exacte

Comme l’état interrompu est persisté, l’humain peut répondre une minute ou un jour plus tard, depuis un autre processus. Le HITL n’est pas un mode bloquant en mémoire : c’est un état gelé sur disque qu’on réveille avec le bon thread_id.

Inspecter et réécrire l’état

La persistance rend l’agent introspectable. À tout moment, on peut lire l’état courant d’un thread :

snapshot = agent.get_state(config)
print(snapshot.values["messages"])   # l'état complet
print(snapshot.next)                  # le(s) prochain(s) nœud(s) à exécuter

On peut même corriger l’état avant de reprendre — par exemple injecter une réponse d’outil rectifiée par un opérateur :

agent.update_state(config, {"messages": [correction]})
agent.invoke(None, config)  # reprend avec l'état corrigé

invoke(None, …) signifie « ne fournis pas de nouvelle entrée, reprends depuis le checkpoint ». C’est le geste de base du time-travel et de la reprise sur panne.

Time-travel : rembobiner le graphe

Chaque checkpoint a un identifiant. En relisant l’historique d’un thread, on peut repartir d’un point antérieur — pour déboguer, ou pour explorer une autre branche de décision.

historique = list(agent.get_state_history(config))
# Reprendre depuis un checkpoint précis :
config_passe = historique[2].config
agent.invoke(None, config_passe)

Reprendre depuis un ancien checkpoint fork l’exécution : on rejoue la suite à partir de cet instant, sans détruire la ligne d’origine. Précieux pour comparer deux trajectoires d’un même agent face à la même situation.

Fig.03 · Time-travel & fork
thread · ligne d'origine ckpt-2 branche · invoke(None, ckpt-2)
Reprendre depuis ckpt-2 ne réécrit pas l'historique : une nouvelle branche démarre à partir de ce point, et la ligne d'origine reste intacte.

Penser la persistance dès la conception

Bonnes pratiques & pièges

Bonnes pratiques
  • Mappez le thread_id sur un identifiant métier (ticket, session, user), pas un UUID anonyme.
  • Backend durable (Postgres ou SQLite) en production ; InMemorySaver réservé aux tests.
  • Placez tout effet de bord irréversible APRÈS la barrière interrupt().
  • Surveillez le checkpointer comme votre base principale : sa panne rend l'agent indisponible.
  • Réutilisez un pool de connexions ; n'appelez setup() qu'une fois, en migration.
  • Prévoyez une stratégie de fenêtre de contexte (résumé, troncature) pour les longs threads.
Pièges à éviter
  • InMemorySaver en production : tout l'historique s'évapore au moindre redémarrage.
  • Un effet de bord placé avant interrupt() : il sera rejoué à la reprise (double envoi).
  • Oublier le thread_id : l'agent repart d'une ardoise vierge à chaque appel.
  • Croire que resume reprend « à la ligne » : c'est le nœud entier qui est ré-exécuté.
  • Laisser l'historique grossir sans limite : coût et latence dérivent silencieusement.

Ce qu’il faut retenir

  • Le checkpointer persiste l’état à chaque super-step, indexé par thread.
  • Cette unique primitive débloque mémoire, reprise sur panne, time-travel et HITL.
  • interrupt() + Command(resume=…) réalisent un human-in-the-loop non bloquant, parce que l’état attend sur disque, pas en RAM.
  • En production : backend durable (Postgres), effets de bord après validation, et supervision du checkpointer.

La persistance vous donne un agent fiable. Reste à le rendre observable : savoir, en production, pourquoi il a pris telle décision, ce qu’elle a coûté et si elle était bonne. C’est tout l’objet du cas pratique de support tracé avec LangFuse.

#langgraph#checkpointer#human-in-the-loop#persistance#production