GenerativSchool
Dossier 001 · Tutoriel

Retrouver la source avant de reprendre une information : construire un assistant documentaire à partir du cas Généthon

Construire un assistant documentaire qui relie une information à sa publication : résultat visible, code Python et limites de recherche.

Par GenerativSchool · Mis à jour le · 18 min de lecture

Prototype limité

Vous préparez une note de vulgarisation sur la recherche génétique. Vous lisez qu’une étude a suivi huit participants pendant une durée médiane de 7,6 ans. Avant de reprendre ce chiffre, vous voulez retrouver la publication et savoir s’il concerne des personnes ou une expérience sur la souris. Un assistant documentaire peut réunir ces repères : une phrase, le type d’étude et un lien pour vérifier ce qu’elle dit.

Cette situation est un usage illustratif pour un rédacteur, un étudiant ou une personne chargée de documentation ; aucun usage chez Généthon ni gain de temps n’a été mesuré. Le point de départ est un cas concret : Généthon, laboratoire créé par l’AFM-Téléthon, dont le Téléthon soutient la recherche. Présentation de Généthon.

1. Ce que vous obtenez avant de construire

Dans le programme, saisissez exactement suivi médian 7,6 ans. Cette requête courte a été exécutée ; la scène racontée plus haut n’est pas une autre question testée. La sortie conservée contient cette reformulation française :

Suivi publié en 2022 : huit participants, suivi médian de 7,6 ans.

Elle est associée à D01, l’identifiant de la fiche, au niveau clinique — une étude chez l’humain — et à la publication de Magnani et ses collègues, Nature Medicine, 2022. Le lien mène au résumé de la publication, intitulé Abstract. Vous pouvez y retrouver le nombre de participants et la durée de suivi, puis lire l’étude avant d’en tirer une conclusion. Le programme ne rédige pas une analyse : il restitue une fiche préparée à partir de cette source.

Dans le terminal, ces éléments sont rangés en JSON, un format structuré lisible par un autre programme. Dans le navigateur, le fichier HTML présente les fiches et leurs références. Vous construirez ces deux sorties à partir du même moteur, sans serveur, sans clé et sans modèle IA.

L’intérêt envisagé est de garder le chemin de vérification à côté de l’information. Ce chemin ne garantit pas la pertinence : une question mêlant « suivi médian » et « astronomie » fait aussi remonter D01 dans les essais conservés. Le lien est correct, la réponse est hors sujet. Relisez donc la fiche autant que sa référence.

GenerativSchool, laboratoire indépendant d’expérimentation IA fondé par Soufiane Lemqari, propose ce parcours pour comprendre comment assembler et contrôler ce type d’outil. L’« atlas » est ici un petit ensemble de fiches reliées aux publications, sans partenariat avec Généthon.

2. Comprendre le trajet d’une question

Le programme ouvre les fiches et vérifie leurs champs. Il compare ensuite les mots de votre question à leur texte, classe les correspondances et assemble les fiches retenues avec leurs références. Enfin, il affiche les données dans le terminal ou fabrique une page à ouvrir dans votre navigateur. Voici où intervient chaque fichier :

Architecture du parcours local
  1. Question → app.run
  2. corpus.json → atlas.load
  3. retrieval.search : filtrer et classer
  4. retrieval.answer → JSON référencé
  5. atlas.render → HTML local
  6. check.py : comparer les moteurs
  7. Extension proposée : model_adapter (non exécutée)
Voir le diagramme source Mermaid
flowchart TD
    Q[Question au terminal] --> A[app.run]
    C[corpus.json] --> L[atlas.load : contrôler les cartes]
    L --> R[retrieval.search : filtrer et classer]
    A --> R
    R --> S[retrieval.answer : associer textes et références]
    S --> J[JSON dans le terminal]
    R --> H[atlas.render : HTML local]
    S -. extension non exécutée .-> M[model_adapter : sélectionner des identifiants]
    T[Questions figées] --> E[check.py : comparer les moteurs]
    R --> E

Un framework est un cadre logiciel qui organise une application et fournit des composants prêts à l’emploi. Ce prototype n’en requiert pas : argparse lit les arguments du terminal, pathlib gère les chemins et json transporte les données. Sur six cartes, cette organisation laisse visibles les opérations que l’on veut comprendre.

L’interface entre les composants est simple : le chargeur retourne une liste de cartes ; la recherche reçoit cette liste, une question et éventuellement un niveau ; la réponse transforme les résultats en textes assortis d’identifiants. JSON est le format de sortie, pas une base de données.

