# Parameters
BATCH_MODE = "true"5. RAG Modern - Retrieval Augmented Generation
Navigation : Index | << Précédent | Suivant >>
Durée estimée : 65 minutes
Prérequis : Notebooks 1 (OpenAI Intro), 4 (Function Calling)
Objectifs
La RAG (Retrieval Augmented Generation) permet d’enrichir les réponses d’un LLM avec des données externes (documents, bases de connaissances). Ce notebook couvre :
- Fondamentaux RAG : Embeddings, chunking, recherche vectorielle
- Stratégies de chunking : Fixe, sémantique, récursif
- Responses API : Multi-turn RAG avec
previous_response_id - Mesure d’usage : Comparaison des champs
usagerenvoyés à chaque tour - Citations et sources : Traçabilité des réponses
Pourquoi la RAG ?
| Problème LLM seul | Solution RAG |
|---|---|
| Connaissances figées (cutoff date) | Données actualisées en temps réel |
| Hallucinations fréquentes | Réponses basées sur sources vérifiables |
| Pas de données privées | Accès à vos documents internes |
| Contexte limité | Fenêtre étendue via retrieval |
Configuration
# Dépendances pré-provisionnées (openai, tiktoken, python-dotenv, scikit-learn, numpy, pandas, requests, beautifulsoup4, lxml) : voir GenAI/requirements.txt
import openai, tiktoken, dotenv, sklearn, numpy, pandas, requests, bs4, lxml
print("Dépendances RAG pré-chargées avec succès.")Dépendances RAG pré-chargées avec succès.
Les dépendances étant installées, le client OpenAI et les variables de configuration sont initialisés. La détection du fichier .env garantit que la clé API est chargée quel que soit le répertoire de travail courant.
# Import guards - verification des dependances
try:
import openai
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
try:
import anthropic
ANTHROPIC_AVAILABLE = True
except ImportError:
ANTHROPIC_AVAILABLE = False
try:
import requests
REQUESTS_AVAILABLE = True
except ImportError:
REQUESTS_AVAILABLE = False
try:
import matplotlib
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
try:
import numpy
NUMPY_AVAILABLE = True
except ImportError:
NUMPY_AVAILABLE = False
try:
import pandas
PANDAS_AVAILABLE = True
except ImportError:
PANDAS_AVAILABLE = False
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
try:
import IPython
IPYTHON_AVAILABLE = True
except ImportError:
IPYTHON_AVAILABLE = False
try:
from dotenv import load_dotenv
DOTENV_AVAILABLE = True
except ImportError:
DOTENV_AVAILABLE = False
try:
import torch
TORCH_AVAILABLE = True
except ImportError:
TORCH_AVAILABLE = False
try:
import transformers
TRANSFORMERS_AVAILABLE = True
except ImportError:
TRANSFORMERS_AVAILABLE = False
try:
import cv2
CV2_AVAILABLE = True
except ImportError:
CV2_AVAILABLE = False
try:
import pydantic
PYDANTIC_AVAILABLE = True
except ImportError:
PYDANTIC_AVAILABLE = False
# Resume des dependances disponibles
_DEPS = {
'openai': OPENAI_AVAILABLE,
'anthropic': ANTHROPIC_AVAILABLE,
'requests': REQUESTS_AVAILABLE,
'matplotlib': MATPLOTLIB_AVAILABLE,
'numpy': NUMPY_AVAILABLE,
'pandas': PANDAS_AVAILABLE,
'PIL': PIL_AVAILABLE,
'IPython': IPYTHON_AVAILABLE,
'dotenv': DOTENV_AVAILABLE,
'torch': TORCH_AVAILABLE,
'transformers': TRANSFORMERS_AVAILABLE,
'cv2': CV2_AVAILABLE,
'pydantic': PYDANTIC_AVAILABLE,
}
available = [k for k, v in _DEPS.items() if v]
missing = [k for k, v in _DEPS.items() if not v]
print(f"Dependances: {len(available)}/{len(_DEPS)} disponibles"
+ (f" | Manquantes: {missing}" if missing else ""))
from pathlib import Path
import os
import json
import numpy as np
import pandas as pd
from typing import List, Dict, Any
from dotenv import load_dotenv
from openai import OpenAI
# Charger les variables d'environnement
# Chargement robuste de la configuration .env
from dotenv import load_dotenv
import os
# Recherche du .env dans tous les parents (pour Papermill qui change le cwd)
current_path = Path.cwd()
env_loaded = False
for _ in range(10):
env_path = current_path / ".env"
if env_path.exists():
load_dotenv(env_path)
print(f".env charge depuis: {env_path.name}")
env_loaded = True
break
if current_path.name == "GenAI" or len(current_path.parts) <= 1:
break
current_path = current_path.parent
if not env_loaded:
print("WARNING: .env non trouve, utilisation variables environnement")
# Mode batch pour tests automatisés
BATCH_MODE = os.getenv("BATCH_MODE", "false").lower() == "true"
# Client OpenAI
client = OpenAI()
# Modèle par défaut depuis .env
DEFAULT_MODEL = os.getenv("OPENAI_MODEL", "gpt-5-mini")
print(f"Configuration chargée - Mode batch: {BATCH_MODE}")
print(f"Modèle embeddings: text-embedding-3-large")
print(f"Modèle génération: {DEFAULT_MODEL}")Dependances: 12/13 disponibles | Manquantes: ['cv2']
.env charge depuis: .env
Configuration chargée - Mode batch: False
Modèle embeddings: text-embedding-3-large
Modèle génération: gpt-5-mini
Interprétation de la configuration
La cellule précédente initialise l’environnement RAG avec plusieurs composants clés.
Configuration validée :
| Composant | Statut | Valeur |
|---|---|---|
| Client OpenAI | ✓ Initialisé | Authentification via OPENAI_API_KEY dans .env |
| Mode batch | Variable | BATCH_MODE depuis .env (default: false) |
| Modèle génération | Configurable | OPENAI_MODEL depuis .env (default: gpt-5-mini) |
| Modèle embeddings | Fixe | text-embedding-3-large (hardcodé pour précision) |
Choix techniques justifiés :
text-embedding-3-large: Modèle d’embedding le plus performant d’OpenAI- 3072 dimensions vs 1536 pour
-small - Meilleure capture des nuances sémantiques
- Coût : ~0.13$ pour 1M tokens (acceptable pour RAG)
- 3072 dimensions vs 1536 pour
- Mode batch : Permet l’exécution automatisée (Papermill, CI/CD)
BATCH_MODE=true: Skip les widgets interactifs- Utile pour tests et validation
- Modèle par défaut :
gpt-5-mini- Rapide et économique pour la génération
- Peut être remplacé par
gpt-5pour plus de précision si besoin
Variables d’environnement requises (.env) :
OPENAI_API_KEY=sk-... # Obligatoire
OPENAI_MODEL=gpt-5-mini # Optionnel (default)
BATCH_MODE=false # Optionnel (default)Point important : L’initialisation du client OpenAI échouera si OPENAI_API_KEY n’est pas définie. Assurez-vous d’avoir copié .env.example vers .env avec vos vraies clés.
Bibliothèques utilisées
Ce notebook nécessite plusieurs dépendances clés :
- openai : API OpenAI pour embeddings et génération
- tiktoken : Tokenizer officiel pour compter les tokens
- scikit-learn : k-Nearest Neighbors pour la recherche vectorielle
- numpy/pandas : Manipulation de données et vecteurs
- requests/beautifulsoup4 : Scraping web pour récupérer le document source
- python-dotenv : Gestion sécurisée des clés API
Note : En production, remplacer scikit-learn par une vraie base vectorielle (Pinecone, Qdrant, Weaviate).
Partie 1 : Fondamentaux RAG
1.1 Architecture RAG
Ancrage scholarly. Le terme RAG (Retrieval-Augmented Generation) a été introduit par Lewis et al. (2020) – Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (arXiv:2005.11401) – qui proposent d’apprendre un retriever et un générateur de façon jointe pour réduire les hallucinations sur les tâches connaissance-intensives. Le notebook ci-dessous en implémente une variante moderne (embeddings pré-entraînés + LLM gelé).
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Documents │ ──► │ Chunking │ ──► │ Embeddings │
└─────────────┘ └──────────────┘ └──────┬──────┘
│
▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Réponse │ ◄── │ LLM │ ◄── │ Retrieval │
└─────────────┘ └──────────────┘ └─────────────┘
▲
│
┌──────┴──────┐
│ Question │
└─────────────┘
Étapes : 1. Indexation : Documents → Chunks → Embeddings → Base vectorielle 2. Retrieval : Question → Embedding → k-NN → Chunks pertinents 3. Generation : Question + Chunks → LLM → Réponse augmentée
Le diagramme ci-dessous rend ce pipeline RAG sous forme de graphe : la phase d’indexation (Documents -> Retrieval) et la phase de requête/génération (Question + Retrieval -> LLM -> Reponse).
flowchart LR
D["Documents"] --> CH["Chunking"] --> EMB["Embeddings"] --> R["Retrieval"]
Q["Question"] --> LLM["LLM"]
R --> LLM
LLM --> REP(["Reponse"])
classDef idx fill:#cfe2ff,stroke:#084298,color:#052c65
classDef gen fill:#fff3cd,stroke:#b8860b,color:#5c4400
classDef out fill:#d1e7dd,stroke:#0f5132,color:#0a3622
class D,CH,EMB,R idx
class Q,LLM gen
class REP out
Lecture. En amont (hors ligne), les documents sont decoupes en chunks, encodes en embeddings et indexes pour le retrieval. A la requête, la question declenche une recherche des passages pertinents, et le LLM synthetise la reponse en s’appuyant sur ces passages – c’est l’apport du RAG : ancrer la génération dans des sources externes plutôt que dans la seule memoire parametrique.
1.2 Préparation des données
Pour cet exemple, nous utilisons le débat Lincoln-Douglas (1858), un document historique public.
Pourquoi le débat Lincoln-Douglas ?
Ce document historique est un excellent cas d’usage RAG :
- Domaine public : Accessible librement, pas de problèmes de copyright
- Taille idéale : ~98k caractères, assez long pour nécessiter du chunking
- Structure narrative : Discours structurés avec arguments clairs
- Questions naturelles : “Quelle était la position de Lincoln ?” → Réponses vérifiables
Fallback pour les tests : Le code inclut un texte de secours si le scraping échoue (mode batch, timeout réseau, etc.). Cela garantit que le notebook peut s’exécuter même sans connexion.
Point technique : Nous récupérons uniquement le premier débat (Ottawa, 21 août 1858). La série complète comprend 7 débats.
import requests
from bs4 import BeautifulSoup
def fetch_debate_text() -> str:
"""Récupère le texte du premier débat Lincoln-Douglas."""
url = "https://home.nps.gov/liho/learn/historyculture/debate1.htm"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
main_div = soup.select_one("div.ColumnMain")
if main_div:
return main_div.get_text(separator="\n", strip=True)
else:
raise ValueError("Conteneur principal non trouvé")
except Exception as e:
print(f"Erreur lors du scraping: {e}")
# Texte de fallback pour le mode batch
return """Lincoln-Douglas Debate - Ottawa, Illinois, August 21, 1858
Abraham Lincoln argued that slavery was morally wrong and should not be extended
into new territories. He believed in the principle that "all men are created equal"
as stated in the Declaration of Independence.
Lincoln stated: "I have no purpose to introduce political and social equality
between the white and black races. There is a physical difference between the two,
which, in my judgment, will probably forever forbid their living together upon the
footing of perfect equality."
However, Lincoln maintained that Black Americans had the right to earn their own
bread and improve their condition. He opposed the expansion of slavery while
acknowledging the constitutional protections it had in existing states.
Douglas advocated for popular sovereignty, allowing each territory to decide
the slavery question for itself. He accused Lincoln of wanting to make all
states free states, which Lincoln denied.
The debate centered on the interpretation of the Dred Scott decision and
whether Congress could prohibit slavery in the territories."""
# Récupération du texte
debate_text = fetch_debate_text()
print(f"Texte récupéré: {len(debate_text)} caractères")
print(f"Aperçu: {debate_text[:500]}...")Texte récupéré: 98529 caractères
Aperçu: First Debate: Ottawa, Illinois
August 21, 1858
It was dry and dusty, between 10,000 and 12,000 people were in attendance when the debate began at 2:00 p.m. There were no seats or bleachers.
Douglas charged Lincoln with trying to “abolitionize” the Whig and Democratic Parties. He also charged Lincoln had been present when a very radical “abolitionist” type platform had been written by the Republican Party in 1854. Douglas accused Lincoln of taking the side of the common enemy in the Mexican War. ...
Analyse du document récupéré
Le scraping a réussi et notre base de données source est prête pour le chunking.
Caractéristiques du document :
| Aspect | Valeur | Interprétation |
|---|---|---|
| Taille | 98,529 caractères | Document substantiel, nécessite chunking |
| Source | NPS.gov (National Park Service) | Source fiable, domaine public |
| Date | 21 août 1858 (Ottawa, Illinois) | Premier des 7 débats Lincoln-Douglas |
| Audience estimée | 10,000-12,000 personnes | Événement historique majeur |
Aperçu du contenu :
Le texte commence directement avec les accusations de Douglas contre Lincoln : - “Abolitionize” les partis politiques - Plateforme “abolitionniste radicale” de 1854 - Position controversée sur la guerre du Mexique
Stratégie de chunking recommandée :
Pour un document narratif de cette taille (~98k caractères), nous utiliserons : - Chunking fixe : 400 mots avec overlap de 50 - Raison : Le débat suit une structure de discours/réponse linéaire - Résultat attendu : ~50 chunks de taille équilibrée
Alternative avec fallback : Le code inclut un texte de secours (mode batch/timeout réseau) qui garantit l’exécution même sans connexion. C’est une bonne pratique pour les notebooks pédagogiques.
Prochaine étape : Découper ce texte en chunks optimisés pour la recherche sémantique.
Implémentation des stratégies de chunking
Les trois fonctions suivantes implémentent les stratégies présentées ci-dessus. Chacune a ses avantages selon le type de document.
Partie 2 : Stratégies de Chunking
Le chunking est critique pour la qualité de la RAG. Trois stratégies principales :
| Stratégie | Avantages | Inconvénients |
|---|---|---|
| Fixe | Simple, prévisible | Coupe au milieu des phrases |
| Sémantique | Respecte le sens | Plus complexe, variable |
| Récursif | Équilibre taille/sens | Nécessite des délimiteurs |
Préparation du DataFrame de chunks
Nous créons maintenant notre base de connaissances : un DataFrame pandas contenant tous les chunks avec leurs métadonnées.
Structure du DataFrame : - chunk_id : Identifiant unique (0-50) - source : Source du document (pour traçabilité) - text : Contenu textuel du chunk - embedding : Vecteur de 3072 dimensions (ajouté à l’étape suivante)
Cette structure permet de facilement filtrer, rechercher et enrichir les chunks avec des métadonnées supplémentaires (date, auteur, catégorie, etc.).
def chunk_fixed(text: str, chunk_size: int = 400, overlap: int = 50) -> List[str]:
"""
Chunking fixe par nombre de mots.
Args:
text: Texte à découper
chunk_size: Nombre de mots par chunk
overlap: Chevauchement entre chunks (évite les coupures brutales)
"""
words = text.split()
chunks = []
start = 0
while start < len(words):
end = start + chunk_size
chunk = " ".join(words[start:end])
chunks.append(chunk)
start += (chunk_size - overlap)
return chunks
def chunk_semantic(text: str, max_sentences: int = 5) -> List[str]:
"""
Chunking sémantique par phrases.
Regroupe les phrases en chunks cohérents.
"""
import re
# Découpage par phrases (approximatif)
sentences = re.split(r'(?<=[.!?])\s+', text)
chunks = []
current_chunk = []
for sentence in sentences:
current_chunk.append(sentence)
if len(current_chunk) >= max_sentences:
chunks.append(" ".join(current_chunk))
current_chunk = []
# Dernier chunk
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks
def chunk_recursive(text: str, max_size: int = 500, delimiters: List[str] = None) -> List[str]:
"""
Chunking récursif avec délimiteurs hiérarchiques.
Essaie de découper par paragraphes, puis phrases, puis mots.
"""
if delimiters is None:
delimiters = ["\n\n", "\n", ". ", " "]
if len(text) <= max_size or not delimiters:
return [text] if text.strip() else []
delimiter = delimiters[0]
parts = text.split(delimiter)
chunks = []
current = ""
for part in parts:
if len(current) + len(part) + len(delimiter) <= max_size:
current += (delimiter if current else "") + part
else:
if current:
chunks.append(current)
# Récursion avec délimiteur suivant si le part est trop grand
if len(part) > max_size:
chunks.extend(chunk_recursive(part, max_size, delimiters[1:]))
else:
current = part
if current:
chunks.append(current)
return chunks
# Comparaison des stratégies
print("=== Comparaison des stratégies de chunking ===")
print(f"\nTexte source: {len(debate_text)} caractères")
chunks_fixed = chunk_fixed(debate_text, chunk_size=100, overlap=20)
chunks_semantic = chunk_semantic(debate_text, max_sentences=3)
chunks_recursive = chunk_recursive(debate_text, max_size=500)
print(f"\nChunking fixe (100 mots, overlap 20): {len(chunks_fixed)} chunks")
print(f"Chunking sémantique (3 phrases): {len(chunks_semantic)} chunks")
print(f"Chunking récursif (max 500 chars): {len(chunks_recursive)} chunks")=== Comparaison des stratégies de chunking ===
Texte source: 98529 caractères
Chunking fixe (100 mots, overlap 20): 222 chunks
Chunking sémantique (3 phrases): 214 chunks
Chunking récursif (max 500 chars): 327 chunks
Exercice 1 - Stratégie de chunking hybride
Le chunking fixe coupe au milieu des phrases, le sémantique produit des chunks de taille variable, et le récursif necessite de bons delimiteurs. Une stratégie hybride combine les avantages de plusieurs approches.
Objectif : Implementer une fonction chunk_hybrid(text, target_size, tolerance, overlap) qui decoupe un texte en chunks de taille cible tout en respectant les frontieres de phrases.
Indices : - Le chunking hybride fonctionne en deux phases : decouper par phrases (comme le sémantique), puis regrouper les phrases tant que la taille reste dans [target_size - tolerance, target_size + tolerance] - Utiliser re.split(r'(?<=[.!?])\s+', text) pour obtenir les phrases - Pour chaque phrase, l’ajouter au chunk courant si len(chunk) + len(phrase) <= target_size + tolerance - Ajouter un overlap : conserver les N derniers caractères du chunk précèdent comme debut du suivant - # Étape 1 : Decouper le texte en phrases avec re.split() - # Étape 2 : Regrouper les phrases en chunks tout en respectant target_size + tolerance - # Étape 3 : Ajouter l’overlap entre chunks consécutifs et retourner la liste - # Indice : Verifier votre résultat avec len(chunk_hybrid(debate_text, 400, 100, 50)) et comparer avec les 51 chunks du chunking fixe
# Exercice 1 - Strategie de chunking hybride
# TODO etudiant : implementer chunk_hybrid
import re
def chunk_hybrid(text: str, target_size: int = 400, tolerance: int = 100, overlap: int = 50) -> List[str]:
"""
Chunking hybride : decoupe par phrases tout en visant une taille cible.
Args:
text: Texte a decouper
target_size: Taille cible en caracteres
tolerance: Ecart acceptable par rapport a la taille cible
overlap: Nombre de caracteres de chevauchement entre chunks
Returns:
Liste de chunks texte
"""
return None # TODO etudiant : implementer le chunking hybride
# Etape 1 : Tester la fonction sur le texte du debat
# hybrid_chunks = chunk_hybrid(debate_text, target_size=400, tolerance=100, overlap=50)
# print(f"Chunking hybride: {len(hybrid_chunks)} chunks")
# Etape 2 : Comparer avec le chunking fixe
# print(f"Chunking fixe: {len(chunks)} chunks")
# print(f"Premier chunk hybride: {hybrid_chunks[0][:200]}...")
# Etape 3 : Verifier la taille des chunks
# sizes = [len(c) for c in hybrid_chunks]
# print(f"Taille min: {min(sizes)}, max: {max(sizes)}, moyenne: {sum(sizes)/len(sizes):.0f}")
print("Exercice a completer")Exercice a completer
Les trois stratégies de chunking étant implementées, cette cellule compare leurs résultats sur le même texte source. Les différences de granularité entre chunking fixe, sémantique et récursif seront visibles dans les comptes de chunks produits.
# Visualisation des chunks
print("=== Exemple chunk fixe ===")
print(chunks_fixed[0][:300] + "..." if len(chunks_fixed[0]) > 300 else chunks_fixed[0])
print("\n=== Exemple chunk sémantique ===")
print(chunks_semantic[0][:300] + "..." if len(chunks_semantic[0]) > 300 else chunks_semantic[0])
print("\n=== Exemple chunk récursif ===")
print(chunks_recursive[0][:300] + "..." if len(chunks_recursive[0]) > 300 else chunks_recursive[0])=== Exemple chunk fixe ===
First Debate: Ottawa, Illinois August 21, 1858 It was dry and dusty, between 10,000 and 12,000 people were in attendance when the debate began at 2:00 p.m. There were no seats or bleachers. Douglas charged Lincoln with trying to “abolitionize” the Whig and Democratic Parties. He also charged Lincoln...
=== Exemple chunk sémantique ===
First Debate: Ottawa, Illinois
August 21, 1858
It was dry and dusty, between 10,000 and 12,000 people were in attendance when the debate began at 2:00 p.m. There were no seats or bleachers. Douglas charged Lincoln with trying to “abolitionize” the Whig and Democratic Parties.
=== Exemple chunk récursif ===
First Debate: Ottawa, Illinois
August 21, 1858
It was dry and dusty, between 10,000 and 12,000 people were in attendance when the debate began at 2:00 p.m. There were no seats or bleachers.
Analyse des résultats de chunking
Observons les différences obtenues :
| Stratégie | Nombre de chunks | Observations |
|---|---|---|
| Fixe (100 mots, overlap 20) | 222 | Nombreux chunks petits, overlap assure la continuité |
| Sémantique (3 phrases) | 214 | Chunks de taille variable, respecte le sens |
| Récursif (max 500 chars) | 327 | Plus de petits chunks, découpage hiérarchique |
Points clés :
Chunking fixe : Le premier exemple montre une coupure au milieu d’une phrase (“…He also charged Lincoln…”), typique de cette méthode.
Chunking sémantique : Le découpage respecte les limites de phrases, plus naturel pour la compréhension.
Chunking récursif : Découpe d’abord par paragraphes (
\n\n), puis par phrases si trop long. Résultat : chunks courts mais cohérents.
Pour la suite, nous utilisons le chunking fixe avec 400 mots et overlap 50 - un bon compromis entre taille et continuité pour ce type de document narratif.
2.1 Choix de la stratégie
Recommandations :
- Documents structurés (code, JSON, markdown) → Chunking récursif
- Texte narratif (articles, livres) → Chunking sémantique
- Données tabulaires → Chunking fixe avec métadonnées
Pour la suite, nous utilisons le chunking fixe avec overlap (400 mots, overlap 50).
Comprendre les embeddings
Un embedding est une représentation vectorielle d’un texte dans un espace à haute dimension. Chaque dimension capture un aspect sémantique du texte. Sentence-BERT (Reimers & Gurevych, 2019, arXiv:1908.10084) a rendu les embeddings de phrases efficaces pour la similarité sémantique via un fine-tuning siamese ; DPR (Karpukhin et al., 2020, arXiv:2004.04906) montre qu’un retriever dense bi-encoder surpasse BM25 sur les questions ouvertes.
Exemple simplifié (2D) :
"chat" → [0.8, 0.2] (animal, domestique)
"chien" → [0.7, 0.3] (animal, domestique)
"ordinateur" → [0.1, 0.9] (objet, technologie)
En réalité, text-embedding-3-large génère des vecteurs à 3072 dimensions pour capturer les nuances du langage.
Propriétés clés :
- Similarité sémantique : Textes similaires ont des vecteurs proches (distance cosinus faible)
- Invariance : Même sens avec différents mots → vecteurs similaires
- Efficacité : Comparer 3072 nombres est plus rapide que comparer du texte brut
Batch vs Single : La fonction create_embeddings_batch() est 5-10x plus rapide que des appels individuels pour de gros volumes. OpenAI autorise jusqu’à 2048 textes par batch.
Résultat : 51 vecteurs de 3072 dimensions, prêts pour la recherche vectorielle.
Implémentation de la VectorStore
La classe VectorStore encapsule la logique de recherche vectorielle. Elle utilise scikit-learn pour le prototypage, mais en production vous utiliseriez une vraie base vectorielle.
Points clés de l’implémentation :
- Métrique cosinus :
metric="cosine"mesure l’angle entre vecteurs, pas la distance euclidienne - Validation des embeddings : Filtre les None pour éviter les erreurs
- Retour structuré : Dicts avec
chunk_id,text,source, etscorepour traçabilité complète
# Création du DataFrame de chunks pour la suite
chunks = chunk_fixed(debate_text, chunk_size=400, overlap=50)
df_chunks = pd.DataFrame({
"chunk_id": range(len(chunks)),
"source": "Lincoln-Douglas Debate 1 (Ottawa, 1858)",
"text": chunks
})
print(f"Base de connaissances: {len(df_chunks)} chunks")
df_chunks.head()Base de connaissances: 51 chunks
| chunk_id | source | text | |
|---|---|---|---|
| 0 | 0 | Lincoln-Douglas Debate 1 (Ottawa, 1858) | First Debate: Ottawa, Illinois August 21, 1858... |
| 1 | 1 | Lincoln-Douglas Debate 1 (Ottawa, 1858) | were proclaimed wherever the Constitution rule... |
| 2 | 2 | Lincoln-Douglas Debate 1 (Ottawa, 1858) | the Whig party and the Democratic party both s... |
| 3 | 3 | Lincoln-Douglas Debate 1 (Ottawa, 1858) | name and disguise of a Republican party. (Laug... |
| 4 | 4 | Lincoln-Douglas Debate 1 (Ottawa, 1858) | with such views as the circumstances and exige... |
Analyse de la base de connaissances
Le DataFrame créé contient notre base de connaissances structurée, prête pour la vectorisation.
Structure du DataFrame :
| Colonne | Type | Description |
|---|---|---|
chunk_id |
int | Identifiant unique (0-50) |
source |
str | Source du document (traçabilité) |
text |
str | Contenu textuel du chunk |
Métriques observées :
- Nombre de chunks : 51
- Stratégie : Chunking fixe avec 400 mots et overlap 50
- Résultat : Cohérent avec la taille du document (~98k caractères)
Vérification qualité (via head()) :
Les 5 premiers chunks montrent : 1. Chunk 0 : Début du débat (“First Debate: Ottawa, Illinois August 21, 1858…”) 2. Chunks suivants : Continuité narrative avec overlap 3. Source cohérente : Tous les chunks référencent la même source
Calcul de l’overlap :
Chunk 0: mots 0-400
Chunk 1: mots 350-750 (50 mots de chevauchement avec chunk 0)
Chunk 2: mots 700-1100 (50 mots de chevauchement avec chunk 1)
...
Avantages de l’overlap : - Évite les coupures brutales de contexte - Améliore la récupération de phrases à cheval sur deux chunks - Coût : ~12.5% de redondance (50/400)
Prochaine étape : Générer les embeddings pour chaque chunk avec text-embedding-3-large.
Partie 3 : Embeddings et Recherche Vectorielle
3.1 Génération des embeddings
OpenAI propose plusieurs modèles d’embeddings :
| Modèle | Dimensions | Performance | Coût |
|---|---|---|---|
text-embedding-3-small |
1536 | Bon | $ |
text-embedding-3-large |
3072 | Excellent | $$ |
text-embedding-ada-002 |
1536 | Bon (legacy) | $ |
def create_embedding(text: str, model: str = "text-embedding-3-large") -> List[float]:
"""
Génère un embedding pour un texte donné.
Args:
text: Texte à vectoriser
model: Modèle d'embedding OpenAI
Returns:
Vecteur d'embedding (liste de floats)
"""
try:
response = client.embeddings.create(
model=model,
input=[text]
)
return response.data[0].embedding
except Exception as e:
print(f"Erreur embedding: {e}")
return None
def create_embeddings_batch(texts: List[str], model: str = "text-embedding-3-large") -> List[List[float]]:
"""
Génère des embeddings en batch (plus efficace).
Note: OpenAI supporte jusqu'à 2048 textes par requête.
"""
try:
response = client.embeddings.create(
model=model,
input=texts
)
return [item.embedding for item in response.data]
except Exception as e:
print(f"Erreur batch embedding: {e}")
return [None] * len(texts)
# Génération des embeddings pour tous les chunks
print("Génération des embeddings...")
embeddings = create_embeddings_batch(df_chunks["text"].tolist())
df_chunks["embedding"] = embeddings
print(f"Embeddings générés: {len([e for e in embeddings if e])} / {len(embeddings)}")
print(f"Dimension des vecteurs: {len(embeddings[0]) if embeddings[0] else 'N/A'}")Génération des embeddings...
Embeddings générés: 51 / 51
Dimension des vecteurs: 3072
Analyse des embeddings générés
Les résultats confirment une vectorisation réussie de notre base de connaissances.
Métriques observées :
| Métrique | Valeur | Signification |
|---|---|---|
| Embeddings générés | 51/51 | 100% de succès, aucune erreur API |
| Dimension des vecteurs | 3072 | text-embedding-3-large (haute précision) |
| Chunks indexés | 51 | Base de connaissances complète |
Implications pour la recherche :
- Espace vectoriel : Chaque chunk est maintenant un point dans un espace à 3072 dimensions
- Distance sémantique : Des chunks parlant du même concept seront proches dans cet espace
- Efficacité : La recherche k-NN sera rapide même avec des milliers de chunks
Comparaison des modèles d’embeddings :
| Modèle | Dimensions | Taille base (51 chunks) | Performance recherche |
|---|---|---|---|
text-embedding-3-small |
1536 | ~300 KB | Bonne |
text-embedding-3-large |
3072 | ~600 KB | Excellente ✓ |
text-embedding-ada-002 |
1536 | ~300 KB | Bonne (legacy) |
Note technique : L’utilisation de create_embeddings_batch() au lieu d’appels individuels réduit le temps d’exécution de ~10 secondes à ~2 secondes pour 51 chunks. En production avec des milliers de documents, cette optimisation est critique.
Prochaine étape : Initialiser le VectorStore avec ces embeddings pour la recherche k-NN.
Anatomie d’une réponse RAG
Le pipeline rag_query() exécute 4 étapes :
1. Retrieval (Récupération) :
query_embedding = create_embedding(question)
chunks = vector_store.search(query_embedding, k=3)Convertit la question en vecteur, puis cherche les 3 chunks les plus proches.
2. Construction du contexte :
context = "\n\n".join([f"[Source: {c['source']}]\n{c['text']}" for c in chunks])Assemble les chunks récupérés en un seul texte avec métadonnées.
3. Prompt augmenté :
system_prompt = "Réponds UNIQUEMENT basé sur le contexte fourni..."
user_prompt = f"Contexte:\n{context}\n\nQuestion: {question}"Injecte le contexte dans le prompt pour “augmenter” les connaissances du modèle.
4. Génération :
response = client.chat.completions.create(...)Le LLM génère une réponse basée sur le contexte fourni, pas sur ses connaissances pré-entraînées.
Paramètrès clés : - température : Non supporté par gpt-5-mini (utilise la valeur par défaut 1.0) - max_completion_tokens=2000 : Budget de génération. Pour un modèle de raisonnement comme gpt-5-mini, ce budget doit être suffisamment large car les tokens de raisonnement sont déduits du même budget (un budget trop bas, ex: 500, pouvait être entièrement consommé par le raisonnement sans texte final visible — issue #3571).
Résultat attendu : Une réponse précise citant les chunks utilisés, avec traçabilité complète via les métadonnées de tokens.
3.2 Recherche k-NN (k-Nearest Neighbors)
Pour la recherche, on calcule la distance entre l’embedding de la question et ceux des chunks.
Importance des citations en RAG
Les citations sont essentielles pour :
- Vérifiabilité : L’utilisateur peut vérifier la source de l’information
- Confiance : Une réponse avec sources est plus crédible qu’une affirmation sans preuve
- Debugging : Identifier si le problème vient du retrieval (mauvais chunks) ou de la génération (mauvaise interprétation)
- Conformité : Certains domaines (médical, juridique) exigent la traçabilité complète
Anti-pattern : Générer une réponse sans indiquer les sources utilisées. Cela empêche la vérification et rend le système opaque.
Responses API vs Chat Completions
La Responses API fournit un état de conversation côté serveur, tandis que Chat Completions demande à l’application de gérer l’historique.
Différences techniques :
| Aspect | Chat Completions | Responses API |
|---|---|---|
| Historique | Géré et renvoyé par l’application | Chaîné par previous_response_id |
| Prompt caching | Disponible sous ses propres conditions | Disponible sous les mêmes conditions de cache fournisseur |
| Mesure des tokens | Champs usage de la réponse |
Champs usage de la réponse |
| Persistance | Côté application | Côté serveur avec store=True |
previous_response_id simplifie le transport de l’historique, mais ne rend pas cet historique gratuit : les tokens d’entrée historiques restent comptabilisés. Il ne démontre pas non plus, à lui seul, un hit de prompt cache. La cellule de mesure plus bas affiche donc les champs usage réellement renvoyés pour chaque tour, sans annoncer un pourcentage d’économie théorique. Les appels d’embeddings sont suivis séparément du coût de génération.
Fallback implémenté : si la Responses API n’est pas disponible, le code bascule sur Chat Completions. Ce fallback assure la compatibilité de l’appel courant, mais ne reprend pas automatiquement le même état de conversation via previous_response_id ; l’application devrait reconstruire explicitement l’historique pour conserver cette continuité.
Transition vers les bonnes pratiques
Nous avons maintenant un pipeline RAG complet fonctionnel. Cependant, pour passer en production, plusieurs optimisations sont nécessaires.
Prochaines étapes :
- Monitoring et métriques : Mesurer la qualité (relevance, precision, recall)
- Scaling : Base vectorielle distribuée (Pinecone, Qdrant)
- Optimisation coûts : Cache intelligent, batch processing
- Évaluation continue : Tests A/B sur différentes stratégies de chunking
La section suivante couvre ces aspects avec des recommandations concrètes.
from sklearn.neighbors import NearestNeighbors
class VectorStore:
"""
Base vectorielle simple basée sur scikit-learn.
En production, utiliser Pinecone, Qdrant, Weaviate, ou Chroma.
"""
def __init__(self, df: pd.DataFrame, embedding_col: str = "embedding"):
self.df = df
self.embedding_col = embedding_col
# Filtrer les embeddings valides
valid_mask = df[embedding_col].apply(lambda x: x is not None)
self.valid_indices = df[valid_mask].index.tolist()
# Construire la matrice de vecteurs
vectors = np.array(df.loc[self.valid_indices, embedding_col].tolist())
# Index k-NN
self.nn = NearestNeighbors(n_neighbors=min(5, len(vectors)), metric="cosine")
self.nn.fit(vectors)
print(f"VectorStore initialisé: {len(self.valid_indices)} vecteurs")
def search(self, query_embedding: List[float], k: int = 3) -> List[Dict[str, Any]]:
"""
Recherche les k chunks les plus similaires.
Returns:
Liste de dicts avec chunk_id, text, source, score
"""
distances, indices = self.nn.kneighbors([query_embedding], n_neighbors=k)
results = []
for i, (dist, idx) in enumerate(zip(distances[0], indices[0])):
original_idx = self.valid_indices[idx]
row = self.df.iloc[original_idx]
results.append({
"chunk_id": row["chunk_id"],
"text": row["text"],
"source": row["source"],
"score": 1 - dist # Cosine similarity (1 = identique)
})
return results
# Initialisation du VectorStore
vector_store = VectorStore(df_chunks)VectorStore initialisé: 51 vecteurs
Exercice 2 - Recherche vectorielle avec seuil de confiance
Le VectorStore utilise k-NN pour trouver les chunks les plus proches, mais ne filtre pas les résultats peu pertinents. Dans un système de production, il est essentiel de ne retourner que les chunks dont la similarite depasse un seuil.
Objectif : Implementer une fonction search_with_confidence(vector_store, question, k, min_score) qui effectue une recherche vectorielle et retourne uniquement les chunks dont le score depasse un seuil, avec un niveau de confiance global.
Indices : - Reutiliser create_embedding() pour vectoriser la question, puis vector_store.search() pour la recherche - Filtrer les résultats avec min_score et calculer un niveau de confiance : “haute” si meilleur score > 0.7, “moyenne” si > 0.5, “basse” sinon - Retourner un dict {"results": List[Dict], "confidence": str, "total_found": int, "kept": int} - # Étape 1 : Créer l’embedding de la question avec create_embedding() - # Étape 2 : Appeler vector_store.search(query_embedding, k=k) et filtrer par min_score - # Étape 3 : Determiner le niveau de confiance a partir du meilleur score des résultats filtrès - # Indice : Si aucun résultat ne depasse le seuil, retourner confiance “basse” avec liste vide
# Exercice 2 - Recherche vectorielle avec seuil de confiance
# TODO etudiant : implementer search_with_confidence
def search_with_confidence(vector_store: VectorStore, question: str, k: int = 5, min_score: float = 0.5) -> Dict[str, Any]:
"""
Recherche vectorielle avec filtrage par score et niveau de confiance.
Args:
vector_store: Base vectorielle initiee
question: Question de l'utilisateur
k: Nombre de chunks a recuperer
min_score: Score minimum pour garder un chunk
Returns:
Dict avec results, confidence, total_found, kept
"""
return None # TODO etudiant : implementer la recherche avec confiance
# Etape 1 : Tester avec une question specifique
# result = search_with_confidence(vector_store, "What was Lincoln's position on equality?", k=5, min_score=0.5)
# Etape 2 : Afficher les resultats et le niveau de confiance
# print(f"Confiance: {result['confidence']}")
# print(f"Chunks trouves: {result['total_found']}, gardes: {result['kept']}")
# for r in result['results']:
# print(f" Score: {r['score']:.3f} - {r['text'][:100]}...")
# Etape 3 : Tester avec une question hors sujet pour observer confiance basse
# result_off = search_with_confidence(vector_store, "What is the recipe for chocolate cake?", k=5, min_score=0.5)
# print(f"\nQuestion hors sujet - Confiance: {result_off['confidence']}, gardes: {result_off['kept']}")
print("Exercice a completer")Exercice a completer
Le VectorStore est maintenant initialisé avec les chunks et leurs embeddings. Cette cellule effectue une première requête de recherche sémantique pour valider que la récupération fonctionne correctement avant de l’intégrer dans le pipeline RAG complet.
# Test de recherche
question = "What did Lincoln argue about slavery?"
# Embedding de la question
query_embedding = create_embedding(question)
# Recherche des chunks pertinents
results = vector_store.search(query_embedding, k=3)
print(f"Question: {question}\n")
print("=== Chunks retrouvés ===")
for i, r in enumerate(results):
print(f"\n[{i+1}] Score: {r['score']:.3f} | Source: {r['source']}")
print(f" {r['text'][:400]}...")Question: What did Lincoln argue about slavery?
=== Chunks retrouvés ===
[1] Score: 0.628 | Source: Lincoln-Douglas Debate 1 (Ottawa, 1858)
proclaims his Abolition doctrines. Let me read a part of them. In his speech at Springfield to the Convention, which nominated him for the Senate, he said: "In my opinion it will not cease until a crisis shall have been reached and passed. 'A house divided against itself cannot stand.' I believe this government cannot endure permanently half Slave and half Free . I do not expect the Union to be di...
[2] Score: 0.579 | Source: Lincoln-Douglas Debate 1 (Ottawa, 1858)
them not to have it if they do not want it. [Applause and laughter.] I do not mean that if this vast concourse of people were in a Territory of the United States, any one of them would be obliged to have a slave if he did not want one; but I do say that, as I understand the Dred Scott decision, if any one man wants slaves, all the rest have no way of keeping that one man from holding them. When I ...
[3] Score: 0.571 | Source: Lincoln-Douglas Debate 1 (Ottawa, 1858)
it not exist on the same principles on which our fathers made it? ("It can.")The knew when they framed the Constitution that in a country as wide and broad as this, with such a variety of climate, production and interest, the people necessarily required different laws and institutions in different localities. They knew that the laws and regulations which would suit the granite hills of New Hampshi...
Interprétation des scores de similarité
Les résultats de recherche montrent :
| Rang | Score | Interprétation |
|---|---|---|
| 1 | 0.628 | Bonne correspondance - Le chunk contient directement la doctrine d’abolition de Lincoln |
| 2 | 0.579 | Pertinence moyenne - Discussion sur les droits territoriaux liés à l’esclavage |
| 3 | 0.572 | Pertinence moyenne - Contexte historique sur les principes des pères fondateurs |
Analyse du score :
- Score > 0.7 : Excellent match, très pertinent
- 0.5 < Score < 0.7 : Pertinent, mais contexte indirect
- Score < 0.5 : Faible pertinence, risque de bruit
Ici, le chunk #1 avec score 0.628 est le plus pertinent. Il mentionne explicitement les “Abolition doctrines” de Lincoln dans son discours à Springfield, répondant directement à la question “What did Lincoln argue about slavery?”.
Point important : Même un score de 0.628 (62.8% de similarité) est considéré comme bon en RAG. Les scores parfaits (>0.9) ne s’obtiennent que pour des textes quasi-identiques.
Distance cosinus : Le score affiché est 1 - distance, donc 1 = identique, 0 = orthogonal (aucune similarité).
Partie 4 : RAG avec Chat Completions
4.1 Pipeline RAG classique
Qualité des citations : critères d’évaluation
Une bonne réponse RAG avec citations doit respecter plusieurs critères :
1. Précision des références : - Chaque affirmation doit être associée à une source [1], [2], etc. - Les citations multiples sont encouragées si plusieurs sources confirment la même information
2. Format structuré :
RÉPONSE: Lincoln argued that... [1] while Douglas maintained... [2]
SOURCES UTILISÉES: [1, 2]
CONFIANCE: haute
3. Niveaux de confiance : - Haute : Information présente dans plusieurs sources, cohérente - Moyenne : Information dans une seule source, claire - Basse : Information implicite, nécessite inférence
Exemple de sortie attendue :
RÉPONSE: Les points clés de désaccord étaient :
1. L'extension de l'esclavage [1, 2]
2. La souveraineté populaire vs restriction fédérale [1, 3]
3. L'interprétation de la Déclaration d'Indépendance [2]
SOURCES UTILISÉES: [1, 2, 3]
CONFIANCE: haute
Vérification de qualité : Comparer les extraits des chunks récupérés avec les citations dans la réponse pour s’assurer de la fidélité.
def rag_query(question: str, vector_store: VectorStore, k: int = 3) -> Dict[str, Any]:
"""
Pipeline RAG complet avec Chat Completions.
Args:
question: Question de l'utilisateur
vector_store: Base vectorielle
k: Nombre de chunks à récupérer
Returns:
Dict avec réponse, sources, et métadonnées
"""
# 1. Retrieval
query_embedding = create_embedding(question)
chunks = vector_store.search(query_embedding, k=k)
# 2. Construction du contexte
context = "\n\n".join([
f"[Source: {c['source']} | Chunk {c['chunk_id']}]\n{c['text']}"
for c in chunks
])
# 3. Prompt augmenté
system_prompt = """Tu es un assistant expert en histoire américaine.
Réponds aux questions en te basant UNIQUEMENT sur le contexte fourni.
Si l'information n'est pas dans le contexte, dis-le clairement.
Cite tes sources avec le format [Chunk X]."""
user_prompt = f"""Contexte:
{context}
Question: {question}
Réponds de manière précise en citant les sources."""
# 4. Génération
# Note: temperature not supported by gpt-5-mini, uses default (1.0)
# Budget relevé à 2000 : gpt-5-mini est un modèle de raisonnement dont les
# tokens de raisonnement sont déduits de max_completion_tokens. Avec 500,
# tout le budget pouvait être consommé par le raisonnement sans texte final
# visible (issue #3571). 2000 laisse une marge confortable pour le contenu.
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
max_completion_tokens=2000
)
return {
"question": question,
"answer": response.choices[0].message.content,
"sources": chunks,
"model": DEFAULT_MODEL,
"tokens": {
"prompt": response.usage.prompt_tokens,
"completion": response.usage.completion_tokens
}
}
# Test du pipeline RAG
result = rag_query(
"What were Lincoln's main arguments about slavery in the debate?",
vector_store
)
print("=== Réponse RAG ===")
print(f"\nQuestion: {result['question']}")
print(f"\nRéponse:\n{result['answer']}")
print(f"\nTokens utilisés: {result['tokens']}")=== Réponse RAG ===
Question: What were Lincoln's main arguments about slavery in the debate?
Réponse:
Voici, d'après le seul contexte fourni, les principaux arguments de Lincoln au débat d'Ottawa (1858) :
- La doctrine du "house divided" : Lincoln soutient que "This government cannot endure permanently half Slave and half Free" et qu'il faut s'attendre à ce que le pays devienne "all one thing, or all the other" (il n'attend pas la dissolution de l'Union). [Chunk 9]
- Empêcher l'expansion de l'esclavage pour le conduire à l'extinction : Lincoln dit que les opposants à l'esclavage doivent "arrest the further spread of it, and place it where the public mind shall rest in the belief that it is in the course of ultimate extinction." [Chunk 9]
- Accusation contre Douglas : Lincoln accuse Douglas de chercher à "nationalize" (rendre national) l'esclavage — c'est-à-dire d'étendre la protection ou la légalisation de l'esclavage à tous les États. [Chunk 0]
- Défense face aux attaques de Douglas : Lincoln a nié les allégations de Douglas (que Lincoln aurait voulu "abolitionize" les partis, qu'il avait été lié à une plate-forme « abolitionniste » de 1854, etc.). Le compte rendu dit que, lors de son tour de parole, Lincoln n'a pas répondu aux sept questions de Douglas mais s'est mis sur la défensive en niant ces accusations. [Chunk 0; voir aussi la discussion sur la plate-forme de 1854 dans la réplique de Douglas] [Chunk 41]
Si vous voulez des précisions supplémentaires (par ex. d'autres passages du discours de Lincoln ou des arguments rhétoriques qu'il a employés), ces détails ne figurent pas dans le contexte fourni.
Tokens utilisés: {'prompt': 1634, 'completion': 1281}
Analyse du pipeline RAG classique
L’exécution démontre le fonctionnement complet du pipeline RAG avec Chat Completions.
Résultats observés :
- La sortie précédente affiche directement les champs
promptetcompletionrenvoyés parresponse.usage; elle reste la source de vérité pour ce run. - La réponse cite les chunks récupérés et distingue les principaux arguments attribués à Lincoln.
- La présence d’une citation ne prouve pas à elle seule que le chunk étaye exactement l’affirmation : cette fidélité se contrôle contre le texte source.
- Le modèle indique aussi lorsque des précisions demandées ne figurent pas dans le contexte fourni.
Note sur le budget
max_completion_tokens:gpt-5-miniest un modèle de raisonnement dont les tokens de raisonnement sont déduits du budgetmax_completion_tokens. Un budget trop bas peut être entièrement consommé sans laisser de texte final visible (issue #3571). Le pipeline utilise un budget de 2000 pour laisser une marge au contenu ; il s’agit d’une configuration, pas d’une mesure de consommation.
Limitation identifiée : le notebook mesure les tokens renvoyés par l’API, mais ne déduit pas un coût monétaire sans grille tarifaire versionnée. La section suivante vérifie séparément le chaînage multi-tour avec previous_response_id.
Filtrage par seuil : écarter les résultats faibles
La recherche k-NN renvoie les k voisins les plus proches, même lorsque leurs scores sont faibles. La fonction présentée plus bas applique uniquement un filtrage par seuil au classement existant : elle ne recalcule aucun score et conserve l’ordre des survivants. Il ne s’agit donc pas d’un reranking.
Principe : conserver les chunks dont le score est supérieur ou égal à min_score.
| Exemple illustratif | Chunks k=5 | Après filtrage (min_score=0.3) |
Résultat |
|---|---|---|---|
| Scores tous au-dessus du seuil | [0.8, 0.7, 0.6, 0.4, 0.3] | [0.8, 0.7, 0.6, 0.4, 0.3] | 5 chunks conservés |
| Plusieurs scores faibles | [0.4, 0.3, 0.2, 0.1, 0.05] | [0.4, 0.3] | 3 chunks éliminés |
| Scores intermédiaires | [0.5, 0.5, 0.4, 0.2, 0.1] | [0.5, 0.5, 0.4] | 2 chunks éliminés |
Ces valeurs illustrent le mécanisme, pas des seuils universels de confiance. Un score cosinus dépend du modèle d’embedding, du corpus, de la requête et de la distribution observée. min_score doit être calibré sur un jeu de questions annoté en mesurant notamment la précision et le rappel.
Techniques distinctes et plus avancées : - Reranking cross-encoder : réévaluer les paires (question, chunk) avec un modèle spécialisé tel que ms-marco-MiniLM-L-6-v2, ce qui produit un nouveau score et potentiellement un nouvel ordre ; - Diversification : limiter les chunks redondants, par exemple avec MMR ou clustering ; - Décroissance temporelle : intégrer l’âge des documents lorsque le cas d’usage le justifie.
Le filtrage local n’ajoute aucun appel LLM, mais son intérêt doit être évalué sur la qualité du retrieval plutôt que déduit du seul score.
Partie 5 : RAG avec Responses API (Moderne)
La Responses API (2025) fournit notamment un état de conversation côté serveur :
| Fonctionnalité | Rôle |
|---|---|
store=True |
Persistance des réponses côté serveur |
previous_response_id |
Chaînage multi-tour sans reconstruire manuellement la liste des messages |
Champs usage |
Mesure des tokens comptabilisés pour chaque appel |
| Outils intégrés | Web search, file search, code interpreter |
Le chaînage ne prouve ni un hit de prompt cache ni une réduction de tokens. Ces effets se vérifient uniquement avec les champs d’usage dédiés renvoyés par l’API.
def rag_query_responses_api(
question: str,
vector_store: VectorStore,
previous_response_id: str = None,
k: int = 3
) -> Dict[str, Any]:
"""
Pipeline RAG avec Responses API.
Avantages:
- Conversations multi-turn avec previous_response_id
- Persistence côté serveur
"""
# 1. Retrieval
query_embedding = create_embedding(question)
chunks = vector_store.search(query_embedding, k=k)
# 2. Construction du contexte (format texte simple pour Responses API)
context = "\n\n".join([
f"[Source: {c['source']} | Chunk {c['chunk_id']}]\n{c['text']}"
for c in chunks
])
# 3. Prompt complet
full_prompt = f"""Tu es un assistant expert. Réponds en citant les sources [Chunk X].
Contexte:
{context}
Question: {question}"""
# 4. Appel Responses API
try:
kwargs = {
"model": DEFAULT_MODEL,
"input": full_prompt,
"store": True
}
if previous_response_id:
kwargs["previous_response_id"] = previous_response_id
response = client.responses.create(**kwargs)
# Extraire le texte de la réponse
answer = ""
if response.output:
for item in response.output:
if hasattr(item, 'content'):
for content in item.content:
if hasattr(content, 'text'):
answer += content.text
return {
"question": question,
"answer": answer if answer else "Pas de réponse",
"response_id": response.id, # Pour le chaînage
"sources": chunks,
"api": "responses",
"usage": {
"input_tokens": getattr(response.usage, "input_tokens", None),
"output_tokens": getattr(response.usage, "output_tokens", None),
"total_tokens": getattr(response.usage, "total_tokens", None),
} if getattr(response, "usage", None) else None
}
except Exception as e:
# Fallback sur Chat Completions si Responses API non disponible
print(f"Note: Responses API non disponible. IMPORTANT: Le fallback Chat Completions NE REPREND PAS le même historique via previous_response_id ({e}), utilisation de Chat Completions")
return rag_query(question, vector_store, k)
# Test Responses API
print("=== Test RAG avec Responses API ===")
result1 = rag_query_responses_api(
"What did Lincoln say about slavery?",
vector_store
)
print(f"\nQuestion 1: {result1['question']}")
print(f"Réponse: {result1['answer'][:1000]}...")
print(f"Response ID: {result1.get('response_id', 'N/A')}")=== Test RAG avec Responses API ===
Question 1: What did Lincoln say about slavery?
Réponse: En substance, Lincoln disait que le pays ne pouvait pas rester « à moitié esclave et à moitié libre » : « A house divided against itself cannot stand » — il affirmait que la nation finirait par « cesser d’être divisée » et deviendrait « tout l’un ou tout l’autre », et que les adversaires de l’esclavage devaient en arrêter l’expansion ou bien ses partisans le propageraient partout [Chunk 9]. Il ajoutait qu’il fallait empêcher l’introduction de l’esclavage dans les territoires libres et que permettre son extension n’avait pas plus de justification morale que de rétablir la traite africaine [Chunk 20]. En même temps, il déclara ne pas avoir « de dessein, directement ou indirectement, d’interférer » avec l’esclavage dans les États où il existait, qu’il croyait n’en avoir ni le droit légal ni l’inclination, et qu’il n’avait pas pour but d’introduire l’égalité politique et sociale entre les races [Chunk 20]. Par ailleurs, Douglas rapporte que Lincoln professait la conviction que le Noir étai...
Response ID: resp_0021b4b5320cd4f8006aa057bb1a8487d18d68ca2aea9453c8
La première requête Responses API ayant renvoyé un response_id, cette cellule teste le chaînage multi-tour : le contexte de la conversation précédente est automatiquement préservé sans nécessiter de renvoyer l’historique complet des messages.
# Multi-turn avec previous_response_id
print("=== Conversation multi-turn ===")
if result1.get('response_id'):
# Question de suivi utilisant le contexte précédent
result2 = rag_query_responses_api(
"And what was Douglas's counter-argument?",
vector_store,
previous_response_id=result1['response_id']
)
print(f"\nQuestion 2 (suivi): {result2['question']}")
print(f"Réponse: {result2['answer'][:500]}...")
print("\nNote: Le contexte de la question 1 est automatiquement inclus via previous_response_id")
else:
print("Responses API non disponible pour le multi-turn")=== Conversation multi-turn ===
Question 2 (suivi): And what was Douglas's counter-argument?
Réponse: Douglas répliqua en défendant la souveraineté populaire : le Nebraska Bill avait pour « vrai but et sens » de « ne pas légiférer l’esclavage dans un Territoire ou un État, ni de l’en exclure, mais de laisser le peuple parfaitement libre de former et régler ses institutions domestiques » — autrement dit, ce sont les habitants qui doivent décider de l’esclavage dans leur territoire [Chunk 31]. Il expliqua qu’il avait voté contre l’amendement du sénateur Chase parce que le projet de loi conférait d...
Note: Le contexte de la question 1 est automatiquement inclus via previous_response_id
# Mesure réelle via les champs usage renvoyés par l'API
print("=== Mesure réelle des champs usage (2 tours) ===")
print("| Tour | API | input_tokens | output_tokens | total_tokens |")
print("|------|-----|--------------|---------------|--------------|")
res1 = globals().get("result1")
res2 = globals().get("result2")
for label, result in [("Q1", res1), ("Q2", res2)]:
usage = (result or {}).get("usage") or {}
api_name = (result or {}).get("api", "indisponible")
print(
f"| {label} | {api_name} | {usage.get('input_tokens')} | "
f"{usage.get('output_tokens')} | {usage.get('total_tokens')} |"
)
print(
"Note : seules les valeurs lues dans response.usage sont affichées ; "
"le coût des embeddings n'est pas estimé sans métrique d'usage fournie."
)=== Mesure réelle des champs usage (2 tours) ===
| Tour | API | input_tokens | output_tokens | total_tokens |
|------|-----|--------------|---------------|--------------|
| Q1 | responses | 1531 | 1299 | 2830 |
| Q2 | responses | 3380 | 1055 | 4435 |
Note : seules les valeurs lues dans response.usage sont affichées ; le coût des embeddings n'est pas estimé sans métrique d'usage fournie.
Analyse du multi-turn avec Responses API
Cette exécution vérifie le chaînage de deux appels RAG avec previous_response_id. La table imprimée par la cellule précédente expose directement, pour Q1 et Q2, l’API utilisée ainsi que input_tokens, output_tokens et total_tokens. Cette sortie est l’unique source de vérité numérique : les valeurs ne sont pas recopiées dans cette cellule markdown, car elles peuvent varier d’une génération à l’autre.
Ce que la sortie établit :
- Q1 renvoie un
response_id, ensuite fourni commeprevious_response_idà Q2. - La formulation anaphorique de Q2 produit une réponse centrée sur le contre-argument de Douglas.
- Les valeurs affichées viennent directement de
response.usage; elles ne sont ni estimées ni converties en coût.
Ce que la sortie n’établit pas :
- Elle ne démontre pas une économie de tokens : il faudrait comparer des scénarios équivalents et leurs champs d’usage détaillés.
- Elle ne sépare pas les éventuels tokens mis en cache ou refacturés ; cette information demande des champs d’usage dédiés et une grille tarifaire versionnée.
- Elle ne prouve pas à elle seule que le contexte est reproduit fidèlement : la réponse reste à contrôler contre les chunks cités.
Le bénéfice observé ici est donc la continuité d’API et d’orchestration, pas une réduction de coût mesurée.
5.1 Continuité du Multi-turn RAG
Avec previous_response_id, l’API conserve l’état de la réponse précédente côté serveur et permet de chaîner une question de suivi. Cela évite à l’application de reconstruire manuellement la liste des messages, mais les tokens historiques restent comptabilisés et ce notebook reconstruit encore un contexte RAG frais à chaque tour.
Cas d’usage : - Conversations de recherche documentaire - Analyse progressive de documents - Q&A avec follow-up questions
Partie 6 : Citations et Traçabilité
Un système RAG de qualité doit permettre de vérifier les sources.
def rag_with_citations(
question: str,
vector_store: VectorStore,
k: int = 3
) -> Dict[str, Any]:
"""
RAG avec génération structurée de citations.
"""
# Retrieval
query_embedding = create_embedding(question)
chunks = vector_store.search(query_embedding, k=k)
# Contexte numéroté pour citations
context_parts = []
for i, c in enumerate(chunks):
context_parts.append(f"[{i+1}] {c['text'][:500]}")
context = "\n\n".join(context_parts)
# Prompt demandant des citations explicites
prompt = f"""Basé sur ces sources:
{context}
Question: {question}
Instructions:
1. Réponds à la question en citant les sources entre crochets [1], [2], etc.
2. Si plusieurs sources confirment une information, cite-les tous.
3. Si l'information n'est pas dans les sources, indique-le.
Format de réponse:
RÉPONSE: [ta réponse avec citations]
SOURCES UTILISÉES: [liste des numéros de sources]
CONFIANCE: [haute/moyenne/basse]"""
# Note: temperature not supported by gpt-5-mini, uses default (1.0)
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": prompt}]
)
return {
"question": question,
"response": response.choices[0].message.content,
"retrieved_chunks": [
{
"id": i + 1,
"score": c["score"],
"text": c["text"],
"preview": c["text"][:200],
}
for i, c in enumerate(chunks)
]
}
# Test avec citations
result_cited = rag_with_citations(
"What were the key points of disagreement between Lincoln and Douglas?",
vector_store
)
print("=== RAG avec Citations ===")
print(f"\nQuestion: {result_cited['question']}")
print(f"\n{result_cited['response']}")
print("\n--- Chunks récupérés ---")
for chunk in result_cited['retrieved_chunks']:
print(f"[{chunk['id']}] Score: {chunk['score']:.3f} - {chunk['preview'][:100]}...")=== RAG avec Citations ===
Question: What were the key points of disagreement between Lincoln and Douglas?
RÉPONSE:
Les extraits montrent que les points de désaccord principaux entre Lincoln et Douglas portaient sur la question de l’esclavage et sur la légitimité de l’action politique républicaine.
- Sur la politique à suivre pour l’esclavage : Douglas accuse Lincoln et les Républicains de vouloir imposer une « uniformité » des institutions (c.-à-d. une politique nationale sur l’esclavage) au lieu de respecter le droit de chaque État à décider (la souveraineté populaire) — accusation formulée explicitement par Douglas contre la « nouvelle doctrine » que, selon lui, Lincoln préconise [3]. Douglas reproche aussi à Lincoln (ou à sa position) de vouloir « rendre l’esclavage également licite dans tous les États — anciens comme nouveaux, Nord comme Sud » (formulation rapportée dans le débat) [2].
- Sur le rôle et la nature du parti républicain / de Lincoln : Douglas accuse Lincoln d’avoir cherché à « abolitioniser » les partis Whig et démocrate et d’avoir participé, en 1854, à la rédaction d’une plate-forme républicaine très radicale — c’est une attaque contre les intentions politiques et l’antécédent organisationnel de Lincoln et du parti [1][2].
- Attaques personnelles et d’ordre historique : Douglas reproche aussi à Lincoln d’avoir pris, selon lui, le « parti de l’ennemi » pendant la guerre du Mexique (une accusation portant sur la conduite passée de Lincoln) [1].
Remarque : les extraits fournis rendent surtout compte des accusations et des thèmes du débat du côté de Douglas. Les arguments détaillés de Lincoln en réponse (sa défense des principes républicains, ses réfutations des accusations de Douglas, ou d’autres positions précises comme le traitement du Compromis de 1850 ou la décision Dred Scott) ne sont pas présents dans ces extraits, donc ne peuvent être confirmés ici à partir des sources fournies.
SOURCES UTILISÉES: [1], [2], [3]
CONFIANCE: haute
--- Chunks récupérés ---
[1] Score: 0.673 - First Debate: Ottawa, Illinois August 21, 1858 It was dry and dusty, between 10,000 and 12,000 peopl...
[2] Score: 0.599 - advance, to make slavery alike lawful in all the States-old as well as new, North as well as South. ...
[3] Score: 0.595 - Mr. Lincoln, of uniformity among the institutions of the different States, is a new doctrine, never ...
# Verification de la structure des citations
def verify_citations(result):
"""Verifie les citations."""
response = result.get('response', '')
chunks = result.get('retrieved_chunks', [])
import re
cited = set(re.findall(r'\[' + r'(\d+)\]', response))
if not cited:
return {"verdict": "affirmation non etayee", "details": "Aucune reference citee"}
if not chunks:
return {"verdict": "contexte vide", "details": "Aucun chunk"}
valid = {str(c['id']) for c in chunks}
invalid = cited - valid
if invalid:
return {"verdict": "reference inexistante", "details": "Refs " + str(invalid) + " non presentes"}
return {"verdict": "structure valide", "details": "Toutes les refs OK"}
verification = verify_citations(result_cited)
print("\n=== Verification des citations ===")
print('Verdict:', verification['verdict'])
print('Details:', verification['details'])
=== Verification des citations ===
Verdict: structure valide
Details: Toutes les refs OK
Analyse de la qualité des citations
Cette exécution démontre un système RAG où les citations sont demandées, puis soumises à une vérification structurelle.
Résultats observés :
| Aspect | Valeur | Interprétation |
|---|---|---|
| Chunks récupérés | 3 | k=3 par défaut |
| Références citées | [1], [2], [3] | Les trois identifiants récupérés apparaissent dans la réponse |
| Scores | 0.673, 0.599, 0.595 | Similarité du retrieval, pas score de fidélité de la réponse |
| Vérification automatique | structure valide |
Toutes les références citées correspondent à un chunk existant |
La fonction verify_citations valide uniquement la structure : présence d’au moins une référence, existence de chunks et absence d’identifiant hors ensemble. Elle ne prouve ni que chaque affirmation est étayée par le chunk associé, ni que la confiance « haute » produite par le modèle est calibrée.
Vérification ponctuelle de contenu :
- Affirmation : Douglas accuse Lincoln d’avoir voulu « abolitionize » les partis Whig et démocrate.
- Source [1] : l’extrait récupéré porte bien cette accusation.
- Portée : ce contrôle d’une affirmation ne généralise pas la fidélité à toute la réponse.
La prochaine étape pédagogique consiste donc à comparer manuellement plusieurs affirmations aux textes complets des chunks, puis à mesurer la précision et le rappel du retrieval sur un jeu de références.
EXERCICE : Vérification du contenu des citations
Objectif : distinguer deux questions différentes pour une affirmation citée : est-elle appuyée par l’extrait effectivement transmis au modèle, puis est-elle appuyée ailleurs dans la source complète du chunk ?
Méthode :
- Extraire une affirmation de la réponse avec sa référence, par exemple
[1]. - Retrouver le dictionnaire dont l’
idcorrespond dansresult_cited['retrieved_chunks']. - Reconstituer le contexte exact transmis au modèle avec
text[:500]. Le champtextconserve séparément la source complète du chunk etpreviewn’est qu’un aperçu d’affichage. - Comparer d’abord l’affirmation aux 500 caractères transmis et rendre un verdict explicite :
étayée,non étayéeouambiguëdans le contexte fourni au modèle. - Inspecter ensuite la source complète. Si elle contient un appui absent des 500 premiers caractères, le noter comme
étayée par la source complète, mais non par le contexte transmis: ce passage ne faisait pas partie des éléments donnés au modèle pour cette génération.
Exemple avec la réponse ci-dessus :
source_1 = next(
chunk for chunk in result_cited["retrieved_chunks"]
if chunk["id"] == 1
)
contexte_transmis_1 = source_1["text"][:500]
source_complete_1 = source_1["text"]
print("=== Contexte réellement transmis ===")
print(contexte_transmis_1)
print("\n=== Source complète du chunk ===")
print(source_complete_1)Un score de similarité cosinus élevé indique une proximité entre la question et le chunk ; il ne certifie pas que le passage transmis étaye une affirmation particulière. Le modèle peut citer la mauvaise référence ou tirer une conclusion plus forte que son contexte. Inversement, trouver l’information seulement après le 500e caractère dans la source complète ne prouve pas que le modèle disposait de cette information.
Question pour l’étudiant : choisissez une affirmation de la réponse ci-dessus et rendez les deux verdicts séparés — d’abord sur contexte_transmis_1, puis sur source_complete_1 — en signalant explicitement toute divergence.
# Exemple de filtrage par score (aucun réordonnancement, conservation de l'ordre existant)
def rag_with_score_filtering(
question: str,
vector_store: VectorStore,
k: int = 5,
min_score: float = 0.3
) -> Dict[str, Any]:
"""
RAG avec filtrage par score: filtre les chunks peu pertinents.
"""
query_embedding = create_embedding(question)
all_chunks = vector_store.search(query_embedding, k=k)
# Filtrage par score minimum
filtered_chunks = [c for c in all_chunks if c['score'] >= min_score]
print(f"Chunks récupérés: {len(all_chunks)}, après filtrage (score >= {min_score}): {len(filtered_chunks)}")
if not filtered_chunks:
return {"answer": "Aucun contexte pertinent trouvé pour cette question."}
# Génération avec chunks filtrés
context = "\n\n".join([c['text'] for c in filtered_chunks])
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[
{"role": "system", "content": "Réponds uniquement basé sur le contexte fourni."},
{"role": "user", "content": f"Contexte:\n{context}\n\nQuestion: {question}"}
]
)
return {
"answer": response.choices[0].message.content,
"chunks_used": len(filtered_chunks),
"chunks_filtered_out": len(all_chunks) - len(filtered_chunks)
}
# Test du filtrage par seuil
result_filtered = rag_with_score_filtering(
"What was the outcome of the debate?",
vector_store,
k=5,
min_score=0.3
)
print(f"\nRéponse: {result_filtered['answer'][:500]}...")Chunks récupérés: 5, après filtrage (score >= 0.3): 5
Réponse: Le texte fourni ne donne pas de «vainqueur» officiel ni de verdict formel. Il indique toutefois que Lincoln fut chaleureusement accueilli — «des acclamations longues et prolongées de près des deux tiers du public» — et qu’il passa son temps à nier les accusations de Douglas. Douglas, de son côté, insista surtout sur l’accusation relative à la plateforme républicaine de 1854 et répondit en réplique. En somme : pas d’issue formelle indiquée ; l’avantage auprès du public semble avoir été pour Linco...
Exercice 3 - Evaluation de la qualite de retrieval
Le pipeline RAG depend entierement de la qualite du retrieval. Sans bons chunks, même le meilleur modèle génèrera des reponses médiocre.
Objectif : Implementer une fonction evaluate_retrieval(questions_références, vector_store, k) qui mesure la precision du retrieval sur un jeu de questions avec chunks de référence attendus.
Indices : - Le paramètre questions_références est une liste de dicts {"question": str, "expected_chunk_ids": List[int]} - Pour chaque question, effectuer la recherche k-NN et vérifier si les expected_chunk_ids apparaissent dans les résultats - Calculer deux metriques : precision (fraction des chunks trouves qui sont pertinents) et recall (fraction des chunks pertinents qui ont ete trouves) - # Étape 1 : Construire un petit jeu de 3 questions avec chunks attendus (utiliser les chunks 0, 9, 20 par exemple) - # Étape 2 : Pour chaque question, appeler vector_store.search() et collecter les chunk_id trouves - # Étape 3 : Comparer avec les expected_chunk_ids et calculer precision et recall - # Indice : precision = len(trouves & attendus) / len(trouves), recall = len(trouves & attendus) / len(attendus)
# Exercice 3 - Evaluation de la qualite de retrieval
# TODO etudiant : implementer evaluate_retrieval
def evaluate_retrieval(questions_references: List[Dict], vector_store: VectorStore, k: int = 3) -> Dict[str, float]:
"""
Evalue la qualite du retrieval sur un jeu de questions de reference.
Args:
questions_references: Liste de dicts {"question": str, "expected_chunk_ids": List[int]}
vector_store: Base vectorielle initiee
k: Nombre de chunks a recuperer par recherche
Returns:
Dict avec precision_moyenne, recall_moyen, details_par_question
"""
pass # TODO etudiant : implementer l'evaluation du retrieval
# Etape 1 : Definir un jeu de 3 questions avec chunks attendus
# questions_references = [
# {"question": "What did Lincoln say about a house divided?", "expected_chunk_ids": [9]},
# {"question": "What was the audience size?", "expected_chunk_ids": [0]},
# {"question": "What did Douglas say about popular sovereignty?", "expected_chunk_ids": [20, 31]},
# ]
# Etape 2 : Appeler evaluate_retrieval
# results = evaluate_retrieval(questions_references, vector_store, k=3)
# Etape 3 : Afficher les resultats
# print(f"Precision moyenne: {results['precision_moyenne']:.2f}")
# print(f"Recall moyen: {results['recall_moyen']:.2f}")
print("Exercice a completer")Exercice a completer
Analyse du filtrage et gestion des limites
Cette exécution illustre deux aspects importants : le filtrage par score et la gestion des questions dont la réponse n’est pas explicitement formulée dans le contexte.
Résultats du filtrage :
| Métrique | Valeur | Interprétation |
|---|---|---|
| Chunks récupérés (k=5) | 5 | Recherche k-NN standard |
| Chunks après filtrage (score ≥ 0.3) | 5 | Tous les chunks conservés |
| Chunks filtrés | 0 | Aucun chunk avec score < 0.3 |
Observation : avec ce seuil et cette requête, les cinq résultats sont conservés. Cette mesure ne suffit pas à conclure que chaque chunk est pertinent ; elle montre seulement qu’aucun score n’est inférieur à 0.3.
Synthèse non verbatim de la réponse : le texte ne désigne pas de vainqueur formel. Il décrit la fin du débat et une réaction publique favorable à Lincoln, sans transformer cet indice en résultat officiel.
Points forts observés :
- Prudence : le modèle distingue l’absence de verdict formel de la réaction du public.
- Contexte fourni : il répond à partir des éléments disponibles plutôt que d’inventer un vainqueur.
- Limite explicite : la réponse ne présente pas l’indice d’audience comme un résultat définitif.
Les seuils de similarité ne sont pas universels : ils doivent être calibrés sur un jeu de questions annoté et sur le compromis précision/rappel du cas d’usage. L’exercice précédent propose justement de mesurer ce compromis au lieu d’adopter une table de seuils génériques.
Prochaine étape : évaluer plusieurs seuils sur le jeu de références, puis comparer précision, rappel et qualité des réponses générées.
Conclusion
Points clés
- Chunking : Stratégie critique - fixe, sémantique, ou récursif selon le cas
- Embeddings :
text-embedding-3-largeoffre la meilleure précision - Retrieval : k-NN avec score minimum pour filtrer le bruit
- Responses API :
previous_response_idpour multi-turn efficace - Citations : Toujours demander des références vérifiables
Prochaines étapes
- Notebook 6 : PDF et Web Search intégrés
- Notebook 7 : Code Interpreter pour analyse de données
- Notebook 9 : Patterns de production (batch, retry, monitoring)
Ressources
Références académiques : - Lewis, P. et al. (2020) - Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. arXiv:2005.11401. (RAG originel, nomme et définit le paradigme.) - Karpukhin, V. et al. (2020) - Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2004.04906. (Retriever dense bi-encoder.) - Reimers, N. & Gurevych, I. (2019) - Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. arXiv:1908.10084. (Embeddings de phrases.) - Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2023) - RAGAS: Automated Evaluation of Retrieval Augmented Generation. arXiv:2309.15217. (Évaluation RAG : faithfulness, context precision.)
Documentation API : - OpenAI Embeddings Guide - Responses API Documentation - RAG Best Practices
# Résumé des fonctions créées
print("=== Fonctions RAG disponibles ===")
print("""
Chunking:
- chunk_fixed(text, chunk_size, overlap)
- chunk_semantic(text, max_sentences)
- chunk_recursive(text, max_size, delimiters)
Embeddings:
- create_embedding(text, model)
- create_embeddings_batch(texts, model)
Recherche:
- VectorStore.search(query_embedding, k)
RAG:
- rag_query(question, vector_store, k) # Chat Completions
- rag_query_responses_api(question, ...) # Responses API (multi-turn)
- rag_with_citations(question, vector_store, k) # Avec sources
- rag_with_score_filtering(question, ..., min_score) # Avec filtrage
""")=== Fonctions RAG disponibles ===
Chunking:
- chunk_fixed(text, chunk_size, overlap)
- chunk_semantic(text, max_sentences)
- chunk_recursive(text, max_size, delimiters)
Embeddings:
- create_embedding(text, model)
- create_embeddings_batch(texts, model)
Recherche:
- VectorStore.search(query_embedding, k)
RAG:
- rag_query(question, vector_store, k) # Chat Completions
- rag_query_responses_api(question, ...) # Responses API (multi-turn)
- rag_with_citations(question, vector_store, k) # Avec sources
- rag_with_score_filtering(question, ..., min_score) # Avec filtrage
CHALLENGE BONUS - Mini Moteur FAQ avec RAG
Points : 0.5 pts
Objectif
Créer un mini système RAG qui répond aux questions en cherchant dans une base de connaissances.
Ce que vous avez appris
Ce notebook montre: - Section 2 : Chunking avec chunk_recursive() - Section 3 : Embeddings avec client.embeddings.create() - Section 4 : Recherche vectorielle avec la classe VectorStore - Section 5 : Construction d’un prompt RAG avec contexte
Critères de succès
Contraintes techniques
- Réutiliser les fonctions du notebook (
chunk_recursive,VectorStore) - Afficher les sources (chunks) utilisées pour la réponse
Soumission : PR avec titre “Challenge #5 - [Votre Nom]”, sujet de la FAQ et exemple de requête