SK-5-VectorStores : RAG avec Qdrant

Navigation : << 04-Filters | Index | 06-ProcessFramework >>


Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Comprendre l’architecture Vector Store de SK 2. Generer des embeddings avec OpenAI 3. Utiliser InMemoryVectorStore pour le prototypage 4. Connecter Qdrant pour la production 5. Implementer un pipeline RAG complet

Prerequis

  • Python 3.10+
  • Notebooks 01-04 completes
  • Cle API OpenAI configuree (.env)
  • Acces Qdrant (optionnel, fourni)

Duree estimee : 50 minutes


Sommaire

Section Contenu Concepts cles
1 Introduction Pourquoi les Vector Stores ?
2 Architecture SK VectorStore, Collections, Records
3 Embeddings OpenAITextEmbedding
4 InMemoryVectorStore Prototypage rapide
5 Qdrant Production-ready
6 RAG Pattern Pipeline complet
7 Conclusion Resume, exercices

Qu’est-ce qu’un Vector Store ? Une base de données optimisee pour stocker et rechercher des vecteurs (embeddings). C’est la fondation du RAG (Retrieval-Augmented Generation) qui permet aux LLMs d’acceder a vos données.

# Installation

import os
from dotenv import load_dotenv

# Chargement du fichier .env (cles API)
load_dotenv("../.env")

api_key = os.getenv("OPENAI_API_KEY")
print(f"Configuration: API Key {'OK' if api_key else 'MANQUANTE'}")
print("Dependances installees")
Configuration: API Key OK
Dependances installees

1. Introduction aux Vector Stores

Pourquoi les Vector Stores ?

Les LLMs ont une limite de contexte et pas d’acces a vos données privees. Les Vector Stores resolvent ce problème. Le graphe Mermaid de la cellule suivante sépare les deux chemins qui convergent vers la recherche de similitude : l’indexation des documents et l’encodage de la requête.

Connecteurs SK disponibles

Connecteur Type Cas d’usage
InMemoryVectorStore Local Prototypage, tests
QdrantVectorStore Cloud/Self-hosted Production
AzureAISearchVectorStore Azure Enterprise
PineconeVectorStore Cloud Scalabilite
RedisVectorStore Cache distribue Performance

Le diagramme ci-dessous rend ce pipeline RAG vectoriel sous forme de graphe : l’indexation (Documents -> Vector Store) et la recherche (Query -> similitude -> contexte).

flowchart TD
    D(["Documents"]) --> E["Embeddings"] --> VS[("Vector Store<br/>Qdrant ou autre")]
    Q(["Query : Qu'est-ce que X ?"]) --> EQ["Embedding Query<br/>[0.2, 0.4, ...]"]
    EQ --> SIM["Recherche de similitude"]
    VS --> SIM
    SIM --> TOPK["Top-K documents pertinents"]
    TOPK --> CTX(["Contexte pour le LLM"])
    classDef idx fill:#cfe2ff,stroke:#084298,color:#052c65
    classDef gen fill:#fff3cd,stroke:#b8860b,color:#5c4400
    classDef io fill:#d1e7dd,stroke:#0f5132,color:#0a3622
    class D,E,VS idx
    class EQ,SIM,TOPK gen
    class Q,CTX io

Lecture. A l’indexation, chaque document est converti en embedding (vecteur dense) et stocke dans un vector store (Qdrant ici). A la requête, la question est encodee dans le même espace, puis une recherche de similitude (cosinus / dot product) ramene les Top-K passages les plus proches – qui forment le contexte injecte au LLM. Tout repose sur le fait que proximite vectorielle approxime proximite sémantique.

2. Architecture Vector Store SK

SK utilise une abstraction a trois niveaux :