Une API HTTP avec FastAPI ou une interface Streamlit serait une extension possible. Elle demanderait des dépendances et des tests supplémentaires. Ici, l’API est une fonction Python utilisable par un autre programme : run(query, level=None). Le terminal et le fichier HTML constituent l’interface réellement livrée. Aucune intégration HTTP n’est annoncée comme exécutée.

3. Installer le projet et ouvrir vos premières fiches

Le compagnon contient le projet complet : télécharger le ZIP tutorial-v1 ou consulter le dépôt public. Téléchargez le ZIP, extrayez-le dans un dossier vide et ouvrez un terminal dans le dossier contenant app.py. Ne mélangez pas ses fichiers avec une ancienne version.

Pour suivre le trajet dans le code, repérez ces fichiers :

app.py                   entrée terminal et fonction run
retrieval.py             recherche et réponse référencée
model_adapter.py         extension modèle, inactive par défaut
check.py                 comparaison et contrôles
historical/corpus.json   six cartes déjà préparées
historical/atlas.py      chargement et rendu HTML
historical/questions.json  vingt questions figées
historical/THIRD_PARTY_NOTICES.md  attributions et modifications
runs/clean-1/ et clean-2/  traces des exécutions techniques

L’environnement des exécutions conservées est CPython 3.9.6, macOS 26.2, arm64. Cette version décrit l’expérience historique, pas une recommandation de déploiement. Un interpréteur Python doit déjà être installé. Si python3 --version échoue, préparez cet interpréteur avant de poursuivre. Les autres systèmes et versions ne sont pas validés par ces traces ; l’évaluation emploie resource, indisponible sous Windows natif.

Depuis le dossier contenant app.py :

python3 --version
python3 -m venv --without-pip .venv
.venv/bin/python -B app.py 'suivi médian 7,6 ans' --html sourced.html
.venv/bin/python -B app.py 'astronomie Saturne' --html abstention.html

Aucun paquet n’est à installer. venv crée un environnement Python isolé ; il partage l’interpréteur de votre machine. --without-pip suffit puisque nous n’utilisons que la bibliothèque standard. -B évite de créer des caches Python. Ouvrez les fichiers HTML produits dans votre navigateur, par son menu d’ouverture de fichier. Aucun serveur n’est nécessaire.

La première commande de recherche renvoie D01 ; la seconde renvoie abstention. Nous expliquerons chaque passage de la question au résultat avant d’évaluer ce comportement.

4. Préparer une fiche qui permet de vérifier une information

Pour pouvoir revenir à la publication, une phrase seule ne suffit pas : il faut garder son auteur, son emplacement et ses conditions de réutilisation.

Les six cartes reposent sur deux publications, et non sur six preuves indépendantes. Les cartes D01–D03 renvoient à Magnani et ses collègues, Nature Medicine, 2022 ; D04–D05 à Porro et ses collègues, EMBO Molecular Medicine, 2017. La première porte sur un suivi humain ; la seconde sur un modèle de souris. Nous n’attribuons pas cette seconde étude à Généthon.

Ouvrez historical/corpus.json. Chaque élément de documents rassemble une reformulation et sa provenance. Voici D01, limitée aux champs utilisés par le contrat éditorial :

{
  "id": "D01",
  "source_id": "S08",
  "title": "Long-term safety and efficacy of lentiviral hematopoietic stem/progenitor cell gene therapy for Wiskott–Aldrich syndrome",
  "text": "Suivi publié en 2022 : huit participants, suivi médian de 7,6 ans.",
  "disease": "Wiskott-Aldrich",
  "evidence_level": "clinique",
  "published_at": "2022-01-24",
  "accessed_at": "2026-10-16",
  "url": "https://www.nature.com/articles/s41591-021-01641-x",
  "locator": "Abstract",
  "attribution": "Magnani et al., Nature Medicine",
  "license": "CC-BY-4.0",
  "license_url": "https://creativecommons.org/licenses/by/4.0/",
  "changes": "Sélection, reformulation française et ajout de limites éditoriales ; aucun texte intégral.",
  "text_sha256": "1bc4a9ca220b935a384a54d340d1d39ff3bc496f365d83fdb12b259a61c36354"
}

