Comment c'est construit

Un RAG de bout en bout, où chaque citation est vérifiée par le code

Du téléchargement d'un dépôt GitHub jusqu'à la réponse citée, voici chaque étape, le choix fait à chaque fois, les alternatives écartées et le compromis assumé. Chaque affirmation renvoie au code qui la met en œuvre.

Le pipeline, étape par étape

Les quatre premières étapes ont lieu une fois, à l'import. Les trois dernières, à chaque question. Cliquez sur une étape.

Étape 1 sur 7

Import depuis GitHub

Ce qui se passe

  • L'URL est validée strictement : uniquement https://github.com/propriétaire/dépôt, sans port, sans identifiants, sans chemin en plus.
  • Le dépôt est figé sur le dernier commit de sa branche par défaut : toutes les citations pointeront vers ce commit exact.
  • L'archive est téléchargée en flux, avec une taille maximale et un délai global ; les redirections ne vont que vers des hôtes autorisés.

Le choix

Télécharger l'archive tar.gz du commit, en une requête, plutôt que cloner ou lire les fichiers un par un.

Alternatives écartées

  • git clone — Demande un binaire git sur le serveur et apporte tout l'historique, inutile ici.
  • Un appel d'API par fichier — Des milliers de requêtes : la limite de 60 requêtes par heure sans jeton serait atteinte tout de suite.

En chiffresDépôts jusqu'à 200 Mo, archive jusqu'à 50 Mo, au plus 3 000 fichiers indexés.

L'architecture

Le navigateur ne parle qu'au serveur Next.js, qui détient la clé d'API. L'API et la base ne sont jamais exposées à Internet.

Schéma d'architectureNon fiableServeur (privé)Fournisseurs externesX-API-KeyembeddingsréponseSQL + vecteursarchive du commitNavigateurGitHubNext.jsclé d'API, CSPFastAPIrôles, limites, budgetsPostgreSQLpgvector, HNSWVoyageClaude
🔑 La clé d'API ne quitte jamais le serveurContenu non fiable (visiteurs, dépôts)

Les décisions d'architecture

Chaque choix a un coût. Voici ce qui a été décidé, pourquoi, ce qui a été accepté en échange, et ce qui ferait changer d'avis.

Le serveur résout les citations

Le modèle cite des identifiants opaques ; le serveur les traduit en fichiers et lignes.

Pourquoi
C'est la seule façon de garantir, et pas seulement d'espérer, qu'aucune source n'est inventée.
Compromis accepté
Le modèle ne peut citer qu'un passage entier, pas une ligne précise à l'intérieur.
À revoir si
Des citations plus fines deviennent nécessaires : découper les passages plus petit.
Voir le code ↗

pgvector plutôt qu'une base vectorielle

Vecteurs, passages, snapshots et consommation vivent dans le même PostgreSQL.

Pourquoi
Un seul service à exploiter et à sauvegarder, des transactions et des contraintes d'intégrité réelles.
Compromis accepté
Moins de fonctions avancées qu'une base dédiée ; le filtrage exige les parcours itératifs.
À revoir si
Des dizaines de millions de vecteurs, ou un besoin de recherche multi-tenant à grande échelle.
Voir le code ↗

Des snapshots figés sur un commit

Chaque import crée un snapshot immuable, interrogeable seulement une fois complet.

Pourquoi
Une citation pointe toujours vers les lignes qui ont vraiment été lues, même après des commits.
Compromis accepté
Le dépôt n'est pas à jour tant qu'on ne le réimporte pas.
À revoir si
Il faut suivre un dépôt en continu : import incrémental déclenché par webhook.
Voir le code ↗

Un budget par réservation

Chaque question réserve son coût au pire avant de démarrer, sous un verrou PostgreSQL.

Pourquoi
« Vérifier puis agir » laisse 20 questions simultanées dépasser le budget ensemble.
Compromis accepté
Le budget est sous-utilisé de la marge de sécurité de chaque réservation.
À revoir si
Plusieurs instances : le verrou tient déjà, il suffit que toutes partagent la base.
Voir le code ↗

