Le problème : enseigner ce qu’on a pas encore enseigné

J’ai un cours de programmation avec 47 classes. Chaque classe a des notes (où j’explique des trucs) et des labs (où l’étudiant pratique). Et j’ai un problème : parfois j’utilise des concepts dans les labs que j’ai pas encore expliqués dans les notes.

“Bon, dans cet exercice utilise map pour transformer la liste.”

Le problème ? J’ai pas expliqué c’est quoi map avant trois classes plus tard.

Ça arrive plus souvent qu’on croit. T’as le matériel dans la tête, tu sautes d’un endroit à l’autre, et sans t’en rendre compte tu assumes que l’étudiant sait des trucs que tu lui as pas encore racontés. Le résultat : frustration, confusion, et des étudiants qui pensent qu’ils sont nuls alors que le nul c’est toi.

La solution manuelle serait de réviser chaque lab, noter quels concepts il utilise, et vérifier qu’ils ont été expliqués avant. Mais j’ai 47 classes avec plusieurs notebooks chacune. Ça va pas le faire.

La solution : recherche sémantique avec ChromaDB

L’idée est simple :

  1. Extraire les concepts de chaque notebook (ce qui est enseigné, ce qui est utilisé)
  2. Les sauvegarder dans une base de données qui comprend le sens, pas juste le texte
  3. Pour chaque concept utilisé dans un lab, vérifier qu’il existe dans des notes précédentes

Ce truc de “comprendre le sens” est crucial. Si dans les notes je dis “fonction d’ordre supérieur” et dans le lab j’utilise “higher-order function”, un grep trouvera rien. Mais sémantiquement c’est pareil.

C’est là qu’entre ChromaDB : une base de données de vecteurs qui convertit le texte en embeddings et permet de chercher par similarité. Dit en français : tu sauvegardes du texte, et après tu peux demander “y a-t-il quelque chose de semblable à ça ?” et ça te retourne les plus similaires.

ChromaDB en 5 minutes

ChromaDB c’est comme SQLite mais pour les embeddings. Un seul fichier (ou dossier), sans serveur, sans configuration. Tu installes, tu utilises, et ça roule.

1
2
3
pip install chromadb
# Ou si tu utilises uv:
uv add chromadb

Le concept de base

Dans une base de données normale tu sauvegardes des lignes avec des colonnes. Dans ChromaDB tu sauvegardes des documents avec des embeddings :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import chromadb

# Créer client (persistant sur disque)
client = chromadb.PersistentClient(path="./ma_db")

# Créer une "collection" (comme une table)
collection = client.get_or_create_collection(
    name="concepts",
    metadata={"hnsw:space": "cosine"}  # Distance cosinus
)

# Sauvegarder documents
collection.add(
    ids=["c1", "c2", "c3"],
    documents=["fonction pure", "boucle for", "récursion"],
    metadatas=[
        {"classe": "class_010", "type": "notes"},
        {"classe": "class_015", "type": "notes"},
        {"classe": "class_020", "type": "notes"},
    ]
)

C’est tout. ChromaDB automatiquement :

  1. Génère des embeddings des documents (par défaut utilise all-MiniLM-L6-v2)
  2. Les indexe pour recherche rapide
  3. Les persiste sur disque

Chercher par similarité

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
results = collection.query(
    query_texts=["higher-order function"],
    n_results=3
)

print(results["documents"])
# [['fonction d\'ordre supérieur', 'fonction pure', 'récursion']]

print(results["distances"])
# [[0.23, 0.45, 0.67]]  # Plus petit = plus similaire

Tu vois ? J’ai cherché “higher-order function” et ça a trouvé “fonction d’ordre supérieur” même si le texte est complètement différent. C’est ça la magie des embeddings.

Le système complet : validation curriculaire

Maintenant on va construire le système qui valide que je me plante pas. Le code réel est dans mon projet, mais voici la version simplifiée pour que tu comprennes le concept.

Étape 1 : Extraire les concepts des notebooks