id identifie la carte ; source_id identifie la publication, qui peut être commune à plusieurs cartes. locator indique où relire l’élément. Les dates distinguent publication et consultation. evidence_level est un classement éditorial, pas une note calculée de qualité scientifique. Le texte est une reformulation française : il ne faut pas l’afficher comme une citation originale.

Pour préparer une carte dans une future version, lire la publication et ses conditions, sélectionner un élément précisément localisable, rédiger la reformulation, puis renseigner tous ces champs. Calculer ensuite l’empreinte du texte exact, en UTF-8 :

import hashlib
text = "Suivi publié en 2022 : huit participants, suivi médian de 7,6 ans."
print(hashlib.sha256(text.encode('utf-8')).hexdigest())

La valeur doit être celle de text_sha256 ci-dessus. L’empreinte permet de détecter un changement local ; elle ne prouve pas que la publication est exacte. Pour reproduire notre expérience, gardez le corpus fourni intact. Toute nouvelle carte appartient à une nouvelle version, avec de nouvelles questions.

Les publications portent une licence CC BY 4.0 ; les attributions, liens de licence et modifications sont conservés dans historical/THIRD_PARTY_NOTICES.md, à garder avec les cartes. D06 est une réserve de la rédaction, pas une observation attribuable à Porro. Sa référence à GNT0003 n’est pas documentée dans ce paquet. La notice historique conserve aussi la correction de Magnani publiée en août 2022 et les limites de sa vérification antérieure.

5. Détecter une fiche incomplète avant de chercher

Avant d’afficher une référence, vérifions que la fiche possède les informations promises au lecteur. L’ingestion prend ici un JSON déjà préparé. Elle ne télécharge ni page web ni PDF. Dans historical/atlas.py, le début de load est :

def load(path):
    data = json.loads(Path(path).read_text(encoding='utf-8'))
    docs = data['documents']
    records = []
    for d in docs:
        if any(not isinstance(d.get(k), str) or not d[k].strip() for k in META):
            raise ValueError('Métadonnée éditoriale manquante')
        if d['license'] != 'CC-BY-4.0' or d['license_url'] != 'https://creativecommons.org/licenses/by/4.0/':
            raise ValueError('Licence non admise')
        if engine.digest(d['text']) != d['text_sha256']:
            raise ValueError('Intégrité locale du texte invalide, pas validation de source')
        records.append(dict(d, source_text=d['text'], source_sha256=d['text_sha256'], start=0, end=len(d['text']), synthetic=False, source_type='reformulation_editoriale'))
    engine.validate(records)
    return records

Le contrôle s’arrête si une métadonnée manque, si la licence ne correspond pas au contrat ou si le texte a changé. engine.validate vérifie ensuite notamment les identifiants, dates, niveaux et URL HTTPS. Les champs source_text, start et end assurent la compatibilité avec l’ancien moteur : ils ne transforment pas une reformulation en citation.

Pour vérifier le chargement sans lancer la recherche :

.venv/bin/python -B -c "import retrieval; from pathlib import Path; print(len(retrieval.atlas.load(Path('historical/corpus.json'))))"

Le résultat attendu est 6. Si le chargement échoue, comparez le fichier au manifeste livré. Ne recalculez pas aveuglément l’empreinte pour faire disparaître l’erreur : vous accepteriez alors un texte différent sans l’avoir vérifié.

6. Retrouver et classer les fiches

Comment retrouver une fiche quand le lecteur emploie des mots légèrement différents ? Le moteur transforme les mots pour rapprocher certaines formulations. Voici le dictionnaire et la fonction effectivement présents dans retrieval.py :

ALIASES = {'plaquettes': 'plaquettaire', 'corrigees': 'correction',
           'etude': 'publication'}
GRAMMAR = {'sont', 'elles', 'elle', 'il', 'ils'}

def normalize(text):
    return {ALIASES.get(t, t) for t in legacy.tokens(text) - GRAMMAR}

La tokenisation, qui découpe le texte en termes, retire accents et mots grammaticaux. La révision ajoute trois équivalences explicites et retire quelques pronoms. Ce dictionnaire est visible et limité ; il n’apprend rien.

Au cœur de search, chaque carte passe par le filtre de niveau, puis reçoit un score :

if level and d['evidence_level'] != level:
    continue
overlap = q & normalize(d['text'])
score = len(overlap) / len(q)

Ce bloc provient de la boucle sur les cartes : q contient les termes normalisés de la question, d la carte courante. Une carte est retenue si au moins un terme correspond et si le score atteint 0,6. Le moteur trie par score décroissant, puis par identifiant pour départager les égalités ; il retourne au plus six cartes.

