Mon détecteur de messages marchait sur mon jeu de test. Sur des messages jamais vus, il ratait un vrai cas sur deux.
TL;DR : j'ai construit un petit détecteur qui repère les messages demandant une action. Il repose sur les embeddings, une technique qui transforme une phrase en nombres. Il marche bien, sauf sur un point : il ne comprend pas la négation. « La panne est réparée, merci » déclenche comme « panne ». J'explique pourquoi c'est un défaut de nature, pas de réglage, chiffres à l'appui. Et pourquoi la bonne réponse est d'accepter ce défaut plutôt que de le corriger.
Cet article est pour les devs qui veulent brancher de l'IA sans payer un gros modèle à chaque message. Aucune connaissance en machine learning n'est requise : je définis tout en route.
Le contexte
Dans un projet perso, une IA surveille un chat entre utilisateurs. Quand un message demande une action, un badge s'affiche. Créer un ticket, envoyer un document.
Appeler un LLM sur chaque message coûte cher. En argent et en temps de réponse. J'ai déjà défendu cette idée ici : mets le LLM en dernier.
Le système a donc deux étages. L'étage 1 est un petit modèle, gratuit à faire tourner, qui trie les messages. L'étage 2 est le LLM, appelé seulement quand l'utilisateur clique sur le badge.
L'étage 1 utilise des embeddings. Un embedding transforme une phrase en une liste de nombres. Deux phrases de sens proche donnent des listes de nombres proches. C'est tout ce qu'il faut comprendre pour la suite.
Décider avec un score de ressemblance
Comment un embedding prend une décision ? En mesurant la ressemblance entre deux phrases. Ce score s'appelle la similarité cosinus. Proche de 1 : les phrases se ressemblent beaucoup. Plus bas : elles n'ont rien à voir.
Mon détecteur compare chaque message aux outils disponibles. « Créer un ticket », « envoyer un document ». Si le message ressemble assez à un outil, il est jugé actionnable.
Toute la question tient dans « assez ». C'est là que ça se complique.
La barre fixe ne marche pas
Premier réflexe : fixer une barre. Au-dessus de 0,85 de ressemblance avec un outil, le message est actionnable.
Ça ne marche pas. Mon modèle donne des scores entre 0,80 et 0,92, pour tout. « Bonjour » et « crée un ticket » obtiennent presque le même score.
Ce modèle voit toutes les phrases comme un peu ressemblantes. Ses scores sont tassés dans un mouchoir de poche. Et une barre fixe ne sépare rien dans un mouchoir de poche. Monte la barre, et tu perds les vrais cas avant de perdre le bruit.
Compare à des phrases neutres, pas à une barre
La solution qui a marché : comparer, au lieu de mesurer dans l'absolu.
J'ai écrit une liste de phrases neutres, sans aucune action dedans. « Bonjour », « merci », « ok super ». Je les appelle les ancres.
La règle devient simple. Un message est actionnable s'il ressemble plus à un outil qu'à la meilleure ancre. S'il ressemble surtout à « merci », c'est du bavardage.
# La règle de décision
tool_score = similarity(message, closest_tool)
neutral_score = similarity(message, closest_anchor)
actionable = tool_score > neutral_score
Un détail qui compte : je découpe les messages longs à la ponctuation. Une demande noyée dans la politesse ressort mieux morceau par morceau. Revers de la médaille : sans ponctuation, pas de découpage. La dictée vocale en met rarement.
Un faux positif sur quatre
J'ai évalué sur 300 messages générés. Générés, pas réels : le projet n'avait pas encore d'utilisateurs. Garde ça en tête, la réalité fera moins bien.
Deux mesures comptent, et elles sont simples. La première : sur 100 messages qui méritent une action, combien le détecteur en attrape. Chez moi, 92. C'est le rappel, et c'est un bon score.
La seconde : sur 100 messages anodins, combien il laisse tranquilles. Chez moi, 76. Autrement dit, 24 messages anodins sur 100 déclenchent le badge pour rien. C'est la spécificité, et c'est le chiffre qui fâche.
Un badge qui se trompe une fois sur quatre, l'utilisateur arrête d'y croire. Trop de fausses alertes tuent l'alerte. Le premier chiffre brille dans la démo. Le second se paie en production.
Les embeddings ne savent pas dire non
Le pire cas d'échec a un nom : la négation. « Il y a une panne » demande une action. « La panne est réparée, merci » n'en demande aucune.
Pour le détecteur, ces deux phrases sont presque identiques. Même vocabulaire, donc des nombres presque identiques. La seconde déclenche le badge exactement comme la première.
Ce n'est pas un réglage raté. Un embedding résume une phrase par son sujet. Et le sujet des deux phrases est le même : une panne. Le « c'est réparé » ne pèse presque rien dans les nombres.
Tu peux le vérifier chez toi en deux minutes. Prends tes phrases positives, ajoute « c'est réparé, merci » à la fin de chacune. Puis regarde les scores : ils bougent à peine.
L'examen blanc m'a rattrapé
J'ai d'abord cru pouvoir corriger la négation par le réglage. Douze ancres de plus, du type « c'est réparé », « problème réglé ». Et une barre de décision plus haute.
Sur mon jeu de test, superbe. Alors j'ai fait passer un examen blanc au détecteur. En jargon : un holdout. Un jeu de messages neufs, jamais utilisés pendant le réglage. Des sujets inédits, pas les annales.
Verdict. Les fausses alertes du type « c'est réparé » ont bien disparu. Mais le détecteur est passé de 68 à 48 vrais cas attrapés sur 100. Ma correction ratait plus d'un vrai cas sur deux.
Ça s'appelle le surapprentissage. Mon réglage avait appris mes exemples par cœur, au lieu d'apprendre le problème. Comme un élève qui récite les annales et coule sur un sujet neuf.
Pire : sur l'examen blanc, aucune barre ne donne à la fois un bon rappel et peu de fausses alertes. Le bouton que je tournais n'a plus rien à donner. Le plafond est structurel.
Toute la précision appartient au LLM
Les chiffres m'ont imposé la conclusion. Ce détecteur ne sera jamais précis. En revanche, il peut être exhaustif.
Je lui ai donc choisi un rôle : une porte, pas un juge. Barre au plus bas, pour attraper large. Sur l'examen blanc : 88 vrais cas sur 100 attrapés, et 31 messages anodins sur 100 qui passent pour rien.
Ce bruit est accepté, et écrit noir sur blanc dans le design. Parce que l'étage 2 juge derrière : le LLM, appelé au clic, sait lire « c'est réparé ». Lui filtre la négation et choisit le bon outil.
# La config shippée : une porte, pas un juge
model : multilingual-e5-small # petit, sur CPU, zéro entraînement
threshold : lowest # attraper large, assumé
anchors : smalltalk + resolution # « merci », « c'est réparé »...
result : 88 vrais cas sur 100 · 31 fausses alertes sur 100
Dernière décision, la plus contre-intuitive : ne pas entraîner le modèle. Je n'ai pas de vraies conversations. Et entraîner sur des messages générés, c'est apprendre les tics du générateur. L'examen blanc venait de me montrer ce piège. J'entraînerai quand j'aurai quelques centaines de vrais messages anonymisés.
La checklist avant de shipper un détecteur à embeddings
Avant de mettre un score de ressemblance en production, passe la liste.
- Teste sur des messages jamais vus, pas sur ceux qui ont servi au réglage
- Compte les fausses alertes, pas seulement les bonnes détections
- Essaie des phrases de négation : « c'est réparé », « plus besoin, merci »
- Compare à des phrases neutres plutôt que de fixer une barre absolue
- Donne à l'étage embeddings un rôle de porte : attraper large, laisser juger plus loin
- Sache combien de fausses alertes l'étage suivant peut absorber
- N'entraîne jamais sur des données générées
Ce qu'il faut retenir
Un score de ressemblance ne comprend pas « non ». Ce n'est pas un défaut de réglage, c'est la nature de l'outil.
Alors mesure sur du jamais-vu, publie tes chiffres moches, et place chaque étage là où il est bon. L'embedding attrape. Le LLM comprend.
Tu construis une détection d'intention ou un routage IA, et les chiffres ne tiennent pas en production ? Parlons-en.