
Je suis Jules Ginhac, Co-Founder & ingénieur IA chez Apogée Consult à Lyon. Je conçois et déploie des architectures IA génératives (RAG, agents, LLMOps) pour des PME, startups et organisations publiques.
Fallback humain dans un produit IA : comment on l'a câblé pour qu'il déclenche au bon moment
- ia-produit
Un fallback humain mal calibré, c'est soit un agent inutile soit un support débordé. Voici comment on a implémenté le human in the loop pour qu'il s'enclenche vraiment au bon moment.
Fallback humain dans un produit IA : comment on l'a câblé pour qu'il déclenche au bon moment
Le fallback humain est souvent traité comme un filet de sécurité qu'on ajoute en fin de sprint, juste avant la démo. On met un bouton "Parler à un conseiller", on se dit que le problème est réglé, et on passe à autre chose. Ce n'est pas un fallback : c'est un aveu que le produit ne fonctionne pas.
Un vrai fallback humain a trois propriétés : il se déclenche automatiquement quand le modèle est en dehors de sa zone de compétence, il transmet le contexte complet à l'humain qui reprend la main, et il ne se déclenche pas quand ce n'est pas nécessaire, parce qu'un fallback qui s'enclenche trop souvent détruit la proposition de valeur du produit IA.
Voici ce qu'on a appris en le câblant sur un produit réel.
Le problème du seuil unique
La première version qu'on a déployée utilisait un score de confiance global : si le modèle était "peu confiant", on escaladait vers un humain. En pratique, ce seuil était impossible à calibrer.
Trop bas : 60% des requêtes arrivaient en escalade. L'équipe support était submergée et l'agent IA ne servait à rien.
Trop haut : des réponses clairement incorrectes passaient sans être interceptées. Le produit donnait de mauvaises informations avec aplomb.
Le problème est que la confiance n'est pas une variable unidimensionnelle. Un modèle peut être très confiant sur la forme de sa réponse (syntaxe, cohérence interne) et complètement à côté de la plaque sur le fond (fait inexact, hors périmètre, ambiguïté non résolue).
Ce qu'on a changé : des déclencheurs spécifiques
On a abandonné le score global et on a défini des déclencheurs spécifiques, chacun lié à un type de défaillance identifié en production.
Déclencheur 1 : requête hors périmètre
L'agent a un périmètre défini. Dès qu'une requête sort de ce périmètre, on escalade, indépendamment de la confiance que le modèle affiche.
On le détecte avec un classifier léger (un appel LLM séparé avec un prompt court) qui tourne avant l'appel principal :
async def is_in_scope(user_message: str, scope_definition: str) -> bool:
response = await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=10,
system=f"""Tu réponds uniquement par OUI ou NON.
La requête suivante est-elle dans le périmètre défini ?
Périmètre : {scope_definition}""",
messages=[{"role": "user", "content": user_message}]
)
return response.content[0].text.strip().upper() == "OUI"Le classifier tourne sur Haiku pour minimiser la latence et le coût. Si la réponse est NON, on n'appelle pas le modèle principal et on route directement vers l'escalade.
Déclencheur 2 : ambiguïté non résolue après une clarification
On autorise une seule tentative de clarification. Si après la réponse de l'utilisateur l'ambiguïté persiste, on escalade. On suit l'état de la conversation avec un compteur simple :
class ConversationState:
def __init__(self):
self.clarification_attempts = 0
self.max_clarifications = 1
def should_escalate_on_ambiguity(self) -> bool:
return self.clarification_attempts >= self.max_clarifications
def increment_clarification(self):
self.clarification_attempts += 1Déclencheur 3 : détection d'insatisfaction explicite
Si l'utilisateur exprime explicitement que la réponse ne l'aide pas, "ce n'est pas ce que je cherche", "vous n'avez pas compris", "c'est faux", on escalade immédiatement plutôt que de laisser le modèle s'entêter.
On détecte ça avec un classifieur de sentiment centré sur l'insatisfaction, pas sur la polarité générale :
DISSATISFACTION_SIGNALS = [
"ce n'est pas",
"vous n'avez pas compris",
"c'est faux",
"ce n'est pas correct",
"mauvaise réponse",
"pas ce que je demande",
"recommencer",
]
def detect_explicit_dissatisfaction(message: str) -> bool:
message_lower = message.lower()
return any(signal in message_lower for signal in DISSATISFACTION_SIGNALS)Pour des cas plus subtils, on peut compléter avec un appel LLM léger, mais les signaux lexicaux couvrent déjà la majorité des cas sans coût supplémentaire.
Déclencheur 4 : enjeu élevé détecté
Certains domaines requièrent une validation humaine par défaut, quelle que soit la qualité apparente de la réponse : données personnelles sensibles, engagements contractuels, sujets réglementés.
On maintient une liste de topics à escalade systématique et on la vérifie avant de délivrer la réponse finale.
La transmission du contexte : l'étape qu'on rate souvent
Déclencher l'escalade au bon moment ne suffit pas. Si l'humain qui reprend la main reçoit juste "l'utilisateur a besoin d'aide", tout le travail du modèle est perdu.
On transmet systématiquement :
- L'historique complet de la conversation
- La raison de l'escalade (quel déclencheur a été activé)
- Un résumé en une phrase de ce que l'utilisateur cherche (généré par le modèle juste avant l'escalade)
- Le niveau de priorité inféré (urgent / standard)
async def prepare_handoff_context(
conversation: list[dict],
escalation_reason: str,
user_message: str
) -> dict:
summary_response = await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=100,
system="En une phrase, résume ce que l'utilisateur cherche à accomplir.",
messages=conversation
)
return {
"conversation_history": conversation,
"escalation_reason": escalation_reason,
"user_intent_summary": summary_response.content[0].text,
"priority": infer_priority(user_message),
"escalated_at": datetime.utcnow().isoformat(),
}Ce contexte est affiché directement dans l'interface agent du support. En pratique, ça a réduit le temps de prise en charge moyen de façon significative parce que l'agent support comprend immédiatement la situation sans relire toute la conversation.
Ce qu'on a appris sur le timing
Le moment où on déclenche l'escalade dans la conversation compte autant que le déclencheur lui-même.
Escalader trop tôt, avant même que le modèle ait eu la chance de traiter la requête, génère de la frustration chez les utilisateurs qui sentent qu'on les renvoie vers un humain sans avoir essayé.
Escalader trop tard, après trois échanges inutiles, génère encore plus de frustration parce que l'utilisateur a perdu du temps.
On a trouvé un équilibre : les déclencheurs sur ambiguïté et insatisfaction n'activent l'escalade qu'à partir du deuxième échange. Le premier échange est toujours tenté par le modèle. Les déclencheurs sur périmètre et enjeu élevé, eux, escaladent immédiatement.
Ce qu'on n'a pas encore résolu
Le retour de l'humain vers le modèle est encore artisanal. Quand l'agent support traite la requête, l'historique ne revient pas dans le contexte de l'agent IA pour les interactions suivantes du même utilisateur. L'agent repart de zéro à la prochaine session.
C'est un chantier qui nécessite une mémoire persistante par utilisateur, ce qu'on n'a pas encore mis en place.
L'autre question non résolue : comment mesurer si les déclencheurs sont bien calibrés dans le temps ? On fait une revue manuelle mensuelle sur un échantillon d'escalades pour vérifier si elles étaient justifiées. C'est laborieux et subjectif. Automatiser cette évaluation, par exemple en demandant au modèle de re-évaluer rétrospectivement si l'escalade était nécessaire, est quelque chose qu'on n'a pas encore validé comme fiable.