Le score représente une proportion de mots retrouvés, pas une probabilité que la réponse soit correcte. Les nombres d’un seul caractère sont éliminés par la tokenisation historique : dans « 7,6 », ni 7 ni 6 ne participe au score. Cette limite est essentielle pour comprendre la sortie suivante.

Une porte lexicale bloque aussi certains mots de demande médicale ou de conception biologique. C’est un filtre incomplet, pas un système de sûreté validé. Le filtre de niveau s’applique avec --level preclinique ou --level clinique, avant classement.

7. Utiliser le résultat dans votre programme et votre navigateur

Pour afficher le même résultat ailleurs, exposons une seule fonction qui relie chargement, recherche et réponse. La fonction centrale de app.py tient en trois lignes :

def run(query, level=None):
    records = retrieval.atlas.load(ROOT/'historical/corpus.json')
    return retrieval.answer(retrieval.search(records, query, level))

Les chemins sont résolus depuis app.py. La question doit contenir de 1 à 2 000 caractères ; une question vide ou un niveau inconnu provoque une erreur. La fonction answer construit ensuite le contrat de sortie :

def answer(result):
    # Assemblage déterministe : aucune phrase factuelle nouvelle.
    return dict(schema_version='1.0', status=result['status'], ai_executed=False,
                query=result['query'], reason=result['reason'],
                statements=[dict(text=h['text'], references=[h['id']],
                                 editorial_reservation=h['id']=='D06') for h in result['hits']],
                evidence=result['hits'], search=result)

Chaque texte reste celui d’une carte et sa liste references contient l’identifiant correspondant. evidence transporte les métadonnées permettant de remonter à la publication ; search conserve les détails du classement. Aucune phrase factuelle nouvelle n’est générée.

Pour intégrer cette API Python à votre propre programme, créez demo.py à côté de app.py avec :

from app import run
result = run('suivi médian 7,6 ans')
print(result['status'])
print(result['statements'][0]['references'])

Puis exécutez .venv/bin/python -B demo.py. Les sorties attendues sont extraits_documentaires et ['D01']. C’est la même chaîne de traitement que celle du terminal. Dans main, argparse transmet la question à run, puis json.dumps sérialise le résultat. Avec --html, atlas.render produit une page de cartes et échappe les textes pour l’HTML. Cet export affiche les résultats de recherche, pas une conversation interactive.

Pour utiliser l’invite intégrée :

.venv/bin/python -B app.py

Saisissez votre question après Question documentaire >, puis Entrée.

8. Lire le résultat détaillé et comprendre une recherche vide

Reprenons suivi médian 7,6 ans. Après chargement des six cartes, la question devient ans, median, suivi. Ces trois termes sont présents dans D01 : sa couverture vaut 1,0. Voici une projection de la sortie réellement conservée dans runs/clean-1/sourced.json ; les métadonnées complètes restent dans ce fichier :

{
  "status": "extraits_documentaires",
  "ai_executed": false,
  "statements": [
    {
      "text": "Suivi publié en 2022 : huit participants, suivi médian de 7,6 ans.",
      "references": [
        "D01"
      ],
      "editorial_reservation": false
    }
  ]
}

La référence D01 mène à S08, avec son URL Nature Medicine et le repère Abstract. Le lecteur peut donc remonter de la phrase à la carte puis à la publication. Il doit toujours relire la source pour interpréter le résultat.

Avec astronomie Saturne, aucun terme ne permet de retenir une carte. La sortie observée contient status: "abstention", statements: [], evidence: [] et ai_executed: false. L’abstention signifie ici que le moteur n’a pas trouvé de carte suffisamment correspondante. Elle ne signifie pas qu’aucune publication scientifique pertinente n’existe.

9. Vérifier ce que le moteur retrouve — et ce qu’il manque

Lancez la comparaison dans un dossier neuf :

.venv/bin/python -B check.py --out runs/my-first-run

Si ce dossier existe déjà, choisissez un autre nom. check.py conserve les résultats complets de chaque moteur dans comparison.json, les contrôles dans controls.json et l’environnement ainsi que les mesures dans execution.json. Il vérifie également les empreintes historiques et appelle réellement la CLI dans des sous-processus.

Le critère est précis : pour une question avec réponse attendue, le premier identifiant retourné doit appartenir aux attendus ; pour une abstention attendue, aucun identifiant ne doit être retourné. Le test ne note pas la validité médicale d’une réponse.

