Ressource technique · RUMBA.EX

Rumba, pgvector et RAG avec LLM

Architecture académique de Rumba avec PostgreSQL, pgvector, RAG et LLM pour la récupération de contexte, l'auditabilité et la performance pédagogique.

Une architecture académique entre base relationnelle, vecteurs et modèles de langage

Rumba a été pensée comme une infrastructure de données au service d’usages pédagogiques avancés avec LLM. Son intérêt ne réside pas seulement dans la génération ou l’organisation d’exercices. L’architecture met en relation une base PostgreSQL structurée, une extension vectorielle native avec pgvector et un pipeline de retrieval augmented generation. L’objectif est d’offrir au modèle de langage une mémoire externe contrôlée, explicable et suffisamment rapide pour soutenir une interaction humaine en contexte éducatif.

Dans cette logique, la base ne sert pas uniquement à stocker des contenus. Elle devient une couche d’orchestration entre la structure relationnelle, les embeddings sémantiques et les mécanismes de récupération de contexte. Cette combinaison permet d’améliorer la pertinence des réponses tout en conservant une traçabilité forte.

Pourquoi combiner PostgreSQL, pgvector et RAG

Une base relationnelle classique apporte cohérence, auditabilité et contrôle des dépendances. Pgvector ajoute à PostgreSQL la capacité de stocker et d’interroger des représentations vectorielles. Le schéma RAG permet ensuite à un LLM de récupérer des fragments de contenu pertinents au lieu de s’appuyer uniquement sur ses poids internes. Ensemble, ces briques créent un système où la récupération d’information, la personnalisation pédagogique et l’explicabilité restent compatibles.

Cette articulation est particulièrement importante dans un cadre académique. Elle rend possible une ingénierie des données mesurable, une révision plus rigoureuse des contenus et un meilleur contrôle du contexte injecté dans les réponses générées.

Structuration relationnelle et robustesse temporelle

Le schéma relationnel de Rumba formalise les exercices, leurs métadonnées pédagogiques, les segments textuels et leurs dépendances. Cette structuration limite les redondances, clarifie les contraintes métier et facilite le suivi des révisions. Les identifiants UUID v7 jouent ici un rôle utile. Comme ils sont ordonnés temporellement, ils améliorent la stabilité des index, la localité d’insertion et la lecture chronologique des événements.

Des déclencheurs assurent également la mise à jour cohérente des horodatages. Cette discipline est essentielle pour garantir la reproductibilité des analyses et la compréhension a posteriori de l’évolution de la base.

Pgvector et la vectorisation pédagogique

Avec pgvector, chaque exercice ou segment de texte peut être associé à un embedding dense. Ces vecteurs traduisent le contenu dans un espace sémantique où la proximité représente une similarité conceptuelle. Dans le cadre d’un pipeline RAG avec LLM, cette propriété est décisive. Elle permet de récupérer non pas seulement des occurrences lexicales proches, mais des fragments pédagogiquement pertinents.

La base devient alors une mémoire externe interrogeable en SQL. Les opérateurs de distance, qu’ils reposent sur le cosinus, l’euclidien ou le produit interne, rendent possible un classement directement dans la requête. Ce point est méthodologiquement important, car il maintient la logique de récupération dans un espace déclaratif, auditable et plus transparent qu’une couche externe opaque.

Indexation de voisinage et performance de récupération

Pour desservir de grands volumes de vecteurs à faible latence, l’architecture s’appuie sur des index de voisinage approximatif. Des approches comme IVFFlat ou HNSW permettent de réduire fortement le coût de recherche tout en conservant un bon niveau de rappel. Le compromis entre rapidité et précision peut être ajusté au moyen de paramètres explicites. Cela rend la performance mesurable et pilotable.

Dans un usage RAG, cette performance n’est pas un détail technique secondaire. Elle conditionne la capacité du LLM à recevoir rapidement un contexte utile sans dégrader l’expérience utilisateur. La rapidité de récupération devient alors une composante directe de la qualité pédagogique.

Rumba comme mémoire externe explicable pour un LLM

Dans cette architecture, Rumba joue le rôle d’une mémoire externe contrôlée. Lorsqu’un utilisateur pose une question ou interagit avec un assistant pédagogique, le LLM interroge la base, récupère les exercices ou les segments les plus proches, puis compose une réponse à partir de ces éléments. La table de chunks affine ce mécanisme en autorisant une récupération plus granulaire et plus ciblée.

Cette logique réduit le risque d’hallucination et améliore l’ancrage contextuel des réponses. La double matérialisation des embeddings, sous forme vectorielle pour la recherche et sous forme JSONB pour l’audit, renforce encore cette explicabilité. Rumba n’est donc pas un simple dépôt documentaire. C’est une infrastructure cognitive capable de soutenir un LLM avec des garanties de cohérence et de traçabilité.

Implémentations minimales et reproductibles

Les exemples suivants montrent comment cette architecture peut être mise en œuvre. Ils couvrent le schéma relationnel, la génération d’identifiants temporels, la table de chunks, les requêtes de recherche vectorielle et l’upsert des embeddings.

base_schema.sql

