Plateforme d’analytics locale pour un réseau national de distribution
Contexte
Mission de conseil pour un réseau de distribution national en France, avec plusieurs centaines de magasins organisés en dizaines de regroupements régionaux. Les utilisateurs finaux sont une équipe métier et data, pas des développeurs, donc l’ensemble du produit s’exécute en application locale lancée depuis un seul fichier : un backend FastAPI et un frontend React/TypeScript tournant côte à côte sur la machine de l’utilisateur.
Une contrainte dure a gouverné toute la conception : les données ne quittent jamais cette machine. Pas de cloud, pas d’appel externe. Le client dépose dans l’application un unique fichier de ventes d’environ 10 millions de lignes (2,6 Go), et le logiciel produit seul l’analyse complète du réseau, des simulations multi-agents à 36 mois, et des interprétations des résultats rédigées par l’IA.
Le problème : une IA qui commente des chiffres métier devant des dirigeants est à une hallucination de faire échouer le produit. Pendant les tests, l’assistant intégré a un jour répondu à une question sur un magasin avec un chiffre correct, mais pour tout le réseau : chiffre réel, calcul juste, périmètre faux, sans lever d’erreur.
Une réponse presque juste dite avec une confiance totale est pire qu’une réponse fausse. Le système devait être conçu pour que cette classe d’erreur soit structurellement impossible, pas seulement improbable.
Le système
Toute l’architecture est construite autour d’une liste d’interdits : ce que l’IA n’a pas le droit de faire.
Le système est construit autour de ce que l’IA n’a pas le droit de faire. Le LLM ne calcule jamais un chiffre : des moteurs déterministes calculent tout, et les résultats sont figés au moment de la génération avec leurs marges d’incertitude. L’affichage lit toujours les artefacts figés ; rien n’est jamais régénéré au moment du rendu, si bien que la même question ne peut jamais renvoyer discrètement deux réponses différentes. Chaque KPI affiché correspond à un champ backend dédié, et le frontend ne réagrège, ne filtre ni ne dérive jamais de chiffres de son propre chef. Les constantes métier vivent dans une unique source de vérité Python, répliquée vers TypeScript, avec un test de CI qui fait échouer le build si un nombre magique apparaît ailleurs. Chaque interprétation de l’IA passe un audit automatisé avant d’atteindre l’écran, qui vérifie la fidélité des chiffres par rapport aux agrégats figés, la cohérence des recommandations par rapport au modèle de scoring déterministe, un seuil de confiance, et la validation de la structure de sortie. Le gate renvoie PASS, WARN ou FAIL : en cas de FAIL, l’utilisateur voit un résumé déterministe brut à la place du texte de l’IA. Les identifiants de scénario et le schéma de sortie JSON sont traités comme des invariants de compatibilité frontend : la couche IA peut changer, le contrat ne le peut pas.
Le fichier de ventes de 2,6 Go rendait tout chemin naïf physiquement impossible : les outils tableur plafonnent autour d’un million de lignes, et un chargement complet en mémoire ne tient pas. L’ingestion passe par un streaming DuckDB directement depuis le chemin du fichier sur disque, sans aucun transfert des données elles-mêmes, et se termine en environ 5 secondes, validation comprise, contre un contrat de schéma strict à 22 colonnes. Le problème le plus brut venait d’en amont : une colonne critique de classification de gamme produit arrivait à 84 % nulle. Plutôt que d’imputer ou de dégrader silencieusement, le pipeline a verrouillé le KPI phare concerné, affiché comme indisponible avec la raison, et a quantifié l’écart avec assez de précision pour que l’équipe data du client puisse reconstruire la classification à la source et réexporter un fichier complet. Mesurer rigoureusement un trou de données s’est révélé le moyen le plus rapide de le corriger.
La projection de scénarios s’appuie sur une simulation multi-agents Mesa : un échantillon stratifié d’agents clients (2 000 dans la configuration de référence, testé en charge jusqu’à 30 000) simulé sur 36 mois, répliqué façon Monte-Carlo avec un critère de convergence explicite ; le scénario phare a convergé à 160 réplications. Chaque chiffre clé est livré sous forme de fourchette avec son intervalle de confiance à 95 %, jamais comme un point isolé. Le pipeline initial chargeait les 10 millions de lignes avant d’échantillonner les agents ; en inversant l’ordre, en échantillonnant d’abord la population d’agents puis en ne chargeant que leur historique, une simulation complète est passée de 27 minutes à environ 2 minutes, et cette optimisation n’a été acceptée qu’après qu’une reproduction octet par octet d’une exécution de référence figée a prouvé qu’elle ne changeait rien aux résultats. Le calibrage a fixé la limite de l’honnêteté : les projections au niveau agrégé étaient statistiquement solides, mais à la granularité infra-réseau, la variance imposait un plafond strict au nombre de regroupements pouvant être certifiés, et ajouter des agents ne le relevait pas. Plutôt que de publier des écarts par regroupement que les statistiques ne pouvaient pas garantir, le produit n’expose les résultats qu’au niveau de granularité qu’il peut défendre, et l’indique à l’écran.
L’assistant suit la même doctrine que le reste du système. Une question passe par une analyse d’intention et une résolution de périmètre, le backend exécute une requête déterministe et calcule le chiffre exact, et le LLM se contente de formuler la réponse. Les questions hors périmètre reçoivent un refus explicite, jamais une supposition. La logique de périmètre a été testée dans les deux sens : non seulement si elle peut donner une réponse fausse, mais aussi si elle peut refuser une question légitime. Le passage final sur 33 questions est ressorti net sur les deux plans : zéro réponse fausse, zéro refus injustifié. L’un sans l’autre aurait donné soit un système qui ment, soit un assistant inutile.
Stack: Python, FastAPI, React, TypeScript, Vite, DuckDB, Mesa, scikit-learn, [LLM RUNTIME : À COMPLÉTER PAR BAPTISTE]
Décisions & arbitrages
Une colonne de classification de gamme produit arrivant à 84 % nulle a été verrouillée, pas imputée : le KPI phare concerné s’affiche comme indisponible, avec la raison, plutôt que d’être calculé sur des données incomplètes.
Quantifier l’écart avec assez de précision a permis à l’équipe data du client de reconstruire la classification à la source et de réexporter un fichier complet : mesurer le trou a été le moyen le plus rapide de le corriger.
En inversant le pipeline de simulation, en échantillonnant la population d’agents avant de charger leur historique, une exécution complète est passée de 27 minutes à environ 2 minutes.
L’optimisation n’a été acceptée qu’après qu’une reproduction octet par octet d’une exécution de référence figée a prouvé qu’elle ne changeait rien aux résultats : la vitesse n’a jamais été une justification en soi.
La granularité infra-réseau est plafonnée à l’écran au niveau que les statistiques peuvent réellement certifier.
La variance sur les regroupements plus fins imposait un plafond strict qu’ajouter des agents ne relevait pas, donc le produit ne publie pas d’écarts par regroupement qu’il ne peut pas garantir.
Un scénario de stress dont le mécanisme de compensation produisait un artefact connu a été masqué de la livraison plutôt qu’affiché avec une réserve.
Un chiffre indéfendable n’était pas affiché, réserve ou pas.
Résultat
Livrée en application locale installée chez le client. Validation finale sur 33 questions : zéro réponse fausse, zéro refus injustifié.
Ce que ça montre
Concevoir un système IA structurellement incapable d’inventer un chiffre, de l’ingestion des données à la simulation, jusqu’à la réponse affichée à l’écran.