Pas de Redis ni de file de tâches (pour l'instant)

Les imports tournent dans le processus de l'API, avec une limite de concurrence ; les limites de requêtes sont en mémoire.

Pourquoi
Pour une démo sur un petit serveur, deux services de moins à exploiter, sans perte réelle.
Compromis accepté
Un seul processus d'API ; un redémarrage interrompt les imports en cours (signalés comme échoués).
À revoir si
Plusieurs instances, ou des imports longs et nombreux : file de tâches et compteurs partagés.
Voir le code ↗

Next.js comme intermédiaire

Le navigateur appelle le serveur Next.js, qui ajoute la clé et relaie vers l'API.

Pourquoi
La clé ne quitte jamais le serveur, et l'API n'est pas exposée : pas de CORS à configurer.
Compromis accepté
Un saut réseau de plus, et des routes de relais à maintenir.
À revoir si
Des comptes utilisateurs : sessions côté serveur, avec la même architecture.
Voir le code ↗

Des réponses en cache, dites comme telles

Les questions suggérées sont répondues une fois, puis servies depuis un cache, avec leur date de génération affichée.

Pourquoi
Une démo instantanée et gratuite pour le visiteur, sans faire passer une réponse ancienne pour une réponse en direct.
Compromis accepté
Les suggestions doivent être régénérées après chaque nouvel import du dépôt.
À revoir si
Les questions des visiteurs se répètent : un cache sémantique des questions.
Voir le code ↗

Mesurer avant d'optimiser

Un changement de la recherche n'est gardé que s'il améliore l'évaluation sur le jeu dev, puis sur le jeu test mis de côté.

Pourquoi
Sans mesure, une technique à la mode peut dégrader la qualité sans qu'on le voie. La recherche hybride faisait moins bien : elle reste désactivée. Classer les tests plus bas a fait passer le MRR de Flask de 0,71 à 0,81. Le reclassement semblait à égalité sur le jeu dev ; une règle fixée avant de lancer le jeu test a tranché : 0,76 → 0,91, il est activé (0,90 sur toutes les questions).
Compromis accepté
Annoter des questions à la ligne près prend du temps, et une étiquette oubliée fait paraître la recherche moins bonne : les échecs sont relus un par un.
À revoir si
Jamais : c'est la règle du projet.
Voir le code ↗

Un import public privé, plafonné et éphémère

Un visiteur peut importer son propre dépôt : lui seul le voit, il est supprimé au bout de 24 h, et son coût est estimé avant d'être dépensé.

Pourquoi
Tester sur son propre code convainc plus qu'une démo figée. Mais un import public coûte de l'argent et peut indexer du contenu piégé : chaque visiteur est isolé, et la dépense des visiteurs est bornée par construction (20 imports × 400 000 tokens par jour au plus).
Compromis accepté
Le visiteur est identifié par un cookie anonyme, qu'un robot peut renouveler : ce sont les quotas par adresse et le quota global qui protègent vraiment. Un dépôt déjà indexé est partagé tel quel, sans être mis à jour.
À revoir si
Des comptes utilisateurs arrivent (multi-utilisateur) : l'accès se rattache au compte, plus au cookie.
Voir le code ↗

Sécurité et coûts

Une démo publique est une cible : coût, abus et contenu hostile. Chaque garde-fou est testé.

  • Deux rôles

    La clé de démo ne peut interroger qu'une liste blanche de dépôts, plus ceux que son visiteur a importés. Un autre dépôt reçoit la même 404 qu'un dépôt inconnu.

  • Budgets journaliers

    Obligatoires en production, réservés de façon atomique : des requêtes simultanées ne peuvent pas les dépasser ensemble.

  • Limites

    Requêtes par visiteur, au plus 4 questions en même temps, taille des requêtes bornée.

  • Contenu hostile

    Archives lues en mémoire, passages échappés pour le modèle, réponses affichées sans HTML brut, tests d'injection de prompt.

  • En-têtes de sécurité

    Une Content Security Policy limitée au site lui-même, pas d'affichage dans un cadre, pas de plugins.

  • Traçabilité

    Chaque requête a un identifiant repris dans tous les logs ; chaque appel de modèle est compté et enregistré.

Mesuré, pas supposé

Sur pallets/itsdangerous, avec Claude Opus 5.5 :

Temps de réponseenviron 10 s (7 à 12 s observés)
Tokens par question1 800 à 5 800 en entrée, 400 à 900 en sortie
Coût par questionenviron 0,03 $
Recherche sur Flask, au niveau des ligneshit@1 0,86 · recall@5 0,92 · MRR 0,90
Noms exacts / questions en françaisMRR 1,00 / 1,00
Tests572 backend, 46 frontend, en CI à chaque push

65 questions annotées à la ligne près sur deux dépôts, par catégorie, avec un jeu dev pour régler et un jeu test mis de côté. Classer les tests plus bas a corrigé les questions sur un nom exact, le reclassement les questions en français ; les paraphrases (MRR 0,72) et les questions sur plusieurs fichiers (0,75) restent les plus faibles. Chaque changement n'est gardé que s'il fait mieux sur le jeu test.