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
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
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
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
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
-- 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
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.