Retour aux ressources
// technique · agents

Human-in-the-loop en vrai

Un bon agent ne demande pas tout, et ne décide pas tout seul non plus. Il agit par défaut, et s'arrête pour demander quand c'est vraiment bloquant — puis reprend là où il s'était arrêté, même deux heures plus tard.

Technique9 juil. 2026~7 min · intermédiaire
LangGraphinterruptcheckpointerPostgresact-first
01

Demander vs décider

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.

toujours demander

un interrogatoire.

l'utilisateur fait le travail de l'agent
lent, frustrant
act-first

un bon collègue.

agit avec des hypothèses affichées
ne demande que si c'est bloquant
te laisse corriger après coup
02

S'arrêter proprement : interrupt

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.

clarify.tscopier
import { interrupt } from "@langchain/langgraph";
// dans un noeud : suspendre le run et demander a l'humain
function clarify(state) {
if (state.deadline) return state; // rien a demander -> on continue
const answer = interrupt({ // <- le graphe s'arrete ICI
question: "Quelle est l'echeance visee ?",
context: state.brief,
});
return { ...state, deadline: answer }; // reprise avec la reponse
}
// le graphe (langgraph)
gather
contexte
clarify
question ?
draft
décider
review
auto-relire
apply
appliquer
review ↺ draft — boucle si insuffisantclarify → human-in-the-loop
Postgres — état durableLangfuse — tracé
03

Reprendre le run

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 }).

route.tscopier
import { Command } from "@langchain/langgraph";
// 1er appel : le graphe tourne jusqu'a l'interrupt, puis rend la main
const first = await graph.invoke({ brief }, config);
// -> first.__interrupt__ contient la question posee a l'utilisateur
// ... on affiche la question, l'utilisateur repond ...
// reprise : on relance le MEME run (meme thread_id) avec la reponse
await graph.invoke(new Command({ resume: "fin mars 2026" }), config);
04

Durable : survivre à la pause

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.

graph.tscopier
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
// l'etat du run est persiste -> il survit a un reboot, une repons 2h plus tard
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL);
const graph = builder.compile({ checkpointer });
// chaque run a un thread_id : c'est lui qui permet de reprendre au bon endroit
const config = { configurable: { thread_id: runId } };

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.

05

Portes d'approbation : agir sous condition

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.

Trois réponses possibles, pas deux

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.

acceptl'action part telle quelle, le run continue.
editl'humain modifie les arguments (destinataire, montant), puis on exécute la version corrigée.
rejecton annule l'action et on renvoie l'agent réfléchir, avec le motif du refus.
approval.tscopier
import { interrupt, Command } from "@langchain/langgraph";
// Inside a node, before a destructive tool call
async function sendEmailNode(state: State) {
const draft = state.pendingEmail;
// Surface the proposed action, not an open question
const decision = interrupt({
kind: "approval",
action: "send_email",
to: draft.to,
subject: draft.subject,
body: draft.body,
}) as { type: "accept" | "edit" | "reject"; args?: EmailArgs; reason?: string };
if (decision.type === "reject") {
// Loop back with the reason, no side effect fired
return new Command({
goto: "plan",
update: { note: `Email rejected: ${decision.reason}` },
});
}
const finalDraft = decision.type === "edit" ? decision.args! : draft;
await mailer.send(finalDraft); // the only irreversible line, gated
return { sent: true };
}
!

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.

Timeout et escalade

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 deadline par interrupt en attente, écrit avec l'état
un balayage périodique des runs suspendus au-delà du délai
une décision par défaut sûre : sur une action destructrice, expirer = refuser
une escalade optionnelle vers un second approbateur avant d'abandonner
06

Checkpoint n'est pas exécution durable

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.

checkpointer

« j'ai sauvé ton état ».

reprise possible par thread_id
toi tu détectes l'échec et relances
gare aux effets de bord rejoués
exécution durable

« ça ira jusqu'au bout ».

reprise et retente automatiques
étapes finies mises en cache, pas rejouées
coordination en environnement distribué
i

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.

07

Sources & ressources

01LangGraph — InterruptsLa référence sur interrupt() : suspension, resume via Command, et le piège de la réexécution du nœud depuis le début.02LangGraph — PersistenceCheckpointers, thread_id et PostgresSaver : comment l'état du graphe est persisté pour rendre un run reprenable.03LangGraph — Overview (durable execution)Vue d'ensemble JS/TS : exécution durable, human-in-the-loop et persistence comme briques de base.04Anthropic — Building effective agentsPourquoi placer des points d'arrêt humains avant les actions irréversibles, et les conditions d'arrêt d'un agent.05Diagrid — Checkpoints are not durable executionLa distinction claire entre sauvegarder l'état (checkpoint) et garantir la reprise jusqu'au bout (exécution durable).06AWS — Durable AI agents with LangGraph & DynamoDBUn checkpointer maintenu par AWS pour DynamoDB : alternative à Postgres pour persister l'état d'un agent en prod.

Le human-in-the-loop, ce n'est pas « demander plus », c'est demander mieux : agir par défaut, s'arrêter uniquement quand ça compte, et reprendre proprement grâce à un état durable. L'agent devient un collègue à qui on fait confiance, pas un formulaire.