D’abord on doit sortir les concepts de chaque notebook. Je fais ça avec un LLM (Gemini Flash via OpenRouter), mais tu pourrais le faire avec des regexes si t’es courageux :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def extract_concepts_from_notebook(notebook_path: Path) -> list[dict]:
    """
    Extrait les concepts d'un notebook Jupyter.

    Returns:
        Liste de {"name": "concept", "category": "introduces|uses"}
    """
    content = get_notebook_content(notebook_path)

    # Appeler le LLM pour extraire les concepts
    response = llm.chat(
        messages=[
            {"role": "system", "content": EXTRACTION_PROMPT},
            {"role": "user", "content": content}
        ]
    )

    return json.loads(response)

Le LLM classe chaque concept comme :

  • introduces : Est enseigné avec explication
  • uses : Est utilisé en assumant une connaissance préalable

Étape 2 : Sauvegarder dans ChromaDB

Maintenant on sauvegarde les concepts avec leurs métadonnées :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from chromadb.utils import embedding_functions

# Utiliser modèle multilingue (français + anglais)
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="paraphrase-multilingual-MiniLM-L12-v2"
)

collection = client.get_or_create_collection(
    name="course_concepts",
    embedding_function=embedding_fn,
    metadata={"hnsw:space": "cosine"}
)

# Sauvegarder concepts
for class_id, concepts in course_concepts.items():
    for concept in concepts:
        collection.add(
            ids=[f"{class_id}:{concept['name']}"],
            documents=[concept["name"]],
            metadatas=[{
                "class_id": class_id,
                "category": concept["category"],
                "source_type": concept["source_type"],  # notes ou labs
            }]
        )

Étape 3 : Valider la progression

Voici le truc intéressant. Pour chaque concept “utilisé” dans un lab, on vérifie qu’il existe quelque chose de similaire dans des notes précédentes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def validate_curriculum(course_concepts: dict) -> list[str]:
    """
    Valide que les labs n'utilisent pas des concepts non enseignés.

    Returns:
        Liste d'erreurs trouvées
    """
    errors = []
    known_concepts = set()

    # Traiter les classes dans l'ordre
    for class_id in sorted(course_concepts.keys()):
        class_data = course_concepts[class_id]

        # Ajouter concepts introduits dans notes aux connus
        for c in class_data:
            if c["source_type"] == "notes" and c["category"] == "introduces":
                known_concepts.add(c["name"].lower())

        # Valider concepts utilisés dans labs
        for c in class_data:
            if c["source_type"] == "labs" and c["category"] == "uses":
                if not is_concept_known(c["name"], known_concepts):
                    errors.append(
                        f"{class_id}: '{c['name']}' utilisé sans enseigner"
                    )

    return errors

La fonction is_concept_known c’est là qu’entre ChromaDB. On fait pas de match exact, on fait de la recherche sémantique :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def is_concept_known(concept: str, known_concepts: set) -> bool:
    """Vérifie si un concept est connu (exact ou sémantique)."""

    # 1. Match exact
    if concept.lower() in known_concepts:
        return True

    # 2. Recherche sémantique
    results = collection.query(
        query_texts=[concept],
        n_results=3,
        where={"category": "introduces"}  # Chercher seulement dans "introduces"
    )

    # Si y a quelque chose de très similaire (distance < 0.3), on considère connu
    if results["distances"][0] and results["distances"][0][0] < 0.3:
        return True

    return False

Étape 4 : Le rapport

En exécutant la validation sur mon cours, j’obtiens un joli rapport :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Rapport de Validation Curriculaire

17 problèmes trouvés :

## class_006_abstractions_abstraction_fonction

- **refactoriser** : Concept 'refactoriser' utilisé dans lab mais pas enseigné
  - Fichier : `labs/0.fonctions_basiques.ipynb`

## class_020_sequences_while

- **mémoïsation** : Concept 'mémoïsation' utilisé dans lab mais pas enseigné
  - Fichier : `labs/2.generer_sequences.ipynb`

## class_026_fonctions_ordre_superieur

- **map** : Concept 'map' utilisé dans lab mais pas enseigné
  - Fichier : `labs/0.exercices_applicateurs_listes.ipynb`

Maintenant je sais exactement ce que je dois réparer.

Détails qui comptent

Modèle d’embeddings multilingue

Si ton contenu est en français, utilise un modèle multilingue :

1
2
3
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="paraphrase-multilingual-MiniLM-L12-v2"
)

