Semantic caching

TL;DR

Semantic caching reaproveita respostas inteiras quando uma query nova é semanticamente similar a uma query passada. Diferente do prompt caching (que reaproveita o KV cache do prefixo dentro de uma mesma chamada à API), semantic caching elimina a chamada antes dela acontecer. A stack básica é simples: embedding da query nova → busca em vector DB → se similaridade > threshold, serve resposta cacheada; se não, chama o modelo e armazena o par (query, resposta). Em chatbots de suporte, onde 80% das perguntas são variações de 50 perguntas frequentes, hit rates de 70-85% são comuns — o que significa que 70-85% das chamadas ao LLM nunca acontecem.

O problema que semantic caching resolve

Prompt caching economiza tokens dentro de uma chamada — mas você ainda faz a chamada. Semantic caching economiza a chamada inteira.

Pense em um chatbot de suporte técnico. Todo dia, chegam perguntas como:

  • “Como reseto minha senha?”
  • “Esqueci minha senha, o que faço?”
  • “Não consigo entrar na conta, como recupero o acesso?”
  • “Como faço para redefinir a senha?”

Semanticamente, essas 4 perguntas são a mesma coisa. Sem caching, você paga por 4 chamadas ao LLM, cada uma lendo o contexto de suporte completo e gerando uma resposta similar. Com semantic caching, você paga por 1 chamada e serve os resultados armazenados para as outras 3.

O ganho escala com a repetitividade do domínio. Em sistemas com alta concentração de perguntas recorrentes, semantic caching é a técnica de maior ROI relativo: zero custo de LLM nos hits, custo mínimo de embedding e lookup no vector DB.

Prompt caching vs semantic caching — distinção crítica

AspectoPrompt cachingSemantic caching
O que é cacheadoKV cache do prefixo do promptResposta final completa
Onde mora o cacheNo provider (Anthropic, OpenAI, Google)Na sua infra (Redis, Qdrant, Pinecone)
O que constitui um hitMesmo prefixo exato, palavra por palavraQuery semanticamente similar (cosine ≥ threshold)
Custo eliminado por hit90% dos tokens do prefixo (ainda paga output)100% da chamada ao LLM
Risco de falso positivoNenhum — mesmo prefixo = mesma respostaSim — queries similares podem precisar de respostas diferentes
Latência com hit~Igual (a chamada ainda acontece)~10-50ms (lookup no vector DB, sem LLM)
TTL padrão5 min (Anthropic) / automático (OpenAI)Configurável — horas a semanas dependendo do domínio

São complementares, não concorrentes: semantic cache evita a chamada; se a chamada acontece (miss), prompt cache barateia os tokens do prefixo. Usar os dois juntos maximiza a economia.

Como funciona — o pipeline de lookup

flowchart TD
    A["Query do usuário\n'Esqueci minha senha'"] --> B["Embedding da query\n(text-embedding-3-small)"]
    B --> C["Vector DB: busca por\nsimilaridade cosine"]
    C --> D{{"Cosine ≥ threshold\n(ex: 0.95)?"}}
    D -- "Sim (hit)" --> E["Retorna resposta cacheada\nLatência: 10-50ms\nCusto LLM: $0"]
    D -- "Não (miss)" --> F["Chama LLM\nGera resposta"]
    F --> G["Armazena (query_embedding, resposta)\nno vector DB com TTL"]
    G --> H["Retorna resposta ao usuário\nLatência: 500-3000ms\nCusto LLM: normal"]

    style E fill:#d4edda,stroke:#28a745
    style F fill:#fff3cd,stroke:#856404

Componentes do stack

1. Embedding model — converte queries em vetores de alta dimensão. Escolha por custo e qualidade:

ModeloCustoDimensõesQualidade
text-embedding-3-small (OpenAI)$0.02/M tokens1.536Boa para textos curtos
text-embedding-3-large (OpenAI)$0.13/M tokens3.072Alta qualidade
embed-v4.0 (Cohere)$0.10/M tokensVariávelEspecializado em retrieval
nomic-embed-text (open source)$0768Boa para self-hosted

