Ilyas Ouhnine Produits IA · de bout en bout
← Toutes les références

BidTender — SaaS multi-tenant

Un produit IA entier, construit et exploité seul

Un SaaS multi-tenant pour répondre aux marchés publics marocains : le scraping quotidien qui va chercher les dossiers, l’OCR bilingue qui les lit, la recherche citable, l’extraction structurée, les moteurs de décision, la génération des pièces à rendre, le tunnel commercial et la facturation. 84 routeurs d’API et 300 modules métier, écrits et exploités par une seule personne.

1 personne84 routeurs d’API · 300 modules métier · en production
Rôle
Co-fondateur · ingénieur IA et full-stack
Période
Depuis septembre 2025
Voir le produit — bidtndr.com ↗

Stack

  • PostgreSQL
  • pgvector
  • HNSW
  • BM25
  • Python
  • FastAPI
  • Redis
  • ARQ
  • Playwright
  • Azure Document Intelligence
  • Tesseract
  • OpenAI
  • Gemini
  • Supabase
  • React
  • TypeScript
  • Datadog
  • Docker

Le problème

Avant de répondre à un marché public, une entreprise doit savoir si elle est éligible, quel agrément est exigé, quelle caution provisoire déposer, comment elle sera notée, et quelles clauses vont lui coûter cher. Cette lecture prend des heures, elle est faite par les gens les plus chers de la maison, et la majorité des dossiers finit écartée.

Mettre un LLM dessus est l’idée évidente. C’est aussi là que ça casse : un dossier de consultation n’est pas un document, c’est une pièce contractuelle. Une réponse approximative n’est pas une réponse un peu moins bonne — c’est une candidature rejetée pour non-conformité, ou un engagement pris par erreur.

« Je crois que la caution provisoire est de 50 000 dirhams » est inutilisable. Le même montant accompagné de la phrase exacte du CPS qui l’énonce est utilisable, parce que l’humain vérifie en trois secondes. La contrainte n’est pas la qualité de la réponse : c’est sa traçabilité.

Les contraintes

Deux fondateurs techniques : ce qui n’est pas maintenable par une personne n’est pas construit.

Multi-tenant dès le premier jour, avec des entreprises concurrentes sur la même base. Une fuite entre locataires n’est pas un bug, c’est la fin du produit.

Aucun contrôle sur le format d’entrée : PDF natifs, scans, documents Word, tableurs, plans AutoCAD, et deux langues — français et arabe, y compris en écriture manuscrite ou scannée.

Une réponse fausse coûte plus cher qu’une absence de réponse. « Cette information n’est pas dans le dossier » devait être un résultat de première classe.

Ce que j’ai construit

La provenance, transportée de bout en bout. La citation ne se perd pas à la génération, elle se perd au découpage. Chaque valeur extraite conserve l’extrait verbatim du texte qui l’a produite : 274 376 citations sur 58 champs distincts, dont 99,95 % avec le passage source. Un champ qu’on ne peut pas sourcer est signalé comme incertain plutôt que renvoyé avec aplomb.

Un découpage par clause, pas par fenêtre de tokens. Couper un CPS tous les 800 tokens produit des chunks qui commencent au milieu d’un article. Sur un texte contractuel c’est destructeur : l’unité de sens est l’article numéroté, et une obligation coupée en deux devient une obligation fausse. Le découpage suit la hiérarchie documentaire, la fenêtre de tokens ne sert que de filet.

Recherche hybride pgvector + BM25 — par nécessité, pas par élégance. Le vocabulaire des marchés publics est formulaire : deux passages qui disent l’inverse ont des embeddings quasi identiques. Symétriquement, ce que l’utilisateur cherche — un numéro d’article, une référence de lot, une date, un montant — sont des tokens rares, le terrain de BM25. Aucune des deux approches seule n’atteignait un niveau utilisable. 151 661 chunks, 113 244 vecteurs, index HNSW.

