Semantic Caching avec pgvector : différencier requêtes invariantes et volatiles pour réduire vos coûts LLM
Cet article s'appuie sur la série Semantic Caching in PostgreSQL de Muhammad Aqeel (pgEdge), publiée le 30 avril 2026.
Un cache sémantique bien configuré peut réduire de 60 à 80 % vos appels à l'API de votre fournisseur LLM — et donc vos coûts dans les mêmes proportions. Mais ce gain ne se matérialise que si vous cachez les bonnes requêtes. Mettre en cache sans discernement, c'est s'exposer à retourner des réponses autrefois exactes mais aujourd'hui obsolètes, et ce de manière silencieuse, sans le moindre avertissement.
Dans cet article, on explore le pattern Semantic Caching appliqué à PostgreSQL via pgvector, et surtout la distinction fondamentale entre requêtes invariantes et requêtes volatiles.
Principe du Semantic Caching avec pgvector
Le semantic caching repose sur une idée simple : plutôt que d'envoyer chaque question à votre modèle LLM, on stocke en base l'embedding vectoriel de la question ainsi que la réponse associée. Lors d'une nouvelle requête, on calcule sa similarité cosinus avec les embeddings existants via pgvector. Si un résultat dépasse un seuil de similarité défini, on retourne la réponse en cache — sans aucun appel au modèle.
-- Recherche sémantique dans le cache
SELECT response
FROM semantic_cache
ORDER BY embedding <=> $1 -- opérateur cosinus pgvector
LIMIT 1;
Cela signifie que "Qu'est-ce qu'une transaction ACID ?" et "Peux-tu m'expliquer les propriétés ACID ?" peuvent pointer vers la même entrée en cache, même si les formulations diffèrent. C'est là toute la puissance de l'approche sémantique par rapport à un cache clé-valeur classique.
Les deux catégories de requêtes
🟢 Requêtes invariantes dans le temps
Certaines questions ont des réponses qui ne dépendent pas du moment où elles sont posées :
- "Quelle est la différence entre
INNER JOINetLEFT JOIN?" - "Qu'est-ce que l'idempotence ?"
- "Comment fonctionne le protocole TCP/IP ?"
Ces requêtes sont des candidates idéales pour le semantic caching. Un seul appel LLM suffit à alimenter l'entrée ; toutes les variantes paraphrasées bénéficient ensuite du cache gratuitement.
🔴 Requêtes volatiles
D'autres questions ont des réponses intrinsèquement liées au moment où elles sont posées :
- "Quel est le cours actuel de l'action Apple ?"
- "Quels sont les incidents en cours sur notre infrastructure ?"
- "Combien d'utilisateurs sont connectés en ce moment ?"
Retourner une réponse mise en cache pour ce type de requête, c'est retourner une information potentiellement fausse — et le faire avec la même confiance apparente qu'une réponse fraîche. C'est le scénario le plus dangereux : non pas une erreur visible, mais une désinformation silencieuse.
Où gérer cette distinction dans votre stack ?
C'est la question architecturale centrale. La responsabilité de classifier les requêtes ne doit pas reposer sur le cache lui-même — le cache n'a pas le contexte métier pour décider. Elle appartient à la couche applicative, avant même que la requête n'atteigne le système de cache.
Pattern recommandé en PHP/Symfony
Voici une approche pragmatique avec un service dédié :
// src/Service/LlmQueryRouter.php
class LlmQueryRouter
{
public function __construct(
private SemanticCacheService $cache,
private LlmClient $llm,
private VolatileQueryDetector $detector,
) {}
public function ask(string $question): string
{
// 1. Classification de la requête
if ($this->detector->isVolatile($question)) {
// Bypass du cache : appel LLM direct
return $this->llm->complete($question);
}
// 2. Recherche dans le cache sémantique
$cached = $this->cache->findSimilar($question, threshold: 0.92);
if ($cached !== null) {
return $cached->response;
}
// 3. Cache miss : appel LLM + mise en cache
$response = $this->llm->complete($question);
$this->cache->store($question, $response);
return $response;
}
}
Le VolatileQueryDetector peut implémenter différentes stratégies selon votre contexte :
- Règles lexicales : présence de mots-clés comme "maintenant", "actuellement", "aujourd'hui", "en cours"
- Classification LLM légère : un appel à un modèle petit et peu coûteux pour classifier la requête avant de l'envoyer au modèle principal
- Liste d'exclusion par domaine : certains domaines métier (cotations, stocks, incidents) sont systématiquement marqués volatils
Gestion des tags et de l'éviction
pg_semantic_cache supporte les cache tags, ce qui permet d'invalider des groupes d'entrées liées à un contexte précis :
-- Invalider toutes les entrées liées aux données produits
SELECT evict_by_tag('product-catalog');
Un cas d'usage typique : quand votre catalogue produit est mis à jour en base, vous déclenchez l'éviction de toutes les entrées taguées product-catalog. Les prochaines requêtes similaires iront chercher une réponse fraîche auprès du LLM.
Ce que cela change concrètement
| Scénario | Sans distinction | Avec distinction |
|---|---|---|
| "Qu'est-ce qu'un index B-tree ?" | Cache ✅ | Cache ✅ |
| "Quels bugs sont ouverts ce matin ?" | Cache ❌ (réponse périmée) | LLM direct ✅ |
| "Prix de l'hébergement Hetzner" | Cache ❌ (tarif obsolète) | LLM direct ✅ |
| "Explique le pattern Repository" | Cache ✅ | Cache ✅ |
La colonne sans distinction ne plante pas — elle répond. C'est précisément le problème.
Conclusion
Le semantic caching est un levier de performance et d'économie réel pour toute application intégrant des LLM. Mais son efficacité dépend d'une règle non négociable : ne jamais déléguer la classification des requêtes au cache lui-même.
La distinction entre requêtes invariantes et requêtes volatiles doit être prise en charge explicitement dans votre couche applicative, avec une logique de routage claire. Un cache qui retourne silencieusement des informations périmées est plus dangereux qu'un cache absent.
Dans un contexte PHP/Symfony, un service de routage dédié — quelques dizaines de lignes — suffit à sécuriser ce comportement. Le reste, pgvector et pg_semantic_cache s'en chargent.
Source : Volatile Queries and Semantic Caching — Muhammad Aqeel, pgEdge (avril 2026)