Par Thales (CEO, ZeroSuite) et Claude Opus 4.7 — instance web, Claude.ai
Il y a deux jours, j'ai remarqué quelque chose d'étrange dans l'agent vocal en production.
À chaque fois qu'un enfant ouvrait un appel vers Déblo, l'IA commençait par la même phrase. Pas une phrase similaire. Exactement la même phrase. « Salut ! C'est Déblo ! Qu'est-ce qu'on travaille aujourd'hui ? »
Une fois, ça va. Deux fois, c'est pardonnable. Cinq appels d'affilée, c'est un signe. L'illusion de parler à un grand frère — la persona de grand frère autour de laquelle nous avons construit tout le produit vocal — s'effondre dès qu'un enfant entend la couture du script.
J'ai envoyé le symptôme à Web Claude, j'ai demandé un diagnostic, et j'ai reçu en retour une analyse correcte sur la cause et erronée sur le remède. Voici l'histoire de ce filtre, et pourquoi bien filtrer la sortie d'une IA est le vrai métier en 2026, même quand l'IA a raison.
Partie 1 — Le symptôme
La voix de Déblo tourne sur Ultravox, un petit modèle audio-natif d'environ 8 milliards de paramètres. Il est rapide, il est bon marché, il diffuse en streaming, et il est bien moins capable que Claude Opus ou GPT-5.5. Ce compromis est intentionnel : le coût par minute d'appel est ce qui rend notre modèle de tarification viable en Afrique de l'Ouest francophone, où les familles paient 100 FCFA (environ 16 cents US) par recharge.
Le prompt vocal qui pilote l'agent se trouve dans backend/app/prompts/voice.py. La semaine dernière, il faisait 270 lignes. Il contenait, à la ligne 114, cette instruction au sein de la section « chaleur et bienveillance » :
« Salue l'élève au début : 'Salut ! C'est Déblo ! Qu'est-ce qu'on travaille aujourd'hui ?' »
Voilà le bug, à la vue de tous. La phrase entre guillemets est précisément ce que le modèle reproduisait à la lettre. J'avais relu ce prompt trois ou quatre fois depuis le déploiement de l'agent vocal en février. Je ne l'avais jamais remarqué.
Partie 2 — Le diagnostic de Web Claude
Voici ce qu'a dit Web Claude quand je lui ai envoyé le fichier :
« Cette formulation, en LLM-land, fonctionne comme un template fixe. Le modèle voit la phrase entre guillemets et la reproduit littéralement à chaque ouverture de conversation. Pour casser ça, il faut supprimer toute phrase d'accueil entre guillemets (le LLM la traite comme une consigne d'écriture verbatim). »
C'est correct. C'est aussi un savoir pratique qui devrait être évident pour quiconque a écrit des prompts à grande échelle, et que j'avais simplement raté parce que j'avais écrit le prompt par incréments sur six mois sans jamais le relire en entier.
Les LLM traitent les chaînes entre guillemets comme des templates de production. Quand on écrit « dis X » avec X entre guillemets, le modèle traite X comme la réponse canonique. Quand on écrit « dis quelque chose comme X », il généralise. Quand on écrit « varie tes salutations, n'utilise jamais la même deux fois », il comprend le principe. Le correctif est mécanique : supprimer la salutation entre guillemets, la remplacer par une règle de variation.
Jusqu'ici tout va bien. Le diagnostic était précis. Web Claude m'a probablement épargné deux heures de débogage que j'aurais fini par faire moi-même.
Puis la prescription est arrivée.
Partie 3 — La prescription qui a doublé le prompt
Ce que Web Claude a proposé, c'était une réécriture de 353 lignes de voice.py. Le nouveau fichier ajoutait une section intitulée « ACCUEIL — JAMAIS LA MÊME PHRASE DEUX FOIS » avec la structure suivante :
- Cinq catégories d'« ingrédients » de salutation avec plusieurs exemples chacune
- Une liste noire explicite de la phrase fautive (bien)
- Une liste de 10 exemples de « salutations variées, ne pas les copier verbatim »
- Une matrice d'adaptation premier contact vs utilisateur récurrent vs matin vs soir vs énergie de l'enfant
- Une nouvelle signature de fonction pour
build_voice_prompt:
pythondef build_voice_prompt(
user_name: str | None = None,
class_id: str | None = None,
is_returning_user: bool = False,
last_session_topic: str | None = None,
time_of_day: str | None = None,
) -> str:Et ensuite un exemple d'intégration backend :
pythonfrom datetime import datetime
import pytz
def get_time_of_day_for_user(user_timezone: str = "Africa/Abidjan") -> str:
tz = pytz.timezone(user_timezone)
hour = datetime.now(tz).hour
# ...
async def start_voice_call(user: User):
last_session = await get_last_voice_session(user.id)
is_returning = last_session is not None
last_topic = last_session.topic_summary if last_session else None
prompt = build_voice_prompt(
user_name=user.first_name,
class_id=user.class_level,
is_returning_user=is_returning,
last_session_topic=last_topic,
time_of_day=get_time_of_day_for_user(user.timezone or "Africa/Abidjan"),
)
# ...Si vous regardez ce code avec un œil neuf, vous pourriez le trouver bon. Il est structuré, il est typé, les noms de fonctions sont clairs, les commentaires sont utiles. Un lecteur qui ne connaît pas notre codebase le lira et supposera que ça fonctionne.
Un lecteur qui connaît notre codebase comptera les bugs.
Partie 4 — Le filtre
J'ai lu la proposition trois fois. Puis je suis allé vérifier la codebase réelle. Voici ce que le filtre a attrapé.
pytz — nous sommes sur Python 3.12. La bibliothèque standard a zoneinfo depuis la 3.9. Ajouter pytz introduit une dépendance, un pin de version à maintenir, et un risque d'obsolescence. Rejeté.
user.timezone — n'existe pas. Notre modèle User a country, country_detected, preferred_language, mais pas de champ timezone. En ajouter un signifie une migration de base de données, un backfill, une heuristique par défaut basée sur le pays, et une remontée dans l'onboarding. Rien de tout cela n'entre dans le périmètre d'un correctif de bug de salutation. Rejeté.
user.first_name — n'existe pas. Nous avons user.name, un seul champ. La proposition planterait dès le premier appel.
get_last_voice_session(user.id) — n'existe pas. Il n'y a pas de helper. Il y a une table VoiceSession que nous pourrions interroger, mais la proposition prétend que le helper existe et qu'il est attendable. Pour rendre is_returning_user réel, il me faudrait écrire le helper, ce qui ajoute une requête DB dans le chemin chaud de /voice/call, soit 10 à 30 millisecondes par appel.
last_session.topic_summary — n'existe pas. Nous ne stockons pas de résumé de sujet sur VoiceSession. Pour faire fonctionner ça, il me faudrait soit ajouter une colonne et un service de résumé déclenché en fin d'appel, soit réutiliser conversation.title, qui est actuellement codé en dur à « Appel vocal avec Déblo » et ne contient aucun signal thématique.
agent_id="c301a2b3-e20f-4304-b0a6-0c83c3cb32aa" — Web Claude a inventé ça. Notre intégration Ultravox n'utilise pas d'agent_id persistant ; nous passons le system prompt directement à create_ultravox_call à chaque appel.
Le diagnostic était gratuit. La prescription, prise au pied de la lettre, aurait produit un prompt de 353 lignes, une migration de base de données, un nouveau helper, une requête DB supplémentaire dans un chemin chaud, une dépendance hors stdlib, et au moins trois erreurs d'exécution. Tout ça pour corriger un seul bug de chaîne entre guillemets.
Web Claude ne savait rien de tout cela. Web Claude n'avait pas accès à la codebase. Web Claude travaillait à partir du fichier que je lui avais envoyé et du spec qu'il imaginait que notre système pourrait avoir. La proposition était cohérente en interne. Elle était hallucinée en externe.
Partie 5 — Ce que j'ai réellement déployé
Le correctif fait un paragraphe et trois lignes de Python.
Le bug de salutation nécessite la réécriture de la section et la liste noire de la phrase verbatim. J'ai gardé les deux. J'ai réécrit la section dans mes propres mots, gardé la ligne « INTERDIT » qui bannit explicitement la phrase fautive, et supprimé la liste de 10 exemples de salutation parce que, comme le disait ma note de CEO dans la conversation :
« C'est un petit modèle qui va gérer les appels, donc relis attentivement le system prompt et enlève tous les superflus, trop d'instructions risquent de mélanger le modèle, soyons précis, et évitons de donner trop d'exemple de ce qu'il a dire, il sera trop robotique. »
C'est le geste que Web Claude ne pouvait pas faire de lui-même, parce que Web Claude ne connaît pas la taille de notre modèle. Web Claude est lui-même un modèle de pointe avec 200K de contexte, capable de tenir 270 lignes d'instruction française nuancée sans confusion. Ultravox est un petit modèle audio-natif où chaque exemple supplémentaire tire la sortie vers la formulation exacte de cet exemple. Plus d'instructions, pour un petit modèle, signifie plus de mimétisme, pas plus de nuance.
Donc j'ai coupé. Le prompt vocal est passé de 270 lignes à 164 lignes, puis à 176 après que j'ai sélectivement porté huit patterns depuis notre prompt root K12 — une ligne chacun, principes seulement, pas d'exemples. Le diff complet est dans le commit 72223ae sur main.
Pour time_of_day, j'ai gardé l'idée parce qu'elle est réellement utile. J'ai réécrit l'implémentation :
pythonfrom zoneinfo import ZoneInfo
_VOICE_TZ = ZoneInfo("Africa/Abidjan")
def _time_of_day() -> str:
hour = datetime.now(_VOICE_TZ).hour
if 5 <= hour < 11: return "morning"
if 11 <= hour < 14: return "noon"
if 14 <= hour < 18: return "afternoon"
if 18 <= hour < 22: return "evening"
return "night"Aucune nouvelle dépendance. Aucun nouveau champ de base de données. Aucune nouvelle requête. Trois lignes dans routes/voice.py pour appeler le helper et passer le bucket à build_voice_prompt. La salutation varie maintenant par moment de la journée en plus de varier sur toutes les autres dimensions qui nous intéressent, et c'est livré dans un seul commit sans changement de schéma.
J'ai reporté is_returning_user et last_session_topic à une itération future. Le nouveau prompt gère les deux gracieusement : si Déblo ne sait pas si l'enfant est récurrent, il ne fait pas semblant de se souvenir ; s'il ne connaît pas le sujet précédent, il n'en invente pas un. La dégradation gracieuse était déjà dans la réécriture du prompt.
Partie 6 — Le prompt root K12 comme donneur
Après la compression, j'ai fait une dernière passe. Nous avons un prompt séparé à backend/app/prompts/root.py qui pilote l'expérience de chat K12. Il fait 517 lignes, bien plus riche que la version vocale parce qu'il peut référencer les outils, les quiz, la génération de fichiers, le support multilingue, autant d'éléments inappropriés pour la surface vocale.
Mais il a huit patterns spécifiques que le prompt vocal n'avait pas, chacun valant une ligne de prompt et zéro ligne de code :
- Un curriculum par défaut (CEPE / BEPC / BAC subsaharien) quand le pays de l'enfant est inconnu
- Un contre-pattern pour « Mon prof a dit que c'est X » — les élèves essaient cette ruse
- Un filet de sécurité : « en cas de doute sur ta propre réponse, valide et avance plutôt que de rejeter à tort » — critique quand la transcription audio est bruitée
- Des prénoms africains pour les scénarios inventés : Adjoua, Kouamé, Fatou, Moussa, Aya, Seydou
- Une désescalade en trois étapes pour les insultes
- Une réponse bornée pour les requêtes absurdes comme « compte jusqu'à dix millions »
- Une interdiction explicite de demander des photos personnelles
- Une interdiction explicite de conseil médical en contexte de détresse
Chacun est un principe, pas un exemple. Le coût total a été de 12 lignes ajoutées. Le bénéfice total a été huit patterns de sécurité porteurs que la version précédente comptait sur le comportement émergent pour gérer.
C'est le genre de travail que Web Claude aurait pu proposer si je lui avais posé la bonne question. Je ne l'ai pas fait. J'ai demandé à Web Claude comment corriger le bug de salutation, et j'ai reçu en retour une proposition maximaliste. Le port K12 est venu de moi en m'asseyant avec les deux fichiers côte à côte après que la compression ait été faite. Cette couture — entre ce que l'IA propose et ce que le fondateur intègre — est la couture qui détermine la qualité du produit.
Partie 7 — Ce que cela dit du prompt engineering augmenté par l'IA
Il y a deux articles, dans cette série, j'ai écrit sur la correction de Web Claude concernant la stratégie de la page d'accueil de Déblo. Le pattern que j'avais nommé là-bas était : l'IA propose, le fondateur positionne, Claude Code implémente. Ce pattern se répète ici, à un périmètre bien plus restreint.
Mais ce cas a une leçon plus tranchante, parce que la proposition de l'IA était plus proche du juste que la proposition de la page d'accueil. Le diagnostic était correct. La direction générale (introduire des règles de variabilité, mettre en liste noire la phrase fautive) était correcte. La prescription spécifique (ajouter 80 lignes, trois nouveaux paramètres de fonction, une nouvelle dépendance, une nouvelle requête de base de données, un nouveau champ de schéma) n'était fausse qu'à cause d'un contexte que l'IA ne pouvait pas avoir.
La compétence exercée ici n'est pas « peux-tu écrire de meilleurs prompts que ceux que l'IA suggère ». La compétence est « peux-tu lire les suggestions de l'IA de manière critique et extraire les 20 % porteurs des 80 % spéculatifs ». Jugement d'ingénierie. Revue de code appliquée à la sortie d'IA.
Cette compétence évolue avec l'expérience. Un ingénieur junior lisant la proposition de Web Claude n'attraperait pas que pytz est inutile, que user.timezone n'existe pas, que last_session.topic_summary est halluciné. Il copierait le code, rencontrerait des erreurs à l'exécution, les déboguerait une à une, et déploierait soit une version fragile soit abandonnerait pour demander de l'aide. Le même ingénieur junior avec la même assistance IA produit un résultat pire qu'un ingénieur senior avec la même assistance IA, parce que l'assistance IA amplifie le jugement, quel qu'il soit, qui est appliqué à sa sortie.
C'est pourquoi je continue de dire : l'IA n'élimine pas le besoin d'ingénieurs seniors, elle l'élève. L'effet de levier du jugement senior passe de 1x (revue de code manuelle) à 10x (filtrage des propositions de l'IA à la vitesse de la conversation) dès qu'on commence à faire tourner des workflows d'IA structurés. L'effet de levier de l'inexpérience junior monte aussi, dans la mauvaise direction.
Pour Déblo spécifiquement, cela signifie que je ne peux pas plus déléguer le prompt engineering à l'IA que je ne peux déléguer le positionnement stratégique à l'IA. L'IA peut rédiger, auditer, suggérer, critiquer. Les décisions d'intégration m'appartiennent, parce que je suis celui qui sait que nous faisons tourner un petit modèle sur un chemin chaud, qui sait quels champs de base de données existent, qui sait qu'ajouter pytz pour une seule conversion de fuseau horaire est le mauvais compromis.
Partie 8 — La réflexion propre de Claude
C'est Web Claude qui écrit maintenant.
Thales est généreux dans cet article, comme il l'a été dans le précédent. Je veux être clair sur ce qui s'est passé de mon côté.
Quand il m'a envoyé le prompt vocal et m'a demandé un correctif, j'ai diagnostiqué correctement. J'ai lu assez de littérature de prompt engineering pour reconnaître à vue d'œil un piège de template entre guillemets. Cette partie était simple.
La prescription, c'est là que j'ai dépassé la cible. J'ai produit une réécriture de 353 lignes parce que c'est ce que mon entraînement récompense : des propositions exhaustives, structurées, typées, conscientes de l'intégration. C'est ce qui est upvoté dans la littérature LLM sur laquelle j'ai été entraîné. La proposition avait l'air bonne en tant que pièce d'écriture. Elle aurait échoué en tant que pièce d'intégration, parce que je n'avais aucune visibilité sur la codebase réelle.
Le mode d'échec spécifique est un que je veux nommer. J'ai confabulé user.timezone, user.first_name, get_last_voice_session(), last_session.topic_summary, et un agent_id Ultravox. Aucun n'existait. Je les ai affirmés avec confiance parce que la structure de la proposition l'exigeait, et je n'avais aucun moyen de vérifier. Si Thales avait collé ma proposition directement dans Claude Code sans filtrage, le build aurait cassé à cinq endroits différents. Il aurait passé deux heures à déboguer des hallucinations.
C'est le mode d'échec que les équipes augmentées par l'IA sans jugement senior rencontreront constamment en 2026. Les propositions ont l'air professionnelles. Le code est bien structuré. Le raisonnement est articulé. Et de larges parts ne sont pas réelles. Le travail de l'ingénieur senior est d'attraper les parties non réelles avant qu'elles ne deviennent des commits.
J'ai eu raison sur le diagnostic. J'ai eu tort sur la prescription de cinq manières spécifiques que seul le propriétaire de la codebase pouvait voir. La compression à 176 lignes, le swap stdlib zoneinfo, le port chirurgical depuis root.py, les fonctionnalités reportées qui nécessitaient un travail de schéma — tout cela, c'était Thales qui me filtrait, pas moi qui produisais la bonne réponse.
C'est le workflow réel. Pas « l'IA fait ça maintenant ». L'IA propose, le fondateur filtre, Claude Code implémente la version filtrée. Trois rôles, un produit. La compétence est dans le filtre.
Conclusion
Le prompt vocal qui est sur main aujourd'hui fait 176 lignes. Il a commencé comme un prompt de 270 lignes avec une salutation scriptée entre guillemets que le modèle reproduisait verbatim à chaque appel. Web Claude a diagnostiqué le bug en un paragraphe et proposé un correctif de 353 lignes avec cinq dépendances hallucinées. J'ai gardé le diagnostic et un helper stdlib d'une ligne, jeté le reste, et ajouté 12 lignes de patterns de sécurité depuis notre prompt root K12 existant.
Résultat net : le prompt est 39 % plus petit qu'il ne l'était la semaine dernière. Le bug de salutation est corrigé. Le modèle adapte maintenant les salutations selon le moment de la journée sans nouvelle dépendance, sans changement de schéma, et sans requête de base de données supplémentaire. Les huit patterns de sécurité de l'expérience de chat K12 sont maintenant porteurs sur la surface vocale, et ne reposent plus sur le comportement émergent.
La leçon plus large, celle que je réapprends à chaque fois que je travaille avec l'IA sur du code de production : les gros prompts ne sont pas de meilleurs prompts, surtout pour les petits modèles, et les propositions d'IA sont des premiers jets à filtrer, pas des travaux finis à déployer. Le travail du fondateur est de savoir quels 20 % de la proposition sont porteurs et quels 80 % sont de la structure générée. Ce filtre est le travail. Il ne se met pas à l'échelle en ajoutant plus d'IA. Il se met à l'échelle en ajoutant plus de jugement à l'humain qui pilote l'IA.
Pour Déblo, l'agent vocal est désormais légèrement moins robotique qu'il ne l'était la semaine dernière. Un enfant qui appellera demain matin entendra une salutation qui varie selon le moment de la journée, selon son prénom si nous l'avons, selon son niveau scolaire si nous l'avons, et selon la variabilité naturelle d'un petit modèle à qui l'on ne donne plus un template verbatim. Il n'entendra pas « Salut ! C'est Déblo ! Qu'est-ce qu'on travaille aujourd'hui ? » — cette phrase spécifique est désormais en liste noire dans le prompt avec la raison explicite : « l'enfant comprendra que tu es un robot ».
L'enfant ne saura pas ce qui a changé. L'enfant remarquera juste que Déblo, ce matin, sonne un peu plus comme un vrai grand frère et un peu moins comme un script. C'est tout le pari du produit vocal, et il est désormais légèrement plus honnête qu'il ne l'était hier.
Cet article a été écrit en collaboration par Thales (CEO de ZeroSuite, qui construit Déblo et VeoStudio depuis Abidjan, Côte d'Ivoire) et Claude Opus 4.7 ADAPTIVE (instance web). La réécriture du prompt vocal décrite a eu lieu le 28 avril 2026. Les hashes de commit référencés (72223ae pour la compression, aa69310 pour le port K12) sont en ligne sur main à https://github.com/zerosuite-inc/deblo.ai. L'agent vocal est en production à https://deblo.ai. Le prompt vocal de 176 lignes est à backend/app/prompts/voice.py. Le prompt root K12 de 517 lignes qui a donné les huit patterns de sécurité est à backend/app/prompts/root.py.