Le modèle par défaut (all-MiniLM-L6-v2) est entraîné principalement en anglais et peut donner des résultats bizarres avec le français.

Distance cosinus vs euclidienne

Pour le texte, utilise distance cosinus :

1
2
3
4
collection = client.get_or_create_collection(
    name="concepts",
    metadata={"hnsw:space": "cosine"}  # ← Ça
)

La distance cosinus mesure l’angle entre vecteurs, en ignorant la magnitude. C’est ce que tu veux pour la similarité sémantique.

Convertir distance en similarité

ChromaDB retourne distance (plus petit = plus similaire). Si tu veux similarité (plus grand = plus similaire) :

1
similarity = 1 - distance

Avec distance cosinus, l’intervalle est [0, 2], donc la similarité reste dans [-1, 1]. En pratique, pour des textes similaires c’est souvent dans [0.5, 1].

Persistance

ChromaDB a deux modes :

1
2
3
4
5
# En mémoire (se perd en fermant)
client = chromadb.Client()

# Persistant (se sauvegarde sur disque)
client = chromadb.PersistentClient(path="./data/chroma")

Pour un système de validation que tu vas exécuter à répétition, utilise persistant. Comme ça tu recalcules pas les embeddings à chaque fois.

Filtres avec where

Tu peux filtrer les résultats par métadonnées :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Chercher seulement dans notes
results = collection.query(
    query_texts=["fonction"],
    where={"source_type": "notes"}
)

# Chercher seulement dans classes antérieures à 020
results = collection.query(
    query_texts=["fonction"],
    where={"class_num": {"$lt": 20}}
)

C’est crucial pour la validation : on veut chercher seulement dans des concepts qui ont déjà été enseignés.

Alternatives à ChromaDB

ChromaDB c’est pas la seule option. Voici d’autres :

OutilProsContras
ChromaDBSimple, sans serveur, bonne documentationLimité à millions de vecteurs
PineconeScalable, géréPayant, vendor lock-in
WeaviatePuissant, API GraphQLPlus complexe à configurer
QdrantRapide, RustMoins connu
pgvectorSi tu utilises déjà PostgreSQLNécessite PostgreSQL

Pour un projet comme ça (milliers de concepts, pas millions), ChromaDB est parfait. Si tu dois scaler à milliards de vecteurs ou haute disponibilité, regarde les alternatives.

Le hook de pre-commit

Pour que ce soit vraiment utile, je l’ai intégré dans le workflow git :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash
# .git/hooks/pre-commit

echo "🔍 Validation progression curriculaire..."

if python bin/concept_index.py validate; then
    echo "✓ Validation réussie"
    exit 0
else
    echo "❌ Y a des concepts utilisés sans enseigner"
    echo "   Exécute : make concept-validate-report"
    exit 1
fi

Maintenant, chaque fois que j’essaie de faire un commit, le système vérifie que je me plante pas. S’il y a des violations, le commit se bloque et ça me dit quoi réparer.

Conclusion

ChromaDB c’est un de ces outils que quand tu le découvres tu penses “comment j’ai vécu sans ça ?”. C’est SQLite pour embeddings : simple, local, et ça marche.

Le cas d’usage que je t’ai montré (validation curriculaire) c’est juste un exemple. Les bases de données vectorielles servent pour :

  • Recherche sémantique dans documents
  • RAG (Retrieval-Augmented Generation) pour LLMs
  • Détection de doublons sémantiques
  • Recommandations basées sur similarité
  • Clustering de contenu

Et le mieux : la barrière d’entrée est minimale. Tu installes, tu sauvegardes des documents, tu cherches. Rien de configurer serveurs, schémas, ou index compliqués.

Si t’as un problème où tu dois trouver “des trucs semblables à ça”, essaie ChromaDB. Le pire qui peut arriver c’est que ça marche trop bien et tu te demandes pourquoi tu l’as pas utilisé avant.


TL;DR : ChromaDB c’est une base de données vectorielle locale et simple. On l’utilise pour vérifier qu’un cours de programmation n’utilise pas des concepts avant de les enseigner, en utilisant la recherche sémantique pour détecter des concepts similaires même si le texte est différent.