BidTender — SaaS multi-tenant
RAG citable sur des documents qui cassent les pipelines
Un dossier de consultation marocain arrive en dix à trente fichiers — PDF, Word, Excel, plans AutoCAD — en français et en arabe, souvent scannés, jusqu’à 3 592 pages pour un seul marché. J’ai construit le système qui les lit, avec chaque valeur extraite rattachée à l’extrait de texte qui l’a produite.
- Rôle
- Co-fondateur · ingénieur IA et full-stack
- Période
- Depuis septembre 2025
Stack
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.
Résultats
- 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.