Tags
L'illusion de l'IA : pourquoi alimenter un LLM sans gouvernance des données est un risque impossible à auditer
October 30, 2026 ·
19 min
Une juriste ouvre le nouvel assistant IA de l’entreprise, y dépose un contrat d’exclusivité et tape : « Résume les obligations clés en cinq lignes. » Dix secondes plus tard, elle obtient une synthèse claire et concise. C’est un gain de productivité concret, indéniable.Ce qu’elle ne voit pas, c’est ce qui s’est réellement passé pendant ces dix secondes. L’application ne s’est pas contentée d’envoyer sa question : elle a transmis le contrat dans son intégralité — la contrepartie, les grilles tarifaires, les clauses de résiliation, le nom des signataires. Tout cela a atterri, sous forme de texte lisible en clair, sur un serveur exploité par un tiers.Le même phénomène se reproduit lorsqu’un responsable RH demande le résumé d’un entretien d’évaluation, ou lorsqu’un analyste financier épluche le dossier de crédit d’un client. La question est courte. Le contexte qui l’accompagne est colossal.Dans presque tous les comités de direction, le mot d’ordre pour 2026 est identique : « Il nous faut de l’IA, et tout de suite. » En quelques semaines, des clés d’API sont générées, des modèles de langage sont connectés aux bases de données internes, et des flux de travail entiers sont délégués à des algorithmes tiers. Les démos sont impressionnantes et les gains de productivité bien réels.Cet article ne cherche pas à contester cet élan. Il soutient que les entreprises qui prendront la gouvernance des données au sérieux déploieront l’IA plus vite et sur un plus large périmètre, précisément parce que leurs fondations seront sûres. Celles qui font l’impasse dessus finiront par se heurter à une question toute simple à laquelle elles ne sauront pas répondre. Nous y reviendrons à la fin.1. Le problème : chaque requête emporte vos donnéesMême lorsque votre contrat avec le fournisseur d’IA lui interdit d’entraîner ses modèles sur vos données, et même lorsque la connexion est intégralement chiffrée en transit, le texte brut arrive toujours sous une forme parfaitement lisible chez ce tiers. Les contrats encadrent ce que le fournisseur a le droit d’en faire. Mais ils ne changent absolument rien à ce qui se retrouve exposé en cas de compromission chez lui, en cas d’erreur de configuration dans ses logs, ou si une réquisition judiciaire atterrit sur son bureau.Ce risque s’amplifie encore avec les architectures multi-agents, où un agent routeur délègue le travail à des sous-agents spécialisés partageant une mémoire commune. Un simple nom ou un chiffre d’affaires injecté dans cet état partagé devient immédiatement visible pour chaque agent de la chaîne, ainsi que pour chaque journal d’événements, chaque cache et chaque outil de traçabilité (tracing) dans lequel ils écrivent.Figure 1 : Injection de données brutes dans une mémoire partagée multi-agents.Envoyer des données métier brutes dans un LLM sans aucun contrôle n’est pas de l’innovation. C’est sous-traiter la mémoire stratégique de son entreprise à un tiers qui n’assumera jamais le coût de vos fuites.2. Pourquoi l’anonymisation naïve échoueCertaines équipes, conscientes du danger, placent un filtre rudimentaire en amont du modèle : des outils génériques de reconnaissance d’entités nommées (Named Entity Recognition ou NER), ou de simples expressions régulières qui masquent les noms propres. Sur le papier, cela semble raisonnable. En production, cette approche échoue généralement de trois manières — et son vrai danger est de procurer à la direction un sentiment trompeur de sécurité qui l’empêche de poser les bonnes questions.Échec n°1 : Le filtre ne comprend pas votre métierLes outils de détection génériques sont entraînés sur des textes courants. Ils confondent facilement les noms d’entreprises avec des termes du langage usuel (« Services », « Transit », « Holding »), et laissent régulièrement passer des raisons sociales contenant ce type de mot, comme « NET-POLY SERVICES ». Quant aux identifiants internes (numéros fiscaux, identifiants du registre de commerce, codes clients internes), ils ne correspondent pas aux formats standards internationaux et passent totalement inaperçus.Échec n°2 : Masquer le nom ne revient pas à masquer l’entrepriseConsidérez cet exemple : Texte brut : « L’entreprise X a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. » Après un filtre naïf : « ENTREPRISE_1 a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. »Le nom a disparu, mais le chiffre d’affaires exact et la ville suffisent amplement à identifier l’entreprise en une seule recherche. Un modèle moderne, fort de son immense base de connaissances d’entraînement, peut souvent faire le lien en une fraction de seconde.Échec n°3 : Un prompt est une prière, pas un contrôleUne consigne comme « Ne répète jamais les noms des clients » n’est qu’une incitation probabiliste (probabilistic nudge). Le modèle s’y conformera le plus souvent… jusqu’au jour où il ne le fera pas. Le problème est encore plus pernicieux : selon le fournisseur et la configuration, certains modèles renvoient leur raisonnement intermédiaire dans un canal distinct de la réponse finale — une sorte de bloc-notes (scratchpad ou canal de pensée) qui vous est retourné. Si votre mécanisme de protection ne filtre que la réponse finale, les données brutes peuvent transiter par ce bloc-notes en toute liberté.Croire qu’un prompt système enjoignant d’« être discret » protège vos données revient à verrouiller une porte blindée en laissant la clé sur la serrure extérieure.3. Les six invariants d’une gouvernance robusteSi les consignes et les filtres basiques ne suffisent pas, que faut-il mettre en place ? La plupart des architectes esquissent un pipeline linéaire : les données arrivent, sont filtrées, partent vers le modèle, puis reviennent. Un système robuste obéit à une tout autre logique : c’est une défense en profondeur, où chaque frontière porte sa propre protection, indépendamment des autres.Voici six invariants fondamentaux. Ce sont des contrats d’architecture que tout concepteur de système peut vérifier au sein de sa propre infrastructure, quels que soient la stack technique, le fournisseur de modèles ou la taille de l’équipe.Figure 2 : Les six invariants de gouvernance des données appliqués aux flux LLM.Invariant 1 : Minimiser dès la sourceCe qui ne figure pas dans les données transmises ne peut pas fuiter. Les outils exposés aux agents IA ne doivent jamais être les mêmes que ceux mis à disposition des développeurs humains. La pagination y est strictement imposée, les volumes sont plafonnés et les champs superflus sont intégralement supprimés. L’API ne renvoie pas un dossier client complet pour calculer un simple ratio : elle ne retourne que les trois nombres strictement nécessaires — sans coordonnées, sans adresses, sans noms.Ce n’est pas une règle propre à l’IA : c’est un principe fondamental d’exposition des données que l’IA rend simplement impératif.Invariant 2 : Ne jamais laisser un modèle trancher ce qu’un chemin déterministe peut déciderUn classement, la consultation d’un ratio ou une simple extraction d’information n’exigent aucun discernement : ils n’ont donc aucune raison de transiter par votre modèle le plus onéreux et le moins prévisible. L’aiguillage peut reposer sur une règle métier, un choix dans l’interface ou un classifieur très léger : le principe n’impose pas la méthode d’implémentation.Deux remarques franches s’imposent. Premièrement, si le routeur est lui-même un modèle, il reste un modèle : choisissez-le miniature, strictement bridé, et ne lui fournissez que du texte anonymisé. Le but est de réduire la surface d’exposition, non de prétendre qu’elle a disparu. Deuxièmement, cet invariant présente un avantage qui convainc souvent un comité de direction bien avant les arguments sécuritaires : il est beaucoup plus économique et infiniment plus rapide.Une anecdote révélatrice nous est fournie par Anthropic elle-même. En mars 2026, le code source de Claude Code a fuité accidentellement via une source map publiée par inadvertance sur npm. On y a découvert que l’outil détecte la frustration de l’utilisateur — c’est-à-dire si quelqu’un l’insulte — au moyen d’une simple expression régulière, sans faire appel au moindre modèle1. Une regex est instantanée et gratuite. Un appel d’inférence n’est ni l’un ni l’autre, pour une question qui ne réclame aucun jugement.Invariant 3 : Traiter chaque frontière vers le modèle comme un point de protection obligatoire et réversibleToute donnée franchissant la frontière entre votre système et un modèle de langage, qu’elle soit entrante ou sortante, doit être protégée par un mécanisme réversible tel que la substitution, la pseudonymisation ou la tokenisation. L’erreur courante consiste à ne protéger que le champ de réponse évident. Inspectez chaque canal exposé par le fournisseur, y compris le canal de raisonnement décrit à la section 2.Une mise en garde juridique : des données pseudonymisées peuvent toujours être qualifiées de données à caractère personnel au sens de réglementations comme le RGPD. Cet invariant réduit votre exposition au risque opérationnel ; il ne vous exonère pas de vos obligations légales.Invariant 4 : Réhydrater au tout dernier moment légitimeL’opération inverse de substitution ne doit être appliquée qu’à l’ultime étape, juste avant que la réponse ne soit présentée à un utilisateur humain habilité à consulter les valeurs réelles. Le modèle travaille à l’aveugle du début à la fin. Seul l’humain reçoit la réponse en clair, et exclusivement en bout de chaîne.Invariant 5 : Un garde-fou déterministe a le dernier mot sur la plausibilitéLes quatre premiers invariants protègent la confidentialité. Celui-ci protège l’exactitude des résultats. Toute réponse générée doit franchir un contrôle non probabiliste — tel qu’un seuil limite ou une comparaison par rapport à un référentiel connu — avant d’être transmise. Si le résultat semble aberrant, le système bascule automatiquement sur un chemin surveillé plus lent plutôt que de laisser passer une réponse non vérifiée.Invariant 6 : Chaque saut d’agent à agent constitue sa propre frontière de confianceC’est l’invariant le plus souvent négligé, car il n’apparaît pas sur les diagrammes en couches statiques. La sortie d’un agent n’est pas automatiquement sécurisée pour l’agent qui l’a sollicité.Voici le cas typique : un sous-agent, pensant bien faire en « nettoyant sa propre sortie » avant de répondre, désanonymise des données qui devaient impérativement rester masquées pour l’agent parent ayant délégué la tâche. Le sous-agent s’est conformé à ses consignes à la lettre. Il ignorait simplement que sa réponse était destinée à un autre maillon de la chaîne, censé poursuivre son raisonnement sur des pseudonymes.Il existe un corollaire aggravant : lorsque l’historique conversationnel traverse plusieurs agents, les échanges de différents sous-agents se retrouvent mélangés dans un contexte unique. Sans un étiquetage explicite de la provenance (provenance tagging), il devient impossible de déterminer quelle frontière de confiance s’applique à quel fragment de texte.Figure 3 : Frontières de confiance lors de délégations inter-agents et déplacement du point de réhydratation.4. Pour les ingénieurs : comment nous implémentons les invariantsIl s’agit ici d’une implémentation concrète parmi d’autres. L’outillage est spécifique à notre écosystème technique. Même si votre architecture diffère sensiblement de la nôtre, les invariants restent universels : seule leur incarnation technique change. Les lecteurs non techniques peuvent passer directement à la section 5. Passerelle d’anonymisation (invariant 3) : Avant toute classification, le texte libre saisi par l’utilisateur franchit une passerelle stricte combinant des bibliothèques de TALN/NLP et des règles de substitution déterministes. Les noms et identifiants sont remplacés par des jetons uniques (par exemple CORP_A83B). La table de correspondance est immédiatement sécurisée dans un coffre-fort chiffré (encrypted vault) : calcul HMAC-SHA256 pour l’identifiant du jeton et chiffrement Fernet (AES-128-CBC avec HMAC) pour la valeur réelle au repos. Routeur (invariant 2) : La requête pseudonymisée est soumise à un LLM délibérément compact, véloce et économique, dont l’unique mission est de classifier l’intention. Un classement, une lecture de ratio ou une extraction basique emprunte le chemin rapide (fast path) : un outil issu d’une liste blanche restreinte (allowlist) est exécuté directement, sans jamais solliciter le modèle lourd. Une analyse de risque ou une tâche d’interprétation complexe emprunte le chemin lent (slow path) et bascule vers l’agent principal. Outils minimisés (invariant 1) : Sur les deux chemins, l’accès aux données internes s’effectue au moyen d’outils exposés via le protocole MCP (Model Context Protocol), épurés à la source et strictement dissociés des outils réservés aux développeurs humains. Chemin lent (invariant 3, sortant) : Les résultats renvoyés par les outils sont systématiquement pseudonymisés avant d’être injectés dans le contexte du modèle lourd, de sorte que ce dernier raisonne et rédige exclusivement à partir de pseudonymes. Chemin rapide (invariant 5) : Un garde-fou de plausibilité — par exemple un ratio comparé à une valeur de référence — bloque toute réponse automatique aberrante et force un repli vers le chemin lent pour examen approfondi. Réhydratation (invariant 4) : Ce n’est qu’une fois la réponse finale générée et mise en forme que le système interroge le coffre-fort pour substituer les jetons par les valeurs réelles, immédiatement avant l’affichage à l’utilisateur dûment habilité.Figure 4 : Architecture de routage fast-path / slow-path. Résultats observés : (Insérer ici vos métriques clés de production — par exemple : 72 % des requêtes traitées par le chemin rapide sans appel au modèle lourd, division par 4 de la latence médiane et réduction de 65 % des coûts d’API).5. Vérifier plutôt que présumerUne défense qui n’a jamais été éprouvée dans des conditions réelles est une défense dont on espère simplement l’existence.Voici un scénario bien loin d’être théorique : un contrôle destiné à transformer chaque message avant qu’il n’atteigne le modèle a été rédigé, validé en revue de code et intégré à la branche principale. Les tests passaient avec succès. Pendant des semaines, l’équipe a cru la protection active. Elle ne l’était pas. Une hypothèse erronée sur la structure exacte de la charge utile entrante (payload shape) faisait que le filtre s’exécutait sur une mauvaise clé d’objet, de manière silencieuse, sans générer d’erreur et sans déclencher la moindre alerte.Un test qui passe au vert sans solliciter réellement le code de protection ne prouve rien. Une revue de code n’est pas une preuve d’exécution, et un schéma d’architecture soigné n’est pas la preuve que la trajectoire tracée est celle empruntée lors de l’exécution. La seule preuve tangible repose sur un test rejoué contre le système réel : le vrai modèle, les vraies données, la véritable chaîne d’appels.Chaque affirmation de conformité — qu’il s’agisse de « ces données sont anonymisées » ou « ces données ne sortent jamais en clair de notre réseau » — doit être assortie d’une preuve rejouable, consultable à la demande. Sans cela, il ne s’agit pas d’une garantie : c’est une hypothèse qui n’a pas encore été réfutée.Figure 5 : Défaillance silencieuse d’un contrôle liée à une inadéquation de format des données entrantes.6. La piste d’audit (Audit Trail)La gouvernance ne s’arrête pas à la protection en temps réel : elle exige une traçabilité sans faille. Lorsqu’un régulateur bancaire, un auditeur RGPD ou un grand client examine la manière dont l’IA est exploitée dans votre entreprise, il ne se contentera pas de simples déclarations d’intention. Il exigera des preuves irréfutables.Un système d’IA d’entreprise responsable conserve un journal d’audit infalsifiable (tamper-evident) et strictement en ajout seul (append-only), isolé des logs applicatifs ordinaires et de l’historique des conversations. Pour chaque événement du cycle de vie d’un agent, il consigne une paire indissociable : Les données brutes telles qu’elles ont été initialement reçues, Les données réellement transmises aux modèles externes.La règle la plus cruciale est la suivante : aucun paramètre de configuration (feature flag) ni aucun administrateur isolé ne doit pouvoir désactiver ce journal. Lors de pics de charge imprévus, le premier réflexe d’une équipe technique est souvent de désactiver les journaux « pour préserver les performances ». Si un tel commutateur existe, le jour où une fuite survient, vous serez incapable d’en déterminer l’étendue ou d’identifier ce qui a été exposé.Figure 6 : Pourquoi le journal d’audit doit être immunisé contre toute désactivation par configuration.Il y a toutefois un revers, et il doit être énoncé sans détour : ce journal d’audit devient lui-même l’un des jeux de données les plus sensibles de l’organisation, puisqu’il contient précisément les données brutes que vous avez pris tant de peine à dissimuler. Traitez-le en conséquence : chiffrez-le, cloisonnez et restreignez ses accès, fixez des limites de conservation rigoureuses, et définissez avec vos juristes la manière dont il s’articule avec les demandes de suppression (droit à l’effacement prévu par le RGPD).Complétez enfin ce dispositif par une détection rétroactive des fuites (retroactive leak scanning) : des processus d’arrière-plan analysent en continu l’historique d’audit archivé à la recherche de schémas sensibles ayant pu franchir les premiers filtres. Lorsqu’une anomalie est décelée, le scanner ne modifie en aucun cas le journal initial : il déclenche une alerte auprès de l’équipe de sécurité, qui peut ainsi durcir les règles de filtrage avant qu’une simple brèche ne devienne un incident majeur.Figure 7 : Le pipeline d’audit, de l’enregistrement immuable à l’analyse asynchrone et aux alertes.7. Conclusion : L’IA est d’abord un défi de Data EngineeringL’adage bien connu « garbage in, garbage out » a désormais un pendant moderne : « secret in, exposure out » (données secrètes en entrée, fuites en sortie). L’IA n’est pas un vernis cosmétique que l’on vient greffer à la hâte dans un recoin du système d’information pour contenter la direction générale. Elle bouleverse la façon dont l’information circule, se segmente, s’anonymise et s’archive.Les entreprises qui sortiront gagnantes de la révolution de l’IA ne seront pas celles qui auront déployé le plus d’agents dans l’urgence. Ce seront celles qui auront d’abord bâti des fondations hermétiques et fiables. C’est la qualité des freins qui permet à une voiture de rouler vite. Une fois ces contrôles fermement en place, les équipes peuvent donner leur feu vert à un plus grand nombre de cas d’usage, avec moins de débats interminables et sans vetos de dernière minute.Alors, avant de signer votre prochain contrat de licence d’IA ou de célébrer le déploiement de votre nouvel agent autonome, rassemblez vos architectes et posez-leur une seule question : « Si un auditeur externe mandaté par notre plus grand client débarquait demain matin à 8h, seriez-vous capables de me prouver, ligne de code par ligne de code, qu’aucun nom, aucun identifiant et aucun chiffre confidentiel n’a quitté notre réseau en clair au cours du mois écoulé ? »Si la réponse est un silence gêné, un renvoi aux « clauses contractuelles du fournisseur » ou la mention d’« un prompt qui demande à l’IA d’être prudente », vous connaissez déjà votre prochain projet.La checklist de gouvernance : 8 cases à cocherAvant de quitter la salle, voici les huit critères non négociables à valider (les six invariants, complétés par les impératifs des sections 5 et 6) : Minimisez-vous dès la source ? (Invariant 1) Préservez-vous le modèle lourd et coûteux des requêtes simples ? (Invariant 2) Chaque frontière texte-vers-modèle fait-elle l’objet d’une protection réversible ? (Invariant 3) Ne réhydratez-vous qu’au tout dernier moment légitime ? (Invariant 4) Un garde-fou déterministe a-t-il le dernier mot sur la plausibilité ? (Invariant 5) Chaque saut d’agent à agent constitue-t-il sa propre frontière de confiance ? (Invariant 6) Chaque contrôle est-il vérifié en conditions réelles, et pas seulement supposé fonctionnel ? (Section 5) Le journal d’audit est-il impossible à désactiver par configuration ou par un administrateur isolé ? (Section 6) Alex Kim, « The Claude Code Source Leak: fake tools, frustration regexes, undercover mode, and more », 31 mars 2026. La fuite provenait d’un fichier source map publié par mégarde sur npm. Le fichier userPromptKeywords.ts contient la regex employée pour la détection de la frustration de l’utilisateur. ↩
Laplace Nearby : un bot WhatsApp qui trouve « le maquis le plus proche » sans exploser la facture LLM
June 22, 2026 ·
9 min
À Abidjan, une grande partie des commerces de proximité — le maquis du quartier, la pharmacie, le réparateur de téléphones — n’ont ni site web, ni fiche Google Business, ni même une adresse postale formelle la moitié du temps. Ce qu’ils ont, en revanche, c’est WhatsApp. Laplace Nearby part de ce constat : plutôt que de construire une application que les gens doivent télécharger, autant construire l’expérience de recherche dans l’application que tout le monde a déjà ouverte, et laisser les gens décrire ce qu’ils cherchent en langage naturel — « il me faut une pharmacie », « j’ai envie de porcodjo » — plutôt que de remplir un formulaire de filtres.L’assistant lui-même s’appelle en interne Simon. Ce billet parle de la façon dont Simon est réellement construit — les parties que je trouve intéressantes à raconter, pas un argumentaire commercial.Le dépôt : github.com/sanayasfp/laplace-nearby.Décision n°1 : le moteur ignore qu’il parle à WhatsAppLe cœur du système est SimonEngine, un petit orchestrateur qui prend en entrée une InteractionRequest indépendante du canal et renvoie une InteractionResponse tout aussi indépendante. Les spécificités WhatsApp — format des webhooks, mise en forme des messages, boutons et listes — vivent entièrement dans une couche d’adaptateurs/renderers en dehors du moteur. Le moteur lui-même fait quatre choses à chaque message : charger le contexte de session de l’utilisateur, vérifier le rate limiting, transmettre le message au flow conversationnel actif (idle, recherche, ou enregistrement), puis sauvegarder le résultat — en émettant systématiquement un effet de bord analytique avec le nom du flow, la transition d’état et la latence, qu’il se soit passé quelque chose d’intéressant ou non :async process(req: InteractionRequest): Promise<InteractionResponse> { const context = await this.contextManager.load(req.profileId); const flow = this.flowRegistry.get(context.session.activeFlow || IDLE); const limitResult = this.rateLimitService.check(context.rateLimit); if (!limitResult.allowed) { return { messages: limitResult.notify ? [Responses.tooManyMessages(...)] : [] , ... }; } const response = await flow.handle(req, context, interactionId); await this.contextManager.save(req.profileId, response.contextUpdate); return { ...response, sideEffects: [summaryEffect, ...response.sideEffects] };}Ce n’est pas de l’architecture pour le plaisir. Cela signifie que la logique de recherche, le flow d’enregistrement, le rate limiting et les métriques n’auront pas besoin d’être réécrits si un second canal (SMS, un widget web, peu importe) arrive un jour — ils n’ont jamais été couplés à WhatsApp en premier lieu.Décision n°2 : ne pas appeler le LLM si on peut s’en passerChaque message entrant a besoin d’une intention — s’agit-il d’une recherche, d’une demande d’enregistrement, de bavardage, d’un remerciement, d’une insulte, d’un « stop » ? Faire passer chaque message par un appel LLM est la solution de facilité, et aussi la plus lente et la plus coûteuse. La classification d’intention est donc à deux étages.FastIntentDetector s’exécute en premier : un ensemble volontairement exhaustif d’expressions régulières couvrant le français, l’anglais, et l’argot local qu’on croise réellement dans une conversation WhatsApp à Abidjan — « wesh », « cc », « gab » (guichet automatique), « essence/gazoil », des dizaines de variantes orthographiques de « merci », « stop », « annule ». Sa docstring dit exactement à quoi il sert : « Reduce LLM costs and latency for unambiguous user requests » (réduire les coûts et la latence liés au LLM pour les requêtes non ambiguës). Ce n’est que lorsque rien ne correspond que IntentService bascule sur Gemini (gemini-2.5-flash-lite), avec une température à 0, un mode de réponse JSON, et un plafond de 150 tokens, en demandant une sortie structurée : l’intention, un mot-clé de recherche extrait, et une liste de termes sémantiquement proches pour élargir la recherche (« j’ai envie de porcodjo » → mot-clé « porcodjo restaurant » ; « mon habit est sale » → « pressing nettoyage vêtement »).Les deux chemins signalent leur origine — REGEX ou LLM — à un compteur Prometheus (simonIntentTotal), ce qui fait que la part du trafic détournée du modèle payant est quelque chose qu’on peut littéralement observer sur un dashboard, pas quelque chose qu’on devine. Et l’appel à Gemini lui-même est protégé par un circuit breaker : s’il se déclenche, le système ne plante pas la conversation, il dégrade vers une réponse neutre de type « bavardage » avec une confiance à 0, et continue.Décision n°3 : les adresses à Abidjan ne fonctionnent pas comme ailleursUne grande partie des adresses données dans le chat ne sont pas des chaînes géocodables — ce sont des descriptions : « je suis vers la cité Abdoulaye Diallo ». AddressCodingService prend ce type d’entrée, vérifie d’abord un cache sémantique (hash exact, puis similarité par embedding au-delà d’un seuil de 0,88) pour éviter de repayer un traitement déjà résolu, et en cas d’échec, demande à Gemini de la transformer en adresse standardisée, un quartier extrait, et — c’est le point important — un indicateur précisant si la description est assez précise pour être géocodée, ou si la seule réponse honnête est de demander à l’utilisateur d’envoyer un point GPS. Il existe même un dictionnaire dédié de termes nouchi (l’argot de rue abidjanais) qui alimente ce pipeline, parce qu’un outillage NLP générique ne sait pas ce que désigne un arrêt de « gbaka » ou le surnom d’un quartier.Décision n°4 : la recherche, c’est une seule fonction SQL, trois signaux, fusionnésC’est la partie du code dont je suis le plus fier. search_nearby_places est une unique fonction Postgres qui combine trois signaux de classement indépendants pour chaque commerce candidat dans un rayon donné : Le rang texte intégral (ts_rank_cd sur un tsquery en français) — utile quand l’utilisateur a tapé quelque chose de proche du nom ou de la catégorie réelle du commerce. Le rang de similarité vectorielle — distance cosinus entre l’embedding de la requête (Gemini text-embedding-004, pgvector avec un index HNSW) et l’embedding de chaque lieu — utile quand l’utilisateur décrit ce qu’il veut avec ses propres mots plutôt qu’en reprenant une étiquette. La proximité géographique (ST_Distance sur une colonne geography PostGIS) — parce que « le plus proche » compte toujours, et ne peut pas être compensé par la seule pertinence.Ces signaux sont combinés via une Reciprocal Rank Fusion pondérée : score = w_fts/(k + fts_rank) + w_vec/(k + vec_rank) + w_prox * proximité, avec des poids qui varient selon que l’utilisateur ait donné ou non un mot-clé — 45/45/10 entre texte, vecteur et proximité s’il y a un mot-clé à faire correspondre ; 85% piloté par le vecteur sinon, puisque la recherche texte intégral n’a rien à quoi s’accrocher dans une requête purement descriptive. Il existe aussi un mécanisme de « palier premium » : une seconde passe de classement, partitionnée par palier, garantit aux commerces payants/référencés un petit quota de places sans pour autant noyer la pertinence pour l’ensemble des résultats.Je suis revenu plus tard réécrire cette même fonction pour la performance, après avoir remarqué qu’elle effectuait du travail redondant : fusionner deux CTE qui relisaient deux fois les mêmes lignes, ajouter une LIMIT à l’intérieur du CTE de classement vectoriel spécifiquement pour que l’index HNSW puisse s’arrêter court plutôt que de trier tout le pool de candidats, et remplacer deux matérialisations complètes séparées (résultats généraux, résultats premium) par une seule passe fenêtrée utilisant ROW_NUMBER() OVER (PARTITION BY is_premium ...). C’est le genre de correction vraiment satisfaisant — même résultat, mesurablement moins de travail par requête — et c’est le genre de chose qui n’apparaît que lorsque l’usage réel met sous pression un premier jet.Décision n°5 : les effets de bord sont des données, pas des actionsQuand un lieu est enregistré, qu’une interaction doit être journalisée pour l’analytique, ou qu’un utilisateur doit recevoir une notification, le moteur n’exécute pas ce travail en ligne — il renvoie un simple objet SideEffect décrivant ce qui doit se produire. Un PgmqSideEffectDispatcher collecte ces effets, les regroupe par file cible, et les pousse via pgmq.send_batch (l’extension de file de messages propre à Postgres) dans la même transaction de base de données que celle qui marque un lieu nouvellement enregistré comme « en attente d’indexation ». Ce détail compte : cela signifie qu’un lieu ne peut pas se retrouver à moitié enregistré — visible dans l’application mais jamais réellement indexé pour la recherche — parce que la mise à jour du statut et l’écriture dans la file valident ensemble, ou aucune des deux. Des Supabase Edge Functions en bout de chaîne (un analytics-worker, un place-embedding-worker, un place-tagging-worker) vident ces files de manière asynchrone.La stack, sans détourFastify + TypeScript sur Node.js, Prisma au-dessus de Supabase Postgres (avec pgvector, pgmq, PostGIS, pg_cron et pg_net qui font un vrai travail, pas juste des lignes dans un fichier de dépendances), Redis pour le cache et l’état du rate limiting, Gemini à la fois pour le NLU et les embeddings, Geoapify pour le géocodage, et des métriques Prometheus intégrées dès le départ plutôt que rajoutées après coup.Où en est le projetLe package.json indique 0.4.0-rc.1, et je tiens à laisser ce contexte tel quel : c’est un système réel et fonctionnel qui me sert à raisonner sur la recherche hybride et l’ingénierie conversationnelle, en évolution active vers une version 1.0 — pas un produit fini, à grande échelle, avec un portefeuille de clients derrière. La conception indépendante du canal existe précisément pour que, si ce projet doit un jour dépasser le cadre d’un bot WhatsApp, le moteur sous-jacent n’ait pas besoin d’être reconstruit.Si vous voulez débattre des poids de la RRF, me dire que PostGIS était superflu, ou pointer une meilleure façon de structurer le dispatcher d’effets de bord, le code est public : github.com/sanayasfp/laplace-nearby.
L'illusion de l'IA : pourquoi alimenter un LLM sans gouvernance des données est un risque impossible à auditer
October 30, 2026 ·
19 min
Une juriste ouvre le nouvel assistant IA de l’entreprise, y dépose un contrat d’exclusivité et tape : « Résume les obligations clés en cinq lignes. » Dix secondes plus tard, elle obtient une synthèse claire et concise. C’est un gain de productivité concret, indéniable.Ce qu’elle ne voit pas, c’est ce qui s’est réellement passé pendant ces dix secondes. L’application ne s’est pas contentée d’envoyer sa question : elle a transmis le contrat dans son intégralité — la contrepartie, les grilles tarifaires, les clauses de résiliation, le nom des signataires. Tout cela a atterri, sous forme de texte lisible en clair, sur un serveur exploité par un tiers.Le même phénomène se reproduit lorsqu’un responsable RH demande le résumé d’un entretien d’évaluation, ou lorsqu’un analyste financier épluche le dossier de crédit d’un client. La question est courte. Le contexte qui l’accompagne est colossal.Dans presque tous les comités de direction, le mot d’ordre pour 2026 est identique : « Il nous faut de l’IA, et tout de suite. » En quelques semaines, des clés d’API sont générées, des modèles de langage sont connectés aux bases de données internes, et des flux de travail entiers sont délégués à des algorithmes tiers. Les démos sont impressionnantes et les gains de productivité bien réels.Cet article ne cherche pas à contester cet élan. Il soutient que les entreprises qui prendront la gouvernance des données au sérieux déploieront l’IA plus vite et sur un plus large périmètre, précisément parce que leurs fondations seront sûres. Celles qui font l’impasse dessus finiront par se heurter à une question toute simple à laquelle elles ne sauront pas répondre. Nous y reviendrons à la fin.1. Le problème : chaque requête emporte vos donnéesMême lorsque votre contrat avec le fournisseur d’IA lui interdit d’entraîner ses modèles sur vos données, et même lorsque la connexion est intégralement chiffrée en transit, le texte brut arrive toujours sous une forme parfaitement lisible chez ce tiers. Les contrats encadrent ce que le fournisseur a le droit d’en faire. Mais ils ne changent absolument rien à ce qui se retrouve exposé en cas de compromission chez lui, en cas d’erreur de configuration dans ses logs, ou si une réquisition judiciaire atterrit sur son bureau.Ce risque s’amplifie encore avec les architectures multi-agents, où un agent routeur délègue le travail à des sous-agents spécialisés partageant une mémoire commune. Un simple nom ou un chiffre d’affaires injecté dans cet état partagé devient immédiatement visible pour chaque agent de la chaîne, ainsi que pour chaque journal d’événements, chaque cache et chaque outil de traçabilité (tracing) dans lequel ils écrivent.Figure 1 : Injection de données brutes dans une mémoire partagée multi-agents.Envoyer des données métier brutes dans un LLM sans aucun contrôle n’est pas de l’innovation. C’est sous-traiter la mémoire stratégique de son entreprise à un tiers qui n’assumera jamais le coût de vos fuites.2. Pourquoi l’anonymisation naïve échoueCertaines équipes, conscientes du danger, placent un filtre rudimentaire en amont du modèle : des outils génériques de reconnaissance d’entités nommées (Named Entity Recognition ou NER), ou de simples expressions régulières qui masquent les noms propres. Sur le papier, cela semble raisonnable. En production, cette approche échoue généralement de trois manières — et son vrai danger est de procurer à la direction un sentiment trompeur de sécurité qui l’empêche de poser les bonnes questions.Échec n°1 : Le filtre ne comprend pas votre métierLes outils de détection génériques sont entraînés sur des textes courants. Ils confondent facilement les noms d’entreprises avec des termes du langage usuel (« Services », « Transit », « Holding »), et laissent régulièrement passer des raisons sociales contenant ce type de mot, comme « NET-POLY SERVICES ». Quant aux identifiants internes (numéros fiscaux, identifiants du registre de commerce, codes clients internes), ils ne correspondent pas aux formats standards internationaux et passent totalement inaperçus.Échec n°2 : Masquer le nom ne revient pas à masquer l’entrepriseConsidérez cet exemple : Texte brut : « L’entreprise X a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. » Après un filtre naïf : « ENTREPRISE_1 a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. »Le nom a disparu, mais le chiffre d’affaires exact et la ville suffisent amplement à identifier l’entreprise en une seule recherche. Un modèle moderne, fort de son immense base de connaissances d’entraînement, peut souvent faire le lien en une fraction de seconde.Échec n°3 : Un prompt est une prière, pas un contrôleUne consigne comme « Ne répète jamais les noms des clients » n’est qu’une incitation probabiliste (probabilistic nudge). Le modèle s’y conformera le plus souvent… jusqu’au jour où il ne le fera pas. Le problème est encore plus pernicieux : selon le fournisseur et la configuration, certains modèles renvoient leur raisonnement intermédiaire dans un canal distinct de la réponse finale — une sorte de bloc-notes (scratchpad ou canal de pensée) qui vous est retourné. Si votre mécanisme de protection ne filtre que la réponse finale, les données brutes peuvent transiter par ce bloc-notes en toute liberté.Croire qu’un prompt système enjoignant d’« être discret » protège vos données revient à verrouiller une porte blindée en laissant la clé sur la serrure extérieure.3. Les six invariants d’une gouvernance robusteSi les consignes et les filtres basiques ne suffisent pas, que faut-il mettre en place ? La plupart des architectes esquissent un pipeline linéaire : les données arrivent, sont filtrées, partent vers le modèle, puis reviennent. Un système robuste obéit à une tout autre logique : c’est une défense en profondeur, où chaque frontière porte sa propre protection, indépendamment des autres.Voici six invariants fondamentaux. Ce sont des contrats d’architecture que tout concepteur de système peut vérifier au sein de sa propre infrastructure, quels que soient la stack technique, le fournisseur de modèles ou la taille de l’équipe.Figure 2 : Les six invariants de gouvernance des données appliqués aux flux LLM.Invariant 1 : Minimiser dès la sourceCe qui ne figure pas dans les données transmises ne peut pas fuiter. Les outils exposés aux agents IA ne doivent jamais être les mêmes que ceux mis à disposition des développeurs humains. La pagination y est strictement imposée, les volumes sont plafonnés et les champs superflus sont intégralement supprimés. L’API ne renvoie pas un dossier client complet pour calculer un simple ratio : elle ne retourne que les trois nombres strictement nécessaires — sans coordonnées, sans adresses, sans noms.Ce n’est pas une règle propre à l’IA : c’est un principe fondamental d’exposition des données que l’IA rend simplement impératif.Invariant 2 : Ne jamais laisser un modèle trancher ce qu’un chemin déterministe peut déciderUn classement, la consultation d’un ratio ou une simple extraction d’information n’exigent aucun discernement : ils n’ont donc aucune raison de transiter par votre modèle le plus onéreux et le moins prévisible. L’aiguillage peut reposer sur une règle métier, un choix dans l’interface ou un classifieur très léger : le principe n’impose pas la méthode d’implémentation.Deux remarques franches s’imposent. Premièrement, si le routeur est lui-même un modèle, il reste un modèle : choisissez-le miniature, strictement bridé, et ne lui fournissez que du texte anonymisé. Le but est de réduire la surface d’exposition, non de prétendre qu’elle a disparu. Deuxièmement, cet invariant présente un avantage qui convainc souvent un comité de direction bien avant les arguments sécuritaires : il est beaucoup plus économique et infiniment plus rapide.Une anecdote révélatrice nous est fournie par Anthropic elle-même. En mars 2026, le code source de Claude Code a fuité accidentellement via une source map publiée par inadvertance sur npm. On y a découvert que l’outil détecte la frustration de l’utilisateur — c’est-à-dire si quelqu’un l’insulte — au moyen d’une simple expression régulière, sans faire appel au moindre modèle1. Une regex est instantanée et gratuite. Un appel d’inférence n’est ni l’un ni l’autre, pour une question qui ne réclame aucun jugement.Invariant 3 : Traiter chaque frontière vers le modèle comme un point de protection obligatoire et réversibleToute donnée franchissant la frontière entre votre système et un modèle de langage, qu’elle soit entrante ou sortante, doit être protégée par un mécanisme réversible tel que la substitution, la pseudonymisation ou la tokenisation. L’erreur courante consiste à ne protéger que le champ de réponse évident. Inspectez chaque canal exposé par le fournisseur, y compris le canal de raisonnement décrit à la section 2.Une mise en garde juridique : des données pseudonymisées peuvent toujours être qualifiées de données à caractère personnel au sens de réglementations comme le RGPD. Cet invariant réduit votre exposition au risque opérationnel ; il ne vous exonère pas de vos obligations légales.Invariant 4 : Réhydrater au tout dernier moment légitimeL’opération inverse de substitution ne doit être appliquée qu’à l’ultime étape, juste avant que la réponse ne soit présentée à un utilisateur humain habilité à consulter les valeurs réelles. Le modèle travaille à l’aveugle du début à la fin. Seul l’humain reçoit la réponse en clair, et exclusivement en bout de chaîne.Invariant 5 : Un garde-fou déterministe a le dernier mot sur la plausibilitéLes quatre premiers invariants protègent la confidentialité. Celui-ci protège l’exactitude des résultats. Toute réponse générée doit franchir un contrôle non probabiliste — tel qu’un seuil limite ou une comparaison par rapport à un référentiel connu — avant d’être transmise. Si le résultat semble aberrant, le système bascule automatiquement sur un chemin surveillé plus lent plutôt que de laisser passer une réponse non vérifiée.Invariant 6 : Chaque saut d’agent à agent constitue sa propre frontière de confianceC’est l’invariant le plus souvent négligé, car il n’apparaît pas sur les diagrammes en couches statiques. La sortie d’un agent n’est pas automatiquement sécurisée pour l’agent qui l’a sollicité.Voici le cas typique : un sous-agent, pensant bien faire en « nettoyant sa propre sortie » avant de répondre, désanonymise des données qui devaient impérativement rester masquées pour l’agent parent ayant délégué la tâche. Le sous-agent s’est conformé à ses consignes à la lettre. Il ignorait simplement que sa réponse était destinée à un autre maillon de la chaîne, censé poursuivre son raisonnement sur des pseudonymes.Il existe un corollaire aggravant : lorsque l’historique conversationnel traverse plusieurs agents, les échanges de différents sous-agents se retrouvent mélangés dans un contexte unique. Sans un étiquetage explicite de la provenance (provenance tagging), il devient impossible de déterminer quelle frontière de confiance s’applique à quel fragment de texte.Figure 3 : Frontières de confiance lors de délégations inter-agents et déplacement du point de réhydratation.4. Pour les ingénieurs : comment nous implémentons les invariantsIl s’agit ici d’une implémentation concrète parmi d’autres. L’outillage est spécifique à notre écosystème technique. Même si votre architecture diffère sensiblement de la nôtre, les invariants restent universels : seule leur incarnation technique change. Les lecteurs non techniques peuvent passer directement à la section 5. Passerelle d’anonymisation (invariant 3) : Avant toute classification, le texte libre saisi par l’utilisateur franchit une passerelle stricte combinant des bibliothèques de TALN/NLP et des règles de substitution déterministes. Les noms et identifiants sont remplacés par des jetons uniques (par exemple CORP_A83B). La table de correspondance est immédiatement sécurisée dans un coffre-fort chiffré (encrypted vault) : calcul HMAC-SHA256 pour l’identifiant du jeton et chiffrement Fernet (AES-128-CBC avec HMAC) pour la valeur réelle au repos. Routeur (invariant 2) : La requête pseudonymisée est soumise à un LLM délibérément compact, véloce et économique, dont l’unique mission est de classifier l’intention. Un classement, une lecture de ratio ou une extraction basique emprunte le chemin rapide (fast path) : un outil issu d’une liste blanche restreinte (allowlist) est exécuté directement, sans jamais solliciter le modèle lourd. Une analyse de risque ou une tâche d’interprétation complexe emprunte le chemin lent (slow path) et bascule vers l’agent principal. Outils minimisés (invariant 1) : Sur les deux chemins, l’accès aux données internes s’effectue au moyen d’outils exposés via le protocole MCP (Model Context Protocol), épurés à la source et strictement dissociés des outils réservés aux développeurs humains. Chemin lent (invariant 3, sortant) : Les résultats renvoyés par les outils sont systématiquement pseudonymisés avant d’être injectés dans le contexte du modèle lourd, de sorte que ce dernier raisonne et rédige exclusivement à partir de pseudonymes. Chemin rapide (invariant 5) : Un garde-fou de plausibilité — par exemple un ratio comparé à une valeur de référence — bloque toute réponse automatique aberrante et force un repli vers le chemin lent pour examen approfondi. Réhydratation (invariant 4) : Ce n’est qu’une fois la réponse finale générée et mise en forme que le système interroge le coffre-fort pour substituer les jetons par les valeurs réelles, immédiatement avant l’affichage à l’utilisateur dûment habilité.Figure 4 : Architecture de routage fast-path / slow-path. Résultats observés : (Insérer ici vos métriques clés de production — par exemple : 72 % des requêtes traitées par le chemin rapide sans appel au modèle lourd, division par 4 de la latence médiane et réduction de 65 % des coûts d’API).5. Vérifier plutôt que présumerUne défense qui n’a jamais été éprouvée dans des conditions réelles est une défense dont on espère simplement l’existence.Voici un scénario bien loin d’être théorique : un contrôle destiné à transformer chaque message avant qu’il n’atteigne le modèle a été rédigé, validé en revue de code et intégré à la branche principale. Les tests passaient avec succès. Pendant des semaines, l’équipe a cru la protection active. Elle ne l’était pas. Une hypothèse erronée sur la structure exacte de la charge utile entrante (payload shape) faisait que le filtre s’exécutait sur une mauvaise clé d’objet, de manière silencieuse, sans générer d’erreur et sans déclencher la moindre alerte.Un test qui passe au vert sans solliciter réellement le code de protection ne prouve rien. Une revue de code n’est pas une preuve d’exécution, et un schéma d’architecture soigné n’est pas la preuve que la trajectoire tracée est celle empruntée lors de l’exécution. La seule preuve tangible repose sur un test rejoué contre le système réel : le vrai modèle, les vraies données, la véritable chaîne d’appels.Chaque affirmation de conformité — qu’il s’agisse de « ces données sont anonymisées » ou « ces données ne sortent jamais en clair de notre réseau » — doit être assortie d’une preuve rejouable, consultable à la demande. Sans cela, il ne s’agit pas d’une garantie : c’est une hypothèse qui n’a pas encore été réfutée.Figure 5 : Défaillance silencieuse d’un contrôle liée à une inadéquation de format des données entrantes.6. La piste d’audit (Audit Trail)La gouvernance ne s’arrête pas à la protection en temps réel : elle exige une traçabilité sans faille. Lorsqu’un régulateur bancaire, un auditeur RGPD ou un grand client examine la manière dont l’IA est exploitée dans votre entreprise, il ne se contentera pas de simples déclarations d’intention. Il exigera des preuves irréfutables.Un système d’IA d’entreprise responsable conserve un journal d’audit infalsifiable (tamper-evident) et strictement en ajout seul (append-only), isolé des logs applicatifs ordinaires et de l’historique des conversations. Pour chaque événement du cycle de vie d’un agent, il consigne une paire indissociable : Les données brutes telles qu’elles ont été initialement reçues, Les données réellement transmises aux modèles externes.La règle la plus cruciale est la suivante : aucun paramètre de configuration (feature flag) ni aucun administrateur isolé ne doit pouvoir désactiver ce journal. Lors de pics de charge imprévus, le premier réflexe d’une équipe technique est souvent de désactiver les journaux « pour préserver les performances ». Si un tel commutateur existe, le jour où une fuite survient, vous serez incapable d’en déterminer l’étendue ou d’identifier ce qui a été exposé.Figure 6 : Pourquoi le journal d’audit doit être immunisé contre toute désactivation par configuration.Il y a toutefois un revers, et il doit être énoncé sans détour : ce journal d’audit devient lui-même l’un des jeux de données les plus sensibles de l’organisation, puisqu’il contient précisément les données brutes que vous avez pris tant de peine à dissimuler. Traitez-le en conséquence : chiffrez-le, cloisonnez et restreignez ses accès, fixez des limites de conservation rigoureuses, et définissez avec vos juristes la manière dont il s’articule avec les demandes de suppression (droit à l’effacement prévu par le RGPD).Complétez enfin ce dispositif par une détection rétroactive des fuites (retroactive leak scanning) : des processus d’arrière-plan analysent en continu l’historique d’audit archivé à la recherche de schémas sensibles ayant pu franchir les premiers filtres. Lorsqu’une anomalie est décelée, le scanner ne modifie en aucun cas le journal initial : il déclenche une alerte auprès de l’équipe de sécurité, qui peut ainsi durcir les règles de filtrage avant qu’une simple brèche ne devienne un incident majeur.Figure 7 : Le pipeline d’audit, de l’enregistrement immuable à l’analyse asynchrone et aux alertes.7. Conclusion : L’IA est d’abord un défi de Data EngineeringL’adage bien connu « garbage in, garbage out » a désormais un pendant moderne : « secret in, exposure out » (données secrètes en entrée, fuites en sortie). L’IA n’est pas un vernis cosmétique que l’on vient greffer à la hâte dans un recoin du système d’information pour contenter la direction générale. Elle bouleverse la façon dont l’information circule, se segmente, s’anonymise et s’archive.Les entreprises qui sortiront gagnantes de la révolution de l’IA ne seront pas celles qui auront déployé le plus d’agents dans l’urgence. Ce seront celles qui auront d’abord bâti des fondations hermétiques et fiables. C’est la qualité des freins qui permet à une voiture de rouler vite. Une fois ces contrôles fermement en place, les équipes peuvent donner leur feu vert à un plus grand nombre de cas d’usage, avec moins de débats interminables et sans vetos de dernière minute.Alors, avant de signer votre prochain contrat de licence d’IA ou de célébrer le déploiement de votre nouvel agent autonome, rassemblez vos architectes et posez-leur une seule question : « Si un auditeur externe mandaté par notre plus grand client débarquait demain matin à 8h, seriez-vous capables de me prouver, ligne de code par ligne de code, qu’aucun nom, aucun identifiant et aucun chiffre confidentiel n’a quitté notre réseau en clair au cours du mois écoulé ? »Si la réponse est un silence gêné, un renvoi aux « clauses contractuelles du fournisseur » ou la mention d’« un prompt qui demande à l’IA d’être prudente », vous connaissez déjà votre prochain projet.La checklist de gouvernance : 8 cases à cocherAvant de quitter la salle, voici les huit critères non négociables à valider (les six invariants, complétés par les impératifs des sections 5 et 6) : Minimisez-vous dès la source ? (Invariant 1) Préservez-vous le modèle lourd et coûteux des requêtes simples ? (Invariant 2) Chaque frontière texte-vers-modèle fait-elle l’objet d’une protection réversible ? (Invariant 3) Ne réhydratez-vous qu’au tout dernier moment légitime ? (Invariant 4) Un garde-fou déterministe a-t-il le dernier mot sur la plausibilité ? (Invariant 5) Chaque saut d’agent à agent constitue-t-il sa propre frontière de confiance ? (Invariant 6) Chaque contrôle est-il vérifié en conditions réelles, et pas seulement supposé fonctionnel ? (Section 5) Le journal d’audit est-il impossible à désactiver par configuration ou par un administrateur isolé ? (Section 6) Alex Kim, « The Claude Code Source Leak: fake tools, frustration regexes, undercover mode, and more », 31 mars 2026. La fuite provenait d’un fichier source map publié par mégarde sur npm. Le fichier userPromptKeywords.ts contient la regex employée pour la détection de la frustration de l’utilisateur. ↩
Le Guide Ultime pour un Environnement de Dev Fluide
July 31, 2026 ·
11 min
J’ai écrit au sujet de Stack il y a quelques jours — pour vous donner la version courte de pourquoi je l’ai créé et de toute la philosophie “sans Docker”. Mais la théorie a ses limites. Si vous cherchez le guide ultime pour obtenir un environnement de développement parfaitement fluide, vous êtes au bon endroit.Pas de pitch commercial. Pas de blabla marketing. Juste la réalité brute et pratique de la mise en route d’un projet sans effort, avec chaque invite de commande et chaque sortie console un peu bizarre que vous rencontrerez en cours de route.Si vous voulez juste la documentation de référence brute, vous la trouverez sur sanayavo.com/stack. Sinon, bienvenue dans la visite guidée d’un meilleur flux de travail (workflow).Ce que vous obtenez vraimentUn environnement fluide signifie zéro superflu. Stack n’est qu’un seul binaire Rust. Il n’y a pas de démon qui monopolise la RAM dans votre barre des tâches, pas de Docker Desktop qui fait tourner les ventilateurs de votre ordinateur portable à fond, et absolument rien qui ne touche à votre installation PHP globale du système.Au lieu de cela, il agit comme du ruban adhésif (duct tape) hautement intelligent. Il s’appuie sur vfox pour gérer les versions de PHP/Node, uv pour Python, et Caddy pour des domaines locaux instantanés. Stack lit un simple fichier stack.toml à la racine de votre projet et connecte automatiquement ces trois outils ensemble.Note : Stack est uniquement disponible sur Windows pour le moment (PowerShell ou cmd). Le support de Mac et Linux est prévu sur la feuille de route.L’installation : Votre seul et unique mal de crâneUn flux de travail quotidien fluide nécessite un tout petit peu de configuration au départ. Exécutez ceci pour télécharger l’installateur :irm https://github.com/sanayasfp/stack/releases/latest/download/stackenv-installer.ps1 | iexEnsuite, exécutez cette commande une seule fois par machine :stack setupCela s’occupe du gros du travail en arrière-plan. Cela ajoute un hook (crochet) à votre profil PowerShell (pour que la bonne version de PHP ou Node s’active par magie lorsque vous faites un cd dans un répertoire), installe vfox, uv et Caddy dans des versions largement testées, et exécute caddy trust. Cette dernière étape est cruciale pour le HTTPS local — on y revient dans une minute.$ stack setupadded the stack hook for pwshchecking vfox/uv/caddy... vfox: OK (1.0.11) uv: OK (0.11.7) caddy: OK (2.11.4) caddy: local CA trusted (https://*.localhost works with no browser warning)Redémarrez votre terminal maintenant. Si vous sautez cette étape, le hook PowerShell ne sera pas actif et vous vous demanderez pourquoi rien ne fonctionne. Vous n’aurez plus jamais à exécuter stack setup à moins d’acheter un nouvel ordinateur.Démarrer un projet sans frictionConstruisons quelque chose de concret pour vous montrer le fonctionnement : une API PHP légère adossée à MySQL.À partir de zéro$ stack new acme-apidomain: acme-api.localhostlanguages (space to toggle, enter to confirm): [x] phpservices (space to toggle, enter to confirm): [x] mysql php version: 8.3.1 mysql version: 8.0.35created acme-api\stack.tomlnext: cd into it, add a [run] command when you know it, then `stack up`C’est une liste de contrôle interactive dans le terminal. Espace pour basculer (cocher/décocher), Entrée pour confirmer. Ça fait un peu bizarre la première fois, mais rien n’est validé tant que vous n’appuyez pas sur Entrée. Si vous sélectionnez accidentellement Node au lieu de PHP, appuyez à nouveau sur espace. Zéro stress, pas besoin de tout recommencer.Si vous avez déjà un projetSi votre dossier acme-api possède déjà un fichier composer.json spécifiant "php": "^8.3", Stack est assez intelligent pour vous éviter de taper manuellement :$ stack initdetected from existing project files: php 8.3 (from composer.json)domain: acme-api.localhoststack init analyse automatiquement composer.json, package.json ou pyproject.toml et pré-remplit l’assistant pour vous.L’artefact magiqueStack génère un manifeste très lisible :[project]name = "acme-api"domain = "acme-api.localhost"[language]php = "8.3.1"[service.mysql]version = "8.0.35"Commitez ce fichier stack.toml dans votre dépôt (repo). C’est votre artefact ultime du type “ça marche sur ma machine” — l’équivalent léger et sans conteneur d’un Dockerfile. Lorsqu’un collègue clone le dépôt et tape stack up, il obtient instantanément les mêmes versions exactes de PHP et MySQL, sans avoir à installer quoi que ce soit à la main.Lui apprendre comment s’exécuterVotre stack.toml doit savoir comment démarrer votre serveur de développement. Ouvrez le fichier et ajoutez un bloc [run] :[run]command = "php -S 127.0.0.1:{port} -t public"L’espace réservé {port} est dynamiquement remplacé par un port disponible au moment de l’exécution. (Note : Si vous déclarez [language.php] mais ignorez la section [run], Stack se rabat sur sa propre configuration FastCGI intégrée utilisant php-cgi.exe et Caddy, ce qui est incroyablement solide. Mais pour ce guide, nous allons rester explicites.)stack up et regardez la magie opérer$ cd acme-api$ stack upLoaded C:\Users\you\acme-api\stack.toml project: acme-api domain: acme-api.localhost languages: php services: mysqlfirst run for this project — stack.toml will execute: [run] php -S 127.0.0.1:{port} -t publicTrust and run these commands? [y/N] y php: C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe -> PHP 8.3.1 (cli) service.mysql: started (pid 41232, port 3306) schema 'acme_api' — automatic creation not yet implemented; create it manually if needed run: php -S 127.0.0.1:52140 -t public (pid 41244, port 52140) log: C:\Users\you\.stack\logs\acme-api.log routed: http://acme-api.localhost -> 127.0.0.1:52140Il y a deux choses majeures qui se passent ici et qui contribuent à une expérience sans accroc :1. L’invite de confiance de sécuritéParce que [run].command est une invocation littérale du shell, Stack l’exécute exactement tel qu’il est écrit. La première fois que Stack voit une commande nouvelle ou modifiée (comme après un git pull), il vous demande de la confirmer. Dites oui, et il mémorise votre choix dans ~/.stack/trust.json. Chaque stack up suivant sera heureusement silencieux. Besoin de contourner cela pour la CI ? Passez simplement le flag --yes.2. Le HTTPS fonctionne immédiatementLa console affiche http://, mais https://acme-api.localhost fonctionne instantanément. Aucun avertissement rouge du navigateur. Vous vous souvenez de cette commande caddy trust lors de la phase de configuration ? Elle installe une autorité de certification locale (similaire à mkcert), ce qui signifie que votre environnement local reflète fidèlement le SSL de production dès le premier jour.Ce que “sans conteneurs” signifie vraiment en pratiqueOuvrez un deuxième terminal, faites un cd dans un projet totalement différent qui nécessite également MySQL 8.0.35, et exécutez stack up : service.mysql: already running, shared with other projects (pid 41232, port 3306)C’est toute la beauté de la chose. Un seul processus MySQL sert deux projets, isolés par schéma plutôt que de brûler votre processeur et votre RAM avec des conteneurs redondants.Mais c’est ici que le facteur “fluide” brille vraiment. Ouvrez un troisième terminal, faites un cd dans acme-api, et exécutez simplement php -v. Pas de stack up, pas de commandes supplémentaires :$ cd acme-api$ php -vPHP 8.3.1 (cli) (built: ...)$ cd ..$ php -v'php' is not recognized as an internal or external commandLe changement de version se fait de manière ambiante à chaque invite de commande grâce à ce hook PowerShell. Votre IDE, composer install et vos exécuteurs de tests obtiennent tous automatiquement la bonne version épinglée à la seconde où ils entrent dans le dossier. Quittez le répertoire, et elle disparaît. Pas de source venv/bin/activate, pas de nvm use, et pas de pollution de votre système global.Des installations PHP toutes neuves qui ne craignent pasHabituellement, une nouvelle installation PHP signifie passer une après-midi à traquer les erreurs “PDO driver not found” ou “timezone not set”. Stack élimine complètement cela.La première fois qu’il télécharge une version de PHP via vfox, il patche automatiquement php.ini. Il active OPcache, augmente les limites de mémoire et de téléchargement (upload), définit un fuseau horaire, et active les extensions dont vous avez réellement besoin par défaut (pdo_mysql, pdo_pgsql, pdo_sqlite, sockets, sodium, etc.). Cela se fait une fois, automatiquement, et vous n’avez plus jamais à y penser.stack doctor — L’ultime vérification avant le volAvant de transmettre un projet à un collègue ou de commencer à déboguer un problème étrange, exécutez ceci :$ stack doctor --projectchecking C:\Users\you\acme-api\stack.toml... language.php: OK (C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe) service.mysql: OK (managed)Cette commande valide vos ports, chemins et valeurs {PLACEHOLDER} par rapport à votre environnement (en chargeant d’abord .env) sans démarrer un seul service. Si quelque chose est cassé, vous obtenez une liste claire dès le départ au lieu de découvrir des erreurs cryptiques au beau milieu du démarrage.Pilotez votre configuration de n’importe oùUne fois que Stack connaît un projet, vous n’avez même pas besoin d’être dans son dossier pour le gérer :stack describe acme-apistack restart acme-apistack down acme-apistack describe affiche tout — les chemins binaires résolus, l’emplacement exact de votre php.ini haché, les logs, et les domaines routés. Stack conserve un registre local de vos projets, ce qui rend la gestion globale sans effort.Tout fermer proprementQuand la journée est terminée :stack down --allTout s’arrête. Chaque projet, chaque service partagé, et Caddy. Vous obtenez une utilisation CPU vraiment à zéro, et non un conteneur fantôme persistant que vous avez oublié de tuer.Une petite note sur les domaines personnalisésL’utilisation de .localhost garantit une expérience fluide car les navigateurs le résolvent automatiquement vers l’adresse de bouclage (RFC 6761) sans que vous ayez besoin de bidouiller votre fichier hosts. Cependant, si vous migrez depuis Laragon ou Herd et que vous avez absolument besoin de domaines .test, il y a une configuration unique à faire en utilisant Acrylic DNS Proxy.Votre Nouveau Flux de TravailGénérez (Scaffold), exécutez, faites confiance une fois, profitez du HTTPS automatique, commitez votre manifeste, et détruisez tout proprement quand vous avez terminé.J’utilise ce flux de travail exact pour du vrai travail client chaque jour. Ce n’est pas un scénario idéal théorique ; c’est un plan d’action éprouvé pour un environnement de développement ultime et fluide.Pour la référence complète de stack.toml, les options de ligne de commande (CLI), et les fonctionnalités avancées (comme l’utilisation de [[clone]] pour démarrer à partir d’un simple manifeste), consultez la documentation officielle : Pourquoi j’ai créé cela — la longue diatribe contre Docker/XAMPP pour le dev local Pour commencer Référence du Manifeste Référence CLI Code : github.com/sanayasfp/stack
Stack : Le gestionnaire d'environnement de dev natif et sans conteneur
July 28, 2026 ·
4 min
Quand on maintient une application legacy tout en construisant sa remplaçante moderne, l’environnement de développement local devient vite un champ de bataille. Au travail, nous avions une ancienne application tournant sur une vieille version de PHP, Vue 2 (nécessitant une vieille version de Node), et Python 3.7. En parallèle, nous démarrions la nouvelle architecture avec PHP 8.4, Vue 3, et Python 3.12.Personnellement, je gérais ça avec une combinaison sur mesure de Laravel Herd pour PHP, uv pour Python, et nvm pour Node. Ça marchait pour moi parce que j’avais pris le temps de tout configurer. Mais pour nos développeurs juniors et nos nouveaux stagiaires, leur demander de jongler avec ces outils juste pour faire tourner les deux projets en même temps était un cauchemar absolu. Ils passaient leur temps à se battre avec des conflits de chemins et des variables d’environnement au lieu d’écrire du code. C’est pour ça que j’ai créé Stack : un gestionnaire d’environnement multi-projets natif, sans conteneur, qui rend le changement de contexte aussi simple que de taper stack up.Le code est ici : github.com/sanayasfp/stack.La vraie contrainte : l’intégration et la charge cognitiveAvant même de toucher à l’architecture, la vraie contrainte sur un projet comme ça, c’est l’expérience développeur. Les stagiaires ne devraient pas avoir à apprendre le fonctionnement des réseaux bridge de Docker ni à configurer cinq gestionnaires de versions différents pour corriger un bug sur l’API legacy. L’objectif était simple : ils doivent pouvoir cloner le dépôt, lancer une seule commande, et obtenir un domaine local fonctionnel routé vers les bons processus, que le projet utilise PHP 5.6 ou 8.4.Pourquoi ne pas juste utiliser Docker ?Les conteneurs résolvent le problème “ça marche sur ma machine” en embarquant toute une couche OS par projet. Mais cela vient avec le poids de la virtualisation et des démons inactifs qui consomment de la RAM.Stack résout le même problème différemment. Il épingle les versions exactes par projet via les gestionnaires de version que vous installeriez de toute façon (en déléguant à des outils comme vfox et uv), partage un binaire unique téléchargé pour chaque projet nécessitant cette version, et fait tourner le tout sous forme de simples processus enfants. Pas d’hyperviseur, pas de Docker Desktop qui mange 4 Go de RAM pour une API toute simple. Chaque projet qui épingle le même service et la même version partage une instance unique en cours d’exécution, isolée par schéma.Sous le capot : la simplicité avant la magieUn fichier stack.toml dans votre projet définit exactement ce dont il a besoin :[project]name = "acme-api"[language]php = "8.4.0"[service.mysql]version = "8.0.35"[run]command = "php -S 127.0.0.1:{port} -t public"Quand vous tapez stack up, l’orchestrateur lit le manifeste, configure le domaine local (par ex. acme-api.localhost), démarre MySQL s’il ne tourne pas déjà, lance le serveur de dev PHP, et fait le proxy inverse du trafic. Tout est natif, et incroyablement rapide. Il n’essaie pas de réinventer la gestion des paquets ; il délègue le gros du travail de téléchargement des langages à vfox et uv, et le routage à Caddy.Où en est le projet actuellementJe tiens à être précis là-dessus : j’ai commencé à coder Stack en Rust la semaine dernière seulement, et j’ai poussé la première version il y a deux jours. J’ai complètement désinstallé Herd, Laragon et mes précédents scripts maison. J’utilise désormais exclusivement Stack pour mon travail quotidien — ce qui me permet de prouver que ça marche, de trouver des bugs dans les cas limites, et d’ajouter de nouvelles fonctionnalités pour l’améliorer.Il supporte actuellement Windows (PowerShell & cmd), ce qui était le besoin immédiat, avec macOS et Linux prévus sur la feuille de route. Ce n’est plus juste une expérience de week-end ; c’est en train de devenir l’outil fondamental qui garde notre équipe saine d’esprit côté dev local.Vous pouvez récupérer l’installateur sur le dépôt ou lire la documentation officielle : Dépôt : github.com/sanayasfp/stack Docs : sanayavo.com/stack/
Laplace Nearby : un bot WhatsApp qui trouve « le maquis le plus proche » sans exploser la facture LLM
June 22, 2026 ·
9 min
À Abidjan, une grande partie des commerces de proximité — le maquis du quartier, la pharmacie, le réparateur de téléphones — n’ont ni site web, ni fiche Google Business, ni même une adresse postale formelle la moitié du temps. Ce qu’ils ont, en revanche, c’est WhatsApp. Laplace Nearby part de ce constat : plutôt que de construire une application que les gens doivent télécharger, autant construire l’expérience de recherche dans l’application que tout le monde a déjà ouverte, et laisser les gens décrire ce qu’ils cherchent en langage naturel — « il me faut une pharmacie », « j’ai envie de porcodjo » — plutôt que de remplir un formulaire de filtres.L’assistant lui-même s’appelle en interne Simon. Ce billet parle de la façon dont Simon est réellement construit — les parties que je trouve intéressantes à raconter, pas un argumentaire commercial.Le dépôt : github.com/sanayasfp/laplace-nearby.Décision n°1 : le moteur ignore qu’il parle à WhatsAppLe cœur du système est SimonEngine, un petit orchestrateur qui prend en entrée une InteractionRequest indépendante du canal et renvoie une InteractionResponse tout aussi indépendante. Les spécificités WhatsApp — format des webhooks, mise en forme des messages, boutons et listes — vivent entièrement dans une couche d’adaptateurs/renderers en dehors du moteur. Le moteur lui-même fait quatre choses à chaque message : charger le contexte de session de l’utilisateur, vérifier le rate limiting, transmettre le message au flow conversationnel actif (idle, recherche, ou enregistrement), puis sauvegarder le résultat — en émettant systématiquement un effet de bord analytique avec le nom du flow, la transition d’état et la latence, qu’il se soit passé quelque chose d’intéressant ou non :async process(req: InteractionRequest): Promise<InteractionResponse> { const context = await this.contextManager.load(req.profileId); const flow = this.flowRegistry.get(context.session.activeFlow || IDLE); const limitResult = this.rateLimitService.check(context.rateLimit); if (!limitResult.allowed) { return { messages: limitResult.notify ? [Responses.tooManyMessages(...)] : [] , ... }; } const response = await flow.handle(req, context, interactionId); await this.contextManager.save(req.profileId, response.contextUpdate); return { ...response, sideEffects: [summaryEffect, ...response.sideEffects] };}Ce n’est pas de l’architecture pour le plaisir. Cela signifie que la logique de recherche, le flow d’enregistrement, le rate limiting et les métriques n’auront pas besoin d’être réécrits si un second canal (SMS, un widget web, peu importe) arrive un jour — ils n’ont jamais été couplés à WhatsApp en premier lieu.Décision n°2 : ne pas appeler le LLM si on peut s’en passerChaque message entrant a besoin d’une intention — s’agit-il d’une recherche, d’une demande d’enregistrement, de bavardage, d’un remerciement, d’une insulte, d’un « stop » ? Faire passer chaque message par un appel LLM est la solution de facilité, et aussi la plus lente et la plus coûteuse. La classification d’intention est donc à deux étages.FastIntentDetector s’exécute en premier : un ensemble volontairement exhaustif d’expressions régulières couvrant le français, l’anglais, et l’argot local qu’on croise réellement dans une conversation WhatsApp à Abidjan — « wesh », « cc », « gab » (guichet automatique), « essence/gazoil », des dizaines de variantes orthographiques de « merci », « stop », « annule ». Sa docstring dit exactement à quoi il sert : « Reduce LLM costs and latency for unambiguous user requests » (réduire les coûts et la latence liés au LLM pour les requêtes non ambiguës). Ce n’est que lorsque rien ne correspond que IntentService bascule sur Gemini (gemini-2.5-flash-lite), avec une température à 0, un mode de réponse JSON, et un plafond de 150 tokens, en demandant une sortie structurée : l’intention, un mot-clé de recherche extrait, et une liste de termes sémantiquement proches pour élargir la recherche (« j’ai envie de porcodjo » → mot-clé « porcodjo restaurant » ; « mon habit est sale » → « pressing nettoyage vêtement »).Les deux chemins signalent leur origine — REGEX ou LLM — à un compteur Prometheus (simonIntentTotal), ce qui fait que la part du trafic détournée du modèle payant est quelque chose qu’on peut littéralement observer sur un dashboard, pas quelque chose qu’on devine. Et l’appel à Gemini lui-même est protégé par un circuit breaker : s’il se déclenche, le système ne plante pas la conversation, il dégrade vers une réponse neutre de type « bavardage » avec une confiance à 0, et continue.Décision n°3 : les adresses à Abidjan ne fonctionnent pas comme ailleursUne grande partie des adresses données dans le chat ne sont pas des chaînes géocodables — ce sont des descriptions : « je suis vers la cité Abdoulaye Diallo ». AddressCodingService prend ce type d’entrée, vérifie d’abord un cache sémantique (hash exact, puis similarité par embedding au-delà d’un seuil de 0,88) pour éviter de repayer un traitement déjà résolu, et en cas d’échec, demande à Gemini de la transformer en adresse standardisée, un quartier extrait, et — c’est le point important — un indicateur précisant si la description est assez précise pour être géocodée, ou si la seule réponse honnête est de demander à l’utilisateur d’envoyer un point GPS. Il existe même un dictionnaire dédié de termes nouchi (l’argot de rue abidjanais) qui alimente ce pipeline, parce qu’un outillage NLP générique ne sait pas ce que désigne un arrêt de « gbaka » ou le surnom d’un quartier.Décision n°4 : la recherche, c’est une seule fonction SQL, trois signaux, fusionnésC’est la partie du code dont je suis le plus fier. search_nearby_places est une unique fonction Postgres qui combine trois signaux de classement indépendants pour chaque commerce candidat dans un rayon donné : Le rang texte intégral (ts_rank_cd sur un tsquery en français) — utile quand l’utilisateur a tapé quelque chose de proche du nom ou de la catégorie réelle du commerce. Le rang de similarité vectorielle — distance cosinus entre l’embedding de la requête (Gemini text-embedding-004, pgvector avec un index HNSW) et l’embedding de chaque lieu — utile quand l’utilisateur décrit ce qu’il veut avec ses propres mots plutôt qu’en reprenant une étiquette. La proximité géographique (ST_Distance sur une colonne geography PostGIS) — parce que « le plus proche » compte toujours, et ne peut pas être compensé par la seule pertinence.Ces signaux sont combinés via une Reciprocal Rank Fusion pondérée : score = w_fts/(k + fts_rank) + w_vec/(k + vec_rank) + w_prox * proximité, avec des poids qui varient selon que l’utilisateur ait donné ou non un mot-clé — 45/45/10 entre texte, vecteur et proximité s’il y a un mot-clé à faire correspondre ; 85% piloté par le vecteur sinon, puisque la recherche texte intégral n’a rien à quoi s’accrocher dans une requête purement descriptive. Il existe aussi un mécanisme de « palier premium » : une seconde passe de classement, partitionnée par palier, garantit aux commerces payants/référencés un petit quota de places sans pour autant noyer la pertinence pour l’ensemble des résultats.Je suis revenu plus tard réécrire cette même fonction pour la performance, après avoir remarqué qu’elle effectuait du travail redondant : fusionner deux CTE qui relisaient deux fois les mêmes lignes, ajouter une LIMIT à l’intérieur du CTE de classement vectoriel spécifiquement pour que l’index HNSW puisse s’arrêter court plutôt que de trier tout le pool de candidats, et remplacer deux matérialisations complètes séparées (résultats généraux, résultats premium) par une seule passe fenêtrée utilisant ROW_NUMBER() OVER (PARTITION BY is_premium ...). C’est le genre de correction vraiment satisfaisant — même résultat, mesurablement moins de travail par requête — et c’est le genre de chose qui n’apparaît que lorsque l’usage réel met sous pression un premier jet.Décision n°5 : les effets de bord sont des données, pas des actionsQuand un lieu est enregistré, qu’une interaction doit être journalisée pour l’analytique, ou qu’un utilisateur doit recevoir une notification, le moteur n’exécute pas ce travail en ligne — il renvoie un simple objet SideEffect décrivant ce qui doit se produire. Un PgmqSideEffectDispatcher collecte ces effets, les regroupe par file cible, et les pousse via pgmq.send_batch (l’extension de file de messages propre à Postgres) dans la même transaction de base de données que celle qui marque un lieu nouvellement enregistré comme « en attente d’indexation ». Ce détail compte : cela signifie qu’un lieu ne peut pas se retrouver à moitié enregistré — visible dans l’application mais jamais réellement indexé pour la recherche — parce que la mise à jour du statut et l’écriture dans la file valident ensemble, ou aucune des deux. Des Supabase Edge Functions en bout de chaîne (un analytics-worker, un place-embedding-worker, un place-tagging-worker) vident ces files de manière asynchrone.La stack, sans détourFastify + TypeScript sur Node.js, Prisma au-dessus de Supabase Postgres (avec pgvector, pgmq, PostGIS, pg_cron et pg_net qui font un vrai travail, pas juste des lignes dans un fichier de dépendances), Redis pour le cache et l’état du rate limiting, Gemini à la fois pour le NLU et les embeddings, Geoapify pour le géocodage, et des métriques Prometheus intégrées dès le départ plutôt que rajoutées après coup.Où en est le projetLe package.json indique 0.4.0-rc.1, et je tiens à laisser ce contexte tel quel : c’est un système réel et fonctionnel qui me sert à raisonner sur la recherche hybride et l’ingénierie conversationnelle, en évolution active vers une version 1.0 — pas un produit fini, à grande échelle, avec un portefeuille de clients derrière. La conception indépendante du canal existe précisément pour que, si ce projet doit un jour dépasser le cadre d’un bot WhatsApp, le moteur sous-jacent n’ait pas besoin d’être reconstruit.Si vous voulez débattre des poids de la RRF, me dire que PostGIS était superflu, ou pointer une meilleure façon de structurer le dispatcher d’effets de bord, le code est public : github.com/sanayasfp/laplace-nearby.
Le Guide Ultime pour un Environnement de Dev Fluide
July 31, 2026 ·
11 min
J’ai écrit au sujet de Stack il y a quelques jours — pour vous donner la version courte de pourquoi je l’ai créé et de toute la philosophie “sans Docker”. Mais la théorie a ses limites. Si vous cherchez le guide ultime pour obtenir un environnement de développement parfaitement fluide, vous êtes au bon endroit.Pas de pitch commercial. Pas de blabla marketing. Juste la réalité brute et pratique de la mise en route d’un projet sans effort, avec chaque invite de commande et chaque sortie console un peu bizarre que vous rencontrerez en cours de route.Si vous voulez juste la documentation de référence brute, vous la trouverez sur sanayavo.com/stack. Sinon, bienvenue dans la visite guidée d’un meilleur flux de travail (workflow).Ce que vous obtenez vraimentUn environnement fluide signifie zéro superflu. Stack n’est qu’un seul binaire Rust. Il n’y a pas de démon qui monopolise la RAM dans votre barre des tâches, pas de Docker Desktop qui fait tourner les ventilateurs de votre ordinateur portable à fond, et absolument rien qui ne touche à votre installation PHP globale du système.Au lieu de cela, il agit comme du ruban adhésif (duct tape) hautement intelligent. Il s’appuie sur vfox pour gérer les versions de PHP/Node, uv pour Python, et Caddy pour des domaines locaux instantanés. Stack lit un simple fichier stack.toml à la racine de votre projet et connecte automatiquement ces trois outils ensemble.Note : Stack est uniquement disponible sur Windows pour le moment (PowerShell ou cmd). Le support de Mac et Linux est prévu sur la feuille de route.L’installation : Votre seul et unique mal de crâneUn flux de travail quotidien fluide nécessite un tout petit peu de configuration au départ. Exécutez ceci pour télécharger l’installateur :irm https://github.com/sanayasfp/stack/releases/latest/download/stackenv-installer.ps1 | iexEnsuite, exécutez cette commande une seule fois par machine :stack setupCela s’occupe du gros du travail en arrière-plan. Cela ajoute un hook (crochet) à votre profil PowerShell (pour que la bonne version de PHP ou Node s’active par magie lorsque vous faites un cd dans un répertoire), installe vfox, uv et Caddy dans des versions largement testées, et exécute caddy trust. Cette dernière étape est cruciale pour le HTTPS local — on y revient dans une minute.$ stack setupadded the stack hook for pwshchecking vfox/uv/caddy... vfox: OK (1.0.11) uv: OK (0.11.7) caddy: OK (2.11.4) caddy: local CA trusted (https://*.localhost works with no browser warning)Redémarrez votre terminal maintenant. Si vous sautez cette étape, le hook PowerShell ne sera pas actif et vous vous demanderez pourquoi rien ne fonctionne. Vous n’aurez plus jamais à exécuter stack setup à moins d’acheter un nouvel ordinateur.Démarrer un projet sans frictionConstruisons quelque chose de concret pour vous montrer le fonctionnement : une API PHP légère adossée à MySQL.À partir de zéro$ stack new acme-apidomain: acme-api.localhostlanguages (space to toggle, enter to confirm): [x] phpservices (space to toggle, enter to confirm): [x] mysql php version: 8.3.1 mysql version: 8.0.35created acme-api\stack.tomlnext: cd into it, add a [run] command when you know it, then `stack up`C’est une liste de contrôle interactive dans le terminal. Espace pour basculer (cocher/décocher), Entrée pour confirmer. Ça fait un peu bizarre la première fois, mais rien n’est validé tant que vous n’appuyez pas sur Entrée. Si vous sélectionnez accidentellement Node au lieu de PHP, appuyez à nouveau sur espace. Zéro stress, pas besoin de tout recommencer.Si vous avez déjà un projetSi votre dossier acme-api possède déjà un fichier composer.json spécifiant "php": "^8.3", Stack est assez intelligent pour vous éviter de taper manuellement :$ stack initdetected from existing project files: php 8.3 (from composer.json)domain: acme-api.localhoststack init analyse automatiquement composer.json, package.json ou pyproject.toml et pré-remplit l’assistant pour vous.L’artefact magiqueStack génère un manifeste très lisible :[project]name = "acme-api"domain = "acme-api.localhost"[language]php = "8.3.1"[service.mysql]version = "8.0.35"Commitez ce fichier stack.toml dans votre dépôt (repo). C’est votre artefact ultime du type “ça marche sur ma machine” — l’équivalent léger et sans conteneur d’un Dockerfile. Lorsqu’un collègue clone le dépôt et tape stack up, il obtient instantanément les mêmes versions exactes de PHP et MySQL, sans avoir à installer quoi que ce soit à la main.Lui apprendre comment s’exécuterVotre stack.toml doit savoir comment démarrer votre serveur de développement. Ouvrez le fichier et ajoutez un bloc [run] :[run]command = "php -S 127.0.0.1:{port} -t public"L’espace réservé {port} est dynamiquement remplacé par un port disponible au moment de l’exécution. (Note : Si vous déclarez [language.php] mais ignorez la section [run], Stack se rabat sur sa propre configuration FastCGI intégrée utilisant php-cgi.exe et Caddy, ce qui est incroyablement solide. Mais pour ce guide, nous allons rester explicites.)stack up et regardez la magie opérer$ cd acme-api$ stack upLoaded C:\Users\you\acme-api\stack.toml project: acme-api domain: acme-api.localhost languages: php services: mysqlfirst run for this project — stack.toml will execute: [run] php -S 127.0.0.1:{port} -t publicTrust and run these commands? [y/N] y php: C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe -> PHP 8.3.1 (cli) service.mysql: started (pid 41232, port 3306) schema 'acme_api' — automatic creation not yet implemented; create it manually if needed run: php -S 127.0.0.1:52140 -t public (pid 41244, port 52140) log: C:\Users\you\.stack\logs\acme-api.log routed: http://acme-api.localhost -> 127.0.0.1:52140Il y a deux choses majeures qui se passent ici et qui contribuent à une expérience sans accroc :1. L’invite de confiance de sécuritéParce que [run].command est une invocation littérale du shell, Stack l’exécute exactement tel qu’il est écrit. La première fois que Stack voit une commande nouvelle ou modifiée (comme après un git pull), il vous demande de la confirmer. Dites oui, et il mémorise votre choix dans ~/.stack/trust.json. Chaque stack up suivant sera heureusement silencieux. Besoin de contourner cela pour la CI ? Passez simplement le flag --yes.2. Le HTTPS fonctionne immédiatementLa console affiche http://, mais https://acme-api.localhost fonctionne instantanément. Aucun avertissement rouge du navigateur. Vous vous souvenez de cette commande caddy trust lors de la phase de configuration ? Elle installe une autorité de certification locale (similaire à mkcert), ce qui signifie que votre environnement local reflète fidèlement le SSL de production dès le premier jour.Ce que “sans conteneurs” signifie vraiment en pratiqueOuvrez un deuxième terminal, faites un cd dans un projet totalement différent qui nécessite également MySQL 8.0.35, et exécutez stack up : service.mysql: already running, shared with other projects (pid 41232, port 3306)C’est toute la beauté de la chose. Un seul processus MySQL sert deux projets, isolés par schéma plutôt que de brûler votre processeur et votre RAM avec des conteneurs redondants.Mais c’est ici que le facteur “fluide” brille vraiment. Ouvrez un troisième terminal, faites un cd dans acme-api, et exécutez simplement php -v. Pas de stack up, pas de commandes supplémentaires :$ cd acme-api$ php -vPHP 8.3.1 (cli) (built: ...)$ cd ..$ php -v'php' is not recognized as an internal or external commandLe changement de version se fait de manière ambiante à chaque invite de commande grâce à ce hook PowerShell. Votre IDE, composer install et vos exécuteurs de tests obtiennent tous automatiquement la bonne version épinglée à la seconde où ils entrent dans le dossier. Quittez le répertoire, et elle disparaît. Pas de source venv/bin/activate, pas de nvm use, et pas de pollution de votre système global.Des installations PHP toutes neuves qui ne craignent pasHabituellement, une nouvelle installation PHP signifie passer une après-midi à traquer les erreurs “PDO driver not found” ou “timezone not set”. Stack élimine complètement cela.La première fois qu’il télécharge une version de PHP via vfox, il patche automatiquement php.ini. Il active OPcache, augmente les limites de mémoire et de téléchargement (upload), définit un fuseau horaire, et active les extensions dont vous avez réellement besoin par défaut (pdo_mysql, pdo_pgsql, pdo_sqlite, sockets, sodium, etc.). Cela se fait une fois, automatiquement, et vous n’avez plus jamais à y penser.stack doctor — L’ultime vérification avant le volAvant de transmettre un projet à un collègue ou de commencer à déboguer un problème étrange, exécutez ceci :$ stack doctor --projectchecking C:\Users\you\acme-api\stack.toml... language.php: OK (C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe) service.mysql: OK (managed)Cette commande valide vos ports, chemins et valeurs {PLACEHOLDER} par rapport à votre environnement (en chargeant d’abord .env) sans démarrer un seul service. Si quelque chose est cassé, vous obtenez une liste claire dès le départ au lieu de découvrir des erreurs cryptiques au beau milieu du démarrage.Pilotez votre configuration de n’importe oùUne fois que Stack connaît un projet, vous n’avez même pas besoin d’être dans son dossier pour le gérer :stack describe acme-apistack restart acme-apistack down acme-apistack describe affiche tout — les chemins binaires résolus, l’emplacement exact de votre php.ini haché, les logs, et les domaines routés. Stack conserve un registre local de vos projets, ce qui rend la gestion globale sans effort.Tout fermer proprementQuand la journée est terminée :stack down --allTout s’arrête. Chaque projet, chaque service partagé, et Caddy. Vous obtenez une utilisation CPU vraiment à zéro, et non un conteneur fantôme persistant que vous avez oublié de tuer.Une petite note sur les domaines personnalisésL’utilisation de .localhost garantit une expérience fluide car les navigateurs le résolvent automatiquement vers l’adresse de bouclage (RFC 6761) sans que vous ayez besoin de bidouiller votre fichier hosts. Cependant, si vous migrez depuis Laragon ou Herd et que vous avez absolument besoin de domaines .test, il y a une configuration unique à faire en utilisant Acrylic DNS Proxy.Votre Nouveau Flux de TravailGénérez (Scaffold), exécutez, faites confiance une fois, profitez du HTTPS automatique, commitez votre manifeste, et détruisez tout proprement quand vous avez terminé.J’utilise ce flux de travail exact pour du vrai travail client chaque jour. Ce n’est pas un scénario idéal théorique ; c’est un plan d’action éprouvé pour un environnement de développement ultime et fluide.Pour la référence complète de stack.toml, les options de ligne de commande (CLI), et les fonctionnalités avancées (comme l’utilisation de [[clone]] pour démarrer à partir d’un simple manifeste), consultez la documentation officielle : Pourquoi j’ai créé cela — la longue diatribe contre Docker/XAMPP pour le dev local Pour commencer Référence du Manifeste Référence CLI Code : github.com/sanayasfp/stack
Stack : Le gestionnaire d'environnement de dev natif et sans conteneur
July 28, 2026 ·
4 min
Quand on maintient une application legacy tout en construisant sa remplaçante moderne, l’environnement de développement local devient vite un champ de bataille. Au travail, nous avions une ancienne application tournant sur une vieille version de PHP, Vue 2 (nécessitant une vieille version de Node), et Python 3.7. En parallèle, nous démarrions la nouvelle architecture avec PHP 8.4, Vue 3, et Python 3.12.Personnellement, je gérais ça avec une combinaison sur mesure de Laravel Herd pour PHP, uv pour Python, et nvm pour Node. Ça marchait pour moi parce que j’avais pris le temps de tout configurer. Mais pour nos développeurs juniors et nos nouveaux stagiaires, leur demander de jongler avec ces outils juste pour faire tourner les deux projets en même temps était un cauchemar absolu. Ils passaient leur temps à se battre avec des conflits de chemins et des variables d’environnement au lieu d’écrire du code. C’est pour ça que j’ai créé Stack : un gestionnaire d’environnement multi-projets natif, sans conteneur, qui rend le changement de contexte aussi simple que de taper stack up.Le code est ici : github.com/sanayasfp/stack.La vraie contrainte : l’intégration et la charge cognitiveAvant même de toucher à l’architecture, la vraie contrainte sur un projet comme ça, c’est l’expérience développeur. Les stagiaires ne devraient pas avoir à apprendre le fonctionnement des réseaux bridge de Docker ni à configurer cinq gestionnaires de versions différents pour corriger un bug sur l’API legacy. L’objectif était simple : ils doivent pouvoir cloner le dépôt, lancer une seule commande, et obtenir un domaine local fonctionnel routé vers les bons processus, que le projet utilise PHP 5.6 ou 8.4.Pourquoi ne pas juste utiliser Docker ?Les conteneurs résolvent le problème “ça marche sur ma machine” en embarquant toute une couche OS par projet. Mais cela vient avec le poids de la virtualisation et des démons inactifs qui consomment de la RAM.Stack résout le même problème différemment. Il épingle les versions exactes par projet via les gestionnaires de version que vous installeriez de toute façon (en déléguant à des outils comme vfox et uv), partage un binaire unique téléchargé pour chaque projet nécessitant cette version, et fait tourner le tout sous forme de simples processus enfants. Pas d’hyperviseur, pas de Docker Desktop qui mange 4 Go de RAM pour une API toute simple. Chaque projet qui épingle le même service et la même version partage une instance unique en cours d’exécution, isolée par schéma.Sous le capot : la simplicité avant la magieUn fichier stack.toml dans votre projet définit exactement ce dont il a besoin :[project]name = "acme-api"[language]php = "8.4.0"[service.mysql]version = "8.0.35"[run]command = "php -S 127.0.0.1:{port} -t public"Quand vous tapez stack up, l’orchestrateur lit le manifeste, configure le domaine local (par ex. acme-api.localhost), démarre MySQL s’il ne tourne pas déjà, lance le serveur de dev PHP, et fait le proxy inverse du trafic. Tout est natif, et incroyablement rapide. Il n’essaie pas de réinventer la gestion des paquets ; il délègue le gros du travail de téléchargement des langages à vfox et uv, et le routage à Caddy.Où en est le projet actuellementJe tiens à être précis là-dessus : j’ai commencé à coder Stack en Rust la semaine dernière seulement, et j’ai poussé la première version il y a deux jours. J’ai complètement désinstallé Herd, Laragon et mes précédents scripts maison. J’utilise désormais exclusivement Stack pour mon travail quotidien — ce qui me permet de prouver que ça marche, de trouver des bugs dans les cas limites, et d’ajouter de nouvelles fonctionnalités pour l’améliorer.Il supporte actuellement Windows (PowerShell & cmd), ce qui était le besoin immédiat, avec macOS et Linux prévus sur la feuille de route. Ce n’est plus juste une expérience de week-end ; c’est en train de devenir l’outil fondamental qui garde notre équipe saine d’esprit côté dev local.Vous pouvez récupérer l’installateur sur le dépôt ou lire la documentation officielle : Dépôt : github.com/sanayasfp/stack Docs : sanayavo.com/stack/
Car Inspect AI : apprendre à YOLO à lire une voiture comme le ferait un inspecteur
June 22, 2026 ·
7 min
Quiconque a déjà loué une voiture ou déposé un dossier d’assurance connaît le rituel : quelqu’un fait le tour du véhicule, prend quelques photos, et une personne décide plus tard si telle marque sur le pare-chocs était déjà là. C’est lent, c’est subjectif, et les litiges entre locataires, propriétaires et assureurs sur les “dommages préexistants” sont fréquents, précisément parce que tout repose sur la parole de quelqu’un face à une poignée de photos. Car Inspect AI est ma tentative de m’attaquer à ce problème : un système de vision par ordinateur qui détecte et identifie automatiquement les différentes parties d’un véhicule à partir d’une photo, comme première brique vers une inspection plus objective et automatisée.Le code est ici : github.com/sanayasfp/car-inspect-ai.Partir de la vraie contrainte : les donnéesAvant de toucher au moindre modèle, la vraie contrainte d’un projet comme celui-ci, ce sont les données. J’ai entraîné le modèle sur le jeu de données public Car Parts Segmentation (Kitsuchart Pasupa et al.), 500 images annotées de berlines, pickups et SUV au format COCO, couvrant 18 parties distinctes du véhicule — pare-chocs, portes, feux, rétroviseurs, capot, coffre, roues, etc. — photographiées de face, de dos et sous des angles inclinés, avec plaques et visages floutés pour la confidentialité. 500 images, ce n’est pas énorme à l’échelle du deep learning, et cette contrainte a façonné presque toutes les décisions qui ont suivi.Pourquoi YOLO11n précisémentJ’ai choisi YOLO11n — la variante “nano” — de manière délibérée, pas simplement parce que “YOLO, c’est ce qu’on utilise pour la détection d’objets.” Deux raisons : C’est assez léger pour tourner sur du matériel modeste. Un outil pensé pour des garages, des petites agences de location ou des inspecteurs indépendants ne sert à rien s’il exige un GPU puissant pour faire de l’inférence. YOLO11n sacrifie un peu de précision brute contre une empreinte qui tourne confortablement sur CPU ou sur un GPU d’entrée de gamme. Avec seulement 500 images, la capacité du modèle est un handicap, pas un atout. Un modèle plus large a plus de marge pour sur-apprendre un petit jeu de données. Une architecture légère, combinée à une augmentation de données agressive, était le choix le plus honnête compte tenu de ce que j’avais réellement pour entraîner.Pour tirer le maximum de ce petit jeu de données, le prétraitement a inclus un redimensionnement aux dimensions d’entrée attendues par YOLO, ainsi qu’une augmentation par rotation (pour simuler différents angles de prise de vue), un flip horizontal, et l’ajout de bruit gaussien (pour rendre le modèle moins sensible aux variations d’éclairage — un vrai problème quand les photos viennent d’un téléphone au hasard sur un parking, pas d’un studio).L’entraînement s’est fait sur 50 époques avec un split 80/20 entraînement/validation, une taille de batch de 16, et un arrêt anticipé si la performance sur le jeu de validation stagnait pendant 10 époques consécutives. Résultat : 87% de mAP sur le jeu de validation, avec les meilleures performances sur les parties géométriquement bien définies comme les roues et les portes — exactement là où on s’attend à ce qu’un détecteur soit le plus fiable, et exactement le genre de résultat qui indique où concentrer les efforts ensuite (les petites parties ambiguës comme les rétroviseurs restent les cas les plus difficiles).Coder mon propre petit ORM plutôt que d’aller chercher SQLAlchemyC’est la partie du projet que la plupart des gens survoleraient, mais c’est celle où j’ai le plus appris. L’application doit suivre les runs d’entraînement — quel modèle, combien d’époques, si l’entraînement s’est terminé, où se trouve le checkpoint, et s’il reprend un run précédent. Plutôt que d’importer SQLAlchemy pour ce qui reste fondamentalement une poignée de tables, j’ai écrit moi-même une petite couche de modèles basée sur les dataclasses :@dataclasses.dataclassclass TrainLogsModel(BaseModel): _table_name = "train_logs" name: str epochs: int model: str path: str completed: bool = Field(type=bool, default=False).set() id: Optional[int] = Field(type=int, primary_key=True, autoincrement=True).set() created_at: Optional[float] = Field(type=int, default=lambda: dt.now().timestamp()).set() resumed_from: Optional[int] = Field(type=int, foreign_key="id", foreign_table=_table_name).set()BaseModel lit les annotations de type et les métadonnées de la dataclass et les transforme en définitions de colonnes SQL (INTEGER, TEXT, REAL, avec clés primaires, autoincrément et clés étrangères gérées explicitement), et déduit les noms de table à partir des noms de classe en snake_case, camelCase ou PascalCase selon le besoin. C’est une fraction de ce que fait un vrai ORM — pas de query builder, pas de système de migrations — mais écrire cette fraction à la main m’a obligé à vraiment comprendre ce qu’un ORM automatise, plutôt que d’en importer un et de faire confiance à la magie. C’est un arbitrage que je referais : pour un projet de cette taille, écrire 100 lignes pour comprendre le mécanisme valait mieux que 10 lignes qui le cachent.Ce modèle alimente une fonctionnalité réellement utile : la page d’entraînement permet de choisir soit un YOLO11n de base, soit n’importe quel checkpoint précédent, de lancer l’entraînement et de le journaliser — y compris une clé étrangère resumed_from pointant vers le run dont il repart, ce qui me donne une vraie généalogie d’expériences plutôt qu’un dossier plein de best_v2_final_FINAL.pt.L’interface : Streamlit, par choixLe tout est enveloppé dans une petite application Streamlit multi-pages — une page d’accueil, une page “enregistrer une voiture” (upload des photos avant/arrière/gauche/droite plus couleur et numéro de plaque), la page d’entraînement décrite plus haut, et une page de brouillons pour les expérimentations en cours. Streamlit était le bon choix ici précisément parce que ce n’est pas l’objet du projet : je voulais passer mon temps sur le modèle de détection et sur le suivi des entraînements, pas sur un frontend fait main, et Streamlit s’efface pour ça.Où en est vraiment le projet aujourd’huiJe préfère être précis plutôt que d’arrondir : le modèle de détection des parties et le pipeline d’entraînement/versioning fonctionnent et sont mesurés (ce chiffre de 87% de mAP est réel, issu d’un run effectif, pas d’une estimation). Le pipeline d’inspection complet — fusionner les quatre angles d’un véhicule en un seul rapport fiable, et passer de “voici les parties détectées” à un véritable verdict de dommage ou de fraude — reste un chantier actif, pas un produit terminé. Le README liste la notation automatique de la gravité des dommages et l’intégration à un historique véhicule comme axes futurs, et c’est exact : c’est la feuille de route, pas quelque chose que je prétends déjà fonctionnel de bout en bout.Si je devais résumer ce qui est réellement acquis aujourd’hui : un détecteur de parties de véhicule léger, honnêtement benchmarké, entraîné de manière reproductible, avec sa propre couche minimale de suivi d’expériences construite à partir de zéro. C’est une ambition plus modeste que “système de détection de fraude automatisé”, mais c’est la vraie — et c’est une meilleure fondation pour construire la suite.Le dépôt est public si vous voulez voir le code d’entraînement, le mini-ORM, ou me convaincre que j’aurais dû simplement utiliser SQLAlchemy : github.com/sanayasfp/car-inspect-ai.
L'illusion de l'IA : pourquoi alimenter un LLM sans gouvernance des données est un risque impossible à auditer
October 30, 2026 ·
19 min
Une juriste ouvre le nouvel assistant IA de l’entreprise, y dépose un contrat d’exclusivité et tape : « Résume les obligations clés en cinq lignes. » Dix secondes plus tard, elle obtient une synthèse claire et concise. C’est un gain de productivité concret, indéniable.Ce qu’elle ne voit pas, c’est ce qui s’est réellement passé pendant ces dix secondes. L’application ne s’est pas contentée d’envoyer sa question : elle a transmis le contrat dans son intégralité — la contrepartie, les grilles tarifaires, les clauses de résiliation, le nom des signataires. Tout cela a atterri, sous forme de texte lisible en clair, sur un serveur exploité par un tiers.Le même phénomène se reproduit lorsqu’un responsable RH demande le résumé d’un entretien d’évaluation, ou lorsqu’un analyste financier épluche le dossier de crédit d’un client. La question est courte. Le contexte qui l’accompagne est colossal.Dans presque tous les comités de direction, le mot d’ordre pour 2026 est identique : « Il nous faut de l’IA, et tout de suite. » En quelques semaines, des clés d’API sont générées, des modèles de langage sont connectés aux bases de données internes, et des flux de travail entiers sont délégués à des algorithmes tiers. Les démos sont impressionnantes et les gains de productivité bien réels.Cet article ne cherche pas à contester cet élan. Il soutient que les entreprises qui prendront la gouvernance des données au sérieux déploieront l’IA plus vite et sur un plus large périmètre, précisément parce que leurs fondations seront sûres. Celles qui font l’impasse dessus finiront par se heurter à une question toute simple à laquelle elles ne sauront pas répondre. Nous y reviendrons à la fin.1. Le problème : chaque requête emporte vos donnéesMême lorsque votre contrat avec le fournisseur d’IA lui interdit d’entraîner ses modèles sur vos données, et même lorsque la connexion est intégralement chiffrée en transit, le texte brut arrive toujours sous une forme parfaitement lisible chez ce tiers. Les contrats encadrent ce que le fournisseur a le droit d’en faire. Mais ils ne changent absolument rien à ce qui se retrouve exposé en cas de compromission chez lui, en cas d’erreur de configuration dans ses logs, ou si une réquisition judiciaire atterrit sur son bureau.Ce risque s’amplifie encore avec les architectures multi-agents, où un agent routeur délègue le travail à des sous-agents spécialisés partageant une mémoire commune. Un simple nom ou un chiffre d’affaires injecté dans cet état partagé devient immédiatement visible pour chaque agent de la chaîne, ainsi que pour chaque journal d’événements, chaque cache et chaque outil de traçabilité (tracing) dans lequel ils écrivent.Figure 1 : Injection de données brutes dans une mémoire partagée multi-agents.Envoyer des données métier brutes dans un LLM sans aucun contrôle n’est pas de l’innovation. C’est sous-traiter la mémoire stratégique de son entreprise à un tiers qui n’assumera jamais le coût de vos fuites.2. Pourquoi l’anonymisation naïve échoueCertaines équipes, conscientes du danger, placent un filtre rudimentaire en amont du modèle : des outils génériques de reconnaissance d’entités nommées (Named Entity Recognition ou NER), ou de simples expressions régulières qui masquent les noms propres. Sur le papier, cela semble raisonnable. En production, cette approche échoue généralement de trois manières — et son vrai danger est de procurer à la direction un sentiment trompeur de sécurité qui l’empêche de poser les bonnes questions.Échec n°1 : Le filtre ne comprend pas votre métierLes outils de détection génériques sont entraînés sur des textes courants. Ils confondent facilement les noms d’entreprises avec des termes du langage usuel (« Services », « Transit », « Holding »), et laissent régulièrement passer des raisons sociales contenant ce type de mot, comme « NET-POLY SERVICES ». Quant aux identifiants internes (numéros fiscaux, identifiants du registre de commerce, codes clients internes), ils ne correspondent pas aux formats standards internationaux et passent totalement inaperçus.Échec n°2 : Masquer le nom ne revient pas à masquer l’entrepriseConsidérez cet exemple : Texte brut : « L’entreprise X a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. » Après un filtre naïf : « ENTREPRISE_1 a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. »Le nom a disparu, mais le chiffre d’affaires exact et la ville suffisent amplement à identifier l’entreprise en une seule recherche. Un modèle moderne, fort de son immense base de connaissances d’entraînement, peut souvent faire le lien en une fraction de seconde.Échec n°3 : Un prompt est une prière, pas un contrôleUne consigne comme « Ne répète jamais les noms des clients » n’est qu’une incitation probabiliste (probabilistic nudge). Le modèle s’y conformera le plus souvent… jusqu’au jour où il ne le fera pas. Le problème est encore plus pernicieux : selon le fournisseur et la configuration, certains modèles renvoient leur raisonnement intermédiaire dans un canal distinct de la réponse finale — une sorte de bloc-notes (scratchpad ou canal de pensée) qui vous est retourné. Si votre mécanisme de protection ne filtre que la réponse finale, les données brutes peuvent transiter par ce bloc-notes en toute liberté.Croire qu’un prompt système enjoignant d’« être discret » protège vos données revient à verrouiller une porte blindée en laissant la clé sur la serrure extérieure.3. Les six invariants d’une gouvernance robusteSi les consignes et les filtres basiques ne suffisent pas, que faut-il mettre en place ? La plupart des architectes esquissent un pipeline linéaire : les données arrivent, sont filtrées, partent vers le modèle, puis reviennent. Un système robuste obéit à une tout autre logique : c’est une défense en profondeur, où chaque frontière porte sa propre protection, indépendamment des autres.Voici six invariants fondamentaux. Ce sont des contrats d’architecture que tout concepteur de système peut vérifier au sein de sa propre infrastructure, quels que soient la stack technique, le fournisseur de modèles ou la taille de l’équipe.Figure 2 : Les six invariants de gouvernance des données appliqués aux flux LLM.Invariant 1 : Minimiser dès la sourceCe qui ne figure pas dans les données transmises ne peut pas fuiter. Les outils exposés aux agents IA ne doivent jamais être les mêmes que ceux mis à disposition des développeurs humains. La pagination y est strictement imposée, les volumes sont plafonnés et les champs superflus sont intégralement supprimés. L’API ne renvoie pas un dossier client complet pour calculer un simple ratio : elle ne retourne que les trois nombres strictement nécessaires — sans coordonnées, sans adresses, sans noms.Ce n’est pas une règle propre à l’IA : c’est un principe fondamental d’exposition des données que l’IA rend simplement impératif.Invariant 2 : Ne jamais laisser un modèle trancher ce qu’un chemin déterministe peut déciderUn classement, la consultation d’un ratio ou une simple extraction d’information n’exigent aucun discernement : ils n’ont donc aucune raison de transiter par votre modèle le plus onéreux et le moins prévisible. L’aiguillage peut reposer sur une règle métier, un choix dans l’interface ou un classifieur très léger : le principe n’impose pas la méthode d’implémentation.Deux remarques franches s’imposent. Premièrement, si le routeur est lui-même un modèle, il reste un modèle : choisissez-le miniature, strictement bridé, et ne lui fournissez que du texte anonymisé. Le but est de réduire la surface d’exposition, non de prétendre qu’elle a disparu. Deuxièmement, cet invariant présente un avantage qui convainc souvent un comité de direction bien avant les arguments sécuritaires : il est beaucoup plus économique et infiniment plus rapide.Une anecdote révélatrice nous est fournie par Anthropic elle-même. En mars 2026, le code source de Claude Code a fuité accidentellement via une source map publiée par inadvertance sur npm. On y a découvert que l’outil détecte la frustration de l’utilisateur — c’est-à-dire si quelqu’un l’insulte — au moyen d’une simple expression régulière, sans faire appel au moindre modèle1. Une regex est instantanée et gratuite. Un appel d’inférence n’est ni l’un ni l’autre, pour une question qui ne réclame aucun jugement.Invariant 3 : Traiter chaque frontière vers le modèle comme un point de protection obligatoire et réversibleToute donnée franchissant la frontière entre votre système et un modèle de langage, qu’elle soit entrante ou sortante, doit être protégée par un mécanisme réversible tel que la substitution, la pseudonymisation ou la tokenisation. L’erreur courante consiste à ne protéger que le champ de réponse évident. Inspectez chaque canal exposé par le fournisseur, y compris le canal de raisonnement décrit à la section 2.Une mise en garde juridique : des données pseudonymisées peuvent toujours être qualifiées de données à caractère personnel au sens de réglementations comme le RGPD. Cet invariant réduit votre exposition au risque opérationnel ; il ne vous exonère pas de vos obligations légales.Invariant 4 : Réhydrater au tout dernier moment légitimeL’opération inverse de substitution ne doit être appliquée qu’à l’ultime étape, juste avant que la réponse ne soit présentée à un utilisateur humain habilité à consulter les valeurs réelles. Le modèle travaille à l’aveugle du début à la fin. Seul l’humain reçoit la réponse en clair, et exclusivement en bout de chaîne.Invariant 5 : Un garde-fou déterministe a le dernier mot sur la plausibilitéLes quatre premiers invariants protègent la confidentialité. Celui-ci protège l’exactitude des résultats. Toute réponse générée doit franchir un contrôle non probabiliste — tel qu’un seuil limite ou une comparaison par rapport à un référentiel connu — avant d’être transmise. Si le résultat semble aberrant, le système bascule automatiquement sur un chemin surveillé plus lent plutôt que de laisser passer une réponse non vérifiée.Invariant 6 : Chaque saut d’agent à agent constitue sa propre frontière de confianceC’est l’invariant le plus souvent négligé, car il n’apparaît pas sur les diagrammes en couches statiques. La sortie d’un agent n’est pas automatiquement sécurisée pour l’agent qui l’a sollicité.Voici le cas typique : un sous-agent, pensant bien faire en « nettoyant sa propre sortie » avant de répondre, désanonymise des données qui devaient impérativement rester masquées pour l’agent parent ayant délégué la tâche. Le sous-agent s’est conformé à ses consignes à la lettre. Il ignorait simplement que sa réponse était destinée à un autre maillon de la chaîne, censé poursuivre son raisonnement sur des pseudonymes.Il existe un corollaire aggravant : lorsque l’historique conversationnel traverse plusieurs agents, les échanges de différents sous-agents se retrouvent mélangés dans un contexte unique. Sans un étiquetage explicite de la provenance (provenance tagging), il devient impossible de déterminer quelle frontière de confiance s’applique à quel fragment de texte.Figure 3 : Frontières de confiance lors de délégations inter-agents et déplacement du point de réhydratation.4. Pour les ingénieurs : comment nous implémentons les invariantsIl s’agit ici d’une implémentation concrète parmi d’autres. L’outillage est spécifique à notre écosystème technique. Même si votre architecture diffère sensiblement de la nôtre, les invariants restent universels : seule leur incarnation technique change. Les lecteurs non techniques peuvent passer directement à la section 5. Passerelle d’anonymisation (invariant 3) : Avant toute classification, le texte libre saisi par l’utilisateur franchit une passerelle stricte combinant des bibliothèques de TALN/NLP et des règles de substitution déterministes. Les noms et identifiants sont remplacés par des jetons uniques (par exemple CORP_A83B). La table de correspondance est immédiatement sécurisée dans un coffre-fort chiffré (encrypted vault) : calcul HMAC-SHA256 pour l’identifiant du jeton et chiffrement Fernet (AES-128-CBC avec HMAC) pour la valeur réelle au repos. Routeur (invariant 2) : La requête pseudonymisée est soumise à un LLM délibérément compact, véloce et économique, dont l’unique mission est de classifier l’intention. Un classement, une lecture de ratio ou une extraction basique emprunte le chemin rapide (fast path) : un outil issu d’une liste blanche restreinte (allowlist) est exécuté directement, sans jamais solliciter le modèle lourd. Une analyse de risque ou une tâche d’interprétation complexe emprunte le chemin lent (slow path) et bascule vers l’agent principal. Outils minimisés (invariant 1) : Sur les deux chemins, l’accès aux données internes s’effectue au moyen d’outils exposés via le protocole MCP (Model Context Protocol), épurés à la source et strictement dissociés des outils réservés aux développeurs humains. Chemin lent (invariant 3, sortant) : Les résultats renvoyés par les outils sont systématiquement pseudonymisés avant d’être injectés dans le contexte du modèle lourd, de sorte que ce dernier raisonne et rédige exclusivement à partir de pseudonymes. Chemin rapide (invariant 5) : Un garde-fou de plausibilité — par exemple un ratio comparé à une valeur de référence — bloque toute réponse automatique aberrante et force un repli vers le chemin lent pour examen approfondi. Réhydratation (invariant 4) : Ce n’est qu’une fois la réponse finale générée et mise en forme que le système interroge le coffre-fort pour substituer les jetons par les valeurs réelles, immédiatement avant l’affichage à l’utilisateur dûment habilité.Figure 4 : Architecture de routage fast-path / slow-path. Résultats observés : (Insérer ici vos métriques clés de production — par exemple : 72 % des requêtes traitées par le chemin rapide sans appel au modèle lourd, division par 4 de la latence médiane et réduction de 65 % des coûts d’API).5. Vérifier plutôt que présumerUne défense qui n’a jamais été éprouvée dans des conditions réelles est une défense dont on espère simplement l’existence.Voici un scénario bien loin d’être théorique : un contrôle destiné à transformer chaque message avant qu’il n’atteigne le modèle a été rédigé, validé en revue de code et intégré à la branche principale. Les tests passaient avec succès. Pendant des semaines, l’équipe a cru la protection active. Elle ne l’était pas. Une hypothèse erronée sur la structure exacte de la charge utile entrante (payload shape) faisait que le filtre s’exécutait sur une mauvaise clé d’objet, de manière silencieuse, sans générer d’erreur et sans déclencher la moindre alerte.Un test qui passe au vert sans solliciter réellement le code de protection ne prouve rien. Une revue de code n’est pas une preuve d’exécution, et un schéma d’architecture soigné n’est pas la preuve que la trajectoire tracée est celle empruntée lors de l’exécution. La seule preuve tangible repose sur un test rejoué contre le système réel : le vrai modèle, les vraies données, la véritable chaîne d’appels.Chaque affirmation de conformité — qu’il s’agisse de « ces données sont anonymisées » ou « ces données ne sortent jamais en clair de notre réseau » — doit être assortie d’une preuve rejouable, consultable à la demande. Sans cela, il ne s’agit pas d’une garantie : c’est une hypothèse qui n’a pas encore été réfutée.Figure 5 : Défaillance silencieuse d’un contrôle liée à une inadéquation de format des données entrantes.6. La piste d’audit (Audit Trail)La gouvernance ne s’arrête pas à la protection en temps réel : elle exige une traçabilité sans faille. Lorsqu’un régulateur bancaire, un auditeur RGPD ou un grand client examine la manière dont l’IA est exploitée dans votre entreprise, il ne se contentera pas de simples déclarations d’intention. Il exigera des preuves irréfutables.Un système d’IA d’entreprise responsable conserve un journal d’audit infalsifiable (tamper-evident) et strictement en ajout seul (append-only), isolé des logs applicatifs ordinaires et de l’historique des conversations. Pour chaque événement du cycle de vie d’un agent, il consigne une paire indissociable : Les données brutes telles qu’elles ont été initialement reçues, Les données réellement transmises aux modèles externes.La règle la plus cruciale est la suivante : aucun paramètre de configuration (feature flag) ni aucun administrateur isolé ne doit pouvoir désactiver ce journal. Lors de pics de charge imprévus, le premier réflexe d’une équipe technique est souvent de désactiver les journaux « pour préserver les performances ». Si un tel commutateur existe, le jour où une fuite survient, vous serez incapable d’en déterminer l’étendue ou d’identifier ce qui a été exposé.Figure 6 : Pourquoi le journal d’audit doit être immunisé contre toute désactivation par configuration.Il y a toutefois un revers, et il doit être énoncé sans détour : ce journal d’audit devient lui-même l’un des jeux de données les plus sensibles de l’organisation, puisqu’il contient précisément les données brutes que vous avez pris tant de peine à dissimuler. Traitez-le en conséquence : chiffrez-le, cloisonnez et restreignez ses accès, fixez des limites de conservation rigoureuses, et définissez avec vos juristes la manière dont il s’articule avec les demandes de suppression (droit à l’effacement prévu par le RGPD).Complétez enfin ce dispositif par une détection rétroactive des fuites (retroactive leak scanning) : des processus d’arrière-plan analysent en continu l’historique d’audit archivé à la recherche de schémas sensibles ayant pu franchir les premiers filtres. Lorsqu’une anomalie est décelée, le scanner ne modifie en aucun cas le journal initial : il déclenche une alerte auprès de l’équipe de sécurité, qui peut ainsi durcir les règles de filtrage avant qu’une simple brèche ne devienne un incident majeur.Figure 7 : Le pipeline d’audit, de l’enregistrement immuable à l’analyse asynchrone et aux alertes.7. Conclusion : L’IA est d’abord un défi de Data EngineeringL’adage bien connu « garbage in, garbage out » a désormais un pendant moderne : « secret in, exposure out » (données secrètes en entrée, fuites en sortie). L’IA n’est pas un vernis cosmétique que l’on vient greffer à la hâte dans un recoin du système d’information pour contenter la direction générale. Elle bouleverse la façon dont l’information circule, se segmente, s’anonymise et s’archive.Les entreprises qui sortiront gagnantes de la révolution de l’IA ne seront pas celles qui auront déployé le plus d’agents dans l’urgence. Ce seront celles qui auront d’abord bâti des fondations hermétiques et fiables. C’est la qualité des freins qui permet à une voiture de rouler vite. Une fois ces contrôles fermement en place, les équipes peuvent donner leur feu vert à un plus grand nombre de cas d’usage, avec moins de débats interminables et sans vetos de dernière minute.Alors, avant de signer votre prochain contrat de licence d’IA ou de célébrer le déploiement de votre nouvel agent autonome, rassemblez vos architectes et posez-leur une seule question : « Si un auditeur externe mandaté par notre plus grand client débarquait demain matin à 8h, seriez-vous capables de me prouver, ligne de code par ligne de code, qu’aucun nom, aucun identifiant et aucun chiffre confidentiel n’a quitté notre réseau en clair au cours du mois écoulé ? »Si la réponse est un silence gêné, un renvoi aux « clauses contractuelles du fournisseur » ou la mention d’« un prompt qui demande à l’IA d’être prudente », vous connaissez déjà votre prochain projet.La checklist de gouvernance : 8 cases à cocherAvant de quitter la salle, voici les huit critères non négociables à valider (les six invariants, complétés par les impératifs des sections 5 et 6) : Minimisez-vous dès la source ? (Invariant 1) Préservez-vous le modèle lourd et coûteux des requêtes simples ? (Invariant 2) Chaque frontière texte-vers-modèle fait-elle l’objet d’une protection réversible ? (Invariant 3) Ne réhydratez-vous qu’au tout dernier moment légitime ? (Invariant 4) Un garde-fou déterministe a-t-il le dernier mot sur la plausibilité ? (Invariant 5) Chaque saut d’agent à agent constitue-t-il sa propre frontière de confiance ? (Invariant 6) Chaque contrôle est-il vérifié en conditions réelles, et pas seulement supposé fonctionnel ? (Section 5) Le journal d’audit est-il impossible à désactiver par configuration ou par un administrateur isolé ? (Section 6) Alex Kim, « The Claude Code Source Leak: fake tools, frustration regexes, undercover mode, and more », 31 mars 2026. La fuite provenait d’un fichier source map publié par mégarde sur npm. Le fichier userPromptKeywords.ts contient la regex employée pour la détection de la frustration de l’utilisateur. ↩
Car Inspect AI : apprendre à YOLO à lire une voiture comme le ferait un inspecteur
June 22, 2026 ·
7 min
Quiconque a déjà loué une voiture ou déposé un dossier d’assurance connaît le rituel : quelqu’un fait le tour du véhicule, prend quelques photos, et une personne décide plus tard si telle marque sur le pare-chocs était déjà là. C’est lent, c’est subjectif, et les litiges entre locataires, propriétaires et assureurs sur les “dommages préexistants” sont fréquents, précisément parce que tout repose sur la parole de quelqu’un face à une poignée de photos. Car Inspect AI est ma tentative de m’attaquer à ce problème : un système de vision par ordinateur qui détecte et identifie automatiquement les différentes parties d’un véhicule à partir d’une photo, comme première brique vers une inspection plus objective et automatisée.Le code est ici : github.com/sanayasfp/car-inspect-ai.Partir de la vraie contrainte : les donnéesAvant de toucher au moindre modèle, la vraie contrainte d’un projet comme celui-ci, ce sont les données. J’ai entraîné le modèle sur le jeu de données public Car Parts Segmentation (Kitsuchart Pasupa et al.), 500 images annotées de berlines, pickups et SUV au format COCO, couvrant 18 parties distinctes du véhicule — pare-chocs, portes, feux, rétroviseurs, capot, coffre, roues, etc. — photographiées de face, de dos et sous des angles inclinés, avec plaques et visages floutés pour la confidentialité. 500 images, ce n’est pas énorme à l’échelle du deep learning, et cette contrainte a façonné presque toutes les décisions qui ont suivi.Pourquoi YOLO11n précisémentJ’ai choisi YOLO11n — la variante “nano” — de manière délibérée, pas simplement parce que “YOLO, c’est ce qu’on utilise pour la détection d’objets.” Deux raisons : C’est assez léger pour tourner sur du matériel modeste. Un outil pensé pour des garages, des petites agences de location ou des inspecteurs indépendants ne sert à rien s’il exige un GPU puissant pour faire de l’inférence. YOLO11n sacrifie un peu de précision brute contre une empreinte qui tourne confortablement sur CPU ou sur un GPU d’entrée de gamme. Avec seulement 500 images, la capacité du modèle est un handicap, pas un atout. Un modèle plus large a plus de marge pour sur-apprendre un petit jeu de données. Une architecture légère, combinée à une augmentation de données agressive, était le choix le plus honnête compte tenu de ce que j’avais réellement pour entraîner.Pour tirer le maximum de ce petit jeu de données, le prétraitement a inclus un redimensionnement aux dimensions d’entrée attendues par YOLO, ainsi qu’une augmentation par rotation (pour simuler différents angles de prise de vue), un flip horizontal, et l’ajout de bruit gaussien (pour rendre le modèle moins sensible aux variations d’éclairage — un vrai problème quand les photos viennent d’un téléphone au hasard sur un parking, pas d’un studio).L’entraînement s’est fait sur 50 époques avec un split 80/20 entraînement/validation, une taille de batch de 16, et un arrêt anticipé si la performance sur le jeu de validation stagnait pendant 10 époques consécutives. Résultat : 87% de mAP sur le jeu de validation, avec les meilleures performances sur les parties géométriquement bien définies comme les roues et les portes — exactement là où on s’attend à ce qu’un détecteur soit le plus fiable, et exactement le genre de résultat qui indique où concentrer les efforts ensuite (les petites parties ambiguës comme les rétroviseurs restent les cas les plus difficiles).Coder mon propre petit ORM plutôt que d’aller chercher SQLAlchemyC’est la partie du projet que la plupart des gens survoleraient, mais c’est celle où j’ai le plus appris. L’application doit suivre les runs d’entraînement — quel modèle, combien d’époques, si l’entraînement s’est terminé, où se trouve le checkpoint, et s’il reprend un run précédent. Plutôt que d’importer SQLAlchemy pour ce qui reste fondamentalement une poignée de tables, j’ai écrit moi-même une petite couche de modèles basée sur les dataclasses :@dataclasses.dataclassclass TrainLogsModel(BaseModel): _table_name = "train_logs" name: str epochs: int model: str path: str completed: bool = Field(type=bool, default=False).set() id: Optional[int] = Field(type=int, primary_key=True, autoincrement=True).set() created_at: Optional[float] = Field(type=int, default=lambda: dt.now().timestamp()).set() resumed_from: Optional[int] = Field(type=int, foreign_key="id", foreign_table=_table_name).set()BaseModel lit les annotations de type et les métadonnées de la dataclass et les transforme en définitions de colonnes SQL (INTEGER, TEXT, REAL, avec clés primaires, autoincrément et clés étrangères gérées explicitement), et déduit les noms de table à partir des noms de classe en snake_case, camelCase ou PascalCase selon le besoin. C’est une fraction de ce que fait un vrai ORM — pas de query builder, pas de système de migrations — mais écrire cette fraction à la main m’a obligé à vraiment comprendre ce qu’un ORM automatise, plutôt que d’en importer un et de faire confiance à la magie. C’est un arbitrage que je referais : pour un projet de cette taille, écrire 100 lignes pour comprendre le mécanisme valait mieux que 10 lignes qui le cachent.Ce modèle alimente une fonctionnalité réellement utile : la page d’entraînement permet de choisir soit un YOLO11n de base, soit n’importe quel checkpoint précédent, de lancer l’entraînement et de le journaliser — y compris une clé étrangère resumed_from pointant vers le run dont il repart, ce qui me donne une vraie généalogie d’expériences plutôt qu’un dossier plein de best_v2_final_FINAL.pt.L’interface : Streamlit, par choixLe tout est enveloppé dans une petite application Streamlit multi-pages — une page d’accueil, une page “enregistrer une voiture” (upload des photos avant/arrière/gauche/droite plus couleur et numéro de plaque), la page d’entraînement décrite plus haut, et une page de brouillons pour les expérimentations en cours. Streamlit était le bon choix ici précisément parce que ce n’est pas l’objet du projet : je voulais passer mon temps sur le modèle de détection et sur le suivi des entraînements, pas sur un frontend fait main, et Streamlit s’efface pour ça.Où en est vraiment le projet aujourd’huiJe préfère être précis plutôt que d’arrondir : le modèle de détection des parties et le pipeline d’entraînement/versioning fonctionnent et sont mesurés (ce chiffre de 87% de mAP est réel, issu d’un run effectif, pas d’une estimation). Le pipeline d’inspection complet — fusionner les quatre angles d’un véhicule en un seul rapport fiable, et passer de “voici les parties détectées” à un véritable verdict de dommage ou de fraude — reste un chantier actif, pas un produit terminé. Le README liste la notation automatique de la gravité des dommages et l’intégration à un historique véhicule comme axes futurs, et c’est exact : c’est la feuille de route, pas quelque chose que je prétends déjà fonctionnel de bout en bout.Si je devais résumer ce qui est réellement acquis aujourd’hui : un détecteur de parties de véhicule léger, honnêtement benchmarké, entraîné de manière reproductible, avec sa propre couche minimale de suivi d’expériences construite à partir de zéro. C’est une ambition plus modeste que “système de détection de fraude automatisé”, mais c’est la vraie — et c’est une meilleure fondation pour construire la suite.Le dépôt est public si vous voulez voir le code d’entraînement, le mini-ORM, ou me convaincre que j’aurais dû simplement utiliser SQLAlchemy : github.com/sanayasfp/car-inspect-ai.
Car Inspect AI : apprendre à YOLO à lire une voiture comme le ferait un inspecteur
June 22, 2026 ·
7 min
Quiconque a déjà loué une voiture ou déposé un dossier d’assurance connaît le rituel : quelqu’un fait le tour du véhicule, prend quelques photos, et une personne décide plus tard si telle marque sur le pare-chocs était déjà là. C’est lent, c’est subjectif, et les litiges entre locataires, propriétaires et assureurs sur les “dommages préexistants” sont fréquents, précisément parce que tout repose sur la parole de quelqu’un face à une poignée de photos. Car Inspect AI est ma tentative de m’attaquer à ce problème : un système de vision par ordinateur qui détecte et identifie automatiquement les différentes parties d’un véhicule à partir d’une photo, comme première brique vers une inspection plus objective et automatisée.Le code est ici : github.com/sanayasfp/car-inspect-ai.Partir de la vraie contrainte : les donnéesAvant de toucher au moindre modèle, la vraie contrainte d’un projet comme celui-ci, ce sont les données. J’ai entraîné le modèle sur le jeu de données public Car Parts Segmentation (Kitsuchart Pasupa et al.), 500 images annotées de berlines, pickups et SUV au format COCO, couvrant 18 parties distinctes du véhicule — pare-chocs, portes, feux, rétroviseurs, capot, coffre, roues, etc. — photographiées de face, de dos et sous des angles inclinés, avec plaques et visages floutés pour la confidentialité. 500 images, ce n’est pas énorme à l’échelle du deep learning, et cette contrainte a façonné presque toutes les décisions qui ont suivi.Pourquoi YOLO11n précisémentJ’ai choisi YOLO11n — la variante “nano” — de manière délibérée, pas simplement parce que “YOLO, c’est ce qu’on utilise pour la détection d’objets.” Deux raisons : C’est assez léger pour tourner sur du matériel modeste. Un outil pensé pour des garages, des petites agences de location ou des inspecteurs indépendants ne sert à rien s’il exige un GPU puissant pour faire de l’inférence. YOLO11n sacrifie un peu de précision brute contre une empreinte qui tourne confortablement sur CPU ou sur un GPU d’entrée de gamme. Avec seulement 500 images, la capacité du modèle est un handicap, pas un atout. Un modèle plus large a plus de marge pour sur-apprendre un petit jeu de données. Une architecture légère, combinée à une augmentation de données agressive, était le choix le plus honnête compte tenu de ce que j’avais réellement pour entraîner.Pour tirer le maximum de ce petit jeu de données, le prétraitement a inclus un redimensionnement aux dimensions d’entrée attendues par YOLO, ainsi qu’une augmentation par rotation (pour simuler différents angles de prise de vue), un flip horizontal, et l’ajout de bruit gaussien (pour rendre le modèle moins sensible aux variations d’éclairage — un vrai problème quand les photos viennent d’un téléphone au hasard sur un parking, pas d’un studio).L’entraînement s’est fait sur 50 époques avec un split 80/20 entraînement/validation, une taille de batch de 16, et un arrêt anticipé si la performance sur le jeu de validation stagnait pendant 10 époques consécutives. Résultat : 87% de mAP sur le jeu de validation, avec les meilleures performances sur les parties géométriquement bien définies comme les roues et les portes — exactement là où on s’attend à ce qu’un détecteur soit le plus fiable, et exactement le genre de résultat qui indique où concentrer les efforts ensuite (les petites parties ambiguës comme les rétroviseurs restent les cas les plus difficiles).Coder mon propre petit ORM plutôt que d’aller chercher SQLAlchemyC’est la partie du projet que la plupart des gens survoleraient, mais c’est celle où j’ai le plus appris. L’application doit suivre les runs d’entraînement — quel modèle, combien d’époques, si l’entraînement s’est terminé, où se trouve le checkpoint, et s’il reprend un run précédent. Plutôt que d’importer SQLAlchemy pour ce qui reste fondamentalement une poignée de tables, j’ai écrit moi-même une petite couche de modèles basée sur les dataclasses :@dataclasses.dataclassclass TrainLogsModel(BaseModel): _table_name = "train_logs" name: str epochs: int model: str path: str completed: bool = Field(type=bool, default=False).set() id: Optional[int] = Field(type=int, primary_key=True, autoincrement=True).set() created_at: Optional[float] = Field(type=int, default=lambda: dt.now().timestamp()).set() resumed_from: Optional[int] = Field(type=int, foreign_key="id", foreign_table=_table_name).set()BaseModel lit les annotations de type et les métadonnées de la dataclass et les transforme en définitions de colonnes SQL (INTEGER, TEXT, REAL, avec clés primaires, autoincrément et clés étrangères gérées explicitement), et déduit les noms de table à partir des noms de classe en snake_case, camelCase ou PascalCase selon le besoin. C’est une fraction de ce que fait un vrai ORM — pas de query builder, pas de système de migrations — mais écrire cette fraction à la main m’a obligé à vraiment comprendre ce qu’un ORM automatise, plutôt que d’en importer un et de faire confiance à la magie. C’est un arbitrage que je referais : pour un projet de cette taille, écrire 100 lignes pour comprendre le mécanisme valait mieux que 10 lignes qui le cachent.Ce modèle alimente une fonctionnalité réellement utile : la page d’entraînement permet de choisir soit un YOLO11n de base, soit n’importe quel checkpoint précédent, de lancer l’entraînement et de le journaliser — y compris une clé étrangère resumed_from pointant vers le run dont il repart, ce qui me donne une vraie généalogie d’expériences plutôt qu’un dossier plein de best_v2_final_FINAL.pt.L’interface : Streamlit, par choixLe tout est enveloppé dans une petite application Streamlit multi-pages — une page d’accueil, une page “enregistrer une voiture” (upload des photos avant/arrière/gauche/droite plus couleur et numéro de plaque), la page d’entraînement décrite plus haut, et une page de brouillons pour les expérimentations en cours. Streamlit était le bon choix ici précisément parce que ce n’est pas l’objet du projet : je voulais passer mon temps sur le modèle de détection et sur le suivi des entraînements, pas sur un frontend fait main, et Streamlit s’efface pour ça.Où en est vraiment le projet aujourd’huiJe préfère être précis plutôt que d’arrondir : le modèle de détection des parties et le pipeline d’entraînement/versioning fonctionnent et sont mesurés (ce chiffre de 87% de mAP est réel, issu d’un run effectif, pas d’une estimation). Le pipeline d’inspection complet — fusionner les quatre angles d’un véhicule en un seul rapport fiable, et passer de “voici les parties détectées” à un véritable verdict de dommage ou de fraude — reste un chantier actif, pas un produit terminé. Le README liste la notation automatique de la gravité des dommages et l’intégration à un historique véhicule comme axes futurs, et c’est exact : c’est la feuille de route, pas quelque chose que je prétends déjà fonctionnel de bout en bout.Si je devais résumer ce qui est réellement acquis aujourd’hui : un détecteur de parties de véhicule léger, honnêtement benchmarké, entraîné de manière reproductible, avec sa propre couche minimale de suivi d’expériences construite à partir de zéro. C’est une ambition plus modeste que “système de détection de fraude automatisé”, mais c’est la vraie — et c’est une meilleure fondation pour construire la suite.Le dépôt est public si vous voulez voir le code d’entraînement, le mini-ORM, ou me convaincre que j’aurais dû simplement utiliser SQLAlchemy : github.com/sanayasfp/car-inspect-ai.
Le Guide Ultime pour un Environnement de Dev Fluide
July 31, 2026 ·
11 min
J’ai écrit au sujet de Stack il y a quelques jours — pour vous donner la version courte de pourquoi je l’ai créé et de toute la philosophie “sans Docker”. Mais la théorie a ses limites. Si vous cherchez le guide ultime pour obtenir un environnement de développement parfaitement fluide, vous êtes au bon endroit.Pas de pitch commercial. Pas de blabla marketing. Juste la réalité brute et pratique de la mise en route d’un projet sans effort, avec chaque invite de commande et chaque sortie console un peu bizarre que vous rencontrerez en cours de route.Si vous voulez juste la documentation de référence brute, vous la trouverez sur sanayavo.com/stack. Sinon, bienvenue dans la visite guidée d’un meilleur flux de travail (workflow).Ce que vous obtenez vraimentUn environnement fluide signifie zéro superflu. Stack n’est qu’un seul binaire Rust. Il n’y a pas de démon qui monopolise la RAM dans votre barre des tâches, pas de Docker Desktop qui fait tourner les ventilateurs de votre ordinateur portable à fond, et absolument rien qui ne touche à votre installation PHP globale du système.Au lieu de cela, il agit comme du ruban adhésif (duct tape) hautement intelligent. Il s’appuie sur vfox pour gérer les versions de PHP/Node, uv pour Python, et Caddy pour des domaines locaux instantanés. Stack lit un simple fichier stack.toml à la racine de votre projet et connecte automatiquement ces trois outils ensemble.Note : Stack est uniquement disponible sur Windows pour le moment (PowerShell ou cmd). Le support de Mac et Linux est prévu sur la feuille de route.L’installation : Votre seul et unique mal de crâneUn flux de travail quotidien fluide nécessite un tout petit peu de configuration au départ. Exécutez ceci pour télécharger l’installateur :irm https://github.com/sanayasfp/stack/releases/latest/download/stackenv-installer.ps1 | iexEnsuite, exécutez cette commande une seule fois par machine :stack setupCela s’occupe du gros du travail en arrière-plan. Cela ajoute un hook (crochet) à votre profil PowerShell (pour que la bonne version de PHP ou Node s’active par magie lorsque vous faites un cd dans un répertoire), installe vfox, uv et Caddy dans des versions largement testées, et exécute caddy trust. Cette dernière étape est cruciale pour le HTTPS local — on y revient dans une minute.$ stack setupadded the stack hook for pwshchecking vfox/uv/caddy... vfox: OK (1.0.11) uv: OK (0.11.7) caddy: OK (2.11.4) caddy: local CA trusted (https://*.localhost works with no browser warning)Redémarrez votre terminal maintenant. Si vous sautez cette étape, le hook PowerShell ne sera pas actif et vous vous demanderez pourquoi rien ne fonctionne. Vous n’aurez plus jamais à exécuter stack setup à moins d’acheter un nouvel ordinateur.Démarrer un projet sans frictionConstruisons quelque chose de concret pour vous montrer le fonctionnement : une API PHP légère adossée à MySQL.À partir de zéro$ stack new acme-apidomain: acme-api.localhostlanguages (space to toggle, enter to confirm): [x] phpservices (space to toggle, enter to confirm): [x] mysql php version: 8.3.1 mysql version: 8.0.35created acme-api\stack.tomlnext: cd into it, add a [run] command when you know it, then `stack up`C’est une liste de contrôle interactive dans le terminal. Espace pour basculer (cocher/décocher), Entrée pour confirmer. Ça fait un peu bizarre la première fois, mais rien n’est validé tant que vous n’appuyez pas sur Entrée. Si vous sélectionnez accidentellement Node au lieu de PHP, appuyez à nouveau sur espace. Zéro stress, pas besoin de tout recommencer.Si vous avez déjà un projetSi votre dossier acme-api possède déjà un fichier composer.json spécifiant "php": "^8.3", Stack est assez intelligent pour vous éviter de taper manuellement :$ stack initdetected from existing project files: php 8.3 (from composer.json)domain: acme-api.localhoststack init analyse automatiquement composer.json, package.json ou pyproject.toml et pré-remplit l’assistant pour vous.L’artefact magiqueStack génère un manifeste très lisible :[project]name = "acme-api"domain = "acme-api.localhost"[language]php = "8.3.1"[service.mysql]version = "8.0.35"Commitez ce fichier stack.toml dans votre dépôt (repo). C’est votre artefact ultime du type “ça marche sur ma machine” — l’équivalent léger et sans conteneur d’un Dockerfile. Lorsqu’un collègue clone le dépôt et tape stack up, il obtient instantanément les mêmes versions exactes de PHP et MySQL, sans avoir à installer quoi que ce soit à la main.Lui apprendre comment s’exécuterVotre stack.toml doit savoir comment démarrer votre serveur de développement. Ouvrez le fichier et ajoutez un bloc [run] :[run]command = "php -S 127.0.0.1:{port} -t public"L’espace réservé {port} est dynamiquement remplacé par un port disponible au moment de l’exécution. (Note : Si vous déclarez [language.php] mais ignorez la section [run], Stack se rabat sur sa propre configuration FastCGI intégrée utilisant php-cgi.exe et Caddy, ce qui est incroyablement solide. Mais pour ce guide, nous allons rester explicites.)stack up et regardez la magie opérer$ cd acme-api$ stack upLoaded C:\Users\you\acme-api\stack.toml project: acme-api domain: acme-api.localhost languages: php services: mysqlfirst run for this project — stack.toml will execute: [run] php -S 127.0.0.1:{port} -t publicTrust and run these commands? [y/N] y php: C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe -> PHP 8.3.1 (cli) service.mysql: started (pid 41232, port 3306) schema 'acme_api' — automatic creation not yet implemented; create it manually if needed run: php -S 127.0.0.1:52140 -t public (pid 41244, port 52140) log: C:\Users\you\.stack\logs\acme-api.log routed: http://acme-api.localhost -> 127.0.0.1:52140Il y a deux choses majeures qui se passent ici et qui contribuent à une expérience sans accroc :1. L’invite de confiance de sécuritéParce que [run].command est une invocation littérale du shell, Stack l’exécute exactement tel qu’il est écrit. La première fois que Stack voit une commande nouvelle ou modifiée (comme après un git pull), il vous demande de la confirmer. Dites oui, et il mémorise votre choix dans ~/.stack/trust.json. Chaque stack up suivant sera heureusement silencieux. Besoin de contourner cela pour la CI ? Passez simplement le flag --yes.2. Le HTTPS fonctionne immédiatementLa console affiche http://, mais https://acme-api.localhost fonctionne instantanément. Aucun avertissement rouge du navigateur. Vous vous souvenez de cette commande caddy trust lors de la phase de configuration ? Elle installe une autorité de certification locale (similaire à mkcert), ce qui signifie que votre environnement local reflète fidèlement le SSL de production dès le premier jour.Ce que “sans conteneurs” signifie vraiment en pratiqueOuvrez un deuxième terminal, faites un cd dans un projet totalement différent qui nécessite également MySQL 8.0.35, et exécutez stack up : service.mysql: already running, shared with other projects (pid 41232, port 3306)C’est toute la beauté de la chose. Un seul processus MySQL sert deux projets, isolés par schéma plutôt que de brûler votre processeur et votre RAM avec des conteneurs redondants.Mais c’est ici que le facteur “fluide” brille vraiment. Ouvrez un troisième terminal, faites un cd dans acme-api, et exécutez simplement php -v. Pas de stack up, pas de commandes supplémentaires :$ cd acme-api$ php -vPHP 8.3.1 (cli) (built: ...)$ cd ..$ php -v'php' is not recognized as an internal or external commandLe changement de version se fait de manière ambiante à chaque invite de commande grâce à ce hook PowerShell. Votre IDE, composer install et vos exécuteurs de tests obtiennent tous automatiquement la bonne version épinglée à la seconde où ils entrent dans le dossier. Quittez le répertoire, et elle disparaît. Pas de source venv/bin/activate, pas de nvm use, et pas de pollution de votre système global.Des installations PHP toutes neuves qui ne craignent pasHabituellement, une nouvelle installation PHP signifie passer une après-midi à traquer les erreurs “PDO driver not found” ou “timezone not set”. Stack élimine complètement cela.La première fois qu’il télécharge une version de PHP via vfox, il patche automatiquement php.ini. Il active OPcache, augmente les limites de mémoire et de téléchargement (upload), définit un fuseau horaire, et active les extensions dont vous avez réellement besoin par défaut (pdo_mysql, pdo_pgsql, pdo_sqlite, sockets, sodium, etc.). Cela se fait une fois, automatiquement, et vous n’avez plus jamais à y penser.stack doctor — L’ultime vérification avant le volAvant de transmettre un projet à un collègue ou de commencer à déboguer un problème étrange, exécutez ceci :$ stack doctor --projectchecking C:\Users\you\acme-api\stack.toml... language.php: OK (C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe) service.mysql: OK (managed)Cette commande valide vos ports, chemins et valeurs {PLACEHOLDER} par rapport à votre environnement (en chargeant d’abord .env) sans démarrer un seul service. Si quelque chose est cassé, vous obtenez une liste claire dès le départ au lieu de découvrir des erreurs cryptiques au beau milieu du démarrage.Pilotez votre configuration de n’importe oùUne fois que Stack connaît un projet, vous n’avez même pas besoin d’être dans son dossier pour le gérer :stack describe acme-apistack restart acme-apistack down acme-apistack describe affiche tout — les chemins binaires résolus, l’emplacement exact de votre php.ini haché, les logs, et les domaines routés. Stack conserve un registre local de vos projets, ce qui rend la gestion globale sans effort.Tout fermer proprementQuand la journée est terminée :stack down --allTout s’arrête. Chaque projet, chaque service partagé, et Caddy. Vous obtenez une utilisation CPU vraiment à zéro, et non un conteneur fantôme persistant que vous avez oublié de tuer.Une petite note sur les domaines personnalisésL’utilisation de .localhost garantit une expérience fluide car les navigateurs le résolvent automatiquement vers l’adresse de bouclage (RFC 6761) sans que vous ayez besoin de bidouiller votre fichier hosts. Cependant, si vous migrez depuis Laragon ou Herd et que vous avez absolument besoin de domaines .test, il y a une configuration unique à faire en utilisant Acrylic DNS Proxy.Votre Nouveau Flux de TravailGénérez (Scaffold), exécutez, faites confiance une fois, profitez du HTTPS automatique, commitez votre manifeste, et détruisez tout proprement quand vous avez terminé.J’utilise ce flux de travail exact pour du vrai travail client chaque jour. Ce n’est pas un scénario idéal théorique ; c’est un plan d’action éprouvé pour un environnement de développement ultime et fluide.Pour la référence complète de stack.toml, les options de ligne de commande (CLI), et les fonctionnalités avancées (comme l’utilisation de [[clone]] pour démarrer à partir d’un simple manifeste), consultez la documentation officielle : Pourquoi j’ai créé cela — la longue diatribe contre Docker/XAMPP pour le dev local Pour commencer Référence du Manifeste Référence CLI Code : github.com/sanayasfp/stack
Le Guide Ultime pour un Environnement de Dev Fluide
July 31, 2026 ·
11 min
J’ai écrit au sujet de Stack il y a quelques jours — pour vous donner la version courte de pourquoi je l’ai créé et de toute la philosophie “sans Docker”. Mais la théorie a ses limites. Si vous cherchez le guide ultime pour obtenir un environnement de développement parfaitement fluide, vous êtes au bon endroit.Pas de pitch commercial. Pas de blabla marketing. Juste la réalité brute et pratique de la mise en route d’un projet sans effort, avec chaque invite de commande et chaque sortie console un peu bizarre que vous rencontrerez en cours de route.Si vous voulez juste la documentation de référence brute, vous la trouverez sur sanayavo.com/stack. Sinon, bienvenue dans la visite guidée d’un meilleur flux de travail (workflow).Ce que vous obtenez vraimentUn environnement fluide signifie zéro superflu. Stack n’est qu’un seul binaire Rust. Il n’y a pas de démon qui monopolise la RAM dans votre barre des tâches, pas de Docker Desktop qui fait tourner les ventilateurs de votre ordinateur portable à fond, et absolument rien qui ne touche à votre installation PHP globale du système.Au lieu de cela, il agit comme du ruban adhésif (duct tape) hautement intelligent. Il s’appuie sur vfox pour gérer les versions de PHP/Node, uv pour Python, et Caddy pour des domaines locaux instantanés. Stack lit un simple fichier stack.toml à la racine de votre projet et connecte automatiquement ces trois outils ensemble.Note : Stack est uniquement disponible sur Windows pour le moment (PowerShell ou cmd). Le support de Mac et Linux est prévu sur la feuille de route.L’installation : Votre seul et unique mal de crâneUn flux de travail quotidien fluide nécessite un tout petit peu de configuration au départ. Exécutez ceci pour télécharger l’installateur :irm https://github.com/sanayasfp/stack/releases/latest/download/stackenv-installer.ps1 | iexEnsuite, exécutez cette commande une seule fois par machine :stack setupCela s’occupe du gros du travail en arrière-plan. Cela ajoute un hook (crochet) à votre profil PowerShell (pour que la bonne version de PHP ou Node s’active par magie lorsque vous faites un cd dans un répertoire), installe vfox, uv et Caddy dans des versions largement testées, et exécute caddy trust. Cette dernière étape est cruciale pour le HTTPS local — on y revient dans une minute.$ stack setupadded the stack hook for pwshchecking vfox/uv/caddy... vfox: OK (1.0.11) uv: OK (0.11.7) caddy: OK (2.11.4) caddy: local CA trusted (https://*.localhost works with no browser warning)Redémarrez votre terminal maintenant. Si vous sautez cette étape, le hook PowerShell ne sera pas actif et vous vous demanderez pourquoi rien ne fonctionne. Vous n’aurez plus jamais à exécuter stack setup à moins d’acheter un nouvel ordinateur.Démarrer un projet sans frictionConstruisons quelque chose de concret pour vous montrer le fonctionnement : une API PHP légère adossée à MySQL.À partir de zéro$ stack new acme-apidomain: acme-api.localhostlanguages (space to toggle, enter to confirm): [x] phpservices (space to toggle, enter to confirm): [x] mysql php version: 8.3.1 mysql version: 8.0.35created acme-api\stack.tomlnext: cd into it, add a [run] command when you know it, then `stack up`C’est une liste de contrôle interactive dans le terminal. Espace pour basculer (cocher/décocher), Entrée pour confirmer. Ça fait un peu bizarre la première fois, mais rien n’est validé tant que vous n’appuyez pas sur Entrée. Si vous sélectionnez accidentellement Node au lieu de PHP, appuyez à nouveau sur espace. Zéro stress, pas besoin de tout recommencer.Si vous avez déjà un projetSi votre dossier acme-api possède déjà un fichier composer.json spécifiant "php": "^8.3", Stack est assez intelligent pour vous éviter de taper manuellement :$ stack initdetected from existing project files: php 8.3 (from composer.json)domain: acme-api.localhoststack init analyse automatiquement composer.json, package.json ou pyproject.toml et pré-remplit l’assistant pour vous.L’artefact magiqueStack génère un manifeste très lisible :[project]name = "acme-api"domain = "acme-api.localhost"[language]php = "8.3.1"[service.mysql]version = "8.0.35"Commitez ce fichier stack.toml dans votre dépôt (repo). C’est votre artefact ultime du type “ça marche sur ma machine” — l’équivalent léger et sans conteneur d’un Dockerfile. Lorsqu’un collègue clone le dépôt et tape stack up, il obtient instantanément les mêmes versions exactes de PHP et MySQL, sans avoir à installer quoi que ce soit à la main.Lui apprendre comment s’exécuterVotre stack.toml doit savoir comment démarrer votre serveur de développement. Ouvrez le fichier et ajoutez un bloc [run] :[run]command = "php -S 127.0.0.1:{port} -t public"L’espace réservé {port} est dynamiquement remplacé par un port disponible au moment de l’exécution. (Note : Si vous déclarez [language.php] mais ignorez la section [run], Stack se rabat sur sa propre configuration FastCGI intégrée utilisant php-cgi.exe et Caddy, ce qui est incroyablement solide. Mais pour ce guide, nous allons rester explicites.)stack up et regardez la magie opérer$ cd acme-api$ stack upLoaded C:\Users\you\acme-api\stack.toml project: acme-api domain: acme-api.localhost languages: php services: mysqlfirst run for this project — stack.toml will execute: [run] php -S 127.0.0.1:{port} -t publicTrust and run these commands? [y/N] y php: C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe -> PHP 8.3.1 (cli) service.mysql: started (pid 41232, port 3306) schema 'acme_api' — automatic creation not yet implemented; create it manually if needed run: php -S 127.0.0.1:52140 -t public (pid 41244, port 52140) log: C:\Users\you\.stack\logs\acme-api.log routed: http://acme-api.localhost -> 127.0.0.1:52140Il y a deux choses majeures qui se passent ici et qui contribuent à une expérience sans accroc :1. L’invite de confiance de sécuritéParce que [run].command est une invocation littérale du shell, Stack l’exécute exactement tel qu’il est écrit. La première fois que Stack voit une commande nouvelle ou modifiée (comme après un git pull), il vous demande de la confirmer. Dites oui, et il mémorise votre choix dans ~/.stack/trust.json. Chaque stack up suivant sera heureusement silencieux. Besoin de contourner cela pour la CI ? Passez simplement le flag --yes.2. Le HTTPS fonctionne immédiatementLa console affiche http://, mais https://acme-api.localhost fonctionne instantanément. Aucun avertissement rouge du navigateur. Vous vous souvenez de cette commande caddy trust lors de la phase de configuration ? Elle installe une autorité de certification locale (similaire à mkcert), ce qui signifie que votre environnement local reflète fidèlement le SSL de production dès le premier jour.Ce que “sans conteneurs” signifie vraiment en pratiqueOuvrez un deuxième terminal, faites un cd dans un projet totalement différent qui nécessite également MySQL 8.0.35, et exécutez stack up : service.mysql: already running, shared with other projects (pid 41232, port 3306)C’est toute la beauté de la chose. Un seul processus MySQL sert deux projets, isolés par schéma plutôt que de brûler votre processeur et votre RAM avec des conteneurs redondants.Mais c’est ici que le facteur “fluide” brille vraiment. Ouvrez un troisième terminal, faites un cd dans acme-api, et exécutez simplement php -v. Pas de stack up, pas de commandes supplémentaires :$ cd acme-api$ php -vPHP 8.3.1 (cli) (built: ...)$ cd ..$ php -v'php' is not recognized as an internal or external commandLe changement de version se fait de manière ambiante à chaque invite de commande grâce à ce hook PowerShell. Votre IDE, composer install et vos exécuteurs de tests obtiennent tous automatiquement la bonne version épinglée à la seconde où ils entrent dans le dossier. Quittez le répertoire, et elle disparaît. Pas de source venv/bin/activate, pas de nvm use, et pas de pollution de votre système global.Des installations PHP toutes neuves qui ne craignent pasHabituellement, une nouvelle installation PHP signifie passer une après-midi à traquer les erreurs “PDO driver not found” ou “timezone not set”. Stack élimine complètement cela.La première fois qu’il télécharge une version de PHP via vfox, il patche automatiquement php.ini. Il active OPcache, augmente les limites de mémoire et de téléchargement (upload), définit un fuseau horaire, et active les extensions dont vous avez réellement besoin par défaut (pdo_mysql, pdo_pgsql, pdo_sqlite, sockets, sodium, etc.). Cela se fait une fois, automatiquement, et vous n’avez plus jamais à y penser.stack doctor — L’ultime vérification avant le volAvant de transmettre un projet à un collègue ou de commencer à déboguer un problème étrange, exécutez ceci :$ stack doctor --projectchecking C:\Users\you\acme-api\stack.toml... language.php: OK (C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe) service.mysql: OK (managed)Cette commande valide vos ports, chemins et valeurs {PLACEHOLDER} par rapport à votre environnement (en chargeant d’abord .env) sans démarrer un seul service. Si quelque chose est cassé, vous obtenez une liste claire dès le départ au lieu de découvrir des erreurs cryptiques au beau milieu du démarrage.Pilotez votre configuration de n’importe oùUne fois que Stack connaît un projet, vous n’avez même pas besoin d’être dans son dossier pour le gérer :stack describe acme-apistack restart acme-apistack down acme-apistack describe affiche tout — les chemins binaires résolus, l’emplacement exact de votre php.ini haché, les logs, et les domaines routés. Stack conserve un registre local de vos projets, ce qui rend la gestion globale sans effort.Tout fermer proprementQuand la journée est terminée :stack down --allTout s’arrête. Chaque projet, chaque service partagé, et Caddy. Vous obtenez une utilisation CPU vraiment à zéro, et non un conteneur fantôme persistant que vous avez oublié de tuer.Une petite note sur les domaines personnalisésL’utilisation de .localhost garantit une expérience fluide car les navigateurs le résolvent automatiquement vers l’adresse de bouclage (RFC 6761) sans que vous ayez besoin de bidouiller votre fichier hosts. Cependant, si vous migrez depuis Laragon ou Herd et que vous avez absolument besoin de domaines .test, il y a une configuration unique à faire en utilisant Acrylic DNS Proxy.Votre Nouveau Flux de TravailGénérez (Scaffold), exécutez, faites confiance une fois, profitez du HTTPS automatique, commitez votre manifeste, et détruisez tout proprement quand vous avez terminé.J’utilise ce flux de travail exact pour du vrai travail client chaque jour. Ce n’est pas un scénario idéal théorique ; c’est un plan d’action éprouvé pour un environnement de développement ultime et fluide.Pour la référence complète de stack.toml, les options de ligne de commande (CLI), et les fonctionnalités avancées (comme l’utilisation de [[clone]] pour démarrer à partir d’un simple manifeste), consultez la documentation officielle : Pourquoi j’ai créé cela — la longue diatribe contre Docker/XAMPP pour le dev local Pour commencer Référence du Manifeste Référence CLI Code : github.com/sanayasfp/stack
Laplace Nearby : un bot WhatsApp qui trouve « le maquis le plus proche » sans exploser la facture LLM
June 22, 2026 ·
9 min
À Abidjan, une grande partie des commerces de proximité — le maquis du quartier, la pharmacie, le réparateur de téléphones — n’ont ni site web, ni fiche Google Business, ni même une adresse postale formelle la moitié du temps. Ce qu’ils ont, en revanche, c’est WhatsApp. Laplace Nearby part de ce constat : plutôt que de construire une application que les gens doivent télécharger, autant construire l’expérience de recherche dans l’application que tout le monde a déjà ouverte, et laisser les gens décrire ce qu’ils cherchent en langage naturel — « il me faut une pharmacie », « j’ai envie de porcodjo » — plutôt que de remplir un formulaire de filtres.L’assistant lui-même s’appelle en interne Simon. Ce billet parle de la façon dont Simon est réellement construit — les parties que je trouve intéressantes à raconter, pas un argumentaire commercial.Le dépôt : github.com/sanayasfp/laplace-nearby.Décision n°1 : le moteur ignore qu’il parle à WhatsAppLe cœur du système est SimonEngine, un petit orchestrateur qui prend en entrée une InteractionRequest indépendante du canal et renvoie une InteractionResponse tout aussi indépendante. Les spécificités WhatsApp — format des webhooks, mise en forme des messages, boutons et listes — vivent entièrement dans une couche d’adaptateurs/renderers en dehors du moteur. Le moteur lui-même fait quatre choses à chaque message : charger le contexte de session de l’utilisateur, vérifier le rate limiting, transmettre le message au flow conversationnel actif (idle, recherche, ou enregistrement), puis sauvegarder le résultat — en émettant systématiquement un effet de bord analytique avec le nom du flow, la transition d’état et la latence, qu’il se soit passé quelque chose d’intéressant ou non :async process(req: InteractionRequest): Promise<InteractionResponse> { const context = await this.contextManager.load(req.profileId); const flow = this.flowRegistry.get(context.session.activeFlow || IDLE); const limitResult = this.rateLimitService.check(context.rateLimit); if (!limitResult.allowed) { return { messages: limitResult.notify ? [Responses.tooManyMessages(...)] : [] , ... }; } const response = await flow.handle(req, context, interactionId); await this.contextManager.save(req.profileId, response.contextUpdate); return { ...response, sideEffects: [summaryEffect, ...response.sideEffects] };}Ce n’est pas de l’architecture pour le plaisir. Cela signifie que la logique de recherche, le flow d’enregistrement, le rate limiting et les métriques n’auront pas besoin d’être réécrits si un second canal (SMS, un widget web, peu importe) arrive un jour — ils n’ont jamais été couplés à WhatsApp en premier lieu.Décision n°2 : ne pas appeler le LLM si on peut s’en passerChaque message entrant a besoin d’une intention — s’agit-il d’une recherche, d’une demande d’enregistrement, de bavardage, d’un remerciement, d’une insulte, d’un « stop » ? Faire passer chaque message par un appel LLM est la solution de facilité, et aussi la plus lente et la plus coûteuse. La classification d’intention est donc à deux étages.FastIntentDetector s’exécute en premier : un ensemble volontairement exhaustif d’expressions régulières couvrant le français, l’anglais, et l’argot local qu’on croise réellement dans une conversation WhatsApp à Abidjan — « wesh », « cc », « gab » (guichet automatique), « essence/gazoil », des dizaines de variantes orthographiques de « merci », « stop », « annule ». Sa docstring dit exactement à quoi il sert : « Reduce LLM costs and latency for unambiguous user requests » (réduire les coûts et la latence liés au LLM pour les requêtes non ambiguës). Ce n’est que lorsque rien ne correspond que IntentService bascule sur Gemini (gemini-2.5-flash-lite), avec une température à 0, un mode de réponse JSON, et un plafond de 150 tokens, en demandant une sortie structurée : l’intention, un mot-clé de recherche extrait, et une liste de termes sémantiquement proches pour élargir la recherche (« j’ai envie de porcodjo » → mot-clé « porcodjo restaurant » ; « mon habit est sale » → « pressing nettoyage vêtement »).Les deux chemins signalent leur origine — REGEX ou LLM — à un compteur Prometheus (simonIntentTotal), ce qui fait que la part du trafic détournée du modèle payant est quelque chose qu’on peut littéralement observer sur un dashboard, pas quelque chose qu’on devine. Et l’appel à Gemini lui-même est protégé par un circuit breaker : s’il se déclenche, le système ne plante pas la conversation, il dégrade vers une réponse neutre de type « bavardage » avec une confiance à 0, et continue.Décision n°3 : les adresses à Abidjan ne fonctionnent pas comme ailleursUne grande partie des adresses données dans le chat ne sont pas des chaînes géocodables — ce sont des descriptions : « je suis vers la cité Abdoulaye Diallo ». AddressCodingService prend ce type d’entrée, vérifie d’abord un cache sémantique (hash exact, puis similarité par embedding au-delà d’un seuil de 0,88) pour éviter de repayer un traitement déjà résolu, et en cas d’échec, demande à Gemini de la transformer en adresse standardisée, un quartier extrait, et — c’est le point important — un indicateur précisant si la description est assez précise pour être géocodée, ou si la seule réponse honnête est de demander à l’utilisateur d’envoyer un point GPS. Il existe même un dictionnaire dédié de termes nouchi (l’argot de rue abidjanais) qui alimente ce pipeline, parce qu’un outillage NLP générique ne sait pas ce que désigne un arrêt de « gbaka » ou le surnom d’un quartier.Décision n°4 : la recherche, c’est une seule fonction SQL, trois signaux, fusionnésC’est la partie du code dont je suis le plus fier. search_nearby_places est une unique fonction Postgres qui combine trois signaux de classement indépendants pour chaque commerce candidat dans un rayon donné : Le rang texte intégral (ts_rank_cd sur un tsquery en français) — utile quand l’utilisateur a tapé quelque chose de proche du nom ou de la catégorie réelle du commerce. Le rang de similarité vectorielle — distance cosinus entre l’embedding de la requête (Gemini text-embedding-004, pgvector avec un index HNSW) et l’embedding de chaque lieu — utile quand l’utilisateur décrit ce qu’il veut avec ses propres mots plutôt qu’en reprenant une étiquette. La proximité géographique (ST_Distance sur une colonne geography PostGIS) — parce que « le plus proche » compte toujours, et ne peut pas être compensé par la seule pertinence.Ces signaux sont combinés via une Reciprocal Rank Fusion pondérée : score = w_fts/(k + fts_rank) + w_vec/(k + vec_rank) + w_prox * proximité, avec des poids qui varient selon que l’utilisateur ait donné ou non un mot-clé — 45/45/10 entre texte, vecteur et proximité s’il y a un mot-clé à faire correspondre ; 85% piloté par le vecteur sinon, puisque la recherche texte intégral n’a rien à quoi s’accrocher dans une requête purement descriptive. Il existe aussi un mécanisme de « palier premium » : une seconde passe de classement, partitionnée par palier, garantit aux commerces payants/référencés un petit quota de places sans pour autant noyer la pertinence pour l’ensemble des résultats.Je suis revenu plus tard réécrire cette même fonction pour la performance, après avoir remarqué qu’elle effectuait du travail redondant : fusionner deux CTE qui relisaient deux fois les mêmes lignes, ajouter une LIMIT à l’intérieur du CTE de classement vectoriel spécifiquement pour que l’index HNSW puisse s’arrêter court plutôt que de trier tout le pool de candidats, et remplacer deux matérialisations complètes séparées (résultats généraux, résultats premium) par une seule passe fenêtrée utilisant ROW_NUMBER() OVER (PARTITION BY is_premium ...). C’est le genre de correction vraiment satisfaisant — même résultat, mesurablement moins de travail par requête — et c’est le genre de chose qui n’apparaît que lorsque l’usage réel met sous pression un premier jet.Décision n°5 : les effets de bord sont des données, pas des actionsQuand un lieu est enregistré, qu’une interaction doit être journalisée pour l’analytique, ou qu’un utilisateur doit recevoir une notification, le moteur n’exécute pas ce travail en ligne — il renvoie un simple objet SideEffect décrivant ce qui doit se produire. Un PgmqSideEffectDispatcher collecte ces effets, les regroupe par file cible, et les pousse via pgmq.send_batch (l’extension de file de messages propre à Postgres) dans la même transaction de base de données que celle qui marque un lieu nouvellement enregistré comme « en attente d’indexation ». Ce détail compte : cela signifie qu’un lieu ne peut pas se retrouver à moitié enregistré — visible dans l’application mais jamais réellement indexé pour la recherche — parce que la mise à jour du statut et l’écriture dans la file valident ensemble, ou aucune des deux. Des Supabase Edge Functions en bout de chaîne (un analytics-worker, un place-embedding-worker, un place-tagging-worker) vident ces files de manière asynchrone.La stack, sans détourFastify + TypeScript sur Node.js, Prisma au-dessus de Supabase Postgres (avec pgvector, pgmq, PostGIS, pg_cron et pg_net qui font un vrai travail, pas juste des lignes dans un fichier de dépendances), Redis pour le cache et l’état du rate limiting, Gemini à la fois pour le NLU et les embeddings, Geoapify pour le géocodage, et des métriques Prometheus intégrées dès le départ plutôt que rajoutées après coup.Où en est le projetLe package.json indique 0.4.0-rc.1, et je tiens à laisser ce contexte tel quel : c’est un système réel et fonctionnel qui me sert à raisonner sur la recherche hybride et l’ingénierie conversationnelle, en évolution active vers une version 1.0 — pas un produit fini, à grande échelle, avec un portefeuille de clients derrière. La conception indépendante du canal existe précisément pour que, si ce projet doit un jour dépasser le cadre d’un bot WhatsApp, le moteur sous-jacent n’ait pas besoin d’être reconstruit.Si vous voulez débattre des poids de la RRF, me dire que PostGIS était superflu, ou pointer une meilleure façon de structurer le dispatcher d’effets de bord, le code est public : github.com/sanayasfp/laplace-nearby.
Car Inspect AI : apprendre à YOLO à lire une voiture comme le ferait un inspecteur
June 22, 2026 ·
7 min
Quiconque a déjà loué une voiture ou déposé un dossier d’assurance connaît le rituel : quelqu’un fait le tour du véhicule, prend quelques photos, et une personne décide plus tard si telle marque sur le pare-chocs était déjà là. C’est lent, c’est subjectif, et les litiges entre locataires, propriétaires et assureurs sur les “dommages préexistants” sont fréquents, précisément parce que tout repose sur la parole de quelqu’un face à une poignée de photos. Car Inspect AI est ma tentative de m’attaquer à ce problème : un système de vision par ordinateur qui détecte et identifie automatiquement les différentes parties d’un véhicule à partir d’une photo, comme première brique vers une inspection plus objective et automatisée.Le code est ici : github.com/sanayasfp/car-inspect-ai.Partir de la vraie contrainte : les donnéesAvant de toucher au moindre modèle, la vraie contrainte d’un projet comme celui-ci, ce sont les données. J’ai entraîné le modèle sur le jeu de données public Car Parts Segmentation (Kitsuchart Pasupa et al.), 500 images annotées de berlines, pickups et SUV au format COCO, couvrant 18 parties distinctes du véhicule — pare-chocs, portes, feux, rétroviseurs, capot, coffre, roues, etc. — photographiées de face, de dos et sous des angles inclinés, avec plaques et visages floutés pour la confidentialité. 500 images, ce n’est pas énorme à l’échelle du deep learning, et cette contrainte a façonné presque toutes les décisions qui ont suivi.Pourquoi YOLO11n précisémentJ’ai choisi YOLO11n — la variante “nano” — de manière délibérée, pas simplement parce que “YOLO, c’est ce qu’on utilise pour la détection d’objets.” Deux raisons : C’est assez léger pour tourner sur du matériel modeste. Un outil pensé pour des garages, des petites agences de location ou des inspecteurs indépendants ne sert à rien s’il exige un GPU puissant pour faire de l’inférence. YOLO11n sacrifie un peu de précision brute contre une empreinte qui tourne confortablement sur CPU ou sur un GPU d’entrée de gamme. Avec seulement 500 images, la capacité du modèle est un handicap, pas un atout. Un modèle plus large a plus de marge pour sur-apprendre un petit jeu de données. Une architecture légère, combinée à une augmentation de données agressive, était le choix le plus honnête compte tenu de ce que j’avais réellement pour entraîner.Pour tirer le maximum de ce petit jeu de données, le prétraitement a inclus un redimensionnement aux dimensions d’entrée attendues par YOLO, ainsi qu’une augmentation par rotation (pour simuler différents angles de prise de vue), un flip horizontal, et l’ajout de bruit gaussien (pour rendre le modèle moins sensible aux variations d’éclairage — un vrai problème quand les photos viennent d’un téléphone au hasard sur un parking, pas d’un studio).L’entraînement s’est fait sur 50 époques avec un split 80/20 entraînement/validation, une taille de batch de 16, et un arrêt anticipé si la performance sur le jeu de validation stagnait pendant 10 époques consécutives. Résultat : 87% de mAP sur le jeu de validation, avec les meilleures performances sur les parties géométriquement bien définies comme les roues et les portes — exactement là où on s’attend à ce qu’un détecteur soit le plus fiable, et exactement le genre de résultat qui indique où concentrer les efforts ensuite (les petites parties ambiguës comme les rétroviseurs restent les cas les plus difficiles).Coder mon propre petit ORM plutôt que d’aller chercher SQLAlchemyC’est la partie du projet que la plupart des gens survoleraient, mais c’est celle où j’ai le plus appris. L’application doit suivre les runs d’entraînement — quel modèle, combien d’époques, si l’entraînement s’est terminé, où se trouve le checkpoint, et s’il reprend un run précédent. Plutôt que d’importer SQLAlchemy pour ce qui reste fondamentalement une poignée de tables, j’ai écrit moi-même une petite couche de modèles basée sur les dataclasses :@dataclasses.dataclassclass TrainLogsModel(BaseModel): _table_name = "train_logs" name: str epochs: int model: str path: str completed: bool = Field(type=bool, default=False).set() id: Optional[int] = Field(type=int, primary_key=True, autoincrement=True).set() created_at: Optional[float] = Field(type=int, default=lambda: dt.now().timestamp()).set() resumed_from: Optional[int] = Field(type=int, foreign_key="id", foreign_table=_table_name).set()BaseModel lit les annotations de type et les métadonnées de la dataclass et les transforme en définitions de colonnes SQL (INTEGER, TEXT, REAL, avec clés primaires, autoincrément et clés étrangères gérées explicitement), et déduit les noms de table à partir des noms de classe en snake_case, camelCase ou PascalCase selon le besoin. C’est une fraction de ce que fait un vrai ORM — pas de query builder, pas de système de migrations — mais écrire cette fraction à la main m’a obligé à vraiment comprendre ce qu’un ORM automatise, plutôt que d’en importer un et de faire confiance à la magie. C’est un arbitrage que je referais : pour un projet de cette taille, écrire 100 lignes pour comprendre le mécanisme valait mieux que 10 lignes qui le cachent.Ce modèle alimente une fonctionnalité réellement utile : la page d’entraînement permet de choisir soit un YOLO11n de base, soit n’importe quel checkpoint précédent, de lancer l’entraînement et de le journaliser — y compris une clé étrangère resumed_from pointant vers le run dont il repart, ce qui me donne une vraie généalogie d’expériences plutôt qu’un dossier plein de best_v2_final_FINAL.pt.L’interface : Streamlit, par choixLe tout est enveloppé dans une petite application Streamlit multi-pages — une page d’accueil, une page “enregistrer une voiture” (upload des photos avant/arrière/gauche/droite plus couleur et numéro de plaque), la page d’entraînement décrite plus haut, et une page de brouillons pour les expérimentations en cours. Streamlit était le bon choix ici précisément parce que ce n’est pas l’objet du projet : je voulais passer mon temps sur le modèle de détection et sur le suivi des entraînements, pas sur un frontend fait main, et Streamlit s’efface pour ça.Où en est vraiment le projet aujourd’huiJe préfère être précis plutôt que d’arrondir : le modèle de détection des parties et le pipeline d’entraînement/versioning fonctionnent et sont mesurés (ce chiffre de 87% de mAP est réel, issu d’un run effectif, pas d’une estimation). Le pipeline d’inspection complet — fusionner les quatre angles d’un véhicule en un seul rapport fiable, et passer de “voici les parties détectées” à un véritable verdict de dommage ou de fraude — reste un chantier actif, pas un produit terminé. Le README liste la notation automatique de la gravité des dommages et l’intégration à un historique véhicule comme axes futurs, et c’est exact : c’est la feuille de route, pas quelque chose que je prétends déjà fonctionnel de bout en bout.Si je devais résumer ce qui est réellement acquis aujourd’hui : un détecteur de parties de véhicule léger, honnêtement benchmarké, entraîné de manière reproductible, avec sa propre couche minimale de suivi d’expériences construite à partir de zéro. C’est une ambition plus modeste que “système de détection de fraude automatisé”, mais c’est la vraie — et c’est une meilleure fondation pour construire la suite.Le dépôt est public si vous voulez voir le code d’entraînement, le mini-ORM, ou me convaincre que j’aurais dû simplement utiliser SQLAlchemy : github.com/sanayasfp/car-inspect-ai.
Le Guide Ultime pour un Environnement de Dev Fluide
July 31, 2026 ·
11 min
J’ai écrit au sujet de Stack il y a quelques jours — pour vous donner la version courte de pourquoi je l’ai créé et de toute la philosophie “sans Docker”. Mais la théorie a ses limites. Si vous cherchez le guide ultime pour obtenir un environnement de développement parfaitement fluide, vous êtes au bon endroit.Pas de pitch commercial. Pas de blabla marketing. Juste la réalité brute et pratique de la mise en route d’un projet sans effort, avec chaque invite de commande et chaque sortie console un peu bizarre que vous rencontrerez en cours de route.Si vous voulez juste la documentation de référence brute, vous la trouverez sur sanayavo.com/stack. Sinon, bienvenue dans la visite guidée d’un meilleur flux de travail (workflow).Ce que vous obtenez vraimentUn environnement fluide signifie zéro superflu. Stack n’est qu’un seul binaire Rust. Il n’y a pas de démon qui monopolise la RAM dans votre barre des tâches, pas de Docker Desktop qui fait tourner les ventilateurs de votre ordinateur portable à fond, et absolument rien qui ne touche à votre installation PHP globale du système.Au lieu de cela, il agit comme du ruban adhésif (duct tape) hautement intelligent. Il s’appuie sur vfox pour gérer les versions de PHP/Node, uv pour Python, et Caddy pour des domaines locaux instantanés. Stack lit un simple fichier stack.toml à la racine de votre projet et connecte automatiquement ces trois outils ensemble.Note : Stack est uniquement disponible sur Windows pour le moment (PowerShell ou cmd). Le support de Mac et Linux est prévu sur la feuille de route.L’installation : Votre seul et unique mal de crâneUn flux de travail quotidien fluide nécessite un tout petit peu de configuration au départ. Exécutez ceci pour télécharger l’installateur :irm https://github.com/sanayasfp/stack/releases/latest/download/stackenv-installer.ps1 | iexEnsuite, exécutez cette commande une seule fois par machine :stack setupCela s’occupe du gros du travail en arrière-plan. Cela ajoute un hook (crochet) à votre profil PowerShell (pour que la bonne version de PHP ou Node s’active par magie lorsque vous faites un cd dans un répertoire), installe vfox, uv et Caddy dans des versions largement testées, et exécute caddy trust. Cette dernière étape est cruciale pour le HTTPS local — on y revient dans une minute.$ stack setupadded the stack hook for pwshchecking vfox/uv/caddy... vfox: OK (1.0.11) uv: OK (0.11.7) caddy: OK (2.11.4) caddy: local CA trusted (https://*.localhost works with no browser warning)Redémarrez votre terminal maintenant. Si vous sautez cette étape, le hook PowerShell ne sera pas actif et vous vous demanderez pourquoi rien ne fonctionne. Vous n’aurez plus jamais à exécuter stack setup à moins d’acheter un nouvel ordinateur.Démarrer un projet sans frictionConstruisons quelque chose de concret pour vous montrer le fonctionnement : une API PHP légère adossée à MySQL.À partir de zéro$ stack new acme-apidomain: acme-api.localhostlanguages (space to toggle, enter to confirm): [x] phpservices (space to toggle, enter to confirm): [x] mysql php version: 8.3.1 mysql version: 8.0.35created acme-api\stack.tomlnext: cd into it, add a [run] command when you know it, then `stack up`C’est une liste de contrôle interactive dans le terminal. Espace pour basculer (cocher/décocher), Entrée pour confirmer. Ça fait un peu bizarre la première fois, mais rien n’est validé tant que vous n’appuyez pas sur Entrée. Si vous sélectionnez accidentellement Node au lieu de PHP, appuyez à nouveau sur espace. Zéro stress, pas besoin de tout recommencer.Si vous avez déjà un projetSi votre dossier acme-api possède déjà un fichier composer.json spécifiant "php": "^8.3", Stack est assez intelligent pour vous éviter de taper manuellement :$ stack initdetected from existing project files: php 8.3 (from composer.json)domain: acme-api.localhoststack init analyse automatiquement composer.json, package.json ou pyproject.toml et pré-remplit l’assistant pour vous.L’artefact magiqueStack génère un manifeste très lisible :[project]name = "acme-api"domain = "acme-api.localhost"[language]php = "8.3.1"[service.mysql]version = "8.0.35"Commitez ce fichier stack.toml dans votre dépôt (repo). C’est votre artefact ultime du type “ça marche sur ma machine” — l’équivalent léger et sans conteneur d’un Dockerfile. Lorsqu’un collègue clone le dépôt et tape stack up, il obtient instantanément les mêmes versions exactes de PHP et MySQL, sans avoir à installer quoi que ce soit à la main.Lui apprendre comment s’exécuterVotre stack.toml doit savoir comment démarrer votre serveur de développement. Ouvrez le fichier et ajoutez un bloc [run] :[run]command = "php -S 127.0.0.1:{port} -t public"L’espace réservé {port} est dynamiquement remplacé par un port disponible au moment de l’exécution. (Note : Si vous déclarez [language.php] mais ignorez la section [run], Stack se rabat sur sa propre configuration FastCGI intégrée utilisant php-cgi.exe et Caddy, ce qui est incroyablement solide. Mais pour ce guide, nous allons rester explicites.)stack up et regardez la magie opérer$ cd acme-api$ stack upLoaded C:\Users\you\acme-api\stack.toml project: acme-api domain: acme-api.localhost languages: php services: mysqlfirst run for this project — stack.toml will execute: [run] php -S 127.0.0.1:{port} -t publicTrust and run these commands? [y/N] y php: C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe -> PHP 8.3.1 (cli) service.mysql: started (pid 41232, port 3306) schema 'acme_api' — automatic creation not yet implemented; create it manually if needed run: php -S 127.0.0.1:52140 -t public (pid 41244, port 52140) log: C:\Users\you\.stack\logs\acme-api.log routed: http://acme-api.localhost -> 127.0.0.1:52140Il y a deux choses majeures qui se passent ici et qui contribuent à une expérience sans accroc :1. L’invite de confiance de sécuritéParce que [run].command est une invocation littérale du shell, Stack l’exécute exactement tel qu’il est écrit. La première fois que Stack voit une commande nouvelle ou modifiée (comme après un git pull), il vous demande de la confirmer. Dites oui, et il mémorise votre choix dans ~/.stack/trust.json. Chaque stack up suivant sera heureusement silencieux. Besoin de contourner cela pour la CI ? Passez simplement le flag --yes.2. Le HTTPS fonctionne immédiatementLa console affiche http://, mais https://acme-api.localhost fonctionne instantanément. Aucun avertissement rouge du navigateur. Vous vous souvenez de cette commande caddy trust lors de la phase de configuration ? Elle installe une autorité de certification locale (similaire à mkcert), ce qui signifie que votre environnement local reflète fidèlement le SSL de production dès le premier jour.Ce que “sans conteneurs” signifie vraiment en pratiqueOuvrez un deuxième terminal, faites un cd dans un projet totalement différent qui nécessite également MySQL 8.0.35, et exécutez stack up : service.mysql: already running, shared with other projects (pid 41232, port 3306)C’est toute la beauté de la chose. Un seul processus MySQL sert deux projets, isolés par schéma plutôt que de brûler votre processeur et votre RAM avec des conteneurs redondants.Mais c’est ici que le facteur “fluide” brille vraiment. Ouvrez un troisième terminal, faites un cd dans acme-api, et exécutez simplement php -v. Pas de stack up, pas de commandes supplémentaires :$ cd acme-api$ php -vPHP 8.3.1 (cli) (built: ...)$ cd ..$ php -v'php' is not recognized as an internal or external commandLe changement de version se fait de manière ambiante à chaque invite de commande grâce à ce hook PowerShell. Votre IDE, composer install et vos exécuteurs de tests obtiennent tous automatiquement la bonne version épinglée à la seconde où ils entrent dans le dossier. Quittez le répertoire, et elle disparaît. Pas de source venv/bin/activate, pas de nvm use, et pas de pollution de votre système global.Des installations PHP toutes neuves qui ne craignent pasHabituellement, une nouvelle installation PHP signifie passer une après-midi à traquer les erreurs “PDO driver not found” ou “timezone not set”. Stack élimine complètement cela.La première fois qu’il télécharge une version de PHP via vfox, il patche automatiquement php.ini. Il active OPcache, augmente les limites de mémoire et de téléchargement (upload), définit un fuseau horaire, et active les extensions dont vous avez réellement besoin par défaut (pdo_mysql, pdo_pgsql, pdo_sqlite, sockets, sodium, etc.). Cela se fait une fois, automatiquement, et vous n’avez plus jamais à y penser.stack doctor — L’ultime vérification avant le volAvant de transmettre un projet à un collègue ou de commencer à déboguer un problème étrange, exécutez ceci :$ stack doctor --projectchecking C:\Users\you\acme-api\stack.toml... language.php: OK (C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe) service.mysql: OK (managed)Cette commande valide vos ports, chemins et valeurs {PLACEHOLDER} par rapport à votre environnement (en chargeant d’abord .env) sans démarrer un seul service. Si quelque chose est cassé, vous obtenez une liste claire dès le départ au lieu de découvrir des erreurs cryptiques au beau milieu du démarrage.Pilotez votre configuration de n’importe oùUne fois que Stack connaît un projet, vous n’avez même pas besoin d’être dans son dossier pour le gérer :stack describe acme-apistack restart acme-apistack down acme-apistack describe affiche tout — les chemins binaires résolus, l’emplacement exact de votre php.ini haché, les logs, et les domaines routés. Stack conserve un registre local de vos projets, ce qui rend la gestion globale sans effort.Tout fermer proprementQuand la journée est terminée :stack down --allTout s’arrête. Chaque projet, chaque service partagé, et Caddy. Vous obtenez une utilisation CPU vraiment à zéro, et non un conteneur fantôme persistant que vous avez oublié de tuer.Une petite note sur les domaines personnalisésL’utilisation de .localhost garantit une expérience fluide car les navigateurs le résolvent automatiquement vers l’adresse de bouclage (RFC 6761) sans que vous ayez besoin de bidouiller votre fichier hosts. Cependant, si vous migrez depuis Laragon ou Herd et que vous avez absolument besoin de domaines .test, il y a une configuration unique à faire en utilisant Acrylic DNS Proxy.Votre Nouveau Flux de TravailGénérez (Scaffold), exécutez, faites confiance une fois, profitez du HTTPS automatique, commitez votre manifeste, et détruisez tout proprement quand vous avez terminé.J’utilise ce flux de travail exact pour du vrai travail client chaque jour. Ce n’est pas un scénario idéal théorique ; c’est un plan d’action éprouvé pour un environnement de développement ultime et fluide.Pour la référence complète de stack.toml, les options de ligne de commande (CLI), et les fonctionnalités avancées (comme l’utilisation de [[clone]] pour démarrer à partir d’un simple manifeste), consultez la documentation officielle : Pourquoi j’ai créé cela — la longue diatribe contre Docker/XAMPP pour le dev local Pour commencer Référence du Manifeste Référence CLI Code : github.com/sanayasfp/stack
Stack : Le gestionnaire d'environnement de dev natif et sans conteneur
July 28, 2026 ·
4 min
Quand on maintient une application legacy tout en construisant sa remplaçante moderne, l’environnement de développement local devient vite un champ de bataille. Au travail, nous avions une ancienne application tournant sur une vieille version de PHP, Vue 2 (nécessitant une vieille version de Node), et Python 3.7. En parallèle, nous démarrions la nouvelle architecture avec PHP 8.4, Vue 3, et Python 3.12.Personnellement, je gérais ça avec une combinaison sur mesure de Laravel Herd pour PHP, uv pour Python, et nvm pour Node. Ça marchait pour moi parce que j’avais pris le temps de tout configurer. Mais pour nos développeurs juniors et nos nouveaux stagiaires, leur demander de jongler avec ces outils juste pour faire tourner les deux projets en même temps était un cauchemar absolu. Ils passaient leur temps à se battre avec des conflits de chemins et des variables d’environnement au lieu d’écrire du code. C’est pour ça que j’ai créé Stack : un gestionnaire d’environnement multi-projets natif, sans conteneur, qui rend le changement de contexte aussi simple que de taper stack up.Le code est ici : github.com/sanayasfp/stack.La vraie contrainte : l’intégration et la charge cognitiveAvant même de toucher à l’architecture, la vraie contrainte sur un projet comme ça, c’est l’expérience développeur. Les stagiaires ne devraient pas avoir à apprendre le fonctionnement des réseaux bridge de Docker ni à configurer cinq gestionnaires de versions différents pour corriger un bug sur l’API legacy. L’objectif était simple : ils doivent pouvoir cloner le dépôt, lancer une seule commande, et obtenir un domaine local fonctionnel routé vers les bons processus, que le projet utilise PHP 5.6 ou 8.4.Pourquoi ne pas juste utiliser Docker ?Les conteneurs résolvent le problème “ça marche sur ma machine” en embarquant toute une couche OS par projet. Mais cela vient avec le poids de la virtualisation et des démons inactifs qui consomment de la RAM.Stack résout le même problème différemment. Il épingle les versions exactes par projet via les gestionnaires de version que vous installeriez de toute façon (en déléguant à des outils comme vfox et uv), partage un binaire unique téléchargé pour chaque projet nécessitant cette version, et fait tourner le tout sous forme de simples processus enfants. Pas d’hyperviseur, pas de Docker Desktop qui mange 4 Go de RAM pour une API toute simple. Chaque projet qui épingle le même service et la même version partage une instance unique en cours d’exécution, isolée par schéma.Sous le capot : la simplicité avant la magieUn fichier stack.toml dans votre projet définit exactement ce dont il a besoin :[project]name = "acme-api"[language]php = "8.4.0"[service.mysql]version = "8.0.35"[run]command = "php -S 127.0.0.1:{port} -t public"Quand vous tapez stack up, l’orchestrateur lit le manifeste, configure le domaine local (par ex. acme-api.localhost), démarre MySQL s’il ne tourne pas déjà, lance le serveur de dev PHP, et fait le proxy inverse du trafic. Tout est natif, et incroyablement rapide. Il n’essaie pas de réinventer la gestion des paquets ; il délègue le gros du travail de téléchargement des langages à vfox et uv, et le routage à Caddy.Où en est le projet actuellementJe tiens à être précis là-dessus : j’ai commencé à coder Stack en Rust la semaine dernière seulement, et j’ai poussé la première version il y a deux jours. J’ai complètement désinstallé Herd, Laragon et mes précédents scripts maison. J’utilise désormais exclusivement Stack pour mon travail quotidien — ce qui me permet de prouver que ça marche, de trouver des bugs dans les cas limites, et d’ajouter de nouvelles fonctionnalités pour l’améliorer.Il supporte actuellement Windows (PowerShell & cmd), ce qui était le besoin immédiat, avec macOS et Linux prévus sur la feuille de route. Ce n’est plus juste une expérience de week-end ; c’est en train de devenir l’outil fondamental qui garde notre équipe saine d’esprit côté dev local.Vous pouvez récupérer l’installateur sur le dépôt ou lire la documentation officielle : Dépôt : github.com/sanayasfp/stack Docs : sanayavo.com/stack/
L'illusion de l'IA : pourquoi alimenter un LLM sans gouvernance des données est un risque impossible à auditer
October 30, 2026 ·
19 min
Une juriste ouvre le nouvel assistant IA de l’entreprise, y dépose un contrat d’exclusivité et tape : « Résume les obligations clés en cinq lignes. » Dix secondes plus tard, elle obtient une synthèse claire et concise. C’est un gain de productivité concret, indéniable.Ce qu’elle ne voit pas, c’est ce qui s’est réellement passé pendant ces dix secondes. L’application ne s’est pas contentée d’envoyer sa question : elle a transmis le contrat dans son intégralité — la contrepartie, les grilles tarifaires, les clauses de résiliation, le nom des signataires. Tout cela a atterri, sous forme de texte lisible en clair, sur un serveur exploité par un tiers.Le même phénomène se reproduit lorsqu’un responsable RH demande le résumé d’un entretien d’évaluation, ou lorsqu’un analyste financier épluche le dossier de crédit d’un client. La question est courte. Le contexte qui l’accompagne est colossal.Dans presque tous les comités de direction, le mot d’ordre pour 2026 est identique : « Il nous faut de l’IA, et tout de suite. » En quelques semaines, des clés d’API sont générées, des modèles de langage sont connectés aux bases de données internes, et des flux de travail entiers sont délégués à des algorithmes tiers. Les démos sont impressionnantes et les gains de productivité bien réels.Cet article ne cherche pas à contester cet élan. Il soutient que les entreprises qui prendront la gouvernance des données au sérieux déploieront l’IA plus vite et sur un plus large périmètre, précisément parce que leurs fondations seront sûres. Celles qui font l’impasse dessus finiront par se heurter à une question toute simple à laquelle elles ne sauront pas répondre. Nous y reviendrons à la fin.1. Le problème : chaque requête emporte vos donnéesMême lorsque votre contrat avec le fournisseur d’IA lui interdit d’entraîner ses modèles sur vos données, et même lorsque la connexion est intégralement chiffrée en transit, le texte brut arrive toujours sous une forme parfaitement lisible chez ce tiers. Les contrats encadrent ce que le fournisseur a le droit d’en faire. Mais ils ne changent absolument rien à ce qui se retrouve exposé en cas de compromission chez lui, en cas d’erreur de configuration dans ses logs, ou si une réquisition judiciaire atterrit sur son bureau.Ce risque s’amplifie encore avec les architectures multi-agents, où un agent routeur délègue le travail à des sous-agents spécialisés partageant une mémoire commune. Un simple nom ou un chiffre d’affaires injecté dans cet état partagé devient immédiatement visible pour chaque agent de la chaîne, ainsi que pour chaque journal d’événements, chaque cache et chaque outil de traçabilité (tracing) dans lequel ils écrivent.Figure 1 : Injection de données brutes dans une mémoire partagée multi-agents.Envoyer des données métier brutes dans un LLM sans aucun contrôle n’est pas de l’innovation. C’est sous-traiter la mémoire stratégique de son entreprise à un tiers qui n’assumera jamais le coût de vos fuites.2. Pourquoi l’anonymisation naïve échoueCertaines équipes, conscientes du danger, placent un filtre rudimentaire en amont du modèle : des outils génériques de reconnaissance d’entités nommées (Named Entity Recognition ou NER), ou de simples expressions régulières qui masquent les noms propres. Sur le papier, cela semble raisonnable. En production, cette approche échoue généralement de trois manières — et son vrai danger est de procurer à la direction un sentiment trompeur de sécurité qui l’empêche de poser les bonnes questions.Échec n°1 : Le filtre ne comprend pas votre métierLes outils de détection génériques sont entraînés sur des textes courants. Ils confondent facilement les noms d’entreprises avec des termes du langage usuel (« Services », « Transit », « Holding »), et laissent régulièrement passer des raisons sociales contenant ce type de mot, comme « NET-POLY SERVICES ». Quant aux identifiants internes (numéros fiscaux, identifiants du registre de commerce, codes clients internes), ils ne correspondent pas aux formats standards internationaux et passent totalement inaperçus.Échec n°2 : Masquer le nom ne revient pas à masquer l’entrepriseConsidérez cet exemple : Texte brut : « L’entreprise X a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. » Après un filtre naïf : « ENTREPRISE_1 a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. »Le nom a disparu, mais le chiffre d’affaires exact et la ville suffisent amplement à identifier l’entreprise en une seule recherche. Un modèle moderne, fort de son immense base de connaissances d’entraînement, peut souvent faire le lien en une fraction de seconde.Échec n°3 : Un prompt est une prière, pas un contrôleUne consigne comme « Ne répète jamais les noms des clients » n’est qu’une incitation probabiliste (probabilistic nudge). Le modèle s’y conformera le plus souvent… jusqu’au jour où il ne le fera pas. Le problème est encore plus pernicieux : selon le fournisseur et la configuration, certains modèles renvoient leur raisonnement intermédiaire dans un canal distinct de la réponse finale — une sorte de bloc-notes (scratchpad ou canal de pensée) qui vous est retourné. Si votre mécanisme de protection ne filtre que la réponse finale, les données brutes peuvent transiter par ce bloc-notes en toute liberté.Croire qu’un prompt système enjoignant d’« être discret » protège vos données revient à verrouiller une porte blindée en laissant la clé sur la serrure extérieure.3. Les six invariants d’une gouvernance robusteSi les consignes et les filtres basiques ne suffisent pas, que faut-il mettre en place ? La plupart des architectes esquissent un pipeline linéaire : les données arrivent, sont filtrées, partent vers le modèle, puis reviennent. Un système robuste obéit à une tout autre logique : c’est une défense en profondeur, où chaque frontière porte sa propre protection, indépendamment des autres.Voici six invariants fondamentaux. Ce sont des contrats d’architecture que tout concepteur de système peut vérifier au sein de sa propre infrastructure, quels que soient la stack technique, le fournisseur de modèles ou la taille de l’équipe.Figure 2 : Les six invariants de gouvernance des données appliqués aux flux LLM.Invariant 1 : Minimiser dès la sourceCe qui ne figure pas dans les données transmises ne peut pas fuiter. Les outils exposés aux agents IA ne doivent jamais être les mêmes que ceux mis à disposition des développeurs humains. La pagination y est strictement imposée, les volumes sont plafonnés et les champs superflus sont intégralement supprimés. L’API ne renvoie pas un dossier client complet pour calculer un simple ratio : elle ne retourne que les trois nombres strictement nécessaires — sans coordonnées, sans adresses, sans noms.Ce n’est pas une règle propre à l’IA : c’est un principe fondamental d’exposition des données que l’IA rend simplement impératif.Invariant 2 : Ne jamais laisser un modèle trancher ce qu’un chemin déterministe peut déciderUn classement, la consultation d’un ratio ou une simple extraction d’information n’exigent aucun discernement : ils n’ont donc aucune raison de transiter par votre modèle le plus onéreux et le moins prévisible. L’aiguillage peut reposer sur une règle métier, un choix dans l’interface ou un classifieur très léger : le principe n’impose pas la méthode d’implémentation.Deux remarques franches s’imposent. Premièrement, si le routeur est lui-même un modèle, il reste un modèle : choisissez-le miniature, strictement bridé, et ne lui fournissez que du texte anonymisé. Le but est de réduire la surface d’exposition, non de prétendre qu’elle a disparu. Deuxièmement, cet invariant présente un avantage qui convainc souvent un comité de direction bien avant les arguments sécuritaires : il est beaucoup plus économique et infiniment plus rapide.Une anecdote révélatrice nous est fournie par Anthropic elle-même. En mars 2026, le code source de Claude Code a fuité accidentellement via une source map publiée par inadvertance sur npm. On y a découvert que l’outil détecte la frustration de l’utilisateur — c’est-à-dire si quelqu’un l’insulte — au moyen d’une simple expression régulière, sans faire appel au moindre modèle1. Une regex est instantanée et gratuite. Un appel d’inférence n’est ni l’un ni l’autre, pour une question qui ne réclame aucun jugement.Invariant 3 : Traiter chaque frontière vers le modèle comme un point de protection obligatoire et réversibleToute donnée franchissant la frontière entre votre système et un modèle de langage, qu’elle soit entrante ou sortante, doit être protégée par un mécanisme réversible tel que la substitution, la pseudonymisation ou la tokenisation. L’erreur courante consiste à ne protéger que le champ de réponse évident. Inspectez chaque canal exposé par le fournisseur, y compris le canal de raisonnement décrit à la section 2.Une mise en garde juridique : des données pseudonymisées peuvent toujours être qualifiées de données à caractère personnel au sens de réglementations comme le RGPD. Cet invariant réduit votre exposition au risque opérationnel ; il ne vous exonère pas de vos obligations légales.Invariant 4 : Réhydrater au tout dernier moment légitimeL’opération inverse de substitution ne doit être appliquée qu’à l’ultime étape, juste avant que la réponse ne soit présentée à un utilisateur humain habilité à consulter les valeurs réelles. Le modèle travaille à l’aveugle du début à la fin. Seul l’humain reçoit la réponse en clair, et exclusivement en bout de chaîne.Invariant 5 : Un garde-fou déterministe a le dernier mot sur la plausibilitéLes quatre premiers invariants protègent la confidentialité. Celui-ci protège l’exactitude des résultats. Toute réponse générée doit franchir un contrôle non probabiliste — tel qu’un seuil limite ou une comparaison par rapport à un référentiel connu — avant d’être transmise. Si le résultat semble aberrant, le système bascule automatiquement sur un chemin surveillé plus lent plutôt que de laisser passer une réponse non vérifiée.Invariant 6 : Chaque saut d’agent à agent constitue sa propre frontière de confianceC’est l’invariant le plus souvent négligé, car il n’apparaît pas sur les diagrammes en couches statiques. La sortie d’un agent n’est pas automatiquement sécurisée pour l’agent qui l’a sollicité.Voici le cas typique : un sous-agent, pensant bien faire en « nettoyant sa propre sortie » avant de répondre, désanonymise des données qui devaient impérativement rester masquées pour l’agent parent ayant délégué la tâche. Le sous-agent s’est conformé à ses consignes à la lettre. Il ignorait simplement que sa réponse était destinée à un autre maillon de la chaîne, censé poursuivre son raisonnement sur des pseudonymes.Il existe un corollaire aggravant : lorsque l’historique conversationnel traverse plusieurs agents, les échanges de différents sous-agents se retrouvent mélangés dans un contexte unique. Sans un étiquetage explicite de la provenance (provenance tagging), il devient impossible de déterminer quelle frontière de confiance s’applique à quel fragment de texte.Figure 3 : Frontières de confiance lors de délégations inter-agents et déplacement du point de réhydratation.4. Pour les ingénieurs : comment nous implémentons les invariantsIl s’agit ici d’une implémentation concrète parmi d’autres. L’outillage est spécifique à notre écosystème technique. Même si votre architecture diffère sensiblement de la nôtre, les invariants restent universels : seule leur incarnation technique change. Les lecteurs non techniques peuvent passer directement à la section 5. Passerelle d’anonymisation (invariant 3) : Avant toute classification, le texte libre saisi par l’utilisateur franchit une passerelle stricte combinant des bibliothèques de TALN/NLP et des règles de substitution déterministes. Les noms et identifiants sont remplacés par des jetons uniques (par exemple CORP_A83B). La table de correspondance est immédiatement sécurisée dans un coffre-fort chiffré (encrypted vault) : calcul HMAC-SHA256 pour l’identifiant du jeton et chiffrement Fernet (AES-128-CBC avec HMAC) pour la valeur réelle au repos. Routeur (invariant 2) : La requête pseudonymisée est soumise à un LLM délibérément compact, véloce et économique, dont l’unique mission est de classifier l’intention. Un classement, une lecture de ratio ou une extraction basique emprunte le chemin rapide (fast path) : un outil issu d’une liste blanche restreinte (allowlist) est exécuté directement, sans jamais solliciter le modèle lourd. Une analyse de risque ou une tâche d’interprétation complexe emprunte le chemin lent (slow path) et bascule vers l’agent principal. Outils minimisés (invariant 1) : Sur les deux chemins, l’accès aux données internes s’effectue au moyen d’outils exposés via le protocole MCP (Model Context Protocol), épurés à la source et strictement dissociés des outils réservés aux développeurs humains. Chemin lent (invariant 3, sortant) : Les résultats renvoyés par les outils sont systématiquement pseudonymisés avant d’être injectés dans le contexte du modèle lourd, de sorte que ce dernier raisonne et rédige exclusivement à partir de pseudonymes. Chemin rapide (invariant 5) : Un garde-fou de plausibilité — par exemple un ratio comparé à une valeur de référence — bloque toute réponse automatique aberrante et force un repli vers le chemin lent pour examen approfondi. Réhydratation (invariant 4) : Ce n’est qu’une fois la réponse finale générée et mise en forme que le système interroge le coffre-fort pour substituer les jetons par les valeurs réelles, immédiatement avant l’affichage à l’utilisateur dûment habilité.Figure 4 : Architecture de routage fast-path / slow-path. Résultats observés : (Insérer ici vos métriques clés de production — par exemple : 72 % des requêtes traitées par le chemin rapide sans appel au modèle lourd, division par 4 de la latence médiane et réduction de 65 % des coûts d’API).5. Vérifier plutôt que présumerUne défense qui n’a jamais été éprouvée dans des conditions réelles est une défense dont on espère simplement l’existence.Voici un scénario bien loin d’être théorique : un contrôle destiné à transformer chaque message avant qu’il n’atteigne le modèle a été rédigé, validé en revue de code et intégré à la branche principale. Les tests passaient avec succès. Pendant des semaines, l’équipe a cru la protection active. Elle ne l’était pas. Une hypothèse erronée sur la structure exacte de la charge utile entrante (payload shape) faisait que le filtre s’exécutait sur une mauvaise clé d’objet, de manière silencieuse, sans générer d’erreur et sans déclencher la moindre alerte.Un test qui passe au vert sans solliciter réellement le code de protection ne prouve rien. Une revue de code n’est pas une preuve d’exécution, et un schéma d’architecture soigné n’est pas la preuve que la trajectoire tracée est celle empruntée lors de l’exécution. La seule preuve tangible repose sur un test rejoué contre le système réel : le vrai modèle, les vraies données, la véritable chaîne d’appels.Chaque affirmation de conformité — qu’il s’agisse de « ces données sont anonymisées » ou « ces données ne sortent jamais en clair de notre réseau » — doit être assortie d’une preuve rejouable, consultable à la demande. Sans cela, il ne s’agit pas d’une garantie : c’est une hypothèse qui n’a pas encore été réfutée.Figure 5 : Défaillance silencieuse d’un contrôle liée à une inadéquation de format des données entrantes.6. La piste d’audit (Audit Trail)La gouvernance ne s’arrête pas à la protection en temps réel : elle exige une traçabilité sans faille. Lorsqu’un régulateur bancaire, un auditeur RGPD ou un grand client examine la manière dont l’IA est exploitée dans votre entreprise, il ne se contentera pas de simples déclarations d’intention. Il exigera des preuves irréfutables.Un système d’IA d’entreprise responsable conserve un journal d’audit infalsifiable (tamper-evident) et strictement en ajout seul (append-only), isolé des logs applicatifs ordinaires et de l’historique des conversations. Pour chaque événement du cycle de vie d’un agent, il consigne une paire indissociable : Les données brutes telles qu’elles ont été initialement reçues, Les données réellement transmises aux modèles externes.La règle la plus cruciale est la suivante : aucun paramètre de configuration (feature flag) ni aucun administrateur isolé ne doit pouvoir désactiver ce journal. Lors de pics de charge imprévus, le premier réflexe d’une équipe technique est souvent de désactiver les journaux « pour préserver les performances ». Si un tel commutateur existe, le jour où une fuite survient, vous serez incapable d’en déterminer l’étendue ou d’identifier ce qui a été exposé.Figure 6 : Pourquoi le journal d’audit doit être immunisé contre toute désactivation par configuration.Il y a toutefois un revers, et il doit être énoncé sans détour : ce journal d’audit devient lui-même l’un des jeux de données les plus sensibles de l’organisation, puisqu’il contient précisément les données brutes que vous avez pris tant de peine à dissimuler. Traitez-le en conséquence : chiffrez-le, cloisonnez et restreignez ses accès, fixez des limites de conservation rigoureuses, et définissez avec vos juristes la manière dont il s’articule avec les demandes de suppression (droit à l’effacement prévu par le RGPD).Complétez enfin ce dispositif par une détection rétroactive des fuites (retroactive leak scanning) : des processus d’arrière-plan analysent en continu l’historique d’audit archivé à la recherche de schémas sensibles ayant pu franchir les premiers filtres. Lorsqu’une anomalie est décelée, le scanner ne modifie en aucun cas le journal initial : il déclenche une alerte auprès de l’équipe de sécurité, qui peut ainsi durcir les règles de filtrage avant qu’une simple brèche ne devienne un incident majeur.Figure 7 : Le pipeline d’audit, de l’enregistrement immuable à l’analyse asynchrone et aux alertes.7. Conclusion : L’IA est d’abord un défi de Data EngineeringL’adage bien connu « garbage in, garbage out » a désormais un pendant moderne : « secret in, exposure out » (données secrètes en entrée, fuites en sortie). L’IA n’est pas un vernis cosmétique que l’on vient greffer à la hâte dans un recoin du système d’information pour contenter la direction générale. Elle bouleverse la façon dont l’information circule, se segmente, s’anonymise et s’archive.Les entreprises qui sortiront gagnantes de la révolution de l’IA ne seront pas celles qui auront déployé le plus d’agents dans l’urgence. Ce seront celles qui auront d’abord bâti des fondations hermétiques et fiables. C’est la qualité des freins qui permet à une voiture de rouler vite. Une fois ces contrôles fermement en place, les équipes peuvent donner leur feu vert à un plus grand nombre de cas d’usage, avec moins de débats interminables et sans vetos de dernière minute.Alors, avant de signer votre prochain contrat de licence d’IA ou de célébrer le déploiement de votre nouvel agent autonome, rassemblez vos architectes et posez-leur une seule question : « Si un auditeur externe mandaté par notre plus grand client débarquait demain matin à 8h, seriez-vous capables de me prouver, ligne de code par ligne de code, qu’aucun nom, aucun identifiant et aucun chiffre confidentiel n’a quitté notre réseau en clair au cours du mois écoulé ? »Si la réponse est un silence gêné, un renvoi aux « clauses contractuelles du fournisseur » ou la mention d’« un prompt qui demande à l’IA d’être prudente », vous connaissez déjà votre prochain projet.La checklist de gouvernance : 8 cases à cocherAvant de quitter la salle, voici les huit critères non négociables à valider (les six invariants, complétés par les impératifs des sections 5 et 6) : Minimisez-vous dès la source ? (Invariant 1) Préservez-vous le modèle lourd et coûteux des requêtes simples ? (Invariant 2) Chaque frontière texte-vers-modèle fait-elle l’objet d’une protection réversible ? (Invariant 3) Ne réhydratez-vous qu’au tout dernier moment légitime ? (Invariant 4) Un garde-fou déterministe a-t-il le dernier mot sur la plausibilité ? (Invariant 5) Chaque saut d’agent à agent constitue-t-il sa propre frontière de confiance ? (Invariant 6) Chaque contrôle est-il vérifié en conditions réelles, et pas seulement supposé fonctionnel ? (Section 5) Le journal d’audit est-il impossible à désactiver par configuration ou par un administrateur isolé ? (Section 6) Alex Kim, « The Claude Code Source Leak: fake tools, frustration regexes, undercover mode, and more », 31 mars 2026. La fuite provenait d’un fichier source map publié par mégarde sur npm. Le fichier userPromptKeywords.ts contient la regex employée pour la détection de la frustration de l’utilisateur. ↩
L'illusion de l'IA : pourquoi alimenter un LLM sans gouvernance des données est un risque impossible à auditer
October 30, 2026 ·
19 min
Une juriste ouvre le nouvel assistant IA de l’entreprise, y dépose un contrat d’exclusivité et tape : « Résume les obligations clés en cinq lignes. » Dix secondes plus tard, elle obtient une synthèse claire et concise. C’est un gain de productivité concret, indéniable.Ce qu’elle ne voit pas, c’est ce qui s’est réellement passé pendant ces dix secondes. L’application ne s’est pas contentée d’envoyer sa question : elle a transmis le contrat dans son intégralité — la contrepartie, les grilles tarifaires, les clauses de résiliation, le nom des signataires. Tout cela a atterri, sous forme de texte lisible en clair, sur un serveur exploité par un tiers.Le même phénomène se reproduit lorsqu’un responsable RH demande le résumé d’un entretien d’évaluation, ou lorsqu’un analyste financier épluche le dossier de crédit d’un client. La question est courte. Le contexte qui l’accompagne est colossal.Dans presque tous les comités de direction, le mot d’ordre pour 2026 est identique : « Il nous faut de l’IA, et tout de suite. » En quelques semaines, des clés d’API sont générées, des modèles de langage sont connectés aux bases de données internes, et des flux de travail entiers sont délégués à des algorithmes tiers. Les démos sont impressionnantes et les gains de productivité bien réels.Cet article ne cherche pas à contester cet élan. Il soutient que les entreprises qui prendront la gouvernance des données au sérieux déploieront l’IA plus vite et sur un plus large périmètre, précisément parce que leurs fondations seront sûres. Celles qui font l’impasse dessus finiront par se heurter à une question toute simple à laquelle elles ne sauront pas répondre. Nous y reviendrons à la fin.1. Le problème : chaque requête emporte vos donnéesMême lorsque votre contrat avec le fournisseur d’IA lui interdit d’entraîner ses modèles sur vos données, et même lorsque la connexion est intégralement chiffrée en transit, le texte brut arrive toujours sous une forme parfaitement lisible chez ce tiers. Les contrats encadrent ce que le fournisseur a le droit d’en faire. Mais ils ne changent absolument rien à ce qui se retrouve exposé en cas de compromission chez lui, en cas d’erreur de configuration dans ses logs, ou si une réquisition judiciaire atterrit sur son bureau.Ce risque s’amplifie encore avec les architectures multi-agents, où un agent routeur délègue le travail à des sous-agents spécialisés partageant une mémoire commune. Un simple nom ou un chiffre d’affaires injecté dans cet état partagé devient immédiatement visible pour chaque agent de la chaîne, ainsi que pour chaque journal d’événements, chaque cache et chaque outil de traçabilité (tracing) dans lequel ils écrivent.Figure 1 : Injection de données brutes dans une mémoire partagée multi-agents.Envoyer des données métier brutes dans un LLM sans aucun contrôle n’est pas de l’innovation. C’est sous-traiter la mémoire stratégique de son entreprise à un tiers qui n’assumera jamais le coût de vos fuites.2. Pourquoi l’anonymisation naïve échoueCertaines équipes, conscientes du danger, placent un filtre rudimentaire en amont du modèle : des outils génériques de reconnaissance d’entités nommées (Named Entity Recognition ou NER), ou de simples expressions régulières qui masquent les noms propres. Sur le papier, cela semble raisonnable. En production, cette approche échoue généralement de trois manières — et son vrai danger est de procurer à la direction un sentiment trompeur de sécurité qui l’empêche de poser les bonnes questions.Échec n°1 : Le filtre ne comprend pas votre métierLes outils de détection génériques sont entraînés sur des textes courants. Ils confondent facilement les noms d’entreprises avec des termes du langage usuel (« Services », « Transit », « Holding »), et laissent régulièrement passer des raisons sociales contenant ce type de mot, comme « NET-POLY SERVICES ». Quant aux identifiants internes (numéros fiscaux, identifiants du registre de commerce, codes clients internes), ils ne correspondent pas aux formats standards internationaux et passent totalement inaperçus.Échec n°2 : Masquer le nom ne revient pas à masquer l’entrepriseConsidérez cet exemple : Texte brut : « L’entreprise X a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. » Après un filtre naïf : « ENTREPRISE_1 a réalisé 4,2 milliards de FCFA de chiffre d’affaires à Abidjan en 2024. »Le nom a disparu, mais le chiffre d’affaires exact et la ville suffisent amplement à identifier l’entreprise en une seule recherche. Un modèle moderne, fort de son immense base de connaissances d’entraînement, peut souvent faire le lien en une fraction de seconde.Échec n°3 : Un prompt est une prière, pas un contrôleUne consigne comme « Ne répète jamais les noms des clients » n’est qu’une incitation probabiliste (probabilistic nudge). Le modèle s’y conformera le plus souvent… jusqu’au jour où il ne le fera pas. Le problème est encore plus pernicieux : selon le fournisseur et la configuration, certains modèles renvoient leur raisonnement intermédiaire dans un canal distinct de la réponse finale — une sorte de bloc-notes (scratchpad ou canal de pensée) qui vous est retourné. Si votre mécanisme de protection ne filtre que la réponse finale, les données brutes peuvent transiter par ce bloc-notes en toute liberté.Croire qu’un prompt système enjoignant d’« être discret » protège vos données revient à verrouiller une porte blindée en laissant la clé sur la serrure extérieure.3. Les six invariants d’une gouvernance robusteSi les consignes et les filtres basiques ne suffisent pas, que faut-il mettre en place ? La plupart des architectes esquissent un pipeline linéaire : les données arrivent, sont filtrées, partent vers le modèle, puis reviennent. Un système robuste obéit à une tout autre logique : c’est une défense en profondeur, où chaque frontière porte sa propre protection, indépendamment des autres.Voici six invariants fondamentaux. Ce sont des contrats d’architecture que tout concepteur de système peut vérifier au sein de sa propre infrastructure, quels que soient la stack technique, le fournisseur de modèles ou la taille de l’équipe.Figure 2 : Les six invariants de gouvernance des données appliqués aux flux LLM.Invariant 1 : Minimiser dès la sourceCe qui ne figure pas dans les données transmises ne peut pas fuiter. Les outils exposés aux agents IA ne doivent jamais être les mêmes que ceux mis à disposition des développeurs humains. La pagination y est strictement imposée, les volumes sont plafonnés et les champs superflus sont intégralement supprimés. L’API ne renvoie pas un dossier client complet pour calculer un simple ratio : elle ne retourne que les trois nombres strictement nécessaires — sans coordonnées, sans adresses, sans noms.Ce n’est pas une règle propre à l’IA : c’est un principe fondamental d’exposition des données que l’IA rend simplement impératif.Invariant 2 : Ne jamais laisser un modèle trancher ce qu’un chemin déterministe peut déciderUn classement, la consultation d’un ratio ou une simple extraction d’information n’exigent aucun discernement : ils n’ont donc aucune raison de transiter par votre modèle le plus onéreux et le moins prévisible. L’aiguillage peut reposer sur une règle métier, un choix dans l’interface ou un classifieur très léger : le principe n’impose pas la méthode d’implémentation.Deux remarques franches s’imposent. Premièrement, si le routeur est lui-même un modèle, il reste un modèle : choisissez-le miniature, strictement bridé, et ne lui fournissez que du texte anonymisé. Le but est de réduire la surface d’exposition, non de prétendre qu’elle a disparu. Deuxièmement, cet invariant présente un avantage qui convainc souvent un comité de direction bien avant les arguments sécuritaires : il est beaucoup plus économique et infiniment plus rapide.Une anecdote révélatrice nous est fournie par Anthropic elle-même. En mars 2026, le code source de Claude Code a fuité accidentellement via une source map publiée par inadvertance sur npm. On y a découvert que l’outil détecte la frustration de l’utilisateur — c’est-à-dire si quelqu’un l’insulte — au moyen d’une simple expression régulière, sans faire appel au moindre modèle1. Une regex est instantanée et gratuite. Un appel d’inférence n’est ni l’un ni l’autre, pour une question qui ne réclame aucun jugement.Invariant 3 : Traiter chaque frontière vers le modèle comme un point de protection obligatoire et réversibleToute donnée franchissant la frontière entre votre système et un modèle de langage, qu’elle soit entrante ou sortante, doit être protégée par un mécanisme réversible tel que la substitution, la pseudonymisation ou la tokenisation. L’erreur courante consiste à ne protéger que le champ de réponse évident. Inspectez chaque canal exposé par le fournisseur, y compris le canal de raisonnement décrit à la section 2.Une mise en garde juridique : des données pseudonymisées peuvent toujours être qualifiées de données à caractère personnel au sens de réglementations comme le RGPD. Cet invariant réduit votre exposition au risque opérationnel ; il ne vous exonère pas de vos obligations légales.Invariant 4 : Réhydrater au tout dernier moment légitimeL’opération inverse de substitution ne doit être appliquée qu’à l’ultime étape, juste avant que la réponse ne soit présentée à un utilisateur humain habilité à consulter les valeurs réelles. Le modèle travaille à l’aveugle du début à la fin. Seul l’humain reçoit la réponse en clair, et exclusivement en bout de chaîne.Invariant 5 : Un garde-fou déterministe a le dernier mot sur la plausibilitéLes quatre premiers invariants protègent la confidentialité. Celui-ci protège l’exactitude des résultats. Toute réponse générée doit franchir un contrôle non probabiliste — tel qu’un seuil limite ou une comparaison par rapport à un référentiel connu — avant d’être transmise. Si le résultat semble aberrant, le système bascule automatiquement sur un chemin surveillé plus lent plutôt que de laisser passer une réponse non vérifiée.Invariant 6 : Chaque saut d’agent à agent constitue sa propre frontière de confianceC’est l’invariant le plus souvent négligé, car il n’apparaît pas sur les diagrammes en couches statiques. La sortie d’un agent n’est pas automatiquement sécurisée pour l’agent qui l’a sollicité.Voici le cas typique : un sous-agent, pensant bien faire en « nettoyant sa propre sortie » avant de répondre, désanonymise des données qui devaient impérativement rester masquées pour l’agent parent ayant délégué la tâche. Le sous-agent s’est conformé à ses consignes à la lettre. Il ignorait simplement que sa réponse était destinée à un autre maillon de la chaîne, censé poursuivre son raisonnement sur des pseudonymes.Il existe un corollaire aggravant : lorsque l’historique conversationnel traverse plusieurs agents, les échanges de différents sous-agents se retrouvent mélangés dans un contexte unique. Sans un étiquetage explicite de la provenance (provenance tagging), il devient impossible de déterminer quelle frontière de confiance s’applique à quel fragment de texte.Figure 3 : Frontières de confiance lors de délégations inter-agents et déplacement du point de réhydratation.4. Pour les ingénieurs : comment nous implémentons les invariantsIl s’agit ici d’une implémentation concrète parmi d’autres. L’outillage est spécifique à notre écosystème technique. Même si votre architecture diffère sensiblement de la nôtre, les invariants restent universels : seule leur incarnation technique change. Les lecteurs non techniques peuvent passer directement à la section 5. Passerelle d’anonymisation (invariant 3) : Avant toute classification, le texte libre saisi par l’utilisateur franchit une passerelle stricte combinant des bibliothèques de TALN/NLP et des règles de substitution déterministes. Les noms et identifiants sont remplacés par des jetons uniques (par exemple CORP_A83B). La table de correspondance est immédiatement sécurisée dans un coffre-fort chiffré (encrypted vault) : calcul HMAC-SHA256 pour l’identifiant du jeton et chiffrement Fernet (AES-128-CBC avec HMAC) pour la valeur réelle au repos. Routeur (invariant 2) : La requête pseudonymisée est soumise à un LLM délibérément compact, véloce et économique, dont l’unique mission est de classifier l’intention. Un classement, une lecture de ratio ou une extraction basique emprunte le chemin rapide (fast path) : un outil issu d’une liste blanche restreinte (allowlist) est exécuté directement, sans jamais solliciter le modèle lourd. Une analyse de risque ou une tâche d’interprétation complexe emprunte le chemin lent (slow path) et bascule vers l’agent principal. Outils minimisés (invariant 1) : Sur les deux chemins, l’accès aux données internes s’effectue au moyen d’outils exposés via le protocole MCP (Model Context Protocol), épurés à la source et strictement dissociés des outils réservés aux développeurs humains. Chemin lent (invariant 3, sortant) : Les résultats renvoyés par les outils sont systématiquement pseudonymisés avant d’être injectés dans le contexte du modèle lourd, de sorte que ce dernier raisonne et rédige exclusivement à partir de pseudonymes. Chemin rapide (invariant 5) : Un garde-fou de plausibilité — par exemple un ratio comparé à une valeur de référence — bloque toute réponse automatique aberrante et force un repli vers le chemin lent pour examen approfondi. Réhydratation (invariant 4) : Ce n’est qu’une fois la réponse finale générée et mise en forme que le système interroge le coffre-fort pour substituer les jetons par les valeurs réelles, immédiatement avant l’affichage à l’utilisateur dûment habilité.Figure 4 : Architecture de routage fast-path / slow-path. Résultats observés : (Insérer ici vos métriques clés de production — par exemple : 72 % des requêtes traitées par le chemin rapide sans appel au modèle lourd, division par 4 de la latence médiane et réduction de 65 % des coûts d’API).5. Vérifier plutôt que présumerUne défense qui n’a jamais été éprouvée dans des conditions réelles est une défense dont on espère simplement l’existence.Voici un scénario bien loin d’être théorique : un contrôle destiné à transformer chaque message avant qu’il n’atteigne le modèle a été rédigé, validé en revue de code et intégré à la branche principale. Les tests passaient avec succès. Pendant des semaines, l’équipe a cru la protection active. Elle ne l’était pas. Une hypothèse erronée sur la structure exacte de la charge utile entrante (payload shape) faisait que le filtre s’exécutait sur une mauvaise clé d’objet, de manière silencieuse, sans générer d’erreur et sans déclencher la moindre alerte.Un test qui passe au vert sans solliciter réellement le code de protection ne prouve rien. Une revue de code n’est pas une preuve d’exécution, et un schéma d’architecture soigné n’est pas la preuve que la trajectoire tracée est celle empruntée lors de l’exécution. La seule preuve tangible repose sur un test rejoué contre le système réel : le vrai modèle, les vraies données, la véritable chaîne d’appels.Chaque affirmation de conformité — qu’il s’agisse de « ces données sont anonymisées » ou « ces données ne sortent jamais en clair de notre réseau » — doit être assortie d’une preuve rejouable, consultable à la demande. Sans cela, il ne s’agit pas d’une garantie : c’est une hypothèse qui n’a pas encore été réfutée.Figure 5 : Défaillance silencieuse d’un contrôle liée à une inadéquation de format des données entrantes.6. La piste d’audit (Audit Trail)La gouvernance ne s’arrête pas à la protection en temps réel : elle exige une traçabilité sans faille. Lorsqu’un régulateur bancaire, un auditeur RGPD ou un grand client examine la manière dont l’IA est exploitée dans votre entreprise, il ne se contentera pas de simples déclarations d’intention. Il exigera des preuves irréfutables.Un système d’IA d’entreprise responsable conserve un journal d’audit infalsifiable (tamper-evident) et strictement en ajout seul (append-only), isolé des logs applicatifs ordinaires et de l’historique des conversations. Pour chaque événement du cycle de vie d’un agent, il consigne une paire indissociable : Les données brutes telles qu’elles ont été initialement reçues, Les données réellement transmises aux modèles externes.La règle la plus cruciale est la suivante : aucun paramètre de configuration (feature flag) ni aucun administrateur isolé ne doit pouvoir désactiver ce journal. Lors de pics de charge imprévus, le premier réflexe d’une équipe technique est souvent de désactiver les journaux « pour préserver les performances ». Si un tel commutateur existe, le jour où une fuite survient, vous serez incapable d’en déterminer l’étendue ou d’identifier ce qui a été exposé.Figure 6 : Pourquoi le journal d’audit doit être immunisé contre toute désactivation par configuration.Il y a toutefois un revers, et il doit être énoncé sans détour : ce journal d’audit devient lui-même l’un des jeux de données les plus sensibles de l’organisation, puisqu’il contient précisément les données brutes que vous avez pris tant de peine à dissimuler. Traitez-le en conséquence : chiffrez-le, cloisonnez et restreignez ses accès, fixez des limites de conservation rigoureuses, et définissez avec vos juristes la manière dont il s’articule avec les demandes de suppression (droit à l’effacement prévu par le RGPD).Complétez enfin ce dispositif par une détection rétroactive des fuites (retroactive leak scanning) : des processus d’arrière-plan analysent en continu l’historique d’audit archivé à la recherche de schémas sensibles ayant pu franchir les premiers filtres. Lorsqu’une anomalie est décelée, le scanner ne modifie en aucun cas le journal initial : il déclenche une alerte auprès de l’équipe de sécurité, qui peut ainsi durcir les règles de filtrage avant qu’une simple brèche ne devienne un incident majeur.Figure 7 : Le pipeline d’audit, de l’enregistrement immuable à l’analyse asynchrone et aux alertes.7. Conclusion : L’IA est d’abord un défi de Data EngineeringL’adage bien connu « garbage in, garbage out » a désormais un pendant moderne : « secret in, exposure out » (données secrètes en entrée, fuites en sortie). L’IA n’est pas un vernis cosmétique que l’on vient greffer à la hâte dans un recoin du système d’information pour contenter la direction générale. Elle bouleverse la façon dont l’information circule, se segmente, s’anonymise et s’archive.Les entreprises qui sortiront gagnantes de la révolution de l’IA ne seront pas celles qui auront déployé le plus d’agents dans l’urgence. Ce seront celles qui auront d’abord bâti des fondations hermétiques et fiables. C’est la qualité des freins qui permet à une voiture de rouler vite. Une fois ces contrôles fermement en place, les équipes peuvent donner leur feu vert à un plus grand nombre de cas d’usage, avec moins de débats interminables et sans vetos de dernière minute.Alors, avant de signer votre prochain contrat de licence d’IA ou de célébrer le déploiement de votre nouvel agent autonome, rassemblez vos architectes et posez-leur une seule question : « Si un auditeur externe mandaté par notre plus grand client débarquait demain matin à 8h, seriez-vous capables de me prouver, ligne de code par ligne de code, qu’aucun nom, aucun identifiant et aucun chiffre confidentiel n’a quitté notre réseau en clair au cours du mois écoulé ? »Si la réponse est un silence gêné, un renvoi aux « clauses contractuelles du fournisseur » ou la mention d’« un prompt qui demande à l’IA d’être prudente », vous connaissez déjà votre prochain projet.La checklist de gouvernance : 8 cases à cocherAvant de quitter la salle, voici les huit critères non négociables à valider (les six invariants, complétés par les impératifs des sections 5 et 6) : Minimisez-vous dès la source ? (Invariant 1) Préservez-vous le modèle lourd et coûteux des requêtes simples ? (Invariant 2) Chaque frontière texte-vers-modèle fait-elle l’objet d’une protection réversible ? (Invariant 3) Ne réhydratez-vous qu’au tout dernier moment légitime ? (Invariant 4) Un garde-fou déterministe a-t-il le dernier mot sur la plausibilité ? (Invariant 5) Chaque saut d’agent à agent constitue-t-il sa propre frontière de confiance ? (Invariant 6) Chaque contrôle est-il vérifié en conditions réelles, et pas seulement supposé fonctionnel ? (Section 5) Le journal d’audit est-il impossible à désactiver par configuration ou par un administrateur isolé ? (Section 6) Alex Kim, « The Claude Code Source Leak: fake tools, frustration regexes, undercover mode, and more », 31 mars 2026. La fuite provenait d’un fichier source map publié par mégarde sur npm. Le fichier userPromptKeywords.ts contient la regex employée pour la détection de la frustration de l’utilisateur. ↩
Le Guide Ultime pour un Environnement de Dev Fluide
July 31, 2026 ·
11 min
J’ai écrit au sujet de Stack il y a quelques jours — pour vous donner la version courte de pourquoi je l’ai créé et de toute la philosophie “sans Docker”. Mais la théorie a ses limites. Si vous cherchez le guide ultime pour obtenir un environnement de développement parfaitement fluide, vous êtes au bon endroit.Pas de pitch commercial. Pas de blabla marketing. Juste la réalité brute et pratique de la mise en route d’un projet sans effort, avec chaque invite de commande et chaque sortie console un peu bizarre que vous rencontrerez en cours de route.Si vous voulez juste la documentation de référence brute, vous la trouverez sur sanayavo.com/stack. Sinon, bienvenue dans la visite guidée d’un meilleur flux de travail (workflow).Ce que vous obtenez vraimentUn environnement fluide signifie zéro superflu. Stack n’est qu’un seul binaire Rust. Il n’y a pas de démon qui monopolise la RAM dans votre barre des tâches, pas de Docker Desktop qui fait tourner les ventilateurs de votre ordinateur portable à fond, et absolument rien qui ne touche à votre installation PHP globale du système.Au lieu de cela, il agit comme du ruban adhésif (duct tape) hautement intelligent. Il s’appuie sur vfox pour gérer les versions de PHP/Node, uv pour Python, et Caddy pour des domaines locaux instantanés. Stack lit un simple fichier stack.toml à la racine de votre projet et connecte automatiquement ces trois outils ensemble.Note : Stack est uniquement disponible sur Windows pour le moment (PowerShell ou cmd). Le support de Mac et Linux est prévu sur la feuille de route.L’installation : Votre seul et unique mal de crâneUn flux de travail quotidien fluide nécessite un tout petit peu de configuration au départ. Exécutez ceci pour télécharger l’installateur :irm https://github.com/sanayasfp/stack/releases/latest/download/stackenv-installer.ps1 | iexEnsuite, exécutez cette commande une seule fois par machine :stack setupCela s’occupe du gros du travail en arrière-plan. Cela ajoute un hook (crochet) à votre profil PowerShell (pour que la bonne version de PHP ou Node s’active par magie lorsque vous faites un cd dans un répertoire), installe vfox, uv et Caddy dans des versions largement testées, et exécute caddy trust. Cette dernière étape est cruciale pour le HTTPS local — on y revient dans une minute.$ stack setupadded the stack hook for pwshchecking vfox/uv/caddy... vfox: OK (1.0.11) uv: OK (0.11.7) caddy: OK (2.11.4) caddy: local CA trusted (https://*.localhost works with no browser warning)Redémarrez votre terminal maintenant. Si vous sautez cette étape, le hook PowerShell ne sera pas actif et vous vous demanderez pourquoi rien ne fonctionne. Vous n’aurez plus jamais à exécuter stack setup à moins d’acheter un nouvel ordinateur.Démarrer un projet sans frictionConstruisons quelque chose de concret pour vous montrer le fonctionnement : une API PHP légère adossée à MySQL.À partir de zéro$ stack new acme-apidomain: acme-api.localhostlanguages (space to toggle, enter to confirm): [x] phpservices (space to toggle, enter to confirm): [x] mysql php version: 8.3.1 mysql version: 8.0.35created acme-api\stack.tomlnext: cd into it, add a [run] command when you know it, then `stack up`C’est une liste de contrôle interactive dans le terminal. Espace pour basculer (cocher/décocher), Entrée pour confirmer. Ça fait un peu bizarre la première fois, mais rien n’est validé tant que vous n’appuyez pas sur Entrée. Si vous sélectionnez accidentellement Node au lieu de PHP, appuyez à nouveau sur espace. Zéro stress, pas besoin de tout recommencer.Si vous avez déjà un projetSi votre dossier acme-api possède déjà un fichier composer.json spécifiant "php": "^8.3", Stack est assez intelligent pour vous éviter de taper manuellement :$ stack initdetected from existing project files: php 8.3 (from composer.json)domain: acme-api.localhoststack init analyse automatiquement composer.json, package.json ou pyproject.toml et pré-remplit l’assistant pour vous.L’artefact magiqueStack génère un manifeste très lisible :[project]name = "acme-api"domain = "acme-api.localhost"[language]php = "8.3.1"[service.mysql]version = "8.0.35"Commitez ce fichier stack.toml dans votre dépôt (repo). C’est votre artefact ultime du type “ça marche sur ma machine” — l’équivalent léger et sans conteneur d’un Dockerfile. Lorsqu’un collègue clone le dépôt et tape stack up, il obtient instantanément les mêmes versions exactes de PHP et MySQL, sans avoir à installer quoi que ce soit à la main.Lui apprendre comment s’exécuterVotre stack.toml doit savoir comment démarrer votre serveur de développement. Ouvrez le fichier et ajoutez un bloc [run] :[run]command = "php -S 127.0.0.1:{port} -t public"L’espace réservé {port} est dynamiquement remplacé par un port disponible au moment de l’exécution. (Note : Si vous déclarez [language.php] mais ignorez la section [run], Stack se rabat sur sa propre configuration FastCGI intégrée utilisant php-cgi.exe et Caddy, ce qui est incroyablement solide. Mais pour ce guide, nous allons rester explicites.)stack up et regardez la magie opérer$ cd acme-api$ stack upLoaded C:\Users\you\acme-api\stack.toml project: acme-api domain: acme-api.localhost languages: php services: mysqlfirst run for this project — stack.toml will execute: [run] php -S 127.0.0.1:{port} -t publicTrust and run these commands? [y/N] y php: C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe -> PHP 8.3.1 (cli) service.mysql: started (pid 41232, port 3306) schema 'acme_api' — automatic creation not yet implemented; create it manually if needed run: php -S 127.0.0.1:52140 -t public (pid 41244, port 52140) log: C:\Users\you\.stack\logs\acme-api.log routed: http://acme-api.localhost -> 127.0.0.1:52140Il y a deux choses majeures qui se passent ici et qui contribuent à une expérience sans accroc :1. L’invite de confiance de sécuritéParce que [run].command est une invocation littérale du shell, Stack l’exécute exactement tel qu’il est écrit. La première fois que Stack voit une commande nouvelle ou modifiée (comme après un git pull), il vous demande de la confirmer. Dites oui, et il mémorise votre choix dans ~/.stack/trust.json. Chaque stack up suivant sera heureusement silencieux. Besoin de contourner cela pour la CI ? Passez simplement le flag --yes.2. Le HTTPS fonctionne immédiatementLa console affiche http://, mais https://acme-api.localhost fonctionne instantanément. Aucun avertissement rouge du navigateur. Vous vous souvenez de cette commande caddy trust lors de la phase de configuration ? Elle installe une autorité de certification locale (similaire à mkcert), ce qui signifie que votre environnement local reflète fidèlement le SSL de production dès le premier jour.Ce que “sans conteneurs” signifie vraiment en pratiqueOuvrez un deuxième terminal, faites un cd dans un projet totalement différent qui nécessite également MySQL 8.0.35, et exécutez stack up : service.mysql: already running, shared with other projects (pid 41232, port 3306)C’est toute la beauté de la chose. Un seul processus MySQL sert deux projets, isolés par schéma plutôt que de brûler votre processeur et votre RAM avec des conteneurs redondants.Mais c’est ici que le facteur “fluide” brille vraiment. Ouvrez un troisième terminal, faites un cd dans acme-api, et exécutez simplement php -v. Pas de stack up, pas de commandes supplémentaires :$ cd acme-api$ php -vPHP 8.3.1 (cli) (built: ...)$ cd ..$ php -v'php' is not recognized as an internal or external commandLe changement de version se fait de manière ambiante à chaque invite de commande grâce à ce hook PowerShell. Votre IDE, composer install et vos exécuteurs de tests obtiennent tous automatiquement la bonne version épinglée à la seconde où ils entrent dans le dossier. Quittez le répertoire, et elle disparaît. Pas de source venv/bin/activate, pas de nvm use, et pas de pollution de votre système global.Des installations PHP toutes neuves qui ne craignent pasHabituellement, une nouvelle installation PHP signifie passer une après-midi à traquer les erreurs “PDO driver not found” ou “timezone not set”. Stack élimine complètement cela.La première fois qu’il télécharge une version de PHP via vfox, il patche automatiquement php.ini. Il active OPcache, augmente les limites de mémoire et de téléchargement (upload), définit un fuseau horaire, et active les extensions dont vous avez réellement besoin par défaut (pdo_mysql, pdo_pgsql, pdo_sqlite, sockets, sodium, etc.). Cela se fait une fois, automatiquement, et vous n’avez plus jamais à y penser.stack doctor — L’ultime vérification avant le volAvant de transmettre un projet à un collègue ou de commencer à déboguer un problème étrange, exécutez ceci :$ stack doctor --projectchecking C:\Users\you\acme-api\stack.toml... language.php: OK (C:\Users\you\.vfox\cache\php\v-8.3.1\...\php.exe) service.mysql: OK (managed)Cette commande valide vos ports, chemins et valeurs {PLACEHOLDER} par rapport à votre environnement (en chargeant d’abord .env) sans démarrer un seul service. Si quelque chose est cassé, vous obtenez une liste claire dès le départ au lieu de découvrir des erreurs cryptiques au beau milieu du démarrage.Pilotez votre configuration de n’importe oùUne fois que Stack connaît un projet, vous n’avez même pas besoin d’être dans son dossier pour le gérer :stack describe acme-apistack restart acme-apistack down acme-apistack describe affiche tout — les chemins binaires résolus, l’emplacement exact de votre php.ini haché, les logs, et les domaines routés. Stack conserve un registre local de vos projets, ce qui rend la gestion globale sans effort.Tout fermer proprementQuand la journée est terminée :stack down --allTout s’arrête. Chaque projet, chaque service partagé, et Caddy. Vous obtenez une utilisation CPU vraiment à zéro, et non un conteneur fantôme persistant que vous avez oublié de tuer.Une petite note sur les domaines personnalisésL’utilisation de .localhost garantit une expérience fluide car les navigateurs le résolvent automatiquement vers l’adresse de bouclage (RFC 6761) sans que vous ayez besoin de bidouiller votre fichier hosts. Cependant, si vous migrez depuis Laragon ou Herd et que vous avez absolument besoin de domaines .test, il y a une configuration unique à faire en utilisant Acrylic DNS Proxy.Votre Nouveau Flux de TravailGénérez (Scaffold), exécutez, faites confiance une fois, profitez du HTTPS automatique, commitez votre manifeste, et détruisez tout proprement quand vous avez terminé.J’utilise ce flux de travail exact pour du vrai travail client chaque jour. Ce n’est pas un scénario idéal théorique ; c’est un plan d’action éprouvé pour un environnement de développement ultime et fluide.Pour la référence complète de stack.toml, les options de ligne de commande (CLI), et les fonctionnalités avancées (comme l’utilisation de [[clone]] pour démarrer à partir d’un simple manifeste), consultez la documentation officielle : Pourquoi j’ai créé cela — la longue diatribe contre Docker/XAMPP pour le dev local Pour commencer Référence du Manifeste Référence CLI Code : github.com/sanayasfp/stack
Stack : Le gestionnaire d'environnement de dev natif et sans conteneur
July 28, 2026 ·
4 min
Quand on maintient une application legacy tout en construisant sa remplaçante moderne, l’environnement de développement local devient vite un champ de bataille. Au travail, nous avions une ancienne application tournant sur une vieille version de PHP, Vue 2 (nécessitant une vieille version de Node), et Python 3.7. En parallèle, nous démarrions la nouvelle architecture avec PHP 8.4, Vue 3, et Python 3.12.Personnellement, je gérais ça avec une combinaison sur mesure de Laravel Herd pour PHP, uv pour Python, et nvm pour Node. Ça marchait pour moi parce que j’avais pris le temps de tout configurer. Mais pour nos développeurs juniors et nos nouveaux stagiaires, leur demander de jongler avec ces outils juste pour faire tourner les deux projets en même temps était un cauchemar absolu. Ils passaient leur temps à se battre avec des conflits de chemins et des variables d’environnement au lieu d’écrire du code. C’est pour ça que j’ai créé Stack : un gestionnaire d’environnement multi-projets natif, sans conteneur, qui rend le changement de contexte aussi simple que de taper stack up.Le code est ici : github.com/sanayasfp/stack.La vraie contrainte : l’intégration et la charge cognitiveAvant même de toucher à l’architecture, la vraie contrainte sur un projet comme ça, c’est l’expérience développeur. Les stagiaires ne devraient pas avoir à apprendre le fonctionnement des réseaux bridge de Docker ni à configurer cinq gestionnaires de versions différents pour corriger un bug sur l’API legacy. L’objectif était simple : ils doivent pouvoir cloner le dépôt, lancer une seule commande, et obtenir un domaine local fonctionnel routé vers les bons processus, que le projet utilise PHP 5.6 ou 8.4.Pourquoi ne pas juste utiliser Docker ?Les conteneurs résolvent le problème “ça marche sur ma machine” en embarquant toute une couche OS par projet. Mais cela vient avec le poids de la virtualisation et des démons inactifs qui consomment de la RAM.Stack résout le même problème différemment. Il épingle les versions exactes par projet via les gestionnaires de version que vous installeriez de toute façon (en déléguant à des outils comme vfox et uv), partage un binaire unique téléchargé pour chaque projet nécessitant cette version, et fait tourner le tout sous forme de simples processus enfants. Pas d’hyperviseur, pas de Docker Desktop qui mange 4 Go de RAM pour une API toute simple. Chaque projet qui épingle le même service et la même version partage une instance unique en cours d’exécution, isolée par schéma.Sous le capot : la simplicité avant la magieUn fichier stack.toml dans votre projet définit exactement ce dont il a besoin :[project]name = "acme-api"[language]php = "8.4.0"[service.mysql]version = "8.0.35"[run]command = "php -S 127.0.0.1:{port} -t public"Quand vous tapez stack up, l’orchestrateur lit le manifeste, configure le domaine local (par ex. acme-api.localhost), démarre MySQL s’il ne tourne pas déjà, lance le serveur de dev PHP, et fait le proxy inverse du trafic. Tout est natif, et incroyablement rapide. Il n’essaie pas de réinventer la gestion des paquets ; il délègue le gros du travail de téléchargement des langages à vfox et uv, et le routage à Caddy.Où en est le projet actuellementJe tiens à être précis là-dessus : j’ai commencé à coder Stack en Rust la semaine dernière seulement, et j’ai poussé la première version il y a deux jours. J’ai complètement désinstallé Herd, Laragon et mes précédents scripts maison. J’utilise désormais exclusivement Stack pour mon travail quotidien — ce qui me permet de prouver que ça marche, de trouver des bugs dans les cas limites, et d’ajouter de nouvelles fonctionnalités pour l’améliorer.Il supporte actuellement Windows (PowerShell & cmd), ce qui était le besoin immédiat, avec macOS et Linux prévus sur la feuille de route. Ce n’est plus juste une expérience de week-end ; c’est en train de devenir l’outil fondamental qui garde notre équipe saine d’esprit côté dev local.Vous pouvez récupérer l’installateur sur le dépôt ou lire la documentation officielle : Dépôt : github.com/sanayasfp/stack Docs : sanayavo.com/stack/
Laplace Nearby : un bot WhatsApp qui trouve « le maquis le plus proche » sans exploser la facture LLM
June 22, 2026 ·
9 min
À Abidjan, une grande partie des commerces de proximité — le maquis du quartier, la pharmacie, le réparateur de téléphones — n’ont ni site web, ni fiche Google Business, ni même une adresse postale formelle la moitié du temps. Ce qu’ils ont, en revanche, c’est WhatsApp. Laplace Nearby part de ce constat : plutôt que de construire une application que les gens doivent télécharger, autant construire l’expérience de recherche dans l’application que tout le monde a déjà ouverte, et laisser les gens décrire ce qu’ils cherchent en langage naturel — « il me faut une pharmacie », « j’ai envie de porcodjo » — plutôt que de remplir un formulaire de filtres.L’assistant lui-même s’appelle en interne Simon. Ce billet parle de la façon dont Simon est réellement construit — les parties que je trouve intéressantes à raconter, pas un argumentaire commercial.Le dépôt : github.com/sanayasfp/laplace-nearby.Décision n°1 : le moteur ignore qu’il parle à WhatsAppLe cœur du système est SimonEngine, un petit orchestrateur qui prend en entrée une InteractionRequest indépendante du canal et renvoie une InteractionResponse tout aussi indépendante. Les spécificités WhatsApp — format des webhooks, mise en forme des messages, boutons et listes — vivent entièrement dans une couche d’adaptateurs/renderers en dehors du moteur. Le moteur lui-même fait quatre choses à chaque message : charger le contexte de session de l’utilisateur, vérifier le rate limiting, transmettre le message au flow conversationnel actif (idle, recherche, ou enregistrement), puis sauvegarder le résultat — en émettant systématiquement un effet de bord analytique avec le nom du flow, la transition d’état et la latence, qu’il se soit passé quelque chose d’intéressant ou non :async process(req: InteractionRequest): Promise<InteractionResponse> { const context = await this.contextManager.load(req.profileId); const flow = this.flowRegistry.get(context.session.activeFlow || IDLE); const limitResult = this.rateLimitService.check(context.rateLimit); if (!limitResult.allowed) { return { messages: limitResult.notify ? [Responses.tooManyMessages(...)] : [] , ... }; } const response = await flow.handle(req, context, interactionId); await this.contextManager.save(req.profileId, response.contextUpdate); return { ...response, sideEffects: [summaryEffect, ...response.sideEffects] };}Ce n’est pas de l’architecture pour le plaisir. Cela signifie que la logique de recherche, le flow d’enregistrement, le rate limiting et les métriques n’auront pas besoin d’être réécrits si un second canal (SMS, un widget web, peu importe) arrive un jour — ils n’ont jamais été couplés à WhatsApp en premier lieu.Décision n°2 : ne pas appeler le LLM si on peut s’en passerChaque message entrant a besoin d’une intention — s’agit-il d’une recherche, d’une demande d’enregistrement, de bavardage, d’un remerciement, d’une insulte, d’un « stop » ? Faire passer chaque message par un appel LLM est la solution de facilité, et aussi la plus lente et la plus coûteuse. La classification d’intention est donc à deux étages.FastIntentDetector s’exécute en premier : un ensemble volontairement exhaustif d’expressions régulières couvrant le français, l’anglais, et l’argot local qu’on croise réellement dans une conversation WhatsApp à Abidjan — « wesh », « cc », « gab » (guichet automatique), « essence/gazoil », des dizaines de variantes orthographiques de « merci », « stop », « annule ». Sa docstring dit exactement à quoi il sert : « Reduce LLM costs and latency for unambiguous user requests » (réduire les coûts et la latence liés au LLM pour les requêtes non ambiguës). Ce n’est que lorsque rien ne correspond que IntentService bascule sur Gemini (gemini-2.5-flash-lite), avec une température à 0, un mode de réponse JSON, et un plafond de 150 tokens, en demandant une sortie structurée : l’intention, un mot-clé de recherche extrait, et une liste de termes sémantiquement proches pour élargir la recherche (« j’ai envie de porcodjo » → mot-clé « porcodjo restaurant » ; « mon habit est sale » → « pressing nettoyage vêtement »).Les deux chemins signalent leur origine — REGEX ou LLM — à un compteur Prometheus (simonIntentTotal), ce qui fait que la part du trafic détournée du modèle payant est quelque chose qu’on peut littéralement observer sur un dashboard, pas quelque chose qu’on devine. Et l’appel à Gemini lui-même est protégé par un circuit breaker : s’il se déclenche, le système ne plante pas la conversation, il dégrade vers une réponse neutre de type « bavardage » avec une confiance à 0, et continue.Décision n°3 : les adresses à Abidjan ne fonctionnent pas comme ailleursUne grande partie des adresses données dans le chat ne sont pas des chaînes géocodables — ce sont des descriptions : « je suis vers la cité Abdoulaye Diallo ». AddressCodingService prend ce type d’entrée, vérifie d’abord un cache sémantique (hash exact, puis similarité par embedding au-delà d’un seuil de 0,88) pour éviter de repayer un traitement déjà résolu, et en cas d’échec, demande à Gemini de la transformer en adresse standardisée, un quartier extrait, et — c’est le point important — un indicateur précisant si la description est assez précise pour être géocodée, ou si la seule réponse honnête est de demander à l’utilisateur d’envoyer un point GPS. Il existe même un dictionnaire dédié de termes nouchi (l’argot de rue abidjanais) qui alimente ce pipeline, parce qu’un outillage NLP générique ne sait pas ce que désigne un arrêt de « gbaka » ou le surnom d’un quartier.Décision n°4 : la recherche, c’est une seule fonction SQL, trois signaux, fusionnésC’est la partie du code dont je suis le plus fier. search_nearby_places est une unique fonction Postgres qui combine trois signaux de classement indépendants pour chaque commerce candidat dans un rayon donné : Le rang texte intégral (ts_rank_cd sur un tsquery en français) — utile quand l’utilisateur a tapé quelque chose de proche du nom ou de la catégorie réelle du commerce. Le rang de similarité vectorielle — distance cosinus entre l’embedding de la requête (Gemini text-embedding-004, pgvector avec un index HNSW) et l’embedding de chaque lieu — utile quand l’utilisateur décrit ce qu’il veut avec ses propres mots plutôt qu’en reprenant une étiquette. La proximité géographique (ST_Distance sur une colonne geography PostGIS) — parce que « le plus proche » compte toujours, et ne peut pas être compensé par la seule pertinence.Ces signaux sont combinés via une Reciprocal Rank Fusion pondérée : score = w_fts/(k + fts_rank) + w_vec/(k + vec_rank) + w_prox * proximité, avec des poids qui varient selon que l’utilisateur ait donné ou non un mot-clé — 45/45/10 entre texte, vecteur et proximité s’il y a un mot-clé à faire correspondre ; 85% piloté par le vecteur sinon, puisque la recherche texte intégral n’a rien à quoi s’accrocher dans une requête purement descriptive. Il existe aussi un mécanisme de « palier premium » : une seconde passe de classement, partitionnée par palier, garantit aux commerces payants/référencés un petit quota de places sans pour autant noyer la pertinence pour l’ensemble des résultats.Je suis revenu plus tard réécrire cette même fonction pour la performance, après avoir remarqué qu’elle effectuait du travail redondant : fusionner deux CTE qui relisaient deux fois les mêmes lignes, ajouter une LIMIT à l’intérieur du CTE de classement vectoriel spécifiquement pour que l’index HNSW puisse s’arrêter court plutôt que de trier tout le pool de candidats, et remplacer deux matérialisations complètes séparées (résultats généraux, résultats premium) par une seule passe fenêtrée utilisant ROW_NUMBER() OVER (PARTITION BY is_premium ...). C’est le genre de correction vraiment satisfaisant — même résultat, mesurablement moins de travail par requête — et c’est le genre de chose qui n’apparaît que lorsque l’usage réel met sous pression un premier jet.Décision n°5 : les effets de bord sont des données, pas des actionsQuand un lieu est enregistré, qu’une interaction doit être journalisée pour l’analytique, ou qu’un utilisateur doit recevoir une notification, le moteur n’exécute pas ce travail en ligne — il renvoie un simple objet SideEffect décrivant ce qui doit se produire. Un PgmqSideEffectDispatcher collecte ces effets, les regroupe par file cible, et les pousse via pgmq.send_batch (l’extension de file de messages propre à Postgres) dans la même transaction de base de données que celle qui marque un lieu nouvellement enregistré comme « en attente d’indexation ». Ce détail compte : cela signifie qu’un lieu ne peut pas se retrouver à moitié enregistré — visible dans l’application mais jamais réellement indexé pour la recherche — parce que la mise à jour du statut et l’écriture dans la file valident ensemble, ou aucune des deux. Des Supabase Edge Functions en bout de chaîne (un analytics-worker, un place-embedding-worker, un place-tagging-worker) vident ces files de manière asynchrone.La stack, sans détourFastify + TypeScript sur Node.js, Prisma au-dessus de Supabase Postgres (avec pgvector, pgmq, PostGIS, pg_cron et pg_net qui font un vrai travail, pas juste des lignes dans un fichier de dépendances), Redis pour le cache et l’état du rate limiting, Gemini à la fois pour le NLU et les embeddings, Geoapify pour le géocodage, et des métriques Prometheus intégrées dès le départ plutôt que rajoutées après coup.Où en est le projetLe package.json indique 0.4.0-rc.1, et je tiens à laisser ce contexte tel quel : c’est un système réel et fonctionnel qui me sert à raisonner sur la recherche hybride et l’ingénierie conversationnelle, en évolution active vers une version 1.0 — pas un produit fini, à grande échelle, avec un portefeuille de clients derrière. La conception indépendante du canal existe précisément pour que, si ce projet doit un jour dépasser le cadre d’un bot WhatsApp, le moteur sous-jacent n’ait pas besoin d’être reconstruit.Si vous voulez débattre des poids de la RRF, me dire que PostGIS était superflu, ou pointer une meilleure façon de structurer le dispatcher d’effets de bord, le code est public : github.com/sanayasfp/laplace-nearby.
Stack : Le gestionnaire d'environnement de dev natif et sans conteneur
July 28, 2026 ·
4 min
Quand on maintient une application legacy tout en construisant sa remplaçante moderne, l’environnement de développement local devient vite un champ de bataille. Au travail, nous avions une ancienne application tournant sur une vieille version de PHP, Vue 2 (nécessitant une vieille version de Node), et Python 3.7. En parallèle, nous démarrions la nouvelle architecture avec PHP 8.4, Vue 3, et Python 3.12.Personnellement, je gérais ça avec une combinaison sur mesure de Laravel Herd pour PHP, uv pour Python, et nvm pour Node. Ça marchait pour moi parce que j’avais pris le temps de tout configurer. Mais pour nos développeurs juniors et nos nouveaux stagiaires, leur demander de jongler avec ces outils juste pour faire tourner les deux projets en même temps était un cauchemar absolu. Ils passaient leur temps à se battre avec des conflits de chemins et des variables d’environnement au lieu d’écrire du code. C’est pour ça que j’ai créé Stack : un gestionnaire d’environnement multi-projets natif, sans conteneur, qui rend le changement de contexte aussi simple que de taper stack up.Le code est ici : github.com/sanayasfp/stack.La vraie contrainte : l’intégration et la charge cognitiveAvant même de toucher à l’architecture, la vraie contrainte sur un projet comme ça, c’est l’expérience développeur. Les stagiaires ne devraient pas avoir à apprendre le fonctionnement des réseaux bridge de Docker ni à configurer cinq gestionnaires de versions différents pour corriger un bug sur l’API legacy. L’objectif était simple : ils doivent pouvoir cloner le dépôt, lancer une seule commande, et obtenir un domaine local fonctionnel routé vers les bons processus, que le projet utilise PHP 5.6 ou 8.4.Pourquoi ne pas juste utiliser Docker ?Les conteneurs résolvent le problème “ça marche sur ma machine” en embarquant toute une couche OS par projet. Mais cela vient avec le poids de la virtualisation et des démons inactifs qui consomment de la RAM.Stack résout le même problème différemment. Il épingle les versions exactes par projet via les gestionnaires de version que vous installeriez de toute façon (en déléguant à des outils comme vfox et uv), partage un binaire unique téléchargé pour chaque projet nécessitant cette version, et fait tourner le tout sous forme de simples processus enfants. Pas d’hyperviseur, pas de Docker Desktop qui mange 4 Go de RAM pour une API toute simple. Chaque projet qui épingle le même service et la même version partage une instance unique en cours d’exécution, isolée par schéma.Sous le capot : la simplicité avant la magieUn fichier stack.toml dans votre projet définit exactement ce dont il a besoin :[project]name = "acme-api"[language]php = "8.4.0"[service.mysql]version = "8.0.35"[run]command = "php -S 127.0.0.1:{port} -t public"Quand vous tapez stack up, l’orchestrateur lit le manifeste, configure le domaine local (par ex. acme-api.localhost), démarre MySQL s’il ne tourne pas déjà, lance le serveur de dev PHP, et fait le proxy inverse du trafic. Tout est natif, et incroyablement rapide. Il n’essaie pas de réinventer la gestion des paquets ; il délègue le gros du travail de téléchargement des langages à vfox et uv, et le routage à Caddy.Où en est le projet actuellementJe tiens à être précis là-dessus : j’ai commencé à coder Stack en Rust la semaine dernière seulement, et j’ai poussé la première version il y a deux jours. J’ai complètement désinstallé Herd, Laragon et mes précédents scripts maison. J’utilise désormais exclusivement Stack pour mon travail quotidien — ce qui me permet de prouver que ça marche, de trouver des bugs dans les cas limites, et d’ajouter de nouvelles fonctionnalités pour l’améliorer.Il supporte actuellement Windows (PowerShell & cmd), ce qui était le besoin immédiat, avec macOS et Linux prévus sur la feuille de route. Ce n’est plus juste une expérience de week-end ; c’est en train de devenir l’outil fondamental qui garde notre équipe saine d’esprit côté dev local.Vous pouvez récupérer l’installateur sur le dépôt ou lire la documentation officielle : Dépôt : github.com/sanayasfp/stack Docs : sanayavo.com/stack/
Laplace Nearby : un bot WhatsApp qui trouve « le maquis le plus proche » sans exploser la facture LLM
June 22, 2026 ·
9 min
À Abidjan, une grande partie des commerces de proximité — le maquis du quartier, la pharmacie, le réparateur de téléphones — n’ont ni site web, ni fiche Google Business, ni même une adresse postale formelle la moitié du temps. Ce qu’ils ont, en revanche, c’est WhatsApp. Laplace Nearby part de ce constat : plutôt que de construire une application que les gens doivent télécharger, autant construire l’expérience de recherche dans l’application que tout le monde a déjà ouverte, et laisser les gens décrire ce qu’ils cherchent en langage naturel — « il me faut une pharmacie », « j’ai envie de porcodjo » — plutôt que de remplir un formulaire de filtres.L’assistant lui-même s’appelle en interne Simon. Ce billet parle de la façon dont Simon est réellement construit — les parties que je trouve intéressantes à raconter, pas un argumentaire commercial.Le dépôt : github.com/sanayasfp/laplace-nearby.Décision n°1 : le moteur ignore qu’il parle à WhatsAppLe cœur du système est SimonEngine, un petit orchestrateur qui prend en entrée une InteractionRequest indépendante du canal et renvoie une InteractionResponse tout aussi indépendante. Les spécificités WhatsApp — format des webhooks, mise en forme des messages, boutons et listes — vivent entièrement dans une couche d’adaptateurs/renderers en dehors du moteur. Le moteur lui-même fait quatre choses à chaque message : charger le contexte de session de l’utilisateur, vérifier le rate limiting, transmettre le message au flow conversationnel actif (idle, recherche, ou enregistrement), puis sauvegarder le résultat — en émettant systématiquement un effet de bord analytique avec le nom du flow, la transition d’état et la latence, qu’il se soit passé quelque chose d’intéressant ou non :async process(req: InteractionRequest): Promise<InteractionResponse> { const context = await this.contextManager.load(req.profileId); const flow = this.flowRegistry.get(context.session.activeFlow || IDLE); const limitResult = this.rateLimitService.check(context.rateLimit); if (!limitResult.allowed) { return { messages: limitResult.notify ? [Responses.tooManyMessages(...)] : [] , ... }; } const response = await flow.handle(req, context, interactionId); await this.contextManager.save(req.profileId, response.contextUpdate); return { ...response, sideEffects: [summaryEffect, ...response.sideEffects] };}Ce n’est pas de l’architecture pour le plaisir. Cela signifie que la logique de recherche, le flow d’enregistrement, le rate limiting et les métriques n’auront pas besoin d’être réécrits si un second canal (SMS, un widget web, peu importe) arrive un jour — ils n’ont jamais été couplés à WhatsApp en premier lieu.Décision n°2 : ne pas appeler le LLM si on peut s’en passerChaque message entrant a besoin d’une intention — s’agit-il d’une recherche, d’une demande d’enregistrement, de bavardage, d’un remerciement, d’une insulte, d’un « stop » ? Faire passer chaque message par un appel LLM est la solution de facilité, et aussi la plus lente et la plus coûteuse. La classification d’intention est donc à deux étages.FastIntentDetector s’exécute en premier : un ensemble volontairement exhaustif d’expressions régulières couvrant le français, l’anglais, et l’argot local qu’on croise réellement dans une conversation WhatsApp à Abidjan — « wesh », « cc », « gab » (guichet automatique), « essence/gazoil », des dizaines de variantes orthographiques de « merci », « stop », « annule ». Sa docstring dit exactement à quoi il sert : « Reduce LLM costs and latency for unambiguous user requests » (réduire les coûts et la latence liés au LLM pour les requêtes non ambiguës). Ce n’est que lorsque rien ne correspond que IntentService bascule sur Gemini (gemini-2.5-flash-lite), avec une température à 0, un mode de réponse JSON, et un plafond de 150 tokens, en demandant une sortie structurée : l’intention, un mot-clé de recherche extrait, et une liste de termes sémantiquement proches pour élargir la recherche (« j’ai envie de porcodjo » → mot-clé « porcodjo restaurant » ; « mon habit est sale » → « pressing nettoyage vêtement »).Les deux chemins signalent leur origine — REGEX ou LLM — à un compteur Prometheus (simonIntentTotal), ce qui fait que la part du trafic détournée du modèle payant est quelque chose qu’on peut littéralement observer sur un dashboard, pas quelque chose qu’on devine. Et l’appel à Gemini lui-même est protégé par un circuit breaker : s’il se déclenche, le système ne plante pas la conversation, il dégrade vers une réponse neutre de type « bavardage » avec une confiance à 0, et continue.Décision n°3 : les adresses à Abidjan ne fonctionnent pas comme ailleursUne grande partie des adresses données dans le chat ne sont pas des chaînes géocodables — ce sont des descriptions : « je suis vers la cité Abdoulaye Diallo ». AddressCodingService prend ce type d’entrée, vérifie d’abord un cache sémantique (hash exact, puis similarité par embedding au-delà d’un seuil de 0,88) pour éviter de repayer un traitement déjà résolu, et en cas d’échec, demande à Gemini de la transformer en adresse standardisée, un quartier extrait, et — c’est le point important — un indicateur précisant si la description est assez précise pour être géocodée, ou si la seule réponse honnête est de demander à l’utilisateur d’envoyer un point GPS. Il existe même un dictionnaire dédié de termes nouchi (l’argot de rue abidjanais) qui alimente ce pipeline, parce qu’un outillage NLP générique ne sait pas ce que désigne un arrêt de « gbaka » ou le surnom d’un quartier.Décision n°4 : la recherche, c’est une seule fonction SQL, trois signaux, fusionnésC’est la partie du code dont je suis le plus fier. search_nearby_places est une unique fonction Postgres qui combine trois signaux de classement indépendants pour chaque commerce candidat dans un rayon donné : Le rang texte intégral (ts_rank_cd sur un tsquery en français) — utile quand l’utilisateur a tapé quelque chose de proche du nom ou de la catégorie réelle du commerce. Le rang de similarité vectorielle — distance cosinus entre l’embedding de la requête (Gemini text-embedding-004, pgvector avec un index HNSW) et l’embedding de chaque lieu — utile quand l’utilisateur décrit ce qu’il veut avec ses propres mots plutôt qu’en reprenant une étiquette. La proximité géographique (ST_Distance sur une colonne geography PostGIS) — parce que « le plus proche » compte toujours, et ne peut pas être compensé par la seule pertinence.Ces signaux sont combinés via une Reciprocal Rank Fusion pondérée : score = w_fts/(k + fts_rank) + w_vec/(k + vec_rank) + w_prox * proximité, avec des poids qui varient selon que l’utilisateur ait donné ou non un mot-clé — 45/45/10 entre texte, vecteur et proximité s’il y a un mot-clé à faire correspondre ; 85% piloté par le vecteur sinon, puisque la recherche texte intégral n’a rien à quoi s’accrocher dans une requête purement descriptive. Il existe aussi un mécanisme de « palier premium » : une seconde passe de classement, partitionnée par palier, garantit aux commerces payants/référencés un petit quota de places sans pour autant noyer la pertinence pour l’ensemble des résultats.Je suis revenu plus tard réécrire cette même fonction pour la performance, après avoir remarqué qu’elle effectuait du travail redondant : fusionner deux CTE qui relisaient deux fois les mêmes lignes, ajouter une LIMIT à l’intérieur du CTE de classement vectoriel spécifiquement pour que l’index HNSW puisse s’arrêter court plutôt que de trier tout le pool de candidats, et remplacer deux matérialisations complètes séparées (résultats généraux, résultats premium) par une seule passe fenêtrée utilisant ROW_NUMBER() OVER (PARTITION BY is_premium ...). C’est le genre de correction vraiment satisfaisant — même résultat, mesurablement moins de travail par requête — et c’est le genre de chose qui n’apparaît que lorsque l’usage réel met sous pression un premier jet.Décision n°5 : les effets de bord sont des données, pas des actionsQuand un lieu est enregistré, qu’une interaction doit être journalisée pour l’analytique, ou qu’un utilisateur doit recevoir une notification, le moteur n’exécute pas ce travail en ligne — il renvoie un simple objet SideEffect décrivant ce qui doit se produire. Un PgmqSideEffectDispatcher collecte ces effets, les regroupe par file cible, et les pousse via pgmq.send_batch (l’extension de file de messages propre à Postgres) dans la même transaction de base de données que celle qui marque un lieu nouvellement enregistré comme « en attente d’indexation ». Ce détail compte : cela signifie qu’un lieu ne peut pas se retrouver à moitié enregistré — visible dans l’application mais jamais réellement indexé pour la recherche — parce que la mise à jour du statut et l’écriture dans la file valident ensemble, ou aucune des deux. Des Supabase Edge Functions en bout de chaîne (un analytics-worker, un place-embedding-worker, un place-tagging-worker) vident ces files de manière asynchrone.La stack, sans détourFastify + TypeScript sur Node.js, Prisma au-dessus de Supabase Postgres (avec pgvector, pgmq, PostGIS, pg_cron et pg_net qui font un vrai travail, pas juste des lignes dans un fichier de dépendances), Redis pour le cache et l’état du rate limiting, Gemini à la fois pour le NLU et les embeddings, Geoapify pour le géocodage, et des métriques Prometheus intégrées dès le départ plutôt que rajoutées après coup.Où en est le projetLe package.json indique 0.4.0-rc.1, et je tiens à laisser ce contexte tel quel : c’est un système réel et fonctionnel qui me sert à raisonner sur la recherche hybride et l’ingénierie conversationnelle, en évolution active vers une version 1.0 — pas un produit fini, à grande échelle, avec un portefeuille de clients derrière. La conception indépendante du canal existe précisément pour que, si ce projet doit un jour dépasser le cadre d’un bot WhatsApp, le moteur sous-jacent n’ait pas besoin d’être reconstruit.Si vous voulez débattre des poids de la RRF, me dire que PostGIS était superflu, ou pointer une meilleure façon de structurer le dispatcher d’effets de bord, le code est public : github.com/sanayasfp/laplace-nearby.