Récemment, j’ai travaillé sur différentes méthodes de découpage (chunking) de documents. Dans ce projet, je montre comment mettre en place un RAG très simple en utilisant une stratégie de chunking basique : chaque page du PDF est traitée comme un chunk.
Le programme lit un PDF, génère un embedding pour chaque page, récupère les pages les plus pertinentes pour une question donnée, puis génère une réponse en s’appuyant uniquement sur ces pages.
Le PDF utilisé est un faux CV très simple, servant uniquement de document de test.
L’utilisation :
Pour commencer, le programme vous demandera quelle est votre question, puis vous pourrez la saisir afin qu’elle soit traitée
Ensuite, le programme lit le fichier PDF, découpe l’information en plusieurs segments, puis sélectionne ceux qui sont les plus pertinents pour répondre à votre question.
Une fois la réponse identifiée, le programme vous la présente, puis vous demande si vous souhaitez poser une autre question.
Prérequis
# requirements.txt
pypdf
numpy
scikit-learn
ollama
Pour le modèle Ollama, j’ai utilisé qwen3:4b, et pour les embeddings mxbai-embed-large:latest.
Pour la création de ce projet, les fichiers sont chargés localement, je n’ai donc pas utilisé de base de données. En revanche, il est tout à fait possible d’en intégrer une, comme par exemple Supabase, qui est facile à implémenter et à utiliser.
Imports
import ollama
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from pypdf import PdfReader
Charger le PDF
# 1. Load PDF
def load_pdf(path):
reader = PdfReader(path)
texts = []
for page in reader.pages:
text = page.extract_text()
if text:
texts.append(text)
return texts
-
PdfReader(path)ouvre le PDF. -
reader.pagespermet d’accéder à chaque page. -
extract_text()extrait le texte brut. -
Si du texte est trouvé, on l’ajoute dans une liste
texts.
Embeddings
# 2. Embed chunks
def embed_texts(texts):
embeddings = []
for t in texts:
r = ollama.embeddings(model="mxbai-embed-large:latest", prompt=t)
embeddings.append(r["embedding"])
return np.array(embeddings)
-
On crée une liste vide pour stocker les embeddings
-
Pour chaque texte
t, on calcule un embedding- On appelle Ollama avec un modèle d’embedding (
mxbai-embed-large) - On lui donnes le texte de la page
- Et il renvoie un dictionnaire contenant un vecteur numérique dans
r["embedding"]
- On appelle Ollama avec un modèle d’embedding (
Trouver les pages pertinentes
# 3. Retrieve
def retrieve(query, docs, doc_embeddings, k=3):
q_emb = ollama.embeddings(model="mxbai-embed-large:latest", prompt=query)["embedding"]
q_emb = np.array(q_emb).reshape(1, -1)
sims = cosine_similarity(q_emb, doc_embeddings)[0]
top_idx = sims.argsort()[-k:][::-1]
return [(docs[i], sims[i]) for i in top_idx]
- On calcule l’embedding de la question
- On prend la question de l’utilisateur (
query) - On la transforme en vecteur numérique (embedding) comme on l’a fait pour les pages du PDF
- On reshape en
(1, n)pour que la comparaison de similarité fonctionne correctement aveccosine_similarity
- On prend la question de l’utilisateur (
- On calcule la similarité cosinus avec chaque page
- On sélectionne les indices des
kpages les plus pertinentes
Pour information : la similarité cosinus est une mesure de similarité entre deux vecteurs non nuls définis dans un espace vectoriel. Elle correspond au cosinus de l’angle formé entre ces deux vecteurs.
Générer la réponse
# 4. Generate response
def generate_answer(query, retrieved_docs):
context = "\n\n".join([doc for doc, _ in retrieved_docs])
prompt = f"""
Tu es un assistant. Utilise uniquement le contexte ci-dessous pour répondre.
### Contexte :
{context}
### Question :
{query}
### Réponse :
"""
response = ollama.generate(model="qwen3:4b", prompt=prompt)
return response["response"]
-
On construit un contexte à partir des pages récupérées.
-
On crée un prompt : contexte + question.
-
On génère la réponse avec le modèle.
-
On retourne
response["response"].
Main
# MAIN
if __name__ == "__main__":
print("Chargement du PDF")
docs = load_pdf("data/cvtest.pdf")
print(f"{len(docs)} pages chargées.")
print("Calcul des embeddings")
doc_emb = embed_texts(docs)
print("Prêt ! Pose ta question :")
while True:
query = input("\n Question : ")
if query.lower() in ("quit", "exit"):
break
retrieved = retrieve(query, docs, doc_emb)
print("\n Contexte trouvé :")
for i, (d, s) in enumerate(retrieved):
print(f"\n--- Chunk {i+1} (score={s:.3f}) ---\n{d[:300]}...")
print("\n Réponse :")
answer = generate_answer(query, retrieved)
print(answer)
C’est un projet simple pour vous montrer qu’il ne faut pas grand-chose pour créer un petit RAG qui répond à vos questions selon vos besoins.
Il existe plusieurs techniques de chunking et plusieurs façons d’appliquer ce que je viens de vous montrer. Ne vous limitez pas à cet exemple.
Si vous voulez un moyen plus facile d’appliquer le RAG sans code, n’hésitez pas à consulter mon article RAG avec N8N.