base_schema.sqlextrait technique
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TYPE IF NOT EXISTS domain_enum           AS ENUM ('Phonémique','Orthographique','Syntaxe','Comprehension','Ecriture');
CREATE TYPE IF NOT EXISTS level_enum            AS ENUM ('Niveau1','Niveau2','Niveau3','Niveau4','Niveau5','Niveau6');
CREATE TYPE IF NOT EXISTS severity_enum         AS ENUM ('léger','modéré','sévère');
CREATE TYPE IF NOT EXISTS explanation_mode_enum AS ENUM ('succinct','detail','profondeur');

CREATE TABLE IF NOT EXISTS Exercise (
  exercise_id    UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
  title          TEXT        NOT NULL,
  content        TEXT        NOT NULL,
  domain         domain_enum NOT NULL,
  level          level_enum  NOT NULL,
  age_min        INTEGER     NOT NULL CHECK (age_min >= 0),
  age_max        INTEGER     NOT NULL CHECK (age_max >= age_min),
  correct_answer TEXT        NOT NULL,
  hint           TEXT,
  embedding      vector(384),
  created_at     TIMESTAMP   NOT NULL DEFAULT NOW(),
  updated_at     TIMESTAMP   NOT NULL DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION trg_set_updated_at() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at := NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_exercise_updated
  BEFORE UPDATE ON Exercise
  FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();

uuid_v7.sql

uuid_v7.sqlextrait technique
CREATE OR REPLACE FUNCTION uuid_generate_v7() RETURNS uuid AS $$
WITH t AS (
  SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint AS ms,
         gen_random_bytes(10) AS rnd
)
SELECT encode(
  set_byte(set_byte(set_byte(set_byte(set_byte(set_byte(
    repeat(E'\000', 6)::bytea, 0, (ms>>40) & 255), 1, (ms>>32) & 255),
    2, (ms>>24) & 255), 3, (ms>>16) & 255),
    4, (ms>>8) & 255), 5, ms & 255)
  || set_byte(set_byte(substring(E'\000\000'::bytea,1,2), 0, 0x70 | 0x0), 1, 0x00)
  || set_byte(set_byte(rnd, 0, (get_byte(rnd,0) & 0x3F) | 0x80), 1, get_byte(rnd,1))
, 'hex')::uuid;
$$ LANGUAGE SQL IMMUTABLE;

uuid_v7.py

uuid_v7.pyextrait technique
import os, time, uuid

def uuid7() -> uuid.UUID:
    ms = int(time.time() * 1000)
    rnd = bytearray(os.urandom(10))
    b = bytearray(16)
    b[0:6] = ms.to_bytes(6, "big")
    b[6:8] = ((ms & 0xFFFF) | 0x7000).to_bytes(2, "big")
    b[8:16] = rnd
    b[8] = (b[8] & 0x3F) | 0x80
    return uuid.UUID(bytes=bytes(b))

if __name__ == "__main__":
    print(uuid7())

exercisechunk.sql

exercisechunk.sqlextrait technique
CREATE TABLE IF NOT EXISTS ExerciseChunk (
  chunk_id       UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
  exercise_id    UUID NOT NULL REFERENCES Exercise(exercise_id) ON DELETE CASCADE,
  text           TEXT NOT NULL,
  embedding_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  embedding      vector(384),
  created_at     TIMESTAMP NOT NULL DEFAULT NOW(),
  updated_at     TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TRIGGER trg_chunk_updated
  BEFORE UPDATE ON ExerciseChunk
  FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();

CREATE INDEX IF NOT EXISTS exercisechunk_embedding_ivfflat
  ON ExerciseChunk USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);

search_examples.sql

search_examples.sqlextrait technique
-- Requête KNN sur Exercise
SELECT exercise_id, title, domain, level
FROM Exercise
ORDER BY embedding <-> $1::vector
LIMIT 10;

-- Requête KNN sur ExerciseChunk
SELECT chunk_id, exercise_id, text
FROM ExerciseChunk
ORDER BY embedding <-> $1::vector
LIMIT 10;

upsert_embeddings.py

upsert_embeddings.pyextrait technique
import json, psycopg2
from pgvector.psycopg2 import register_vector

def upsert_chunk(conn, chunk_id, exercise_id, text, emb_list):
    register_vector(conn)
    cur = conn.cursor()
    cur.execute(
        """
        INSERT INTO ExerciseChunk (chunk_id, exercise_id, text, embedding_json, embedding)
        VALUES (%s, %s, %s, %s, %s)
        ON CONFLICT (chunk_id) DO UPDATE
        SET text = EXCLUDED.text,
            embedding_json = EXCLUDED.embedding_json,
            embedding = EXCLUDED.embedding
        """,
        (chunk_id, exercise_id, text, json.dumps({"embedding": emb_list}), emb_list)
    )
    conn.commit()

Conclusion

L’architecture de Rumba montre qu’une base relationnelle enrichie par pgvector peut devenir une mémoire externe robuste pour des modèles de langage en contexte RAG. La combinaison d’un schéma structuré, d’identifiants temporellement ordonnés, d’une récupération vectorielle native et d’une logique de chunking permet d’alimenter un LLM avec des contextes plus pertinents, plus rapides à récupérer et plus justifiables a posteriori.

Cette approche réduit les hallucinations, améliore l’alignement pédagogique et renforce l’auditabilité des réponses. La performance n’est donc pas obtenue par une couche opaque ou improvisée. Elle repose sur des choix d’ingénierie des données explicites, mesurables et reproductibles, au service d’une interaction plus fiable entre corpus éducatif, base vectorielle et modèle de langage.