La Génération Augmentée par Récupération (RAG) est devenue l'architecture standard pour ancrer les grands modèles de langage (LLM) dans des données privées. Cependant, à mesure que le trafic de production augmente, l'étape de récupération devient souvent le goulot d'étranglement. Les requêtes sur les bases de données vectorielles, bien que rapides, impliquent une surcharge I/O significative, la génération d'embeddings pour les requêtes utilisateur et une latence réseau. C'est ici que la mise en cache sémantique entre en jeu, non pas comme un simple magasin clé-valeur, mais comme une couche intelligente qui associe les requêtes entrantes aux contextes historiquement récupérés sur la base de la similarité sémantique.
Pourquoi la mise en cache traditionnelle échoue en RAG
La mise en cache HTTP ou API traditionnelle repose sur une correspondance exacte de chaînes de caractères. Si un utilisateur demande « Quelle est la politique de retour ? » et un autre « Comment retourner un article ? », un cache basé sur le hachage traite ces demandes comme deux requêtes distinctes. Dans un contexte RAG, c'est gaspilleur, car la recherche vectorielle sous-jacente renvoie probablement les mêmes fragments de documents pour les deux requêtes sémantiquement équivalentes.
La mise en cache sémantique contourne entièrement la base de données vectorielle pour les motifs de requêtes « connus ». Au lieu d'exécuter une recherche vectorielle coûteuse, nous comparons l'embedding de la nouvelle requête avec un cache d'embeddings de requêtes précédentes. Si le score de similarité dépasse un seuil, nous servons directement le contexte mis en cache (et potentiellement la réponse LLM mise en cache).
Conception architecturale
Les composants principaux d'un cache sémantique pour RAG incluent :
- Service d'Embedding : Pour vectoriser les invites utilisateur entrantes.
- Magasin de cache : Un magasin vectoriel rapide, en mémoire (comme FAISS ou Redis avec des ensembles Vector), qui contient les embeddings de requêtes passées et les fragments récupérés associés.
- Appariement de similarité : Logique pour déterminer si une nouvelle requête est « suffisamment proche » d'une entrée mise en cache.
Il est crucial que ce cache soit placé avant la base de données vectorielle principale. Si une correspondance est trouvée, nous ignorons la recherche dans la base de données principale. Sinon, nous poursuivons avec le flux RAG standard et remplissons le cache.
Exemple d'implémentation
Voici une implémentation Python conceptuelle utilisant un magasin vectoriel en mémoire simplifié. En production, vous remplaceriez cela par une solution persistante et distribuée comme Redis Vector ou Milvus.
import numpy as np
from typing import List, Dict, Optional
import faiss
class SemanticCache:
def __init__(self, dimension: int = 768, threshold: float = 0.95):
self.dim = dimension
self.threshold = threshold
self.index = faiss.IndexFlatIP(dimension) # Produit scalaire pour la similarité cosinus
self.cache_store: Dict[int, str] = {} # ID -> Contexte mis en cache
self.count = 0
def add_to_cache(self, embedding: np.ndarray, context: str) -> int:
"""Stocke une nouvelle paire requête-contexte."""
id = self.count
self.count += 1
# Normaliser pour la similarité cosinus si l'on utilise le produit scalaire
faiss.normalize_L2(embedding)
self.index.add(np.array([embedding], dtype='float32'))
self.cache_store[id] = context
return id
def get_from_cache(self, query_embedding: np.ndarray) -> Optional[str]:
"""Récupère le contexte si une correspondance sémantique existe."""
faiss.normalize_L2(query_embedding)
k = 1
distances, indices = self.index.search(np.array([query_embedding], dtype='float32'), k)
if distances[0][0] >= self.threshold:
match_id = indices[0][0]
return self.cache_store.get(match_id)
return None
# Utilisation dans le pipeline RAG
cache = SemanticCache(dimension=1536, threshold=0.92)
embedding_model = ... # Votre fonction d'embedding
def retrieve_with_cache(query: str) -> str:
q_embed = embedding_model.encode(query)
# 1. Vérifier le cache sémantique
cached_context = cache.get_from_cache(q_embed)
if cached_context:
print("Correspondance dans le cache !")
return cached_context
# 2. Manqué : Effectuer une recherche coûteuse dans la base de données vectorielle
print("Manqué dans le cache : Interrogation de la base principale...")
primary_context = query_primary_vector_db(query)
# 3. Mettre à jour le cache
cache.add_to_cache(q_embed, primary_context)
return primary_context
Points clés à considérer pour la production
- Stratégies d'éviction du cache : Utilisez la durée de vie (TTL) pour éviter les données obsolètes. Si vos documents sources sont mis à jour, vous devez invalider les entrées de cache pertinentes. Une approche hybride utilisant des hachages de version de document est efficace.
- Ajustement du seuil : Un seuil élevé (par ex., 0,95) réduit les faux positifs mais diminue les taux de correspondance. Un seuil plus bas (par ex., 0,85) augmente les correspondances mais peut renvoyer un contexte légèrement non pertinent. Surveillez la précision par rapport au rappel sur un échantillon de votre journal de requêtes.
- Obsolétude vs. Fraîcheur : La mise en cache sémantique est idéale pour les bases de connaissances statiques (documents juridiques, manuels de produit). Pour les données dynamiques (cours de bourse en temps réel, inventaire), désactivez la mise en cache ou utilisez des TTL très courts.
- Analyse des coûts : Les appels d'embedding sont moins coûteux que les scans de base de données vectorielles, mais pas gratuits. Assurez-vous que votre modèle d'embedding est optimisé pour la vitesse. Envisagez de pré-calculer les embeddings pour les motifs de requêtes courants si possible.
Mesurer l'impact
Mettez en place l'observabilité pour suivre :
- Taux de correspondance du cache : Pourcentage de requêtes servies depuis le cache.
- Réduction de la latence : Latence moyenne p95 avec et sans cache.
- Écart de précision : Assurez-vous que les réponses mises en cache ne dégradent pas la qualité des réponses (tests A/B par rapport à une référence non mise en cache).
Conclusion
La mise en cache sémantique est un puissant modèle d'infrastructure pour optimiser les pipelines RAG. En exploitant la similarité des requêtes en langage naturel, vous pouvez réduire considérablement la charge sur les bases de données vectorielles principales, diminuer la latence de récupération et réduire les coûts d'inférence globaux. Pour les applications LLM de production traitant de grands volumes de requêtes, l'intégration d'un cache sémantique bien ajusté n'est plus optionnelle, c'est une optimisation de performance critique. Commencez petit avec un seuil strict, surveillez vos taux de correspondance et faites évoluer votre infrastructure de cache à mesure que votre volume de données augmente.