Para semantic cache, text-embedding-3-small é o padrão: boa qualidade, custo mínimo (o embedding de uma query de 30 tokens custa ~$0.0000006 — desprezível).

2. Vector DB — armazena pares (embedding, resposta_serializada) com busca por similaridade:

OpçãoMelhor paraLatência de lookup
Redis Stack (FT.SEARCH)Produção, já tem Redis5-20ms
QdrantEscala alta, self-hosted10-30ms
PineconeManaged, sem operação20-50ms
ChromaDBDesenvolvimento, local2-10ms
pgvector (PostgreSQL)Já tem Postgres10-50ms

3. Threshold de similaridade — o parâmetro mais crítico e mais difícil de calibrar:

  • 0.99+: Praticamente apenas queries idênticas → hit rate baixíssimo, zero false positives
  • 0.95-0.98: Variações próximas (“reseto senha” vs “redefinir senha”) → bom equilíbrio
  • 0.90-0.94: Queries relacionadas mas distintas → risco de false positives
  • <0.90: False positives frequentes → respostas erradas para queries distintas

Regra de calibração: comece em 0.95, monitore false positive rate (via feedback de usuário ou amostragem manual), ajuste em incrementos de 0.01.

4. TTL (Time-to-Live) — quando a resposta expira:

Tipo de conteúdoTTL recomendado
FAQ estável (como usar X)7-30 dias
Informação de produto24 horas
Preços e disponibilidade15 minutos ou não cachear
Status de incidente5 minutos ou não cachear
Dados ao vivoNão cachear

Implementação — stack mínimo funcional

Opção 1 — Redis Stack (recomendado para produção)

import redis
import numpy as np
from openai import OpenAI
 
openai_client = OpenAI()
redis_client = redis.Redis(host="localhost", port=6379, decode_responses=False)
 
SIMILARITY_THRESHOLD = 0.95
CACHE_TTL = 604800  # 7 dias em segundos
EMBEDDING_MODEL = "text-embedding-3-small"
 
def embed(text: str) -> list[float]:
    response = openai_client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=text
    )
    return response.data[0].embedding
 
def cache_lookup(query: str) -> str | None:
    """Busca resposta semanticamente similar no cache."""
    query_embedding = embed(query)
    
    # Busca os 3 resultados mais próximos
    results = redis_client.ft("idx:semantic_cache").search(
        query=f"*=>[KNN 3 @embedding $vec AS score]",
        query_params={"vec": np.array(query_embedding, dtype=np.float32).tobytes()},
    )
    
    if not results.docs:
        return None
    
    best = results.docs[0]
    similarity = 1 - float(best.score)  # Redis retorna distância, não similaridade
    
    if similarity >= SIMILARITY_THRESHOLD:
        return best.response  # hit!
    return None  # miss
 
def cache_store(query: str, response: str) -> None:
    """Armazena query + resposta no cache com TTL."""
    query_embedding = embed(query)
    cache_key = f"semantic_cache:{hash(query)}"
    
    redis_client.hset(cache_key, mapping={
        "query": query,
        "response": response,
        "embedding": np.array(query_embedding, dtype=np.float32).tobytes(),
    })
    redis_client.expire(cache_key, CACHE_TTL)
 
def llm_with_semantic_cache(query: str, system_prompt: str) -> tuple[str, bool]:
    """
    Wrapper que tenta semantic cache antes de chamar o LLM.
    Retorna (resposta, foi_cache_hit).
    """
    # Tenta cache
    cached = cache_lookup(query)
    if cached:
        return cached, True
    
    # Miss: chama LLM
    response = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": query}
        ]
    )
    result = response.choices[0].message.content
    
    # Armazena para futuras queries similares
    cache_store(query, result)
    
    return result, False

Opção 2 — GPTCache (open source, zero boilerplate)

from gptcache import cache
from gptcache.adapter import openai
from gptcache.embedding import OpenAI as CacheEmbedding
from gptcache.manager import CacheBase, VectorBase, get_data_manager
from gptcache.similarity_evaluation import SearchDistanceEvaluation
 
