Il problema di un chatbot "nudo"
Collegare direttamente un LLM significa rischiare risposte generiche o false (allucinazioni). Su baccalaureat.sn, l'assistente deve rispondere a partire dai MIEI contenuti: prove degli anni precedenti, correzioni, schede per materia e serie. La soluzione: il RAG (Retrieval-Augmented Generation) — si recuperano i passaggi pertinenti dal database, poi si chiede al LLM di rispondere basandosi su di essi.
Il principio in tre fasi
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: iniziare semplicemente con MySQL
Non serve un database vettoriale pesante per iniziare. Un primo RAG efficace può basarsi sulla ricerca full-text MySQL, già in uso.
final class AnnalesRetriever
{
    public function __construct(private Connection $connection) {}

    public function search(string $query, int $limit = 4): array
    {
        $sql = <<connection->executeQuery($sql, [
            'q' => $query, 'limit' => $limit,
        ], ['limit' => \PDO::PARAM_INT])->fetchAllAssociative();
    }
}
Il contenuto è suddiviso in chunk (frammenti) indicizzati in full-text. Si recuperano i 4 più pertinenti per la domanda.
Augment: costruire il 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'istruzione "rispondi UNICAMENTE a partire dal contesto" è la chiave anti-allucinazione: il LLM è vincolato a non inventare.
Verso gli embeddings quando si cresce
Il full-text MySQL ha i suoi limiti: confronta parole, non il significato. "Come calcolare una derivata" e "tasso di variazione istantaneo" sono semanticamente vicini ma non condividono parole chiave. Il passo successivo: indicizzare i chunk in embeddings (vettori di significato) e cercare per similarità coseno.
// 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);
Ma iniziare in full-text permette di consegnare rapidamente e migliorare in seguito — la filosofia che applico su tutti i miei progetti.
Cosa ritengo
  • RAG = prima recuperare, poi generare. Si ancora il LLM ai SUOI dati.
  • Il full-text MySQL basta per un primo RAG utile, senza database vettoriale.
  • L'istruzione "unicamente a partire dal contesto" riduce fortemente le allucinazioni.
  • Suddividere il contenuto in chunk ben dimensionati è determinante per la qualità.
  • Gli embeddings vengono dopo, quando si fa sentire l'esigenza di ricerca semantica.