┌─────────────────────────────────────────────┐
│              VectorStore                    │
│  (InMemory, Qdrant, Azure, Pinecone, ...)   │
│                                             │
│   ┌─────────────────────────────────────┐  │
│   │    VectorStoreRecordCollection      │  │
│   │    (equivalent d'une "table")       │  │
│   │                                     │  │
│   │   ┌─────────────────────────────┐  │  │
│   │   │  VectorStoreRecordDefinition│  │  │
│   │   │  (schema des records)       │  │  │
│   │   └─────────────────────────────┘  │  │
│   └─────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

Concepts cles

Concept Description Analogie SQL
VectorStore Connexion a la base Database connection
Collection Groupe de records Table
Record Document + embedding Row
Key Identifiant unique Primary Key
Vector Embedding du contenu Colonne indexee

La même abstraction à trois niveaux, rendue sous forme de graphe : chaque niveau en encapsule un autre.

flowchart TB
    subgraph VS["VectorStore — InMemory · Qdrant · Azure · Pinecone …"]
        subgraph COL["VectorStoreRecordCollection — équivalent d'une « table »"]
            RD["VectorStoreRecordDefinition<br/>schéma des records"]
        end
    end
    style VS fill:#cfe2ff,stroke:#084298,color:#052c65
    style COL fill:#d1e7dd,stroke:#0f5132,color:#052e16
    style RD fill:#fff3cd,stroke:#b8860b,color:#5c4400

Lecture. L’encapsulation suit l’analogie SQL : le VectorStore est la connexion à la base, la Collection une table, la RecordDefinition le schéma de ses lignes. Changer de backend (InMemory → Qdrant) ne change que le niveau externe : le code SK reste identique.

3. Generation d’Embeddings

from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding

# Configuration
kernel = Kernel()

# Service d'embedding
embedding_service = OpenAITextEmbedding(
    service_id="embedding",
    ai_model_id="text-embedding-3-small"  # Modele recommande (peu couteux, performant)
)
kernel.add_service(embedding_service)

# Test de generation d'embedding
test_texts = [
    "Semantic Kernel est un SDK pour l'IA",
    "Python est un langage de programmation"
]

embeddings = await embedding_service.generate_embeddings(test_texts)

print(f"Nombre de textes: {len(test_texts)}")
print(f"Dimension des embeddings: {len(embeddings[0])}")
print(f"Premier embedding (debut): {embeddings[0][:5]}...")
Nombre de textes: 2
Dimension des embeddings: 1536
Premier embedding (debut): [-0.03924561  0.04214478 -0.02653503 -0.03384399  0.00598907]...

Exercice 1 : Calcul de similarite cosinus entre embeddings

Objectif : Implementer une fonction qui calcule la similarite cosinus entre deux vecteurs d’embedding, puis compare la similarite de paires de textes.

La similarite cosinus est la metrique fondamentale pour evaluer la proximite sémantique entre textes dans un vector store. Comprendre cette metrique est essentiel pour debugger les résultats de recherche vectorielle.

Indices : - # Étape 1 : Implementer le produit scalaire (sum(a[i] * b[i])) - # Étape 2 : Calculer les normes L2 de chaque vecteur (sqrt(sum(x^2))) - # Étape 3 : Diviser le produit scalaire par le produit des normes - # Indice : Les vecteurs OpenAI sont déjà normalises, donc le produit scalaire seul suffit

import math

def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
    """
    Calcule la similarite cosinus entre deux vecteurs.
    
    # Etape 1 : Calculer le produit scalaire (dot product)
    # Etape 2 : Calculer les normes de chaque vecteur
    # Etape 3 : Retourner dot_product / (norm1 * norm2)
    
    # Indice : math.sqrt(sum(x**2 for x in vec)) donne la norme L2
    # Indice : Retourner 0.0 si l'un des normes est nul (eviter division par zero)
    """
    # TODO etudiant : implementer la similarite cosinus
    return 0.0  # TODO etudiant : remplacer par le calcul

# Test avec des vecteurs simples
vec_a = [1.0, 0.0, 0.0]
vec_b = [0.0, 1.0, 0.0]
vec_c = [1.0, 0.0, 0.0]

print(f"Similarite a vs b (orthogonaux) : {cosine_similarity(vec_a, vec_b):.4f}")  # Attendu : ~0.0
print(f"Similarite a vs c (identiques) : {cosine_similarity(vec_a, vec_c):.4f}")  # Attendu : 1.0

# Test avec de vrais embeddings (decommentez apres implementation)
# texts = ["Le chat dort", "Le felin sommeille", "La voiture roule"]
# embs = await embedding_service.generate_embeddings(texts)
# print(f"Similarite 'chat dort' vs 'felin sommeille' : {cosine_similarity(embs[0], embs[1]):.4f}")
# print(f"Similarite 'chat dort' vs 'voiture roule' : {cosine_similarity(embs[0], embs[2]):.4f}")
Similarite a vs b (orthogonaux) : 0.0000
Similarite a vs c (identiques) : 0.0000

Interprétation : Generation d’Embeddings avec OpenAI

Sortie obtenue : 2 embeddings de dimension 1536 generes avec succes.

Metrique Valeur Signification
Dimension 1536 Taille du vecteur (text-embedding-3-small)
Plage valeurs [-1, 1] Normalisees pour calcul cosinus
Nombre embeddings 2 Batch de 2 textes traites simultanement
Premier embedding [-0.039, 0.042, …] Representation sémantique du texte 1

Comparaison des modèles OpenAI :

Modèle Dimension Prix $/1M tokens Performance Usage recommande
text-embedding-3-small 1536 $0.02 62.3% MTEB Production générale
text-embedding-3-large 3072 $0.13 64.6% MTEB Haute precision requise
text-embedding-ada-002 1536 $0.10 61.0% MTEB Legacy (deprecated)

Points cles :

  1. Batch processing : OpenAI accepte jusqu’a 2048 textes par requête (optimisation cout/latence)
  2. Normalisation : Les vecteurs sont normalises (norme L2 = 1) pour similarite cosinus
  3. Determinisme : Même texte = même embedding (pas de randomness)
  4. Dimension reduite : text-embedding-3-small peut etre reduit a 512/256 dimensions avec dimensions parameter

Calcul de similarite :

# Similarite cosinus entre deux embeddings
def cosine_similarity(vec1, vec2):
    dot_product = sum(a * b for a, b in zip(vec1, vec2))
    # Déjà normalises, donc dot_product = cosine similarity
    return dot_product

Performance typique :

  • Latence : ~50-200ms pour 1-10 textes
  • Throughput : ~500-1000 textes/seconde (batch)
  • Cout : 100K mots = ~125K tokens = $0.0025

Notes techniques :

  • SK 1.39+ : OpenAITextEmbedding supporte dimensions parameter pour truncation
  • Les embeddings sont caches automatiquement par SK pour eviter regeneration
  • Pour multilingual, text-embedding-3-small supporte 100+ langues

4. InMemoryVectorStore

Pour le prototypage rapide sans infrastructure externe.

from dataclasses import dataclass
from typing import Annotated
from semantic_kernel.connectors.in_memory import InMemoryStore
from semantic_kernel.data.vector import (
    vectorstoremodel,
    VectorStoreField,
    FieldTypes
)

# Definition du schema de record avec la nouvelle API SK 1.39+
@vectorstoremodel
@dataclass
class DocumentRecord:
    """Schema d'un document dans le vector store."""
    id: Annotated[int, VectorStoreField(field_type=FieldTypes.KEY)]
    content: Annotated[str, VectorStoreField(field_type=FieldTypes.DATA)]
    title: Annotated[str, VectorStoreField(field_type=FieldTypes.DATA)]
    embedding: Annotated[
        list[float] | None,
        VectorStoreField(
            field_type=FieldTypes.VECTOR,
            dimensions=1536
        )
    ] = None

# Creation du store en memoire
memory_store = InMemoryStore()

# Obtenir ou creer une collection
collection = memory_store.get_collection(
    DocumentRecord,
    collection_name="documents"
)

# Creer la collection (si elle n'existe pas)
# SK 1.39+: ensure_collection_exists() au lieu de create_collection_if_not_exists()
await collection.ensure_collection_exists()

print("Collection 'documents' creee")
print(f"Schema: id (key), content (data), title (data), embedding (vector 1536d)")
Collection 'documents' creee
Schema: id (key), content (data), title (data), embedding (vector 1536d)

Exercice 2 : Schema de record personnalise avec metadonnees

Objectif : Créer un schema de record vectoriel adapte a une base de connaissances d’articles scientifiques, avec des metadonnees (auteur, annee, catégorie) permettant le filtrage.

Le schema DocumentRecord utilise precedemment est minimal. En production, les records vectoriels contiennent des metadonnees riches pour filtrer les recherches (par date, auteur, domaine).

Indices : - # Étape 1 : Ajouter des champs DATA pour auteur (str), annee (int) et domaine (str) - # Étape 2 : Créer la collection avec ce nouveau schema - # Étape 3 : Inserer 2-3 records d’exemple avec ces metadonnees - # Indice : Chaque champ DATA supplementaire necessite une annotation VectorStoreField(field_type=FieldTypes.DATA)

@vectorstoremodel
@dataclass
class ArticleRecord:
    """
    Schema pour une base de connaissances d'articles scientifiques.
    
    # Etape 1 : Ajouter les champs auteur, annee et domaine comme VectorStoreField DATA
    # Etape 2 : Respecter le meme pattern que DocumentRecord pour id, content, embedding
    # Etape 3 : Le champ embedding doit rester list[float] | None avec dimensions=1536
    """
    # TODO etudiant : completer le schema avec les 3 champs metadonnees manquants
    id: Annotated[str, VectorStoreField(field_type=FieldTypes.KEY)]
    content: Annotated[str, VectorStoreField(field_type=FieldTypes.DATA)]
    title: Annotated[str, VectorStoreField(field_type=FieldTypes.DATA)]
    # TODO etudiant : ajouter auteur, annee, domaine
    embedding: Annotated[
        list[float] | None,
        VectorStoreField(field_type=FieldTypes.VECTOR, dimensions=1536)
    ] = None

print("Exercice a completer : ajoutez les champs auteur, annee et domaine au schema")
Exercice a completer : ajoutez les champs auteur, annee et domaine au schema

Interprétation : Schema de Record Vector Store

Architecture du schema : Annotation avec decorateurs pour définir la structure des données.

Composant Type Rôle
@vectorstoremodel Decorateur classe Marque la classe comme schema SK
@dataclass Decorateur Python Genere __init__, __repr__, etc.
VectorStoreField Annotation Specifie le type de champ
FieldTypes.KEY Type champ Identifiant unique (primary key)
FieldTypes.DATA Type champ Données textuelles ou metadonnees
FieldTypes.VECTOR Type champ Embedding vectoriel

Points cles :

  1. Separation des concerns : Les champs KEY, DATA, et VECTOR ont des rôles distincts
  2. Dimensions fixes : Le champ vector doit specifier dimensions=1536 pour text-embedding-3-small
  3. Optional embedding : list[float] | None permet de créer des records sans embedding initial
  4. Type safety : Annotated + dataclass assurent la validation des types au runtime
  5. Type de clé et Qdrant : le champ KEY est un int (id de point). Qdrant n’accepte que des entiers non signés ou des UUID comme identifiants de point — une chaîne arbitraire ("doc1") est rejetée à l’upsert. Le int est le choix le plus simple pour ce notebook ; une clé métier en chaîne exigerait un champ KEY paysage + un mapping id_from_record.

Notes techniques :

  • SK 1.39+ utilise ensure_collection_exists() au lieu de create_collection_if_not_exists()
  • Le schema est immuable après creation de la collection
  • Les dimensions doivent correspondre au modèle d’embedding utilise
# Documents d'exemple
documents = [
    {
        "id": 1,
        "title": "Introduction a Semantic Kernel",
        "content": "Semantic Kernel est un SDK open-source de Microsoft pour integrer des LLMs dans vos applications."
    },
    {
        "id": 2,
        "title": "Plugins SK",
        "content": "Les plugins dans Semantic Kernel sont des collections de fonctions que le kernel peut invoquer."
    },
    {
        "id": 3,
        "title": "Agents SK",
        "content": "L'Agent Framework permet de creer des agents autonomes qui utilisent des plugins et collaborent entre eux."
    },
    {
        "id": 4,
        "title": "RAG avec SK",
        "content": "RAG (Retrieval-Augmented Generation) combine la recherche vectorielle avec la generation de texte par LLM."
    }
]

# Generer les embeddings
contents = [doc["content"] for doc in documents]
embeddings = await embedding_service.generate_embeddings(contents)

# Creer les records
records = []
for doc, emb in zip(documents, embeddings):
    record = DocumentRecord(
        id=doc["id"],
        title=doc["title"],
        content=doc["content"],
        embedding=list(emb)
    )
    records.append(record)

# Inserer dans la collection (SK 1.39+: upsert prend une liste)
keys = await collection.upsert(records)
print(f"Documents inseres: {keys}")
Documents inseres: [1, 2, 3, 4]

Interprétation : Pipeline d’Ingestion de Documents

Sortie obtenue : 4 documents inseres avec succes dans la collection InMemory.

Étape Opération Code cle
1. Extraction Recuperer les contenus textuels contents = [doc["content"] for doc in documents]
2. Embedding Generer les vecteurs (batch) generate_embeddings(contents)
3. Record creation Associer metadata + vecteur DocumentRecord(id, title, content, embedding)
4. Upsert Inserer/Mettre a jour en base collection.upsert(records)

Points cles :

  1. Batch embedding : Generer tous les embeddings en une seule requête API (plus efficace et moins couteux)
  2. Upsert semantics : Insere si nouveau, met a jour si existant (base sur la cle id)
  3. Liste de records : SK 1.39+ accepte une liste complete, pas d’itération necessaire
  4. Separation données/vecteurs : Le contenu original est conserve avec l’embedding pour affichage

Calcul des couts :

  • Modèle : text-embedding-3-small ($0.02/1M tokens)
  • 4 documents x ~20 tokens = 80 tokens
  • Cout : ~$0.0000016 (negligeable)

Notes d’optimisation :

  • Pour de gros volumes, utiliser des batches de 100-500 documents
  • Les embeddings peuvent etre pre-calcules et stockes offline
  • Qdrant supporte l’ingestion parallele pour acceleration
# Recherche vectorielle
query = "Comment creer des agents avec Semantic Kernel ?"

# Generer l'embedding de la requete
query_embedding = (await embedding_service.generate_embeddings([query]))[0]

# Rechercher les documents similaires (SK 1.39+: search au lieu de vectorized_search)
results = await collection.search(
    vector=list(query_embedding),
    vector_property_name="embedding",
    top=3,
    include_vectors=False
)

print(f"Query: {query}")
print("\nResultats:")
print("-" * 60)

# SK 1.39+: results.results est un async generator
async for result in results.results:
    print(f"Score: {result.score:.4f}")
    print(f"Title: {result.record.title}")
    print(f"Content: {result.record.content}")
    print("-" * 60)
Query: Comment creer des agents avec Semantic Kernel ?

Resultats:
------------------------------------------------------------
Score: 0.3709
Title: Plugins SK
Content: Les plugins dans Semantic Kernel sont des collections de fonctions que le kernel peut invoquer.
------------------------------------------------------------
Score: 0.4582
Title: Agents SK
Content: L'Agent Framework permet de creer des agents autonomes qui utilisent des plugins et collaborent entre eux.
------------------------------------------------------------
Score: 0.4796
Title: Introduction a Semantic Kernel
Content: Semantic Kernel est un SDK open-source de Microsoft pour integrer des LLMs dans vos applications.
------------------------------------------------------------

Interprétation : Recherche Vectorielle et Distance Cosinus

Sortie obtenue : 3 documents récupérés, triés par score croissant. Le score retourné par défaut est une distance cosinus (0 = vecteurs identiques), donc le premier résultat est le plus pertinent — c’est le contrat normal d’un top=3 : on retourne les meilleurs en tête, jamais le pire.

Document Score (distance) Pertinence Interprétation
Plugins SK 0.3709 Plus proche (meilleur) Distance la plus faible = le plus similaire à la requête
Agents SK 0.4582 Intermédiaire Mentionne explicitement agents + plugins
Introduction SK 0.4796 Plus éloigné (moins pertinent) Distance la plus grande des trois

Points clés :

  1. Distance cosinus, et non similarité : le score est une distance — 0 = vecteurs identiques, et plus il monte, plus le document est éloigné de la requête (Cosine Distance = 1 − Cosine Similarity). Le tri interne est croissant (operator.le) pour servir le meilleur en tête. Inverser ce sens (croire 1 = identique) décrit une similarité, pas ce que retourne la collection par défaut.
  2. Recherche sémantique : la requête « créer des agents » retrouve des documents pertinents sans correspondance de mots exacte.
  3. Top-K : top=3 limite aux 3 meilleurs résultats, meilleur en tête (tri ascendant sur la distance).
  4. Ordre contre-intuitif — une vraie limite d’embedding : pour la question « Comment créer des agents ? », c’est « Plugins SK » qui arrive en tête — alors que le corpus contient « Agents SK » (« L’Agent Framework permet de créer des agents autonomes »), qui répond littéralement à la question. Les embeddings courts capturent mal l’intention fine de la requête (le concept « agents » vs l’action « créer ») ; c’est pourquoi la production ajoute un reranking (cross-encoder) et une recherche hybride (vectorielle + mots-clés BM25) pour corriger ce genre d’inversion.

Paramètres de recherche :

Paramètre Valeur utilisée Impact
vector Embedding de la question Vecteur de référence
vector_property_name “embedding” Champ à comparer
top 3 Nombre de résultats
include_vectors False Ne pas retourner les embeddings (économie mémoire)

Notes techniques :

  • SK 1.39+ : search() remplace vectorized_search()
  • Les résultats sont un async generator (itération asynchrone)
  • distance_function par défaut sur le champ embedding : DistanceFunction.DEFAULT → cosinus distance (cf. semantic_kernel/connectors/in_memory.py : DEFAULT: cosine, tri operator.le, invert_score=False). Déclarer explicitement COSINE_SIMILARITY inverserait le sens du score.

Seuils indicatifs (distance cosinus — plus c’est bas, plus c’est pertinent) :

  • Distance < 0.4 : Très pertinent (similarité > 0.6)
  • Distance 0.4–0.6 : Pertinent (similarité 0.4–0.6)
  • Distance 0.6–0.8 : Potentiellement pertinent
  • Distance > 0.8 : Peu pertinent (similarité < 0.2)

5. Qdrant (Production)

Qdrant est un vector store production-ready. La connexion ci-dessous cible une instance locale (QDRANT_URL, par défaut http://localhost:6333), provisionnée par docker run -p 6333:6333 qdrant/qdrant. La clé d’API vient de l’environnement (QDRANT_API_KEY) — une instance sans authentification fonctionne aussi sans clé. Si l’instance n’est pas disponible, la cellule dégrade proprement en InMemoryStore (le except affiche le type d’exception pour distinguer un timeout d’un 401 ou d’un point-id invalide).

from semantic_kernel.connectors.qdrant import QdrantStore
from qdrant_client import QdrantClient
import warnings
warnings.filterwarnings("ignore", message="Api key is used with an insecure connection.")

# Configuration Qdrant (depuis .env ou valeurs fournies)
QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333")
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")

# Connexion au client Qdrant
qdrant_client = QdrantClient(
    url=QDRANT_URL,
    api_key=QDRANT_API_KEY
)

# Verification de la connexion
try:
    collections = qdrant_client.get_collections()
    print(f"Connexion Qdrant reussie !")
    print(f"Nombre de collections presentes: {len(collections.collections)}")
    print(f"Collection 'sk_demo' deja presente: {'sk_demo' in [c.name for c in collections.collections]}")
except Exception as e:
    print(f"Erreur de connexion: {type(e).__name__}: {e}")
    print("Continuez avec InMemoryStore pour les exemples suivants.")
Connexion Qdrant reussie !
Nombre de collections presentes: 113
Collection 'sk_demo' deja presente: True
# Creation du store Qdrant via SK
try:
    qdrant_store = QdrantStore(
        url=QDRANT_URL,
        api_key=QDRANT_API_KEY
    )
    
    # Collection Qdrant (SK 1.39+)
    qdrant_collection = qdrant_store.get_collection(
        DocumentRecord,
        collection_name="sk_demo"
    )
    
    # SK 1.41 : ensure_collection_exists() n'est PAS idempotent pour Qdrant : il appelle
    # create_collection() sans vérifier l'existence, donc un « sk_demo » résiduel d'une
    # exécution précédente lève un 409. On supprime d'abord une collection existante
    # (exécution idempotente, relançable), puis on crée.
    if await qdrant_collection.collection_exists():
        await qdrant_collection.ensure_collection_deleted()
    await qdrant_collection.ensure_collection_exists()
    
    # Inserer les memes documents
    keys = await qdrant_collection.upsert(records)
    print(f"Documents inseres dans Qdrant: {keys}")
    
    # Recherche
    qdrant_results = await qdrant_collection.search(
        vector=list(query_embedding),
        vector_property_name="embedding",
        top=3
    )
    
    print(f"\nRecherche Qdrant pour: '{query}'")
    async for result in qdrant_results.results:
        print(f"  {result.score:.4f} - {result.record.title}")
        
except Exception as e:
    print(f"Qdrant non disponible: {type(e).__name__}: {e}")
    print("Les exemples utilisent InMemoryStore.")
Documents inseres dans Qdrant: [1, 2, 3, 4]

Recherche Qdrant pour: 'Comment creer des agents avec Semantic Kernel ?'
  0.6290 - Plugins SK
  0.5418 - Agents SK
  0.5204 - Introduction a Semantic Kernel

Interprétation : InMemory vs Qdrant - Choix Architectural

Comparaison detaillee des implementations :

Caractéristique InMemoryVectorStore Qdrant
Persistance Non (RAM seulement) Oui (disque/cloud)
Scalabilite ~10K documents Millions de documents
Performance recherche O(n) lineaire O(log n) avec HNSW
Infrastructure Aucune Docker/Cloud requis
Latence typique <10ms (petits datasets) 10-50ms (optimise)
Cout Gratuit (RAM locale) $0.25-2/GB/mois (cloud)
Usage Dev/Test/Prototypage Production/Scale

Points clés :

Portée de cette exécution. Ce notebook tourne contre une instance Qdrant locale mono-nœud (http://localhost:6333) — un seul nœud, pas de shards ni de replicas. L’architecture sharded décrite ci-dessous est la cible de déploiement production, pas la topologie effective de ce démo.

  1. Courbe de performance : InMemory devient lent au-dela de 10K documents (recherche lineaire)
  2. HNSW advantage : Qdrant utilise Hierarchical Navigable Small World graphs pour recherche sous-lineaire
  3. Même API SK : Le code reste identique, seul le backend change (portabilite garantie)
  4. Persistance critique : InMemory perd tout au redemarrage, Qdrant survit aux crashes

Architecture Qdrant en production :

┌──────────────────────────────────────────┐
│         Application (SK Client)          │
└──────────────┬───────────────────────────┘
               │ REST/gRPC
               v
┌──────────────────────────────────────────┐
│            Qdrant Cluster                │
│  ┌────────┐  ┌────────┐  ┌────────┐     │
│  │ Shard 1│  │ Shard 2│  │ Shard 3│     │
│  │ 10M    │  │ 10M    │  │ 10M    │     │
│  │ docs   │  │ docs   │  │ docs   │     │
│  └────────┘  └────────┘  └────────┘     │
└──────────────────────────────────────────┘

Fonctionnalites Qdrant avancees :

Feature Description Cas d’usage
Payload indexing Index sur metadonnees Filtres rapides (date, catégorie)
Quantization Compression vecteurs Reduction memoire 4-8x
Snapshots Backup/restore Disaster recovery
Replication Replicas pour HA Zero-downtime
Sparse vectors Vecteurs creux Hybrid search BM25+vector

Note sur la convention de score. Les deux moteurs trient par pertinence (meilleur en tête), mais le score ne se lit pas dans le même sens : InMemory (cellule 18) retourne une distance cosinus (0 = identique, plus bas = plus pertinent), tandis que Qdrant (cellule 22) retourne une similarité cosinus (plus haut = plus pertinent). Ne pas confondre les deux en comparant les chiffres bruts d’une cellule à l’autre.

Migration InMemory -> Qdrant :

  1. Export : Sauvegarder les records InMemory en JSON
  2. Setup Qdrant : Deployer via Docker ou cloud
  3. Create collection : Schema identique avec ensure_collection_exists()
  4. Batch upsert : Charger par batches de 100-1000 records
  5. Validation : Tester quelques requêtes pour verifier coherence

Quand migrer vers Qdrant :

  • Dataset > 5K documents
  • Besoin de persistance
  • Latence recherche > 100ms avec InMemory
  • Production deployments
  • Filtres complexes sur metadonnees

Alternatives a Qdrant :

Vector DB Avantage Inconvenient
Pinecone Fully managed, simple Cout eleve
Weaviate GraphQL, multimodal Complexite setup
Milvus Open-source, scale Infrastructure lourde
Chroma Leger, Python-native Performance limitee

Le déploiement Qdrant en production décrit plus haut, rendu sous forme de graphe : un client SK adresse un cluster sharded via REST/gRPC.

flowchart TB
    APP["Application — SK Client"]
    APP -->|REST / gRPC| CLUSTER
    subgraph CLUSTER["Qdrant Cluster"]
        direction LR
        S1["Shard 1<br/>10M docs"]
        S2["Shard 2<br/>10M docs"]
        S3["Shard 3<br/>10M docs"]
    end
    style APP fill:#cfe2ff,stroke:#084298,color:#052c65
    style CLUSTER fill:#f8f9fa,stroke:#495057,color:#212529
    style S1 fill:#d1e7dd,stroke:#0f5132,color:#052e16
    style S2 fill:#d1e7dd,stroke:#0f5132,color:#052e16
    style S3 fill:#d1e7dd,stroke:#0f5132,color:#052e16

Lecture. Le sharding horizontal répartit les documents sur plusieurs nœuds. Ce notebook exécute le cas mono-nœud local ; le cluster sharded est la cible de production. : la capacité croît linéairement (millions de docs) et la recherche reste sous-linéaire grâce à l’index HNSW. En production, on ajoute des replicas par shard pour la haute disponibilité.

6. RAG Pattern Complet

Assemblons tout pour un pipeline RAG fonctionnel.

from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIChatPromptExecutionSettings
from semantic_kernel.contents import ChatHistory

# Ajouter le service de chat
kernel.add_service(OpenAIChatCompletion(service_id="chat"))

async def rag_query(question: str, collection, embedding_service, kernel, top_k: int = 3):
    """Pipeline RAG complet."""
    
    # 1. Generer l'embedding de la question
    query_embedding = (await embedding_service.generate_embeddings([question]))[0]
    
    # 2. Rechercher les documents pertinents (SK 1.39+)
    results = await collection.search(
        vector=list(query_embedding),
        vector_property_name="embedding",
        top=top_k,
        include_vectors=False
    )
    
    # 3. Construire le contexte (SK 1.39+: async iteration)
    context_parts = []
    async for result in results.results:
        context_parts.append(f"- {result.record.title}: {result.record.content}")
    
    context = "\n".join(context_parts)
    
    # 4. Construire le prompt augmente
    augmented_prompt = f"""Tu es un assistant qui repond en utilisant uniquement le contexte fourni.
    
CONTEXTE:
{context}

QUESTION: {question}

REPONSE (basee uniquement sur le contexte):"""
    
    # 5. Appeler le LLM (SK 1.39+: settings requis)
    chat_service = kernel.get_service(service_id="chat")
    history = ChatHistory()
    history.add_user_message(augmented_prompt)
    
    settings = OpenAIChatPromptExecutionSettings()
    response = await chat_service.get_chat_message_contents(
        chat_history=history,
        settings=settings
    )
    
    return {
        "question": question,
        "context": context,
        "answer": str(response[0])
    }

# Test du pipeline RAG
result = await rag_query(
    question="Comment les agents SK peuvent-ils utiliser des plugins ?",
    collection=collection,
    embedding_service=embedding_service,
    kernel=kernel
)

print("=" * 60)
print(f"Question: {result['question']}")
print("=" * 60)
print(f"\nContexte utilise:\n{result['context']}")
print("=" * 60)
print(f"\nReponse:\n{result['answer']}")
============================================================
Question: Comment les agents SK peuvent-ils utiliser des plugins ?
============================================================

Contexte utilise:
- Agents SK: L'Agent Framework permet de creer des agents autonomes qui utilisent des plugins et collaborent entre eux.
- Plugins SK: Les plugins dans Semantic Kernel sont des collections de fonctions que le kernel peut invoquer.
- Introduction a Semantic Kernel: Semantic Kernel est un SDK open-source de Microsoft pour integrer des LLMs dans vos applications.
============================================================

Reponse:
Les agents SK, créés via l’Agent Framework, peuvent utiliser des plugins en invoquant les fonctions fournies par ces plugins. Dans Semantic Kernel, un plugin est une collection de fonctions que le kernel peut appeler ; les agents s’appuient donc sur ces fonctions pour accomplir des tâches et collaborer entre eux.

Exercice 3 : Evaluation de la qualite RAG

Objectif : Implementer une fonction qui evalue si la reponse RAG est coherente avec les documents recuperes.

Dans un système RAG en production, il est crucial de mesurer si la reponse du LLM est effectivement ancre dans le contexte fourni. Vous allez créer une fonction evaluate_rag_response qui compare la reponse avec les documents sources.

Indices : - # Étape 1 : Comparer les mots-cles de la reponse avec ceux du contexte - # Étape 2 : Calculer un score de couverture (fraction des documents sources utilises) - # Étape 3 : Detecter les informations de la reponse absentes du contexte - # Indice : Utilisez des ensembles de mots (set) pour la comparaison lexicale

def evaluate_rag_response(response: str, context_docs: list[str]) -> dict:
    """
    Evalue la coherence entre une reponse RAG et les documents sources.
    
    # Etape 1 : Extraire les mots significatifs (ignorer les mots vides)
    # Etape 2 : Calculer le pourcentage de mots de la reponse presents dans le contexte
    # Etape 3 : Identifier les mots de la reponse absents du contexte (potentielles hallucinations)
    
    # Indice : Utilisez set() pour les operations ensemblistes (intersection, difference)
    # Indice : Filtrez les mots courts (< 4 caracteres) pour ignorer les articles/prepositions
    """
    # TODO etudiant : implementer l'evaluation
    print("Exercice a completer")
    return {"coverage_score": 0.0, "hallucination_risk": "unknown", "ungrounded_words": []}

# Test (decommentez apres implementation)
# test_result = evaluate_rag_response(
#     response="Les agents SK utilisent des plugins pour collaborer.",
#     context_docs=[
#         "L'Agent Framework permet de creer des agents autonomes qui utilisent des plugins.",
#         "Les plugins dans Semantic Kernel sont des collections de fonctions."
#     ]
# )
# print(f"Score de couverture: {test_result['coverage_score']:.2f}")
# print(f"Risque hallucination: {test_result['hallucination_risk']}")
# print(f"Mots non ancres: {test_result['ungrounded_words']}")

Interprétation : Pipeline RAG - Retrieval-Augmented Generation

Sortie obtenue : Reponse LLM basee sur le contexte recupere via recherche vectorielle.

Étape Temps typique Cout Optimisation possible
1. Embedding requête ~50ms $0.000002 Cache pour requêtes frequentes
2. Recherche vectorielle ~10ms (InMemory) Gratuit Index HNSW pour gros volumes
3. Construction prompt <1ms Gratuit Templates pre-compiles
4. Generation LLM ~2000ms $0.0001-0.001 Streaming pour UX

Architecture du prompt augmente :

┌─────────────────────────────────────────┐
│  System: "Tu es un assistant..."       │ <- Instructions comportement
├─────────────────────────────────────────┤
│  CONTEXTE: [Top-K documents]           │ <- Knowledge base dynamique
├─────────────────────────────────────────┤
│  QUESTION: [Question utilisateur]      │ <- Input utilisateur
├─────────────────────────────────────────┤
│  REPONSE: [Generee par LLM]           │ <- Output
└─────────────────────────────────────────┘

Points cles :

  1. Grounding : Le contexte limite les hallucinations en fournissant des faits verifiables
  2. Contexte limite : Avec GPT-4, on peut inclure ~3-10 documents (selon taille)
  3. Instruction stricte : “uniquement sur le contexte” force le LLM a ne pas inventer
  4. Tracabilite : On peut logger les documents utilises pour audit

Comparaison RAG vs Fine-tuning :

Aspect RAG Fine-tuning
Cout initial Faible (~$10-100) Eleve (~$1000-10000)
Mise a jour Immediate (re-index) Lente (re-entrainement)
Données privees Reste externe Integre au modèle
Explicabilite Haute (sources visibles) Basse (boite noire)
Precision Haute (sources exactes) Variable (apprentissage)

Limites du RAG :

  • Chunking critique : Decouper mal les documents = contexte incomplet
  • Limite de tokens : GPT-4 Turbo = 128K tokens, mais cout augmente
  • Qualite embeddings : Un mauvais embedding = mauvaise recherche
  • Ordre des documents : Le LLM peut privileger les premiers documents

Ameliorations avancees :

  1. Reranking : Utiliser un modèle de reranking après la recherche vectorielle
  2. Hybrid search : Combiner recherche vectorielle + BM25 (keywords)
  3. Metadata filtering : Filtrer par date, auteur, type avant recherche
  4. Chain-of-thought : Demander au LLM d’expliquer son raisonnement

Le même prompt augmenté, rendu sous forme de graphe — les blocs empilés qui composent l’entrée envoyée au LLM.

flowchart TB
    SYS["System — « Tu es un assistant… »<br/>instructions de comportement"]
    CTX["CONTEXTE — Top-K documents<br/>base de connaissances dynamique"]
    Q["QUESTION — saisie utilisateur"]
    REP["RÉPONSE — générée par le LLM"]
    SYS --> CTX --> Q --> REP
    classDef instr fill:#cfe2ff,stroke:#084298,color:#052c65
    classDef ctx fill:#fff3cd,stroke:#b8860b,color:#5c4400
    classDef q fill:#d1e7dd,stroke:#0f5132,color:#052e16
    classDef rep fill:#f8d7da,stroke:#842029,color:#2c0b0e
    class SYS instr
    class CTX ctx
    class Q q
    class REP rep

Lecture. Le grounding consiste à injecter le CONTEXTE (documents récupérés par recherche vectorielle) entre les instructions système et la question : le LLM répond à partir de faits vérifiables plutôt que de sa mémoire, ce qui limite les hallucinations et rend les sources traçables. C’est ce prompt que le pipeline RAG assemble à chaque requête.

Conclusion

Resume des concepts

Concept Description Code cle
VectorStore Abstraction de base InMemoryVectorStore(), QdrantStore()
Collection Groupe de records store.get_collection(name, type)
Record Document + embedding @vectorstoremodel @dataclass
Embedding Vecteur sémantique OpenAITextEmbedding.generate_embeddings()
Search Recherche similitude collection.vectorized_search(vector, options)
RAG Retrieval-Augmented Generation Contexte + LLM

Points cles a retenir

  1. InMemory pour dev, Qdrant pour prod - Même API, backend différent
  2. Les embeddings capturent le sens - Pas juste les mots-cles
  3. RAG = Search + Generate - Contexte pertinent pour le LLM
  4. Le chunking est crucial - Decouper les longs documents
  5. Les metadonnees enrichissent - Filtres et contexte additionnel

Exercices suggeres

  1. RAG sur PDF : Ingerer un PDF et poser des questions
  2. Filtres : Ajouter des filtres sur les metadonnees (date, auteur)
  3. Evaluation : Mesurer la qualite des reponses RAG

Pour aller plus loin

Notebook Contenu
06-ProcessFramework Workflows orchestres
07-MultiModal Images et audio

Navigation : << 04-Filters | Index | 06-ProcessFramework >>

Retour au sommet