~/blog / idempotence-agents-ia
🤖 ia en production · cas SaaS automobile

Idempotence : pourquoi 9 agents IA sur 10 cassent en production

Luc Del Beato 11 juin 2026 11 min de lecture

Tout le monde sait démontrer un agent IA. Une démo qui tourne une fois, dans un notebook, sous les applaudissements. Mais presque personne ne le fait tenir en production. Et quand ça casse, neuf fois sur dix, c'est la même cause : un agent non-idempotent qui refait deux fois une action irréversible. Voici comment on blinde ça, avec un vrai cas, de la vraie monnaie.

TL;DR

« Ça marchait une fois dans le notebook »

On vit un moment étrange. Les démos d'agents IA sont partout : un agent qui lit ta boîte mail, un agent qui passe des commandes, un agent qui gère ta trésorerie. C'est impressionnant, et c'est presque toujours faux. Pas faux dans le sens « ça ne marche pas », faux dans le sens « ça marche une fois, dans des conditions parfaites, sous le regard de son créateur ».

La production, c'est l'inverse de la démo. Personne ne regarde. Le process redémarre à 3h du matin parce que le serveur a OOM. Un cron se relance. Un retry réseau rejoue la requête. Un utilisateur recharge la page, ou laisse deux onglets ouverts. Et là, l'agent qui marchait si bien refait son action. Sauf que cette action, elle, n'est pas rejouable sans dégât.

La règle d'or des agents en prod : suppose que chaque action sera tentée au moins deux fois. Conçois pour le redémarrage, pas pour le chemin heureux.

L'idempotence, en une phrase

Une opération est idempotente si l'exécuter dix fois produit exactement le même résultat que l'exécuter une fois. Régler un thermostat à 21° est idempotent. Augmenter la température de 1° ne l'est pas. Envoyer un email, créer une facture, passer une commande : par défaut, aucune de ces actions n'est idempotente. Chaque appel crée un nouvel effet de bord dans le monde réel.

Le piège, c'est que la non-idempotence est invisible en démo. Tu lances l'agent une fois, ça marche, tu passes à autre chose. Le bug ne se révèle qu'au deuxième passage, et le deuxième passage, en production, n'est pas une question de si mais de quand. Un simple restart devient un incident.

Le cas concret : un SaaS automobile, une API bancaire, et de la vraie monnaie

Passons du principe au réel. Sur un SaaS B2B de l'automobile, j'ai construit une intégration de facturation reliée à une API bancaire tierce, la trésorerie de l'entreprise. Pas une simulation : de vrais appels API, de vraies factures, des documents qui ont une valeur légale et comptable. Le genre de système où une erreur ne produit pas un mauvais pixel, mais un double prélèvement.

Le scénario d'horreur est trivial à déclencher. L'utilisateur resoumet un formulaire. La page recharge. Il a deux onglets ouverts. Un retry HTTP rejoue silencieusement la requête. N'importe lequel de ces événements, sans protection, crée une facture en double via l'API bancaire. Une vraie facture, avec un vrai montant, dans la vraie compta. Multiplie par le volume, et tu as un incident financier déclenché par un rechargement de page.

💸
Pourquoi c'est pire qu'un bug normal : une facture en double n'est pas une donnée qu'on supprime d'un DELETE. C'est un document comptable émis, parfois déjà transmis, parfois déjà payé. Le coût du défaut n'est pas technique, il est légal et financier. C'est exactement le terrain où l'idempotence cesse d'être une option.

Première ligne de défense : la guard clause

La protection la plus simple, et la plus importante : avant de créer, on cherche si ça existe déjà. Concrètement, une fonction findActiveInvoiceForDeal qui interroge la base pour toute facture active (non annulée) liée à ce deal / cette inscription. Si elle en trouve une, on bloque la création et on renvoie l'existante. Pas de doublon, point.

C'est l'incarnation de la clé d'idempotence : on ne fait pas confiance à un flag « déjà traité » qui traîne en mémoire, on demande au système de vérité (la base) si l'effet de bord a déjà eu lieu. Le deal lui-même est la clé d'idempotence : à un deal correspond au plus une facture active.

// avant TOUTE création, on interroge la vérité
const existing = await findActiveInvoiceForDeal(dealId);
if (existing) {
  // l'effet de bord a déjà eu lieu → on renvoie l'existant
  return { invoice: existing, created: false };
}
// sinon seulement, on agit, voir plus bas pour la transaction

On double cette protection par une déduplication sur clé métier : un MD5 / une empreinte déterministe de l'opération logique, pour qu'une même opération « créer la facture du deal X au montant Y » mappe toujours vers un seul enregistrement, même si deux requêtes arrivent en parallèle à la milliseconde.

La file d'attente et le marquage atomique

