Il y a quelques semaines, un agent autonome basé sur LangGraph, conçu pour nettoyer périodiquement les comptes de test obsolètes en staging, s'est emballé. En moins de 10 minutes, il a purgé l'équivalent de 48 heures d'activité de test de notre équipe de QA. Voici le post-mortem technique de cette défaillance et comment nous avons repensé la sécurité des graphes.
L'origine du bug : une faille de boucle infinie sur l'état du graphe
Dans LangGraph, l'état (State) est partagé entre les différents nœuds. Notre agent avait un nœud de décision (router) et un outil d'effacement. Le flux nominal était :
state.target_ids).Ce qui a cassé : À cause d'une régression mineure sur l'outil de suppression, celle-ci renvoyait un statut de succès même si la ressource n'existait plus (ID introuvable). De plus, le nœud de suppression ne supprimait pas l'ID traité de la liste state.target_ids en cas d'erreur réseau transitoire.
L'agent s'est retrouvé bloqué dans une boucle infinie de décision :
[LangGraph Exec] Router -> Check: target_ids still has 12 items -> Loop back to delete_tool
[LangGraph Exec] LLM Context depletion -> The agent started hallucinating IDs and deleted real staging accounts matching wildcard queries.
La parade technique : Redesigner avec des gardes-fous stricts
Nous avons reconstruit le graphe d'exécution en y injectant trois principes de défense en profondeur :
1. Immutabilité de l'état et réducteurs (Reducers) stricts
Nous avons redéfini le schéma d'état de LangGraph en forçant l'usage de réducteurs stricts pour éviter les persistances d'ID obsolètes.
import { Annotated } from "@langchain/langgraph";
// Reducer qui remplace proprement la liste ou filtre les IDs traités
function updateTargetIds(current: string[], next: string[]): string[] {
// Garantir l'élimination des doublons et des IDs déjà traités
return Array.from(new Set(next));
}
interface AgentState {
targetIds: Annotated<string[], typeof updateTargetIds>;
processedCount: number;
maxIterations: number;
}2. Le garde-fou des itérations maximums (Max Iterations Guard)
Aucune boucle d'agent ne doit tourner indéfiniment. Nous avons ajouté un nœud de contrôle global qui lève une exception et arrête le graphe dès qu'une limite d'itérations est franchie.
function routeDecision(state: typeof AgentState.State) {
if (state.processedCount > 50) {
throw new Error("Loop Guard Triggered: Max iterations exceeded in staging cleaning.");
}
if (state.targetIds.length > 0) {
return "delete_node";
}
return "__end__";
}3. Double validation humaine (Human-in-the-loop) pour les suppressions de masse
Pour toute action destructive touchant plus de 5 enregistrements, le graphe utilise la fonctionnalité d'interruption (interrupt) de LangGraph pour demander une validation manuelle via Slack/Webhook avant de poursuivre.
Ce que cela change pour vos projets
Les frameworks d'agents comme LangGraph ou CrewAI sont puissants, mais ils masquent la complexité de l'exécution non déterministe. Ne laissez jamais un agent autonome interagir avec des APIs destructives sans un système d'isolation strict (sandboxing), un nombre maximum d'itérations codé en dur, et une interruption humaine pour valider les suppressions.
