La première version de notre chat d'ajustement interrogeait l'utilisateur sans fin, une question après l'autre. Insupportable. La bonne posture est act-first : agir par défaut avec des hypothèses explicites, et ne demander que sur un vrai point bloquant.
un interrogatoire.
un bon collègue.
LangGraph fournit interrupt() : appelé dans un nœud, il suspend le run et remonte une question à ton application. Le graphe ne « bloque » pas un thread serveur — il s'arrête et rend la main.
Côté application, le premier appel s'arrête à l'interrupt et te renvoie la question. Tu l'affiches, l'utilisateur répond, et tu relances le même run avec sa réponse via Command({ resume }).
Pour que l'agent reprenne exactement où il s'était arrêté — même après un redéploiement, ou une réponse deux heures plus tard — l'état doit être persisté. C'est le rôle du checkpointer : il sauvegarde l'état du graphe à chaque étape, ici dans Postgres.
Avec un checkpointer, un run devient une conversation reprenable, pas un appel jetable. C'est ce qui rend le human-in-the-loop utilisable en production réelle.
Réserve la pause aux vrais points de décision (actions destructrices, ambiguïté coûteuse). Un interrupt à chaque étape et tu recrées l'interrogatoire que tu voulais fuir.
L'act-first a une exception nette : les actions irréversibles. Supprimer des données, envoyer un e-mail à un client, valider un paiement, pousser en prod — là, l'agent ne devine pas, il demande une validation. Anthropic le formule bien dans Building effective agents : on place des points d'arrêt avant les actions à fort enjeu, pas à chaque étape. C'est exactement le même interrupt() que pour une clarification, mais ce qu'on remonte n'est plus une question ouverte — c'est un plan à approuver.
Une bonne porte d'approbation ne se limite pas à « oui / non ». On veut aussi laisser l'humain corriger l'action avant qu'elle parte. Le payload d'interrupt remonte l'action proposée, et la réponse via Command({ resume }) porte la décision — que le nœud réexécute ensuite.
Piège classique : au resume, LangGraph réexécute le nœud depuis le début, pas depuis la ligne de l'interrupt — voir la doc Interrupts. Donc tout effet de bord placé avant l'interrupt() (log, écriture, appel d'API) se rejoue. Garde le nœud « pur » avant la porte, et ne mets l'action irréversible qu'après la décision.
Une porte d'approbation sans limite de temps devient un run zombie : suspendu pour toujours parce que personne n'a répondu. Comme l'état est persisté par le checkpointer (thread_id + Postgres), le run ne coûte rien pendant qu'il attend — mais il faut quand même une politique. En pratique, je stocke un deadline avec le checkpoint et je fais tourner un job qui, passé le délai, relance le thread avec une décision par défaut (souvent reject) ou l'escalade à un autre approbateur.
Un checkpointer te donne la reprise : l'état est écrit à chaque étape, tu peux relancer un thread par son thread_id, même après un crash — c'est bien documenté côté Persistence et côté durable execution. Mais reprendre n'est pas exécuter automatiquement jusqu'au bout. Le checkpoint sauve la place ; c'est encore à toi de détecter l'échec et de relancer avec le bon identifiant.
« j'ai sauvé ton état ».
« ça ira jusqu'au bout ».
Diagrid détaille bien la nuance dans Checkpoints are not durable execution. Pour du human-in-the-loop simple, le checkpointer suffit largement. Si tes workflows tournent des heures avec des retries automatiques et plusieurs workers, regarde un vrai moteur d'exécution durable (Temporal, Restate) qui met en cache les étapes finies au lieu de les rejouer.
L’archi qui tient un pic de trafic : EC2 pour le site, RDS pour la base, S3 + Lambda pour des photos produit optimisées en WebP.
Sortir l’agent de la boîte noire : instrumenter chaque étape, mesurer le coût, et le noter avec un LLM-as-a-judge.