La guard clause protège contre les doublons synchrones. Mais il reste le cas du crash en plein milieu d'un batch. Imagine un traitement par lot sur une file bank_pending_invoices : on traite 200 deals, le process meurt au 137ᵉ. Que se passe-t-il au restart ? Sans précaution, on recommence à zéro et on refacture les 136 premiers.

La parade : une fonction markPendingInvoicesProcessed() qui marque les deals comme traités atomiquement, et seulement après le succès de la création. Un deal n'est retiré de la file que quand sa facture existe vraiment. Si le crash survient avant ce marquage, le restart reprend le deal, mais la guard clause voit que la facture existe déjà, et passe son chemin. Les deux mécanismes se complètent : la file gère la reprise, la guard gère le doublon.

🔁
L'idée clé : at-least-once est la réalité, un message sera livré au moins une fois, parfois plus. On ne se bat pas contre ça. On rend chaque traitement sûr à rejouer. Un deal repris dix fois après crash produit exactement une facture. C'est ça, l'idempotence appliquée à un agent.

Les cas qui ne doivent PAS être automatisés

Un agent en production doit aussi savoir ne pas agir. Sur ce SaaS, certains clients sont en facturation « one-time », ils ne doivent surtout pas être refacturés automatiquement par le batch. La tentation naïve, c'est de les inclure dans la boucle « parce que le code est plus simple ». La bonne décision, c'est de les bypasser explicitement et de lever une alerte Sentry à la place.

C'est le principe du human-in-the-loop : pour tout cas sensible ou ambigu, l'agent n'auto-exécute pas, il escalade. Une alerte, un message dans un canal #review, et un humain tranche. Un agent qui sait dire « je ne suis pas sûr, regarde ça » est infiniment plus déployable qu'un agent qui fonce avec confiance dans tous les cas.

La transaction : ton meilleur ami

Toute la création de facture est enveloppée dans une transaction MySQL. Soit tout réussit, l'enregistrement en base, la génération du PDF, l'upload, le marquage, soit tout est annulé. Pas d'état intermédiaire bancal où la facture existe en base mais pas côté API bancaire, ou l'inverse. La transaction, c'est ce qui rend un effet de bord composite atomique.

Détail gagné dans la douleur : j'ai dû monter l'innodb_lock_wait_timeout à 120s. Par défaut il est à 50s, et la génération + upload du PDF est lente. Sous concurrence, deux traitements se bloquaient mutuellement et l'un mourait sur un deadlock avant la fin de l'upload. Allonger le timeout a laissé le temps aux verrous de se libérer proprement. Le genre de réglage qu'on ne voit jamais en démo, et qui décide si le système tient ou pas en prod.

// l'action idempotente complète, en pseudo-code
async function createInvoiceIdempotent(deal) {
  const key = businessKey(deal);              // clé déterministe (deal + montant)

  // 1. la clé a-t-elle déjà été vue ?
  if (await findActiveInvoiceForDeal(deal.id)) {
    return noop("already-invoiced");          // → no-op, zéro effet de bord
  }
  if (isOneTimeClient(deal)) {
    return escalateToHuman(deal);             // → Slack #review + Sentry
  }

  // 2. agir, dans une transaction atomique
  await tx(async (t) => {                      // innodb_lock_wait_timeout = 120s
    const invoice = await bank.createInvoice(deal, t);
    const pdf     = await renderAndUploadPdf(invoice, t);
    await markPendingInvoicesProcessed([deal.id], t);  // 3. enregistrer la clé
  });                                          // commit = tout, ou rollback = rien
}

La doctrine, généralisée à tout agent IA

Ce cas n'est qu'un exemple. Le même squelette s'applique à n'importe quel agent IA en production qui touche au monde réel, c'est exactement la doctrine qu'on applique chez Gérer.ai. Cinq règles, non négociables :

Ce que je retiens

Un agent IA fiable, ce n'est pas un agent plus intelligent, c'est un agent qui survit au redémarrage, escalade ce qu'il ne maîtrise pas, et ne fait jamais deux fois ce qui ne doit arriver qu'une seule fois. Ce n'est pas glamour. C'est ce qui sépare une démo d'un système en production.

// gerer.ai

Des agents IA qui tiennent vraiment en production ?

C'est le cœur de Gérer.ai : des agents idempotents, avec human-in-the-loop, déployés sur votre infra et souverains. On ne fait pas des démos qui marchent une fois, on construit des systèmes qui tiennent quand personne ne regarde.

Découvrir Gérer.ai
L
Luc Del Beato

Senior Lead Engineer, ~20 ans de web. Do-er passionné de résolution de problèmes, de belle architecture et d'automatisation ; les agents IA, c'est ma direction. Mon parcours →