Les équivalences lexicales permettent de retrouver deux formulations auparavant manquées. D’autres problèmes restent visibles : « personnes suivies » n’est pas rapproché de « participants », et des mots communs peuvent faire accepter une question hors sujet. Vous pouvez examiner chaque cas dans comparison.json, en comparant baseline et improved, notamment passed, hits et matched_terms.

Les règles sont déjà présentes dans retrieval.py : inutile de les ajouter à nouveau. Les questions étaient connues pendant le développement. Pour aller plus loin, préparez des questions nouvelles avant de régler le moteur et comptez aussi les réponses qu’il aurait dû refuser. Les résultats chiffrés et leurs conditions figurent dans l’annexe de validation.

10. Prochaine capacité proposée : aider à écarter les fiches hors sujet

Une fois des fiches retrouvées, un modèle pourrait aider à sélectionner celles qui répondent vraiment à la question. C’est un besoin suggéré par les faux positifs observés ; le bénéfice reste à mesurer.

Cette étape décrit une intégration proposée ; aucune inférence IA n’a été exécutée. Le candidat est Qwen2.5-0.5B-Instruct, fiche officielle, sous Apache-2.0, avec environ 0,49 milliard de paramètres. La révision proposée est 7ae557604adf67be50417f59c2c2f167def9a775.

Le rôle proposé est restreint : sélectionner des identifiants parmi les cartes déjà retrouvées. Le modèle ne pourrait pas récupérer une carte manquée par la recherche. Transformers servirait à charger le découpeur de texte (tokenizer) et le modèle ; PyTorch effectuerait les calculs. Ces bibliothèques sont absentes des environnements propres du parcours principal.

L’adaptateur livré utilise un dossier de modèle local, CPU float32, local_files_only=True, trust_remote_code=False, un plafond de 2 048 tokens en entrée et 128 tokens nouveaux. Le calcul théorique des paramètres seuls donne environ 1,96 Go en float32 ; il faut ajouter bibliothèques, activations et cache. Ce n’est pas une mesure locale de mémoire.

La sortie acceptée est uniquement {"references":["D01"]} ou une liste vide. Le validateur refuse les identifiants absents, les doublons, les types incorrects et les clés supplémentaires ; il reconstruit le texte depuis les cartes locales. Voici le branchement à utiliser seulement après préparation séparée d’un environnement compatible et d’un snapshot complet vérifié :

from app import run
from model_adapter import select_local
answer = run('suivi médian 7,6 ans')
selected = select_local(answer, '/chemin/vers/snapshot-verifie')
print(selected)

Ce chemin est à remplacer ; ce bloc n’est pas une commande d’installation validée. Le document MODEL_EXTENSION.md propose Transformers 4.46.0 et PyTorch 2.3.1, sans avoir testé leur combinaison CPU/macOS. Il ne fournit donc pas encore un parcours d’inférence reproductible. Les tests existants passent seulement des chaînes fabriquées à validate_selection : ils vérifient le contrat JSON, pas les capacités du modèle.

Une future exécution doit conserver la provenance et les empreintes du snapshot, l’environnement exact, les sorties brutes, erreurs et ressources. Une erreur de parsing ou de référence doit produire une abstention de l’extension, avec erreur conservée. La sélection peut retirer une bonne carte ; son utilité devra être mesurée sur des questions indépendantes.

Ce que vous pouvez maintenant construire

Vous savez relier une question à des fiches sourcées, afficher leur provenance dans une page locale et appeler le même traitement depuis Python. Vous savez aussi repérer une abstention et examiner les cas où une référence correcte masque une réponse hors sujet. Le compagnon du tutoriel réunit le projet complet et les traces.

La prochaine amélioration utile sera de tester de nouvelles formulations et de mieux refuser les résultats hors sujet, avant d’étendre le corpus ou de brancher un modèle. Ce travail permettra d’évaluer le moteur ; il faudra une étude d’usage distincte pour savoir si l’outil aide réellement ses lecteurs.

Périmètre et droits. Cette expérience documentaire indépendante n’évalue pas les systèmes internes de Généthon et n’apporte ni résultat biologique nouveau ni conseil médical. Les six fiches ne constituent pas une revue exhaustive. Le code est consultable sans licence de réutilisation accordée à ce stade ; la CC BY 4.0 des publications ne couvre pas globalement le projet. Conservez les notices et attributions avec les fiches.