Un routage OCR page par page, bilingue. Les pages sans couche texte exploitable et les pages en écriture arabe partent vers un moteur d’OCR dédié ; les autres non, parce que l’OCR systématique coûte cher et dégrade un PDF natif. Sur les documents concernés, la position et le score de confiance de chaque mot sont conservés — 1,1 million de mots géolocalisés dans leur page.

Isolation multi-tenant dans la recherche, pas après. L’implémentation naïve cherche sur tout l’index puis filtre par locataire : sécurisée, et cassée. Si les meilleurs résultats globaux appartiennent à d’autres organisations, l’utilisateur reçoit zéro résultat sur ses propres documents. Le rappel s’effondre à mesure que la base grossit, et le bug n’apparaît jamais en démo mono-client.

Extraction structurée, tableaux compris. 58 champs extraits par dossier, mais aussi 132 492 exigences documentaires, 29 096 lots, 12 521 critères d’attribution, et 228 771 lignes de bordereau de prix — la partie la plus dure, parce que dans une grille tarifaire la structure du tableau *est* l’information.

Traitement asynchrone avec reprise. Le vrai travail n’est pas la file d’attente : c’est qu’un document qui échoue à la page 180 reprenne là où il s’est arrêté au lieu de repayer 180 pages d’OCR.

Au-delà de la lecture — ce que la plateforme fait vraiment

La recherche citable est la partie visible. Elle représente une dizaine de modules sur 300. Le reste est ce qui sépare une démonstration d’un produit qu’on facture.

Remplir des formulaires officiels sans gabarit. Les pièces à rendre sont des formulaires administratifs dont chaque acheteur a sa variante. Plutôt que de maintenir un gabarit par formulaire, le système détecte géométriquement les zones à remplir, induit quel champ va dans quelle zone, produit le document rempli, puis vérifie le rendu au pixel avant de le proposer à la relecture. C’est le seul endroit du produit où l’analyse d’image, la géométrie et la validation comptent plus que le modèle de langue.

Déduire la règle à partir du résultat. Les procès-verbaux publient le gagnant, mais pas toujours la méthode d’attribution appliquée. Un moteur la déduit : huit signaux calculés sur les seules offres recevables, un arbre de décision, un score de confiance honnête qui préfère répondre « indéterminé » plutôt que de deviner. Validé sur 100 marchés réels : la première version ne passait le seuil de confiance que sur 4 cas, la seconde sur 38, sans aucun faux positif — et elle a mis au jour 7 erreurs d’étiquetage du collecteur que personne n’avait vues.

Un coût par requête, connu et pilotable. Toutes les questions ne méritent pas le même modèle. Un classifieur d’intention de quelques dizaines de jetons, mis en cache par empreinte de la question, envoie les demandes factuelles vers un palier bien moins cher — un rapport d’environ cinquante à un. Derrière un interrupteur, mesuré par client, et coupable sans redéploiement si la qualité bouge.

Des garde-fous, pas seulement des consignes. Les extraits retenus sont inspectés avant d’atteindre le modèle : les passages qui tentent de détourner l’assistant sont écartés et journalisés, ce qui permet de savoir quels documents déposés essaient de le faire. En parallèle, un mode rigueur relit la réponse phrase par phrase contre ses sources et signale visuellement ce qui n’est pas soutenu.

Générer les documents, pas seulement les lire. Mémoires techniques, présentations avec charte et score de qualité, exports PDF et Word, et l’assemblage des pièces en une enveloppe prête à déposer.

Le tunnel commercial, construit dedans. Détection et enrichissement des prospects, machine à états du pipeline qui interdit les transitions illégales, scoring, séquences de relance — et un filtrage de visibilité par rôle appliqué en base : un commercial ne voit que ses dossiers, un manager son périmètre.

Le socle

L’isolation. Sécurité au niveau des lignes côté base avec ses fonctions d’aide, matrice (fonction, rôle) côté application, et le filtre locataire poussé à l’intérieur de la requête de recherche plutôt qu’appliqué après.

Le chemin chaud. La vérification du jeton passe par quatre niveaux de cache — requête, processus, Redis partagé, base en dernier recours — parce qu’on ne peut pas payer une requête d’authentification à chaque appel d’API.

