🤖 Data Engineering for AI
Module : Data Engineering for AI
6. Projet Final — Knowledge Base pour RAG
Durée estimée : 5-6 heures
Prérequis : Modules 1-5 complétés + Docker installé
📍 Où en es-tu dans le parcours ?
DONNÉES BRUTES DATASET PRÊT
(chaos) (training-ready)
│ ▲
│ Module 2 Module 3 Module 4 Module 5 │
│ ────────── ────────── ────────── ────────── │
│ │
▼ │
┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌────────┴───────┐
│ │ │ │ │ │ │ │
│ STOCKER │─────▶│ TRANSFORMER │─────▶│ ENRICHIR │─────▶│ AUTOMATISER & │
│ ✅ │ │ ✅ │ │ ✅ │ │ VERSIONNER ✅ │
│ │ │ │ │ │ │ │
└─────────┘ └─────────────┘ └──────────┘ └────────────────┘
════════════════════════════════
PROJET FINAL
════════════════════════════════
│
▼
┌───────────────────────┐
│ KNOWLEDGE BASE RAG │
│ ▲▲▲▲▲▲ │
│ TU ES ICI │
└───────────────────────┘
🎯 Question de ce module
Comment construire un Knowledge Base prêt à brancher sur un LLM ?
📦 Ce que tu vas livrer
À la fin de ce module, tu auras :
- Un pipeline complet d'ingestion de documents
- Un système de chunking intelligent
- Une vector database Chroma avec embeddings texte
- Un DAG Airflow pour automatiser le refresh
- Un Knowledge Base prêt pour RAG
6.0 C'est quoi RAG ?
Le problème des LLMs
┌─────────────────────────────────────────────────────────────────────────────┐
│ LE PROBLÈME DES LLMs │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ User: "Quelle est la procédure de remboursement chez ACME Corp ?" │
│ │
│ LLM (sans RAG): │
│ "Je ne sais pas, je n'ai pas accès aux documents internes d'ACME." │
│ OU PIRE: │
│ "La procédure est de remplir le formulaire XYZ..." ← HALLUCINATION ! │
│ │
│ Problèmes: │
│ • LLM ne connaît pas les données privées/récentes │
│ • Fine-tuning coûteux et pas temps réel │
│ • Hallucinations sur des sujets spécifiques │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
La solution : RAG
RAG = Retrieval-Augmented Generation
┌─────────────────────────────────────────────────────────────────────────────┐
│ COMMENT RAG FONCTIONNE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. RETRIEVE (Récupérer) │
│ ───────────────────── │
│ Question → Embedding → Chercher dans Vector DB → Documents pertinents │
│ │
│ 2. AUGMENT (Enrichir) │
│ ────────────────── │
│ Ajouter les documents au prompt du LLM │
│ │
│ 3. GENERATE (Générer) │
│ ───────────────── │
│ LLM répond en se basant sur les documents │
│ │
│ ════════════════════════════════════════════════════════════════════ │
│ │
│ User: "Procédure de remboursement ?" │
│ │ │
│ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 1. RETRIEVE │────▶│ 2. AUGMENT │────▶│ 3. GENERATE │ │
│ │ (Vector DB) │ │ (Add to prompt)│ │ (LLM) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ "Doc: Remplir form "Contexte: [doc] "Pour un │
│ RMB-01 sous 30j" Question: ..." remboursement, │
│ remplissez..." │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Le rôle du Data Engineer dans RAG
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUI FAIT QUOI DANS RAG ? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ DATA ENGINEER (ce projet) │ │
│ │ │ │
│ │ Documents Parse Chunk Embed Index │ │
│ │ (PDF, MD) ──▶ (texte) ──▶ (morceaux) ──▶ (vecteurs) ──▶ (Chroma) │ │
│ │ │ │
│ │ + Automatisation (Airflow) │ │
│ │ + Versioning (DVC) │ │
│ │ + Métadonnées (source, date, auteur) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ML ENGINEER / BACKEND │ │
│ │ │ │
│ │ Query → Retrieve → Augment Prompt → LLM → Response │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Ce projet se concentre sur la partie DATA ENGINEERING : construire le Knowledge Base.
Use case du projet
Contexte : Tu es DE dans une entreprise qui veut créer un chatbot interne basé sur sa documentation.
Documents sources :
- Documentation technique (Markdown)
- Procédures internes (PDF)
- FAQs et guides (HTML)
Livrable : Un Knowledge Base dans Chroma, prêt à être requêté.
| Étape | Ce que tu fais | Outil |
|---|---|---|
| 1. Ingest | Collecter les documents | Python, boto3 |
| 2. Parse | Extraire le texte propre | PyMuPDF, BeautifulSoup |
| 3. Chunk | Découper en morceaux | LangChain / Custom |
| 4. Embed | Vectoriser les chunks | Sentence-Transformers |
| 5. Index | Stocker dans Chroma | ChromaDB |
| 6. Test | Requête de recherche | Python |
| 7. Automate | DAG de refresh | Airflow |
6.1 Setup de l'environnement
# Installer les dépendances
pip install \
sentence-transformers \
chromadb \
pymupdf \
beautifulsoup4 \
langchain \
boto3
pythonVoir le code
# ============================================
# Imports
# ============================================
import os
import re
import hashlib
from pathlib import Path
from typing import List, Dict, Optional
from dataclasses import dataclass
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
print("✅ Imports de base chargés")6.2 Ingestion des documents
Types de documents supportés
┌─────────────────────────────────────────────────────────────────────────────┐
│ SOURCES DE DOCUMENTS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Format Parser Difficulté Exemple │
│ ────── ────── ────────── ─────── │
│ │
│ .txt open() Facile Notes, logs │
│ .md open() Facile Documentation │
│ .pdf PyMuPDF Moyen Rapports, procédures │
│ .html BeautifulSoup Moyen Pages web, FAQs │
│ .docx python-docx Moyen Documents Word │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
pythonVoir le code
# ============================================
# Modèle de document
# ============================================
@dataclass
class Document:
"""Représente un document ingéré."""
id: str # Hash unique
source: str # Chemin ou URL
content: str # Texte extrait
metadata: Dict # Infos supplémentaires
@classmethod
def from_file(cls, path: str, content: str) -> 'Document':
"""Crée un document depuis un fichier."""
doc_id = hashlib.md5(content.encode()).hexdigest()[:12]
return cls(
id=doc_id,
source=path,
content=content,
metadata={
'filename': Path(path).name,
'extension': Path(path).suffix,
'size_chars': len(content),
'ingested_at': datetime.now().isoformat()
}
)
print("✅ Modèle Document défini")pythonVoir le code
# ============================================
# Parsers pour différents formats
# ============================================
def parse_text(path: str) -> str:
"""Parse un fichier texte (.txt, .md)."""
with open(path, 'r', encoding='utf-8') as f:
return f.read()
def parse_pdf(path: str) -> str:
"""Parse un fichier PDF avec PyMuPDF."""
try:
import fitz # PyMuPDF
doc = fitz.open(path)
text = ""
for page in doc:
text += page.get_text()
doc.close()
return text
except ImportError:
print("⚠️ PyMuPDF non installé: pip install pymupdf")
return ""
def parse_html(path: str) -> str:
"""Parse un fichier HTML avec BeautifulSoup."""
try:
from bs4 import BeautifulSoup
with open(path, 'r', encoding='utf-8') as f:
soup = BeautifulSoup(f.read(), 'html.parser')
# Supprimer scripts et styles
for tag in soup(['script', 'style', 'nav', 'footer']):
tag.decompose()
return soup.get_text(separator=' ', strip=True)
except ImportError:
print("⚠️ BeautifulSoup non installé: pip install beautifulsoup4")
return ""
def parse_document(path: str) -> Optional[Document]:
"""Parse un document selon son extension."""
ext = Path(path).suffix.lower()
parsers = {
'.txt': parse_text,
'.md': parse_text,
'.pdf': parse_pdf,
'.html': parse_html,
'.htm': parse_html,
}
if ext not in parsers:
print(f"⚠️ Format non supporté: {ext}")
return None
try:
content = parsers[ext](path)
if not content.strip():
print(f"⚠️ Document vide: {path}")
return None
return Document.from_file(path, content)
except Exception as e:
print(f"❌ Erreur parsing {path}: {e}")
return None
print("✅ Parsers définis")pythonVoir le code
# ============================================
# Ingestion d'un dossier
# ============================================
def ingest_folder(folder_path: str) -> List[Document]:
"""
Ingère tous les documents d'un dossier.
Args:
folder_path: Chemin du dossier
Returns:
Liste de Documents
"""
documents = []
supported_extensions = {'.txt', '.md', '.pdf', '.html', '.htm'}
folder = Path(folder_path)
if not folder.exists():
print(f"❌ Dossier non trouvé: {folder_path}")
return documents
print(f"📂 Ingestion de {folder_path}...")
for file_path in folder.rglob('*'):
if file_path.suffix.lower() in supported_extensions:
doc = parse_document(str(file_path))
if doc:
documents.append(doc)
print(f" ✅ {file_path.name} ({doc.metadata['size_chars']} chars)")
print(f"\n📊 {len(documents)} documents ingérés")
return documents
print("✅ Fonction d'ingestion définie")pythonVoir le code
# ============================================
# Créer des documents de test
# ============================================
os.makedirs('docs', exist_ok=True)
# Document 1: Guide de démarrage
doc1 = '''# Guide de démarrage - ACME Corp
## Introduction
Bienvenue chez ACME Corp ! Ce guide vous aidera à démarrer.
## Configuration de votre poste
1. Installez les outils requis : VS Code, Git, Docker
2. Clonez le repository principal : `git clone https://github.com/acme/main`
3. Configurez vos variables d'environnement
## Procédures importantes
### Demande de congés
Pour demander des congés, utilisez le formulaire HR-01 disponible sur l'intranet.
Délai minimum : 2 semaines avant la date souhaitée.
### Remboursement de frais
Remplissez le formulaire RMB-01 avec les justificatifs.
Délai de traitement : 30 jours ouvrés.
Montant maximum sans validation manager : 100€.
'''
# Document 2: FAQ technique
doc2 = '''# FAQ Technique
## Comment accéder au VPN ?
1. Téléchargez le client VPN depuis l'intranet
2. Utilisez vos identifiants Windows
3. Connectez-vous au serveur vpn.acme.internal
## Mot de passe oublié ?
Contactez le support IT : support@acme.corp ou extension 1234.
Le support est disponible de 9h à 18h.
## Comment déployer en production ?
1. Créez une Pull Request vers la branche main
2. Obtenez 2 approbations minimum
3. Le déploiement est automatique via CI/CD
4. Vérifiez les logs dans Datadog
'''
# Document 3: Architecture
doc3 = '''# Architecture Technique
## Vue d'ensemble
Notre stack technique comprend :
- Frontend : React, TypeScript
- Backend : Python, FastAPI
- Base de données : PostgreSQL, Redis
- Infrastructure : Kubernetes sur AWS
## Microservices
### Service Utilisateurs
Gère l'authentification et les profils.
Port : 8001
Repository : github.com/acme/user-service
### Service Commandes
Gère les commandes et le panier.
Port : 8002
Repository : github.com/acme/order-service
### Service Notifications
Envoie les emails et notifications push.
Port : 8003
Repository : github.com/acme/notification-service
'''
# Sauvegarder
with open('docs/guide_demarrage.md', 'w') as f:
f.write(doc1)
with open('docs/faq_technique.md', 'w') as f:
f.write(doc2)
with open('docs/architecture.md', 'w') as f:
f.write(doc3)
print("✅ 3 documents de test créés dans docs/")pythonVoir le code
# ============================================
# Tester l'ingestion
# ============================================
documents = ingest_folder('docs')
# Afficher un aperçu
if documents:
print("\n📄 Aperçu du premier document:")
print(f" ID: {documents[0].id}")
print(f" Source: {documents[0].source}")
print(f" Contenu: {documents[0].content[:200]}...")💡 Option : Ingestion depuis MinIO
Pour la cohérence avec le Module 2, tu peux aussi ingérer depuis MinIO :
import boto3
from botocore.client import Config
s3_client = boto3.client(
's3',
endpoint_url="http://localhost:9000",
aws_access_key_id="minioadmin",
aws_secret_access_key="minioadmin123",
config=Config(signature_version='s3v4')
)
def ingest_from_minio(bucket: str, prefix: str = "") -> List[Document]:
"""Ingère les documents depuis MinIO."""
documents = []
paginator = s3_client.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get('Contents', []):
key = obj['Key']
if key.endswith(('.txt', '.md', '.pdf')):
response = s3_client.get_object(Bucket=bucket, Key=key)
content = response['Body'].read().decode('utf-8')
documents.append(Document.from_file(f"s3://{bucket}/{key}", content))
return documents
# Utilisation
# documents = ingest_from_minio("documents", "docs/")
💡 En production, les documents viennent souvent de SharePoint, Confluence, ou Notion plutôt que de MinIO. Le pattern reste le même : collecter → parser → chunker → embedder → indexer.
6.3 Chunking — Découper les documents
Pourquoi découper ?
┌─────────────────────────────────────────────────────────────────────────────┐
│ POURQUOI LE CHUNKING ? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ PROBLÈME: Document de 50 pages │
│ ───────────────────────────── │
│ │
│ • Trop long pour le contexte du LLM (4K-128K tokens max) │
│ • Embedding d'un doc entier = trop vague, pas précis │
│ • Recherche retourne tout le doc au lieu de la partie pertinente │
│ │
│ SOLUTION: Découper en chunks │
│ ─────────────────────────── │
│ │
│ • Chunks de 500-1000 tokens = taille optimale │
│ • Chaque chunk a son propre embedding │
│ • Recherche retourne les chunks pertinents seulement │
│ │
│ ════════════════════════════════════════════════════════════════════ │
│ │
│ Document (10,000 tokens) Chunks (500 tokens chacun) │
│ ┌────────────────────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ ──▶ │ 1 │ │ 2 │ │ 3 │ │ ... │ │
│ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │
│ └────────────────────┘ = 20 chunks │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Stratégies de chunking
| Stratégie | Description | Quand l'utiliser |
|---|---|---|
| Fixed size | N caractères/tokens | Simple, rapide |
| Sentence | Par phrases | Texte continu |
| Paragraph | Par paragraphes | Documentation structurée |
| Recursive | Hiérarchique (sections, paragraphes, phrases) | ✅ Recommandé |
pythonVoir le code
# ============================================
# Modèle de Chunk
# ============================================
@dataclass
class Chunk:
"""Représente un morceau de document."""
id: str # Hash unique
doc_id: str # ID du document parent
content: str # Texte du chunk
metadata: Dict # Source, position, etc.
@classmethod
def from_text(cls, text: str, doc: Document, index: int) -> 'Chunk':
"""Crée un chunk depuis du texte."""
chunk_id = hashlib.md5(f"{doc.id}-{index}-{text[:50]}".encode()).hexdigest()[:12]
return cls(
id=chunk_id,
doc_id=doc.id,
content=text,
metadata={
'source': doc.source,
'filename': doc.metadata['filename'],
'chunk_index': index,
'size_chars': len(text)
}
)
print("✅ Modèle Chunk défini")pythonVoir le code
# ============================================
# Chunker récursif
# ============================================
class RecursiveChunker:
"""
Découpe les documents de manière récursive.
Stratégie:
1. Essaye de découper par sections (##)
2. Si trop grand, découpe par paragraphes
3. Si encore trop grand, découpe par phrases
4. En dernier recours, découpe par caractères
"""
def __init__(
self,
chunk_size: int = 500, # Taille cible en caractères
chunk_overlap: int = 50 # Chevauchement entre chunks
):
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
# Séparateurs par ordre de priorité
self.separators = [
"\n## ", # Titres niveau 2
"\n### ", # Titres niveau 3
"\n\n", # Paragraphes
"\n", # Lignes
". ", # Phrases
" ", # Mots
]
def _split_text(self, text: str, separator: str) -> List[str]:
"""Découpe le texte par un séparateur."""
if separator:
parts = text.split(separator)
# Réajouter le séparateur au début de chaque partie (sauf la première)
return [parts[0]] + [separator + p for p in parts[1:]]
return [text]
def _recursive_split(self, text: str, separators: List[str]) -> List[str]:
"""Découpe récursivement avec les séparateurs."""
if not separators:
# Plus de séparateurs, retourner tel quel
return [text]
sep = separators[0]
remaining_seps = separators[1:]
if sep not in text:
# Ce séparateur n'existe pas, essayer le suivant
return self._recursive_split(text, remaining_seps)
parts = self._split_text(text, sep)
result = []
for part in parts:
if len(part) <= self.chunk_size:
result.append(part)
else:
# Trop grand, découper plus finement
result.extend(self._recursive_split(part, remaining_seps))
return result
def _merge_small_chunks(self, chunks: List[str]) -> List[str]:
"""Fusionne les petits chunks avec overlap."""
merged = []
current = ""
for chunk in chunks:
chunk = chunk.strip()
if not chunk:
continue
if len(current) + len(chunk) <= self.chunk_size:
current += (" " if current else "") + chunk
else:
if current:
merged.append(current)
# Ajouter overlap du chunk précédent
if merged and self.chunk_overlap > 0:
overlap = merged[-1][-self.chunk_overlap:]
current = overlap + " " + chunk
else:
current = chunk
if current:
merged.append(current)
return merged
def chunk_document(self, doc: Document) -> List[Chunk]:
"""Découpe un document en chunks."""
# 1. Découpage récursif
raw_chunks = self._recursive_split(doc.content, self.separators)
# 2. Fusion des petits chunks
merged = self._merge_small_chunks(raw_chunks)
# 3. Créer les objets Chunk
chunks = []
for i, text in enumerate(merged):
if text.strip():
chunks.append(Chunk.from_text(text.strip(), doc, i))
return chunks
print("✅ RecursiveChunker défini")pythonVoir le code
# ============================================
# Comparaison : Fixed-size vs Recursive
# ============================================
def chunk_fixed_size(text: str, size: int = 500) -> List[str]:
"""Découpage naïf par taille fixe."""
return [text[i:i+size] for i in range(0, len(text), size)]
# Test sur le même document
test_doc = documents[0]
print(f"📄 Document: {test_doc.metadata['filename']}")
print(f" Taille: {len(test_doc.content)} caractères\n")
# Fixed-size
fixed_chunks = chunk_fixed_size(test_doc.content, size=500)
print(f"🔲 FIXED-SIZE (500 chars):")
print(f" {len(fixed_chunks)} chunks")
print(f" Exemple chunk 1: '{fixed_chunks[0][:80]}...'")
print(f" ⚠️ Coupe au milieu des mots/phrases\n")
# Recursive
recursive_chunks = chunker.chunk_document(test_doc)
print(f"🔄 RECURSIVE (500 chars, respect structure):")
print(f" {len(recursive_chunks)} chunks")
print(f" Exemple chunk 1: '{recursive_chunks[0].content[:80]}...'")
print(f" ✅ Respecte les sections et paragraphes")pythonVoir le code
# ============================================
# Tester le chunking
# ============================================
chunker = RecursiveChunker(chunk_size=500, chunk_overlap=50)
all_chunks = []
for doc in documents:
chunks = chunker.chunk_document(doc)
all_chunks.extend(chunks)
print(f"📄 {doc.metadata['filename']}: {len(chunks)} chunks")
print(f"\n📊 Total: {len(all_chunks)} chunks")
# Aperçu
if all_chunks:
print(f"\n📝 Exemple de chunk:")
print(f" ID: {all_chunks[0].id}")
print(f" Taille: {all_chunks[0].metadata['size_chars']} chars")
print(f" Contenu: {all_chunks[0].content[:200]}...")6.4 Embeddings texte avec Sentence-Transformers
CLIP vs Sentence-Transformers
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUEL MODÈLE POUR QUOI ? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CLIP (Module 4) Sentence-Transformers (Module 6) │
│ ─────────────── ───────────────────────────────── │
│ │
│ • Images + Texte • Texte uniquement │
│ • Recherche multimodale • Recherche sémantique texte │
│ • 512 dimensions • 384-768 dimensions │
│ • Bon pour image → texte • Excellent pour texte → texte │
│ │
│ Use case: Use case: │
│ "Trouve des images de chats" "Trouve des docs sur le VPN" │
│ │
│ Pour RAG, on utilise Sentence-Transformers car on cherche du texte. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Modèles recommandés
| Modèle | Dimensions | Langue | Vitesse |
|---|---|---|---|
all-MiniLM-L6-v2 |
384 | Anglais | ✅ Très rapide |
all-mpnet-base-v2 |
768 | Anglais | Rapide |
paraphrase-multilingual-MiniLM-L12-v2 |
384 | Multi (FR) | Rapide |
pythonVoir le code
# ============================================
# Embedder avec Sentence-Transformers
# ============================================
from sentence_transformers import SentenceTransformer
import numpy as np
class TextEmbedder:
"""
Génère des embeddings pour du texte.
Modèles recommandés:
- paraphrase-multilingual-MiniLM-L12-v2 : Multilingue (FR) ✅
- dangvantuan/sentence-camembert-large : Français spécifique
- all-MiniLM-L6-v2 : Anglais seulement
"""
def __init__(self, model_name: str = "paraphrase-multilingual-MiniLM-L12-v2"):
print(f"📥 Chargement du modèle {model_name}...")
self.model = SentenceTransformer(model_name)
self.model_name = model_name
self.dimension = self.model.get_sentence_embedding_dimension()
print(f"✅ Modèle chargé (dimension: {self.dimension})")
def embed(self, text: str) -> np.ndarray:
"""Génère l'embedding d'un texte."""
return self.model.encode(text, normalize_embeddings=True)
def embed_batch(self, texts: List[str], batch_size: int = 32) -> np.ndarray:
"""Génère les embeddings pour une liste de textes."""
return self.model.encode(
texts,
batch_size=batch_size,
normalize_embeddings=True,
show_progress_bar=True
)
# Initialiser avec le modèle MULTILINGUE (supporte le français)
embedder = TextEmbedder("paraphrase-multilingual-MiniLM-L12-v2")pythonVoir le code
# ============================================
# Tester les embeddings
# ============================================
# Test simple
test_embedding = embedder.embed("Comment configurer le VPN ?")
print(f"📊 Embedding de test:")
print(f" Shape: {test_embedding.shape}")
print(f" Norme: {np.linalg.norm(test_embedding):.4f}")
# Test de similarité
queries = [
"Comment se connecter au VPN ?",
"Procédure de remboursement",
"Architecture des microservices"
]
print(f"\n🔍 Test de similarité:")
q1_emb = embedder.embed(queries[0])
q2_emb = embedder.embed(queries[1])
q3_emb = embedder.embed(queries[2])
sim_12 = np.dot(q1_emb, q2_emb)
sim_13 = np.dot(q1_emb, q3_emb)
print(f" '{queries[0]}' vs '{queries[1]}': {sim_12:.3f}")
print(f" '{queries[0]}' vs '{queries[2]}': {sim_13:.3f}")6.5 Indexation dans Chroma
pythonVoir le code
# ============================================
# Configurer Chroma pour RAG
# ============================================
import chromadb
# Client persistant
chroma_client = chromadb.PersistentClient(path="./chroma_rag_db")
# Collection pour le knowledge base
COLLECTION_NAME = "knowledge_base"
# Supprimer si existe (pour repartir de zéro)
try:
chroma_client.delete_collection(COLLECTION_NAME)
except:
pass
# Créer la collection
collection = chroma_client.create_collection(
name=COLLECTION_NAME,
metadata={"description": "Knowledge Base pour RAG - Documentation ACME"}
)
print(f"✅ Collection Chroma créée: {COLLECTION_NAME}")pythonVoir le code
# ============================================
# Indexer les chunks
# ============================================
def index_chunks(chunks: List[Chunk], embedder: TextEmbedder, collection) -> int:
"""
Indexe les chunks dans Chroma.
Args:
chunks: Liste de Chunks
embedder: Modèle d'embedding
collection: Collection Chroma
Returns:
Nombre de chunks indexés
"""
if not chunks:
return 0
print(f"📤 Indexation de {len(chunks)} chunks...")
# Extraire les contenus
texts = [c.content for c in chunks]
# Générer les embeddings
embeddings = embedder.embed_batch(texts)
# Préparer les données pour Chroma
ids = [c.id for c in chunks]
metadatas = [c.metadata for c in chunks]
# Indexer
collection.add(
ids=ids,
embeddings=embeddings.tolist(),
documents=texts,
metadatas=metadatas
)
print(f"✅ {len(chunks)} chunks indexés")
return len(chunks)
# Indexer nos chunks
n_indexed = index_chunks(all_chunks, embedder, collection)
print(f"\n📊 Collection Chroma: {collection.count()} documents")6.6 Recherche sémantique
pythonVoir le code
# ============================================
# Fonction de recherche
# ============================================
def search(query: str, n_results: int = 3) -> List[Dict]:
"""
Recherche les chunks les plus pertinents.
Args:
query: Question de l'utilisateur
n_results: Nombre de résultats
Returns:
Liste de résultats avec score et contenu
"""
# Embed la query
query_embedding = embedder.embed(query)
# Rechercher dans Chroma
results = collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=n_results,
include=['documents', 'metadatas', 'distances']
)
# Formatter les résultats
formatted = []
for i in range(len(results['ids'][0])):
formatted.append({
'id': results['ids'][0][i],
'content': results['documents'][0][i],
'source': results['metadatas'][0][i].get('filename', 'Unknown'),
'score': 1 - results['distances'][0][i] # Convertir distance en score
})
return formatted
print("✅ Fonction de recherche définie")pythonVoir le code
# ============================================
# Tester la recherche
# ============================================
test_queries = [
"Comment se connecter au VPN ?",
"Quelle est la procédure de remboursement ?",
"Quels sont les microservices ?"
]
for query in test_queries:
print(f"\n🔍 Query: '{query}'")
print("─" * 50)
results = search(query, n_results=2)
for i, r in enumerate(results, 1):
print(f"\n 📄 Résultat {i} (score: {r['score']:.3f})")
print(f" Source: {r['source']}")
print(f" Contenu: {r['content'][:200]}...")6.6b Évaluer la qualité du retrieval
Pourquoi évaluer ?
┌─────────────────────────────────────────────────────────────────────────────┐
│ POURQUOI ÉVALUER LE RETRIEVAL ? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ "Mon retrieval marche bien" │
│ → Comment tu sais ? Tu as mesuré ? │
│ │
│ Métriques clés: │
│ │
│ HIT RATE @K │
│ ─────────── │
│ "Sur 100 questions, combien ont le bon doc dans le top K ?" │
│ Hit Rate @3 = 85% → 85 questions trouvent la réponse dans le top 3 │
│ │
│ MRR (Mean Reciprocal Rank) │
│ ────────────────────────── │
│ "En moyenne, à quelle position est le bon doc ?" │
│ MRR = 0.8 → Le bon doc est en moyenne à la position 1.25 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
pythonVoir le code
# ============================================
# Évaluation du retrieval
# ============================================
def evaluate_retrieval(
test_queries: List[Dict], # [{"query": "...", "expected_source": "..."}]
k: int = 3
) -> Dict:
"""
Évalue la qualité du retrieval.
Args:
test_queries: Liste de queries avec la source attendue
k: Nombre de résultats à considérer
Returns:
Métriques d'évaluation
"""
hits = 0
reciprocal_ranks = []
print(f"📊 Évaluation sur {len(test_queries)} queries (k={k})\n")
for item in test_queries:
query = item['query']
expected = item['expected_source']
results = search(query, n_results=k)
sources = [r['source'] for r in results]
# Hit Rate
if expected in sources:
hits += 1
rank = sources.index(expected) + 1
reciprocal_ranks.append(1.0 / rank)
status = f"✅ Trouvé (rang {rank})"
else:
reciprocal_ranks.append(0.0)
status = "❌ Non trouvé"
print(f" Query: '{query[:40]}...'")
print(f" Attendu: {expected} → {status}")
# Calculer les métriques
hit_rate = hits / len(test_queries)
mrr = sum(reciprocal_ranks) / len(reciprocal_ranks)
print(f"\n" + "="*50)
print(f"📈 RÉSULTATS")
print(f"="*50)
print(f" Hit Rate @{k}: {hit_rate:.1%}")
print(f" MRR: {mrr:.3f}")
return {'hit_rate': hit_rate, 'mrr': mrr}
# Créer un jeu de test
test_queries = [
{"query": "Comment demander des congés ?", "expected_source": "guide_demarrage.md"},
{"query": "Procédure de remboursement de frais", "expected_source": "guide_demarrage.md"},
{"query": "Comment se connecter au VPN ?", "expected_source": "faq_technique.md"},
{"query": "Mot de passe oublié que faire", "expected_source": "faq_technique.md"},
{"query": "Architecture des microservices", "expected_source": "architecture.md"},
{"query": "Quel port pour le service utilisateurs", "expected_source": "architecture.md"},
]
# Évaluer
metrics = evaluate_retrieval(test_queries, k=3)6.7 Pipeline RAG complet
pythonVoir le code
# ============================================
# Pipeline RAG complet
# ============================================
class RAGPipeline:
"""
Pipeline complet pour construire un Knowledge Base RAG.
Étapes:
1. Ingest: Charger les documents
2. Chunk: Découper en morceaux
3. Embed: Vectoriser
4. Index: Stocker dans Chroma
"""
def __init__(
self,
collection_name: str = "rag_knowledge_base",
chunk_size: int = 500,
chunk_overlap: int = 50,
embedding_model: str = "all-MiniLM-L6-v2"
):
self.collection_name = collection_name
self.chunker = RecursiveChunker(chunk_size, chunk_overlap)
self.embedder = TextEmbedder(embedding_model)
# Créer/reset la collection
try:
chroma_client.delete_collection(collection_name)
except:
pass
self.collection = chroma_client.create_collection(name=collection_name)
self.stats = {
'documents': 0,
'chunks': 0,
'indexed': 0
}
def run(self, source_folder: str) -> Dict:
"""
Exécute le pipeline complet.
"""
print("="*60)
print("🚀 PIPELINE RAG - KNOWLEDGE BASE")
print("="*60)
# 1. Ingest
print("\n📥 Étape 1: Ingestion...")
documents = ingest_folder(source_folder)
self.stats['documents'] = len(documents)
# 2. Chunk
print("\n✂️ Étape 2: Chunking...")
all_chunks = []
for doc in documents:
chunks = self.chunker.chunk_document(doc)
all_chunks.extend(chunks)
print(f" {doc.metadata['filename']}: {len(chunks)} chunks")
self.stats['chunks'] = len(all_chunks)
# 3. Embed & Index
print("\n🧠 Étape 3: Embedding & Indexation...")
n_indexed = index_chunks(all_chunks, self.embedder, self.collection)
self.stats['indexed'] = n_indexed
# Résumé
print("\n" + "="*60)
print("📊 RÉSUMÉ")
print("="*60)
print(f" Documents traités: {self.stats['documents']}")
print(f" Chunks créés: {self.stats['chunks']}")
print(f" Chunks indexés: {self.stats['indexed']}")
print(f" Collection: {self.collection_name}")
return self.stats
def search(self, query: str, n_results: int = 3) -> List[Dict]:
"""Recherche dans le knowledge base."""
query_embedding = self.embedder.embed(query)
results = self.collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=n_results,
include=['documents', 'metadatas', 'distances']
)
formatted = []
for i in range(len(results['ids'][0])):
formatted.append({
'content': results['documents'][0][i],
'source': results['metadatas'][0][i].get('filename'),
'score': 1 - results['distances'][0][i]
})
return formatted
print("✅ RAGPipeline défini")pythonVoir le code
# ============================================
# Exécuter le pipeline
# ============================================
# Créer et exécuter
rag_pipeline = RAGPipeline(
collection_name="acme_knowledge_base",
chunk_size=500,
chunk_overlap=50
)
stats = rag_pipeline.run("docs")
# Test de recherche
print("\n" + "="*60)
print("🔍 TEST DE RECHERCHE")
print("="*60)
query = "Comment demander un remboursement ?"
print(f"\nQuery: '{query}'")
results = rag_pipeline.search(query, n_results=2)
for i, r in enumerate(results, 1):
print(f"\n📄 Résultat {i} (score: {r['score']:.3f}) - {r['source']}")
print(f" {r['content'][:300]}...")6.8 Démonstration : Appel LLM final
Options pour le LLM
┌─────────────────────────────────────────────────────────────────────────────┐
│ OPTIONS LLM POUR RAG │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ OPTION 1: OLLAMA (Recommandé pour ce bootcamp) │
│ ───────────────────────────────────────────── │
│ ✅ Gratuit │
│ ✅ Fonctionne en local (pas besoin d'internet stable) │
│ ✅ Pas de clé API │
│ ✅ Modèles multilingues (Mistral, Llama3) │
│ │
│ Installation: │
│ curl -fsSL https://ollama.com/install.sh | sh │
│ ollama pull mistral │
│ │
│ ════════════════════════════════════════════════════════════════════ │
│ │
│ OPTION 2: APIs GRATUITES (si pas de GPU) │
│ ──────────────────────────────────────── │
│ │
│ 🟢 Groq → console.groq.com (Llama3, Mixtral — ultra-rapide) │
│ 🟢 Mistral → console.mistral.ai (Mistral 7B, Nemo — bon en FR) │
│ │
│ ✅ Gratuit (free tier généreux) │
│ ✅ Pas de GPU local requis │
│ ⚠️ Clé API requise (inscription gratuite) │
│ ⚠️ Nécessite connexion internet │
│ │
│ ════════════════════════════════════════════════════════════════════ │
│ │
│ OPTION 3: API Cloud Payante (Anthropic, OpenAI) │
│ ───────────────────────────────────────────── │
│ ⚠️ Payant (nécessite carte bancaire) │
│ ⚠️ Nécessite connexion internet │
│ ✅ Le plus puissant (Claude Opus, GPT-4o) │
│ ✅ Pas de GPU local requis │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
⚠️ Rappel : Scope du DE
TON TRAVAIL DE DE s'arrête à : Knowledge Base prêt dans Chroma ✅
Cette section est une DÉMONSTRATION pour voir le résultat. En production, c'est le ML Engineer / Backend qui gère l'appel LLM.
pythonVoir le code
# ============================================
# Setup Ollama
# ============================================
# Installation (exécuter dans le terminal):
# curl -fsSL https://ollama.com/install.sh | sh
# ollama pull mistral
# Vérifier qu'Ollama tourne
import requests
OLLAMA_URL = "http://localhost:11434"
try:
response = requests.get(f"{OLLAMA_URL}/api/tags")
models = [m['name'] for m in response.json().get('models', [])]
print(f"✅ Ollama connecté")
print(f" Modèles disponibles: {models}")
except:
print("⚠️ Ollama non détecté")
print(" Installation: curl -fsSL https://ollama.com/install.sh | sh")
print(" Puis: ollama pull mistral")pythonVoir le code
# ============================================
# RAG complet avec Ollama
# ============================================
def call_ollama(prompt: str, model: str = "mistral") -> str:
"""
Appelle Ollama en local.
Modèles recommandés:
- mistral : Bon en français, léger (4GB)
- llama3 : Plus puissant, plus lourd (8GB)
- phi3 : Très léger (2GB), moins bon
"""
try:
response = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False
},
timeout=60
)
return response.json()['response']
except Exception as e:
return f"Erreur Ollama: {e}"
def rag_query(question: str, n_context: int = 3, model: str = "mistral") -> str:
"""
Exécute une requête RAG complète.
1. Retrieve : Cherche les chunks pertinents
2. Augment : Construit le prompt avec le contexte
3. Generate : Appelle le LLM local (Ollama)
"""
# 1. RETRIEVE
print(f"🔍 Question: {question}")
print(f"\n📚 Étape 1: RETRIEVE")
results = search(question, n_results=n_context)
context_parts = []
for i, r in enumerate(results, 1):
print(f" [{i}] {r['source']} (score: {r['score']:.2f})")
context_parts.append(f"[Source: {r['source']}]\n{r['content']}")
context = "\n\n---\n\n".join(context_parts)
# 2. AUGMENT
print(f"\n📝 Étape 2: AUGMENT")
prompt = f"""Tu es un assistant qui répond aux questions en te basant UNIQUEMENT sur le contexte fourni.
Si la réponse n'est pas dans le contexte, dis-le clairement.
Réponds en français.
CONTEXTE:
{context}
QUESTION: {question}
RÉPONSE:"""
print(f" Prompt: {len(prompt)} caractères")
# 3. GENERATE
print(f"\n🤖 Étape 3: GENERATE (Ollama/{model})")
answer = call_ollama(prompt, model=model)
return answer
print("✅ Fonction RAG définie")pythonVoir le code
# ============================================
# Test RAG complet
# ============================================
print("="*60)
print("🚀 TEST RAG COMPLET AVEC OLLAMA")
print("="*60 + "\n")
question = "Comment demander un remboursement de frais ?"
try:
answer = rag_query(question, n_context=2, model="mistral")
print("\n" + "="*60)
print("💬 RÉPONSE DU LLM:")
print("="*60)
print(answer)
except Exception as e:
print(f"\n⚠️ Erreur: {e}")
print("\n💡 Assure-toi qu'Ollama est lancé:")
print(" ollama serve")
print(" ollama pull mistral")pythonVoir le code
# ============================================
# Alternative : API Cloud (si tu as une clé)
# ============================================
# Décommenter pour utiliser
'''
# ===== OPTION A: GROQ (Gratuit) =====
# pip install groq
# Clé API gratuite sur : https://console.groq.com
from groq import Groq
groq_client = Groq(api_key="ta-clé-groq")
def call_groq(prompt: str, model: str = "llama3-8b-8192") -> str:
"""
Appelle Groq (gratuit, ultra-rapide).
Modèles disponibles:
- llama3-8b-8192 : Léger et rapide ✅
- llama3-70b-8192 : Plus puissant
- mixtral-8x7b : Bon en français
"""
response = groq_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=500
)
return response.choices[0].message.content
# ===== OPTION A-bis: Mistral API (Gratuit, bon en français) =====
# pip install mistralai
# Clé API gratuite sur : https://console.mistral.ai
from mistralai import Mistral
mistral_client = Mistral(api_key="ta-clé-mistral")
def call_mistral(prompt: str, model: str = "mistral-small-latest") -> str:
"""
Appelle Mistral API (gratuit, excellent en français).
Modèles gratuits:
- mistral-small-latest : Rapide et efficace ✅
- open-mistral-nemo : Très léger, gratuit
"""
response = mistral_client.chat.complete(
model=model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
# ===== OPTION B: Anthropic (Claude) =====
import anthropic
client = anthropic.Anthropic(api_key="ta-clé-api")
def call_claude(prompt: str) -> str:
response = client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=500,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
# ===== OPTION C: OpenAI =====
from openai import OpenAI
client = OpenAI(api_key="ta-clé-api")
def call_openai(prompt: str) -> str:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
'''
print("💡 Alternatives cloud disponibles (décommenter si tu as une clé API):")
print(" - Groq (gratuit, rapide) : console.groq.com")
print(" - Mistral API (gratuit, bon en FR) : console.mistral.ai
print(" - Anthropic Claude (payant)")
print(" - OpenAI GPT (payant)")pythonVoir le code
# ============================================
# Valider le dataset de fine-tuning
# ============================================
def validate_finetuning_dataset(path: str) -> Dict:
"""
Valide un dataset JSONL pour fine-tuning.
Checks:
- Format JSONL valide
- Structure messages correcte
- Rôles valides (system, user, assistant)
- Pas de contenu vide
"""
stats = {
'total': 0,
'valid': 0,
'errors': []
}
with open(path, 'r') as f:
for i, line in enumerate(f, 1):
stats['total'] += 1
try:
data = json.loads(line)
# Check structure
if 'messages' not in data:
stats['errors'].append(f"Ligne {i}: 'messages' manquant")
continue
# Check messages
for msg in data['messages']:
if 'role' not in msg or 'content' not in msg:
stats['errors'].append(f"Ligne {i}: message mal formé")
continue
if msg['role'] not in ['system', 'user', 'assistant']:
stats['errors'].append(f"Ligne {i}: rôle invalide '{msg['role']}'")
continue
if not msg['content'].strip():
stats['errors'].append(f"Ligne {i}: contenu vide")
continue
stats['valid'] += 1
except json.JSONDecodeError:
stats['errors'].append(f"Ligne {i}: JSON invalide")
# Résumé
print(f"📊 Validation de {path}")
print(f" Total: {stats['total']}")
print(f" Valides: {stats['valid']}")
print(f" Erreurs: {len(stats['errors'])}")
if stats['errors']:
print("\n⚠️ Erreurs:")
for err in stats['errors'][:5]:
print(f" {err}")
else:
print("\n✅ Dataset valide !")
return stats
# Valider
validate_finetuning_dataset("finetuning_dataset.jsonl")6.8 Code source et ressources
pythonVoir le code
# ============================================
# Générer le package téléchargeable
# ============================================
import zipfile
import base64
from IPython.display import HTML, display
# Créer la structure
os.makedirs('module6-rag-project/src', exist_ok=True)
os.makedirs('module6-rag-project/docs', exist_ok=True)
# requirements.txt
requirements = '''sentence-transformers>=2.2.0
chromadb>=0.4.0
pymupdf>=1.23.0
beautifulsoup4>=4.12.0
boto3>=1.28.0
'''
with open('module6-rag-project/requirements.txt', 'w') as f:
f.write(requirements)
# Copier les docs de test
for doc_file in Path('docs').glob('*.md'):
with open(doc_file, 'r') as src:
with open(f'module6-rag-project/docs/{doc_file.name}', 'w') as dst:
dst.write(src.read())
# Créer le ZIP
zip_path = 'module6-rag-project.zip'
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk('module6-rag-project'):
for file in files:
file_path = os.path.join(root, file)
zipf.write(file_path, file_path)
# Lien de téléchargement
with open(zip_path, 'rb') as f:
b64 = base64.b64encode(f.read()).decode()
html = f'''
<a download="module6-rag-project.zip"
href="data:application/zip;base64,{b64}"
style="display: inline-block; padding: 10px 20px;
background-color: #4CAF50; color: white;
text-decoration: none; border-radius: 5px;
font-weight: bold;">
📦 Télécharger module6-rag-project.zip
</a>
'''
display(HTML(html))
print(f"\n✅ Archive créée: {zip_path}")🚀 Et après ? Les patterns RAG avancés
Tu maîtrises maintenant le Naive RAG — la fondation de tout pipeline RAG. Mais en production, il existe plusieurs patterns plus avancés selon les besoins.
🎯 Par où continuer ?
| Pattern | Quand l'utiliser | Difficulté |
|---|---|---|
| Retrieve-and-Rerank | Quand la précision du retrieval est insuffisante | ⭐⭐ |
| Hybrid RAG | Quand les requêtes sont très spécifiques (mots-clés exacts) | ⭐⭐ |
| Multimodal RAG | Quand tes documents contiennent des images/schémas | ⭐⭐⭐ |
| Graph RAG | Données très interconnectées (ex: documentation technique dense) | ⭐⭐⭐⭐ |
| Agentic RAG | Quand une seule stratégie de retrieval ne suffit pas | ⭐⭐⭐⭐ |
| Multi-Agent RAG | Systèmes à grande échelle, sources multiples | ⭐⭐⭐⭐⭐ |
💡 Conseil : Commence toujours par le Naive RAG en production. Mesure ses limites avec les métriques vues en 6.6b, puis upgrade vers Retrieve-and-Rerank ou Hybrid selon ce que les métriques te disent. Ne sur-ingénie pas dès le départ.
🎯 Checkpoint — Quiz Final
Dernière ligne droite !
Question 1 : Que signifie RAG ?
A) Random Access Generation
B) Retrieval-Augmented Generation
C) Rapid AI Generation
D) Recursive Artificial Growth
📖 Voir la réponse
Réponse : B
RAG = Retrieval-Augmented Generation
- Retrieval : Récupérer les documents pertinents
- Augmented : Enrichir le prompt avec ces documents
- Generation : Le LLM génère une réponse basée sur le contexte
Question 2 : Pourquoi découper les documents en chunks ?
A) Pour économiser de l'espace disque
B) Pour avoir des embeddings plus précis et respecter la limite de contexte du LLM
C) Parce que les LLMs ne comprennent pas les longs textes
D) Pour accélérer le parsing
📖 Voir la réponse
Réponse : B
Deux raisons principales :
-
Embeddings plus précis : Un embedding d'un doc de 50 pages est trop vague. Un chunk de 500 tokens capture une idée spécifique.
-
Limite de contexte : Les LLMs ont une limite (4K-128K tokens). On ne peut pas envoyer tout un document de 50 pages.
Question 3 : Quel modèle d'embedding pour du texte en français ?
A) CLIP
B) all-MiniLM-L6-v2
C) paraphrase-multilingual-MiniLM-L12-v2
D) GPT-4
📖 Voir la réponse
Réponse : C
| Modèle | Langue | Usage |
|---|---|---|
| CLIP | Anglais | Images + Texte |
| all-MiniLM-L6-v2 | Anglais | Texte seulement |
| paraphrase-multilingual-MiniLM | Multilingue (FR) | ✅ Texte français |
| GPT-4 | Tout | LLM, pas un embedder |
Question 4 : Quelle(s) option(s) LLM sont gratuites pour ce bootcamp ?
A) Seulement OpenAI GPT-4
B) Ollama (local), Groq et Mistral API (cloud)
C) Les APIs cloud sont interdites
D) Seulement Anthropic Claude
📖 Voir la réponse
Réponse : B
| Critère | Ollama | API Cloud |
|---|---|---|
| Prix | ✅ Gratuit | ❌ Payant |
| Clé API | ✅ Pas besoin | ❌ Requise |
| Internet | ✅ Fonctionne hors-ligne | ❌ Requis |
| Carte bancaire | ✅ Pas besoin | ❌ Souvent requise |
Question 5 : Qu'est-ce que le Hit Rate @K ?
A) Le nombre de requêtes par seconde
B) Le pourcentage de queries où le bon document est dans le top K résultats
C) La taille moyenne des chunks
D) Le nombre d'embeddings générés
📖 Voir la réponse
Réponse : B
Hit Rate @K mesure la qualité du retrieval :
- Sur 100 questions de test
- Combien trouvent le bon document dans le top K ?
- Hit Rate @3 = 85% → 85 questions sur 100 trouvent la réponse dans le top 3
Question 6 : Quel est le rôle du Data Engineer dans un projet RAG ?
A) Configurer le LLM et écrire les prompts
B) Construire le Knowledge Base (ingestion → chunking → embedding → indexation)
C) Entraîner le modèle de langage
D) Concevoir l'interface utilisateur
📖 Voir la réponse
Réponse : B
DATA ENGINEER ML ENGINEER / BACKEND
───────────── ─────────────────────
Documents → Parse → Chunk Query → Retrieve → LLM
→ Embed → Index → Response
Livrable: Knowledge Base ✅ Livrable: API/Chatbot
🎉 FÉLICITATIONS !
Tu as terminé le bootcamp Data Engineering for AI — From Zero to Hero !
📚 Récapitulatif complet du bootcamp
Module 1 — Introduction au Data-Centric AI
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ce que tu as appris │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Le paradigme Data-Centric AI (améliorer les données, pas le modèle) │
│ • Le ratio 80/20 (80% du temps sur les données) │
│ • GIGO : Garbage In, Garbage Out │
│ • Les rôles : Data Engineer vs Data Scientist vs ML Engineer │
│ • Le cycle de training : epoch, batch, loss, gradient │
└─────────────────────────────────────────────────────────────────────────────┘
Module 2 — Stocker les données
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ce que tu as appris │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Object Storage : Bucket / Clé / Objet │
│ • MinIO : S3-compatible, local, gratuit │
│ • Formats : Parquet (colonnes), WebDataset (shards TAR) │
│ • Métadonnées : Catalogue DuckDB pour requêtes rapides │
│ • Pipeline d'ingestion avec hash MD5 pour déduplication │
├─────────────────────────────────────────────────────────────────────────────┤
│ Stack : MinIO, boto3, Parquet, DuckDB │
└─────────────────────────────────────────────────────────────────────────────┘
Module 3 — Transformer les données
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ce que tu as appris │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Transformations images : resize, normalize, convert │
│ • Modes de resize : EXACT, FIT, PAD, COVER │
│ • Normalisation : [0,255] → [-1,1] pour stabilité des gradients │
│ • Schema Enforcement avec Pydantic (fail fast) │
│ • Data Quality : détection corruption, validation │
│ • Déduplication : MD5 (exact) + dHash (perceptuel) │
│ • Data Augmentation : flip, rotation, crop, color jitter │
│ • Spark : traitement distribué à grande échelle │
├─────────────────────────────────────────────────────────────────────────────┤
│ Stack : PIL, NumPy, Pydantic, PySpark │
└─────────────────────────────────────────────────────────────────────────────┘
Module 4 — Enrichir les données
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ce que tu as appris │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Embeddings : vecteurs qui capturent le SENS (pas les pixels) │
│ • CLIP : images + texte dans le même espace vectoriel │
│ • Similarité cosinus : mesurer la proximité sémantique │
│ • Vector Database : Chroma pour recherche ANN rapide │
│ • Déduplication sémantique : même sujet, images différentes │
│ • Recherche multimodale : texte → images similaires │
├─────────────────────────────────────────────────────────────────────────────┤
│ Stack : CLIP, Sentence-Transformers, ChromaDB │
└─────────────────────────────────────────────────────────────────────────────┘
Module 5 — Automatiser et Versionner
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ce que tu as appris │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Orchestration : DAG Airflow (tâches + dépendances + schedule) │
│ • Versioning données : DVC (pointeur Git + stockage distant) │
│ • Tracking : MLflow pour les métadonnées du dataset │
│ • Reproductibilité : git checkout + dvc checkout = état exact │
│ • Automatisation : @daily, retry, alertes │
├─────────────────────────────────────────────────────────────────────────────┤
│ Stack : Airflow, DVC, MLflow, Git │
└─────────────────────────────────────────────────────────────────────────────┘
Module 6 — Projet Final : Knowledge Base RAG
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ce que tu as appris │
├─────────────────────────────────────────────────────────────────────────────┤
│ • RAG : Retrieve → Augment → Generate │
│ • Parsing : PDF, Markdown, HTML → texte propre │
│ • Chunking : découper intelligemment (recursive > fixed) │
│ • Embeddings texte : Sentence-Transformers (multilingue) │
│ • Évaluation retrieval : Hit Rate, MRR │
│ • LLM local : Ollama (gratuit, hors-ligne) │
│ • Pipeline complet : Documents → Knowledge Base → Query │
├─────────────────────────────────────────────────────────────────────────────┤
│ Stack : Sentence-Transformers, ChromaDB, Ollama / Groq / Mistral API │
└─────────────────────────────────────────────────────────────────────────────┘
🛠️ Stack technique complète
| Catégorie | Outils |
|---|---|
| Stockage | MinIO, S3, Parquet, WebDataset |
| Transformation | PIL, NumPy, PySpark, Pydantic |
| Embeddings | CLIP (images), Sentence-Transformers (texte) |
| Vector DB | ChromaDB |
| Orchestration | Apache Airflow |
| Versioning | DVC, Git |
| Tracking | MLflow |
| LLM local | Ollama (Mistral, Llama3) |
| LLM cloud gratuit | Groq (Llama3, Mixtral), Mistral API (Mistral 7B, Nemo) |
| Parsing | PyMuPDF, BeautifulSoup |
📊 Le bootcamp en chiffres
| Métrique | Valeur |
|---|---|
| Modules | 6 |
| Cellules | ~200 |
| Questions quiz | 37 |
| Heures estimées | 20-25h |
| Projets pratiques | 6 |
| Coût | Gratuit |
🚀 Prochaines étapes
Pour approfondir
| Domaine | Technologies à explorer |
|---|---|
| Orchestration | Prefect, Dagster, Mage |
| Feature Store | Feast, Tecton |
| Data Quality | Great Expectations, Soda |
| Streaming | Kafka, Spark Streaming |
| MLOps | Kubeflow, MLRun |
| Cloud | AWS, GCP, Azure |
Pour pratiquer
- Applique ce que tu as appris sur un vrai projet
- Contribue à des projets open source
- Partage tes apprentissages (blog, LinkedIn)
- Construis ton portfolio de projets DE
🙏 Merci d'avoir suivi ce bootcamp !
Ce bootcamp a été créé pour rendre le Data Engineering for AI accessible à tous, particulièrement en Afrique francophone.
Tu as des questions ou des suggestions ?
- Rejoins la communauté sur Telegram
- Connecte-toi sur LinkedIn
Bootcamp Data Engineering for AI — From Zero to Hero
Créé par Beerus — from0tohero.dev
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ "La donnée est le nouveau code." │
│ — Andrew Ng │
│ │
│ Tu es maintenant prêt à construire des pipelines de données pour l'IA. │
│ │
│ 🚀 FROM ZERO TO HERO 🚀 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