Mémorisation sémantique pour les agents LLM avec Redis : réduire les coûts et la latence
Comment le cache sémantique réduit les coûts et la latence des LLM : embeddings, seuils, Redis et RedisVL, ajustement de la précision, ainsi que le stockage en cache des sous-questions au sein des workflows d’agent.
Sources: Semantic Caching for AI Agents: How to Make Your LLM Apps Faster and Cheaper
Les appels à un grand modèle de langage coûtent réellement de l’argent et sont lents. Toute personne ayant vu la facture API augmenter après avoir mis un agent entre les vrais utilisateurs connaît ce dilemme : les modèles qui donnent les meilleures réponses sont aussi ceux qui sont les plus chers et les plus lents. Les modèles de type GPT-4 peuvent coûter plusieurs dollars par mille requêtes, et les workflows basés sur des agents aggravent encore la situation, car une seule requête de l’utilisateur peut déclencher de 8 à 20 appels au modèle.
Le cache sémantique vise directement à réduire ces coûts, mais la plupart des équipes l’utilisent insuffisamment. Il est facile de le mettre en place avec Redis.
Pourquoi les caches ordinaires échouent
Un cache classique considère deux requêtes comme identiques uniquement si leurs données binaire correspondent. Cela fonctionne pour les requêtes de base de données et les requêtes HTTP, mais ne fonctionne pas pour le langage naturel. Imaginez un agent de support qui reçoit ces demandes de trois utilisateurs différents :
- Un client demande comment demander un remboursement
- Un autre exige le remboursement de son argent
- Un troisième veut savoir ce que prévoit la politique de remboursement
Un cache au niveau des octets perçoit trois entrées non liées et paie pour trois appels à un LLM. Une personne dirait que les trois veulent la même réponse. Un cache sémantique fonctionne en se basant sur le sens plutôt que sur les caractères.
Le mécanisme en quatre étapes
- Incorporer la requête reçue en la transformant en un vecteur à haute dimension à l’aide d’un modèle d’incorporation.
- Comparer ce vecteur avec chaque entrée stockée, en utilisant la distance cosinus ou une mesure similaire.
- Si l’entrée la plus proche se situe dans la limite de distance que vous avez définie, la considérer comme une correspondance et retourner immédiatement la réponse stockée.
- Si rien n’est suffisamment proche, envoyer la requête par votre pipeline RAG ou à l’LLM, puis stocker la question et la réponse pour la prochaine fois.
Les questions reformulées finissent par partager les mêmes réponses. À mesure que le cache se remplit, une part de plus en plus faible du trafic atteint le modèle.
Une version minimale à partir de zéro
Avant d’utiliser une bibliothèque en production, il est utile de comprendre ses composants. Commencez par les importations :
from sentence_transformers import SentenceTransformer
import numpy as np
Ensuite, intégrez un petit ensemble de FAQ et écrivez une recherche par voisin le plus proche :
model = SentenceTransformer("all-mpnet-base-v2")# Embed your FAQ dataset
faq_questions = ["How do I get a refund?", "Where is my order?", ...]
faq_embeddings = model.encode(faq_questions)def cosine_distance(a, b):
return 1 - np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))def check_cache(query: str, threshold: float = 0.3):
query_embedding = model.encode(query)
distances = [cosine_distance(query_embedding, e) for e in faq_embeddings]
best_idx = np.argmin(distances)
best_distance = distances[best_idx]
if best_distance < threshold:
return faq_answers[best_idx] # Cache hit
return None # Cache miss
Essayez une question de test concernant le nombre de jours nécessaires pour recevoir un remboursement : la recherche renvoie la FAQ enregistrée sur la procédure de remboursement comme entrée la plus proche, avec une distance cosinus d’environ 0,33. Aucune correspondance n’a été trouvée uniquement sur la base des mots communs ; les vecteurs se situent simplement à proximité l’un de l’autre.
L’exécution sur Redis
La version de démonstration ne dispose ni de mécanisme de persistance, ni d’échéance, ni de séparation entre les utilisateurs, et ne propose pas de recherche vectorielle optimisée. Pour tout produit destiné aux clients, il est nécessaire d’utiliser une base de données réelle. Redis, associé au SDK open source RedisVL, fournit ces fonctionnalités prêtes à l’emploi.
from redisvl.extensions.cache.llm import SemanticCache
from redisvl.utils.vectorize import HFTextVectorizer
Choisissez un modèle d’incorporation conçu pour cette tâche, puis créez le cache :
# Load a cache-optimized embedding model
vectorizer = HFTextVectorizer("redis/langcache-embed-v1")# Create the cache
cache = SemanticCache(
name="customer_support_cache",
vectorizer=vectorizer,
redis_client=redis_client,
distance_threshold=0.3
)# Set TTL (time to live) — keeps cache fresh
cache.set_ttl(86400) # 24 hours
Le modèle langcache-embed-v1 est un modèle d’incorporation à poids ouverts affiné pour le cache sémantique, ce qui lui permet généralement de surpasser les incorporateurs polyvalents pour cette tâche.
Trois fonctionnalités de production sont incluses avec l’option Redis :
- TTL. Les entrées expirent automatiquement, ce qui évite que des réponses obsolètes persistent au fur et à mesure que le produit évolue.
- Espace de noms. Les équipes, les utilisateurs ou les locataires peuvent chacun disposer d’un cache isolé au sein d’une même instance Redis.
- Recherche vectorielle optimisée. L’indexation HNSW permet des recherches de voisins les plus proches à faible latence, même à grande échelle.
Quatre indicateurs à surveiller
Créer le cache est la partie facile. Il faut également s’assurer qu’il fonctionne correctement.
Taux de réussite correspond à la part des requêtes qui trouvent une correspondance dans la limite fixée, et c’est ce qui détermine vos économies. Avec un taux de réussite de 30 % pour un système effectuant 10 000 appels LLM par jour, 3 000 appels sont éliminés.
Précision indique quelle fraction des résultats trouvés correspond à la bonne réponse. Un cache qui fournit systématiquement des réponses incorrectes est pire que rien du tout.
Rappel montre quelle fraction des requêtes qui auraient dû trouver une correspondance l’a réellement fait, étant donné l’existence d’une entrée équivalente. Un faible rappel signifie que des économies ne sont pas réalisées.
Amélioration de la latence compare les deux méthodes. Selon les benchmarks, les réponses du cache prennent environ 2 à 5 ms, contre 300 à 500 ms pour un appel typique à GPT-4, soit environ 160 fois plus rapide. Ce que vous ressentez réellement dépend de la fréquence à laquelle vous obtenez des résultats, comme le montre cette formule :
With-Cache Latency = (avg_llm_latency × (1 - hit_rate)) + (avg_cache_latency × hit_rate)
Au niveau de 30 % de hits, on peut s’attendre à une baisse de la latence globale d’environ 25 à 30 %. Lorsque le cache atteint un taux d’utilisation de 60 à 75 %, les gains deviennent significatifs.
Considérer le cache comme un classificateur
Chaque recherche se termine par l’un de quatre résultats :
| Le cache renvoie un hit | Le cache renvoie un miss | |
|---|---|---|
| Un hit aurait dû être obtenu | Vrai positif | Faux négatif |
| Un miss aurait dû être obtenu | Faux positif | Vrai négatif |
Les faux positifs sont les plus dangereux : il s’agit de deux questions qui ressemblent mais ont des significations différentes, ce qui amène le cache à fournir une réponse erronée. Une question concernant les remboursements en cas de changement d’avis et une question sur le processus général de remboursement peuvent se trouver proches l’une de l’autre dans l’espace d’encodage, mais chacune mérite une réponse distincte. C’est dans ce cas que le cache doit faire preuve de la plus grande prudence, car la réponse qu’il fournit semble complètement plausible.
Améliorer la précision
Adapter le seuil. Le seuil de distance est la paramétrisation la plus influente. Une valeur plus basse augmente la précision tout en diminuant le taux de rappel, tandis qu’une valeur plus élevée a l’effet inverse. Essayez différentes valeurs sur un ensemble de test représentatif et choisissez celle qui maximise l’indice F1, qui représente la moyenne harmonique de la précision et du taux de rappel. Cette approche a permis d’augmenter la précision de 79 % à 90 % tout en maintenant un taux de réussite de 25 %.
Réclasser avec un cross-encodeur. Les modèles d’incorporation standards encodent chaque phrase séparément. Un cross-encodeur lit en même temps la requête et les candidats, ce qui permet de détecter des subtilités que les bi-encodeurs manquent. Prenons une phrase concernant une banque qui augmente les taux d’intérêt et une autre concernant une inondation d’un fleuve près de cette banque après de fortes pluies : le mot commun « banque » rapproche leurs embeddings, mais un cross-encodeur perçoit qu’ils ne sont pas liés. Dans les expériences, le réclassage a permis d’atteindre une précision de 94 %. Les cross-encodeurs ne peuvent pas générer de vecteurs exploitables pour la recherche, ils fonctionnent donc comme une deuxième étape par-dessus les candidats top-K récupérés par le modèle d’incorporation.
Demandez à un petit LLM de juger. Pour une précision maximale, faites valider les cas limites par un modèle bon marché comme GPT-4o-mini à l’aide d’une simple demande : ces deux questions sont-elles sémantiquement équivalentes ? Il répond en quelques millisecondes pour un prix inférieur à une fraction de celui d’une consultation complète. Les tests ont montré une précision de 100 %, mais un taux de rappel plus faible, car le juge rejette parfois des correspondances valides. Le fait que cela soit acceptable dépend du cas d’usage. Dans le service client, une réponse en mémoire légèrement erronée peut être pire qu’une nouvelle consultation.
Essayez d’abord la correspondance floue. Avant de calculer tout embedding, effectuez une comparaison basée sur la distance d’édition entre la requête et les entrées stockées. Si elle diffère d’une entrée de deux ou trois caractères transposés, il s’agit presque certainement d’une faute de frappe pour la même question, donc associez-la directement. Cela permet d’éviter complètement l’étape d’embedding, réduisant le temps de réponse à presque zéro. Avec des seuils de similarité élevés, cela a atteint 100 % de précision et 94 % de rappel sur des ensembles de test remplis de fautes de frappe.
Le véritable avantage : le cache au sein des agents
Se contenter de stocker uniquement la réponse finale à une question de l’utilisateur constitue un modèle mental incomplet. Les agents fonctionnent différemment. Un flux typique pour une question de support complexe se présente comme suit :
- Décomposer la requête en 3 à 5 sous-questions.
- Récupérer les documents pertinents dans une base de connaissances.
- Demander à un LLM de synthétiser une réponse.
- Évaluer la qualité de la réponse et itérer si nécessaire.
Chaque sous-question représente une opportunité de mise en cache. Si l’agent a déjà répondu à « Quels sont vos limites de fréquence API ? », il n’a pas besoin de le demander à nouveau lorsque cette sous-question apparaît dans une demande complexe d’un autre utilisateur.
Un agent de recherche avancée basé sur LangGraph a été testé dans trois scénarios :
| Scénario | Taux de réussite | Appels à l’LLM | Latence |
|---|---|---|---|
| Nouvel utilisateur, cache froid | 25% | 8 | environ 20 s |
| Utilisateur régulier, cache chaud | 75% | 4 | environ 13 s |
| Évaluation avant achat | 75% | 6 | environ 18 s |
Dans le troisième scénario, trois des quatre sous-questions provenaient du cache. Le nombre d’appels au modèle est passé de 8 à une fourchette de 4 à 6, et la latence globale a diminué d’environ un tiers. La qualité des réponses est restée identique, avec des réponses personnalisées et précises dans les deux cas.
L’effet s’amplifie. Chaque exécution réussie ajoute du contenu au cache, et les utilisateurs ultérieurs ayant des questions similaires bénéficient de tout ce qui a été stocké précédemment, de sorte que les économies augmentent à mesure que le système traite des centaines ou des milliers de requêtes.