Les traitements longs. File d’attente avec reprise, progression poussée en temps réel vers l’interface, et rien à recommencer quand un document échoue en cours de route.

Ce qu’on facture. Plans, abonnements, compteurs d’usage, quotas, crédits, factures et paiements — un produit qui ne sait pas compter ce qu’il consomme ne sait pas ce qu’il vaut.

Ce qu’on peut prouver. Journal d’audit des actions, traces et journaux applicatifs, alertes de dépassement, et un tableau de bord interne qui corrèle les signaux du dépôt de code, de la supervision et de la base pour suivre ce qui avance réellement.

Résultats

  • 84 routeurs d’API et 300 modules métier en production, écrits et exploités seul — comptés dans le dépôt le 5 septembre 2026
  • Moteur d’inférence de la méthode d’attribution validé sur 100 marchés réels : 38 classés à haute confiance après affinage, 9 cas « au rabais » sur 9 correctement identifiés, aucun faux positif, et 7 erreurs d’étiquetage du collecteur détectées
  • 45 000 fichiers traités en production, 40 800 documents distincts, 56 Go — PDF, Word, Excel, plans AutoCAD
  • Plus de 250 000 pages comptées · dossier médian 58 pages · plus gros dossier rencontré 3 592 pages sur 32 fichiers
  • 274 376 citations de provenance sur 58 champs, dont 99,95 % avec l’extrait verbatim
  • 14 % des documents indexés ont dû passer par l’OCR — 1,1 million de mots conservés avec position et score de confiance
  • 228 771 lignes de bordereau de prix extraites sur 2 950 dossiers
  • Recherche vectorielle en 1 ms côté serveur sur 113 000 vecteurs, index HNSW
  • Un dossier complet analysé de bout en bout en 2 min 11 s médian, pour 0,22 USD
  • 4 organisations clientes actives, dont 3 sur un plan payant
  • Dépouillement d’un dossier : de 4 h à 40 min — et le nombre de dossiers réellement examinés est passé de 10 à 30 par mois, à effectif constant (deux entretiens clients)
  • Exactitude d’extraction mesurée sur un jeu de référence annoté à la main — 30 dossiers stratifiés × 8 champs critiques, 240 cellules : 90,4 % globalement, 86,2 % sur les dossiers scannés, et 75 % sur le sous-ensemble où le document source était relisible, où se trouvent la totalité des erreurs. Échantillon tiré de façon déterministe, protocole et code de scoring reproductibles.

Ce que je referais autrement

Le jeu d’évaluation avant le pipeline. Pendant des mois, « est-ce que c’est meilleur ? » se jugeait à l’œil sur quelques requêtes familières. Le jeu de référence existe aujourd’hui — 30 dossiers stratifiés entre natifs, scannés et plus de cent pages, tirés de façon déterministe pour que n’importe qui puisse rejouer exactement le même échantillon. Deux jours de travail qui auraient rendu chaque décision mesurable au lieu d’opinable. Je le referais en premier.

J’ai sous-estimé les tableaux. Le texte au fil de l’eau se règle vite ; les bordereaux de prix, où la structure de la grille est l’information, ont demandé une chaîne de traitement entièrement séparée. Aujourd’hui, sur un corpus inconnu, ma première question est : quelle part de votre information vit dans des tableaux ? La réponse change le plan de charge du simple au double.

Le silence aurait dû être un objectif produit dès le départ. Un système qui répond toujours est un système qui invente parfois — le seul mode de défaillance vraiment grave sur une pièce contractuelle. En faire une réponse correcte et assumée est autant un travail d’interface que de modèle, et je l’ai traité trop tard.

J’ai mesuré le coût par requête bien trop tard. Pendant longtemps, la dépense en modèles était une ligne globale à la fin du mois : impossible de dire quelle fonctionnalité coûtait quoi, ni quel client était rentable. Le routage par intention et le suivi par requête ont été ajoutés après coup, alors qu’ils auraient dû être là dès la première mise en ligne — ils changent les décisions produit, pas seulement la facture.

Une démo qui marche. Et après ?