# Configurar com Redis como vector store
cache.init(
    embedding_func=CacheEmbedding().to_embeddings,
    data_manager=get_data_manager(
        CacheBase("sqlite"),
        VectorBase("redis", host="localhost", port=6379, password="")
    ),
    similarity_evaluation=SearchDistanceEvaluation(),
)
 
# Drop-in replacement para openai.ChatCompletion.create
response = openai.ChatCompletion.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Como reseto minha senha?"}]
)
# Internamente: verifica cache antes de chamar a API

Opção 3 — Qdrant + custom (self-hosted, alta escala)

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
import uuid
 
qdrant = QdrantClient(host="localhost", port=6333)
 
# Criar coleção (uma vez)
qdrant.create_collection(
    collection_name="semantic_cache",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
 
def cache_lookup_qdrant(query: str, threshold: float = 0.95) -> str | None:
    query_vec = embed(query)
    results = qdrant.search(
        collection_name="semantic_cache",
        query_vector=query_vec,
        limit=1,
        score_threshold=threshold,  # Qdrant filtra automaticamente pelo threshold
    )
    if results:
        return results[0].payload["response"]
    return None
 
def cache_store_qdrant(query: str, response: str) -> None:
    qdrant.upsert(
        collection_name="semantic_cache",
        points=[PointStruct(
            id=str(uuid.uuid4()),
            vector=embed(query),
            payload={"query": query, "response": response}
        )]
    )

O bug clássico — threshold alto demais servindo a resposta errada

O código da Opção 1 funciona, mas esconde um bug que só aparece em produção: se o SIMILARITY_THRESHOLD é calibrado sem dados reais, o cache serve respostas erradas com confiança total — porque, do ponto de vista do sistema, foi um hit.

# BUG: threshold calibrado "no olho", sem validar com queries reais
SIMILARITY_THRESHOLD = 0.85  # parece razoável, mas não é
 
# Cache já populado com esta entrada:
cache_store(
    "como cancelar minha assinatura?",
    "Para cancelar sua assinatura, acesse Configurações > Assinatura > Cancelar. "
    "O cancelamento é imediato e não há reembolso proporcional."
)
 
# Query nova do usuário:
resultado, foi_hit = llm_with_semantic_cache(
    "como suspender minha assinatura?",
    system_prompt="Você é o assistente de suporte ao cliente."
)
# foi_hit == True
# resultado == "Para cancelar sua assinatura, acesse Configurações > ..."

O problema: "cancelar assinatura" e "suspender assinatura" têm cosine similarity em torno de 0.87-0.89 — próximas o bastante para passar de um threshold de 0.85, mas semanticamente distintas. Cancelar é uma ação definitiva e sem reembolso; suspender costuma pausar a cobrança mantendo a conta ativa. O usuário que queria pausar recebe instruções de cancelamento definitivo — um false positive caro, porque a resposta parece correta (é fluente, é sobre o tema certo) e ninguém a questiona até o usuário reclamar ou cancelar por engano.

O bug não está na lógica do cache_lookup — está em tratar o threshold como um número fixo e universal, em vez de um parâmetro que precisa ser validado contra pares de queries que parecem similares mas exigem respostas diferentes (intents conflitantes, não só paráfrases).

Correção — subir o threshold e adicionar uma guarda de intent para pares conhecidos como confundíveis:

SIMILARITY_THRESHOLD = 0.96  # recalibrado após medir false positives reais
 
# Pares de intents que soam parecidos mas exigem respostas diferentes —
# levantados via amostragem manual de cache hits (ver seção Métricas obrigatórias)
INTENTS_CONFLITANTES = [
    {"cancelar", "cancelamento"},
    {"suspender", "suspensão", "pausar", "pausa"},
]
 
def _mesma_intencao(query_nova: str, query_cacheada: str) -> bool:
    """Bloqueia hit se as queries caem em grupos de intent diferentes."""
    palavras_novas = set(query_nova.lower().split())
    palavras_cache = set(query_cacheada.lower().split())
 
    grupo_novo = next((g for g in INTENTS_CONFLITANTES if g & palavras_novas), None)
    grupo_cache = next((g for g in INTENTS_CONFLITANTES if g & palavras_cache), None)
 
    # Se ambas caem em grupos de intent conhecidos e são grupos diferentes, bloqueia
    if grupo_novo and grupo_cache and grupo_novo != grupo_cache:
        return False
    return True
 
def cache_lookup_seguro(query: str) -> str | None:
    """Versão corrigida: threshold mais alto + guarda de intent."""
    query_embedding = embed(query)
 
    results = redis_client.ft("idx:semantic_cache").search(
        query=f"*=>[KNN 3 @embedding $vec AS score]",
        query_params={"vec": np.array(query_embedding, dtype=np.float32).tobytes()},
    )
 
    if not results.docs:
        return None
 
    best = results.docs[0]
    similarity = 1 - float(best.score)
 
    if similarity < SIMILARITY_THRESHOLD:
        return None  # miss — abaixo do threshold recalibrado
 
    if not _mesma_intencao(query, best.query):
        return None  # miss forçado — similaridade alta, mas intent conflitante
 
    return best.response

A guarda de intent não substitui a calibração do threshold — é um cinto de segurança adicional para os pares de queries que a amostragem manual (ver Métricas obrigatórias) já revelou como confundíveis. Threshold alto reduz false positives em geral; a guarda de intent trata os casos específicos que continuam colando mesmo com threshold alto, porque a proximidade semântica bruta (cosine) não captura a diferença entre “parar de vez” e “pausar temporariamente”.

Casos de uso de alto ROI

Caso de usoPor que funcionaHit rate esperado
Chatbot de suporte técnico80% das perguntas são variações de 50 FAQs70-85%
Documentation Q&AMesmas dúvidas recorrentes (“como configurar X?“)60-80%
Análise de stack tracesErros idênticos aparecem milhares de vezes80-95%
Classificação de intentFrases similares mapeiam para mesma intent75-90%
Chatbot de onboardingNovos usuários fazem as mesmas perguntas65-80%
Suporte de e-commercePerguntas de status, devolução, pagamento70-85%

Caso real — chatbot de suporte B2B:

Antes do semantic caching:
  - Chamadas ao LLM: 50.000/mês
  - Custo: $4.200/mês (GPT-4o)
  
Após semantic caching (threshold 0.95, TTL 7 dias):
  - Hit rate: 78%
  - Chamadas evitadas: 39.000/mês
  - Chamadas ao LLM: 11.000/mês
  - Custo: $924/mês
  - Custo de infraestrutura (Redis + embeddings): ~$35/mês
  - Economia líquida: $3.241/mês (77%)
  - Payback da implementação: < 1 semana

Quando NÃO usar

CenárioPor que não cachearAlternativa
Geração de código com variações sutisQueries similares podem precisar de código diferenteSem cache, prompt caching
Conteúdo time-sensitive (preços, status)Resposta de ontem pode estar errada hojeTTL muito curto ou sem cache
Personalização por usuárioResposta depende do perfil do usuárioCache com chave (user_id + query_embedding)
Compliance com audit trailCada resposta precisa ser rastreável individualmenteSem cache
Domínios com alta entropiaCada query é genuinamente única (criação artística, análise de dados específicos)Sem cache

Métricas obrigatórias

MétricaComo medirAlvo saudável
Hit ratehits / (hits + misses) por janela de tempo≥50% para compensar o overhead
False positive ratequeries cujo cache hit recebeu feedback negativo<2%
Latência de lookuptempo do embedding + busca no vector DB<50ms (p95)
Custo de embeddingtokens de embedding / tokens de LLM economizados<5% da economia
Cache coverage% das queries que têm pelo menos 1 similar no cacheCresce com o tempo
Cache efficiencyeconomia real / custo de infraestrutura>10x para justificar

Armadilhas comuns

Threshold baixo demais — false positives

Similaridade de 0.90 parece conservadora, mas queries como “como instalar X?” e “como desinstalar X?” podem ter cosine similarity de 0.91. Servir a resposta de instalação para uma query de desinstalação é um false positive que degrada a confiança do usuário. Comece em 0.95 e só abaixe depois de validar com dados reais.

TTL infinito ou muito longo em conteúdo que muda

Uma resposta sobre preços de 2024 servida em 2026 é um erro de negócio. Categorize seu conteúdo por volatilidade e configure TTLs distintos. Conteúdo de FAQ estável pode ter 30 dias; informação de produto, 24h; qualquer coisa com dados ao vivo, não cachear.

Não medir false positives sistematicamente

Sem feedback loop, o cache pode estar servindo respostas erradas para 5% das queries sem você saber. Implemente pelo menos amostragem manual de 100 cache hits por semana para validar a qualidade. Melhor ainda: integre feedback explícito do usuário (👍/👎) e monitore por tier de similaridade.

Embedding diferente em dev e prod

Se você usar text-embedding-3-small em dev e text-embedding-3-large em prod, o cache fica incompatível — vetores de dimensões diferentes não são comparáveis. Fixe o modelo de embedding como parte do SLA do cache e trate mudanças como migrations (invalidar todo o cache ao trocar o modelo).

Estado da arte — junho 2026

Semantic caching por provider: Em 2026, alguns providers passaram a oferecer semantic caching como feature gerenciada — você define o threshold e o TTL, e a plataforma cuida do vector DB. AWS Bedrock, Azure AI e Google Cloud AI têm variantes em GA ou preview. O tradeoff: mais simples de operar, menos controle e custo por hit mais alto que self-hosted.

Adaptive thresholds: Sistemas avançados ajustam o threshold dinamicamente por domínio e por hora do dia. Em períodos de alta carga, um threshold ligeiramente menor (0.93) aumenta o hit rate e reduz custo; em períodos de baixa carga, threshold mais alto (0.97) garante qualidade sem impactar custo significativamente.

Cache warming: Em vez de esperar o cache encher organicamente, times de produto pre-populam com as top-N queries históricas. Isso garante hit rate alto desde o primeiro dia de produção — em vez de crescer lentamente na semana de lançamento.

Federated semantic cache: Em sistemas multi-tenant, caches compartilhados entre clientes diferentes aumentam o hit rate global mas levantam questões de privacidade. Padrão emergente: cache por domínio de conteúdo (público) com isolamento por usuário para respostas personalizadas.

Reranking com freshness: Além de similaridade pura, sistemas modernos consideram freshness — respostas mais recentes recebem boost no ranking, respostas antigas são depreciadas gradualmente antes do TTL expirar. Isso reduz o risco de respostas desatualizadas sem invalidação imediata.

Casos práticos

Caso 1 — Sistema de análise de logs: Uma plataforma de observabilidade recebia 500k queries/mês sobre stack traces. 90% dos stack traces eram idênticos ou quase idênticos (mesmo bug, diferentes instâncias). Após implementar semantic cache com threshold 0.98 (bem alto, porque stack traces são determinísticos): hit rate de 92%, custo de LLM caiu de 680/mês.

Caso 2 — Chatbot de e-commerce: Um chatbot de suporte de e-commerce tinha 200k interações/mês. Análise mostrou que 80% das perguntas eram sobre: status de pedido, política de devolução, prazo de entrega, e formas de pagamento. Após semantic caching com TTL de 24h para informações de política e 15min para status: hit rate de 73%, custo mensal de 3.2k.

Caso 3 — Threshold calibrado errado: Um time implementou semantic cache com threshold 0.90 “para ser conservador”. Na prática, queries como “cancelar assinatura” e “suspender assinatura” tinham similaridade 0.91 — e recebiam a mesma resposta, que era incorreta para um dos casos. Após aumentar para 0.96 e separar essas intenções no corpus de cache: zero false positives no domínio de cancelamento, hit rate caiu de 78% para 65% (aceitável).

Caso 4 — Cache warming antes do lançamento: Um time coletou as top-200 perguntas dos últimos 6 meses do sistema anterior e pre-populou o semantic cache antes do lançamento da nova plataforma. Hit rate no primeiro dia: 68% (em vez dos 5-10% típicos de um cache vazio). Custo da semana de lançamento foi equivalente ao de uma semana normal de operação madura.

Checklist

  • Medir repetitividade do domínio (% de queries com similar no histórico) antes de implementar
  • Escolher vector DB adequado ao volume esperado (ChromaDB para dev, Redis/Qdrant para produção)
  • Configurar threshold inicial em 0.95 e calibrar com dados reais
  • Definir TTLs por categoria de conteúdo (estável / volátil / tempo-real)
  • Implementar logging de hit/miss por threshold range
  • Criar processo de validação de false positives (sampling ou feedback explícito)
  • Fixar modelo de embedding como parte do contrato de infraestrutura
  • Considerar cache warming com top-N queries históricas antes do lançamento
  • Monitorar custo de embedding vs economia de LLM (alvo: razão >10x)

O que vem a seguir

Semantic caching elimina chamadas repetidas para queries similares. Mas há outra otimização possível para workloads de alto volume que não são repetitivos: processar muitas queries diferentes em batch, pagando preço de API em volume reduzido. 12 - Batch API — economia em volume aborda o Batch API da Anthropic e equivalentes — que oferecem 50% de desconto em exchange por latência maior.

Como explicar em inglês

Semantic caching é o termo padrão; você também verá response caching, LLM caching, e vector cache nos mesmos contextos. O threshold de similaridade é chamado de similarity threshold ou cosine threshold. False positives (respostas erradas por similaridade alta demais) são um conceito que qualquer audiência técnica entende sem tradução.

PortuguêsInglêsContexto de uso
Cache semânticoSemantic cacheO sistema completo de cache por similaridade
Busca por similaridadeSimilarity searchLookup no vector DB por cosine similarity
Threshold de similaridadeSimilarity thresholdO parâmetro que controla quando um hit é aceito
Falso positivoFalse positiveCache hit com resposta semanticamente diferente
TTL (tempo de expiração)TTL (Time-to-Live)Quando a entrada do cache expira
Hit rateHit rate / Cache hit ratio% de queries que encontram resposta no cache
Modelo de embeddingEmbedding modelModelo que converte texto em vetor
Pre-popular o cacheCache warming / Pre-warmingPopulatom o cache com queries históricas antes do lançamento
Threshold adaptativoAdaptive thresholdThreshold que varia por domínio ou carga
Cache federadoFederated cacheCache compartilhado entre múltiplos clientes/tenants

Veja: Building a Semantic Cache for LLM Applications

Canal: Fireship / AI Engineer World’s Fair | Duração: ~22min | Idioma: EN

Tutorial técnico que demonstra a implementação completa de semantic cache com Redis Stack e OpenAI embeddings. Cobre a calibração de threshold com dados reais, métricas de hit rate, e os edge cases de false positives. Inclui demo ao vivo de chatbot de suporte com e sem semantic cache — a diferença de custo é demonstrada em tempo real.

🎬 Assistir no YouTube

Veja também

Fontes

  • ZillizGPTCache: Semantic Cache for LLM Applications (github.com/zilliztech/GPTCache, 2023). Framework open-source que popularizou semantic caching para LLMs — inclui benchmarks de hit rate e false positive rate para diferentes thresholds.
  • RedisVector Similarity Search in Redis (redis.io/docs, 2026). Documentação técnica do FT.SEARCH com KNN — base da implementação de semantic cache em Redis Stack.
  • Eugene YanPatterns for Building LLM-based Systems & Products (eugeneyan.com, 2024). Análise dos padrões mais comuns em sistemas LLM de produção — inclui seção extensa sobre caching e quando cada estratégia se aplica.
  • QdrantSemantic Cache for LLM Applications (qdrant.tech/blog, 2025). Implementação de semantic cache com Qdrant — inclui dados de latência e comparação com Redis e Pinecone.
  • CohereSemantic Caching: How to Reduce LLM Costs (cohere.com/blog, 2025). Análise quantitativa do impacto de semantic caching em diferentes tipos de aplicação, com exemplos reais de hit rates por domínio.
  • WeaviateLLM Caching Strategies for Production (weaviate.io/blog, 2026). Comparação de estratégias de caching (prompt, semantic, response) com dados de custo-efetividade por cenário de uso.