Le problème d'un chatbot « nu »
Brancher un LLM directement, c'est risquer des réponses génériques ou fausses (hallucinations). Sur baccalaureat.sn, l'assistant doit répondre à partir de MES contenus : annales, corrigés, fiches par matière et série. La solution : le RAG (Retrieval-Augmented Generation) — on récupère les passages pertinents de la base, puis on demande au LLM de répondre en s'appuyant dessus.
Le principe en trois temps
Question élève │ ▼ 1. RETRIEVAL : chercher les passages pertinents (annales, fiches) │ ▼ 2. AUGMENT : injecter ces passages dans le prompt comme contexte │ ▼ 3. GENERATION : le LLM répond EN S'APPUYANT sur le contexte fourni
Retrieval : commencer simple avec MySQL
Pas besoin d'une base vectorielle lourde pour démarrer. Un premier RAG efficace peut s'appuyer sur la recherche full-text MySQL, déjà en place.
final class AnnalesRetriever
{
public function search(string $query, int $limit = 4): array
{
$sql = <<connection->executeQuery($sql, [
'q' => $query, 'limit' => $limit,
], ['limit' => \PDO::PARAM_INT])->fetchAllAssociative();
}
}Le contenu est découpé en chunks (morceaux) indexés en full-text. On récupère les 4 plus pertinents pour la question.
Augment : construire le prompt
public function buildPrompt(string $question, array $chunks): array
{
$context = '';
foreach ($chunks as $c) {
$context .= sprintf(
"[%s - %s - %s]\n%s\n\n",
$c['matiere'], $c['serie'], $c['annee'], $c['contenu']
);
}
$system = << 'system', 'content' => $system],
['role' => 'system', 'content' => "CONTEXTE:\n{$context}"],
['role' => 'user', 'content' => $question],
];
}L'instruction « réponds UNIQUEMENT à partir du contexte » est la clé anti-hallucination : le LLM est cadré pour ne pas inventer.
Vers les embeddings quand ça grandit
Le full-text MySQL a ses limites : il matche des mots, pas du sens. « Comment calculer une dérivée » et « taux de variation instantané » sont proches sémantiquement mais ne partagent pas de mots-clés. L'étape suivante : indexer les chunks en embeddings (vecteurs de sens) et chercher par similarité cosinus.
// principe : chaque chunk a un vecteur ; on cherche les plus proches de la question $questionVec = $this->embedder->embed($question); $nearest = $this->vectorStore->search($questionVec, topK: 4);
Mais commencer en full-text permet de livrer vite et d'améliorer ensuite — la philosophie que j'applique sur tous mes projets.
Ce que je retiens
- RAG = récupérer d'abord, générer ensuite. On ancre le LLM sur SES données.
- Le full-text MySQL suffit pour un premier RAG utile, sans base vectorielle.
- L'instruction « uniquement à partir du contexte » réduit fortement les hallucinations.
- Découper le contenu en chunks bien dimensionnés est déterminant pour la qualité.
- Les embeddings viennent après, quand le besoin de recherche sémantique se fait sentir.