← Voltar para o Blog

Cache semântico: cortar custo de IA sem perder qualidade

Em base de conhecimento e atendimento, boa parte das perguntas se repete — com palavras diferentes. Guardar a resposta anterior corta custo e latência, se você souber quando não usar.

Equipe EasyOps Cloud · · 7 min de leitura

Toda aplicação de IA com usuários reais tem uma característica que passa despercebida: as perguntas se repetem muito. Em atendimento e base de conhecimento, uma fatia grande do volume são variações da mesma dúvida.

"Qual o prazo de entrega?", "quanto tempo demora para chegar?", "em quantos dias recebo?" — três textos diferentes, uma resposta.

Gerar essa resposta três vezes custa três vezes, e demora três vezes. Cache resolve, e o mecanismo é mais simples do que o nome sugere.

Dois níveis, de dificuldade bem diferente

Cache exato guarda pela pergunta literal, normalizada. É trivial de implementar, não tem risco de resposta errada, e pega bem menos do que se imagina — porque as pessoas escrevem de formas diferentes.

Cache semântico guarda pelo vetor da pergunta e recupera quando uma nova é suficientemente parecida. Pega muito mais e introduz um risco real: devolver a resposta de uma pergunta que só parecia igual.

Vale implementar os dois, nessa ordem. O exato é gratuito em risco e já entrega alguma coisa; o semântico multiplica o alcance e exige cuidado.

O exato, que vem primeiro

import hashlib, json, redis

r = redis.Redis()

def chave(pergunta, contexto):
    texto = " ".join(pergunta.lower().split())
    base = f"{texto}|{contexto.get('perfil','')}|{contexto.get('versao_base','')}"
    return "ia:exato:" + hashlib.sha256(base.encode()).hexdigest()

def buscar(pergunta, contexto):
    v = r.get(chave(pergunta, contexto))
    return json.loads(v) if v else None

def guardar(pergunta, contexto, resposta, ttl=3600):
    r.setex(chave(pergunta, contexto), ttl, json.dumps(resposta))

Dois detalhes que evitam bug. A normalização precisa colapsar espaço e caixa, mas não deve remover pontuação sem pensar: "posso cancelar?" e "posso cancelar" são a mesma coisa, enquanto "não posso cancelar" é outra.

E a chave precisa incluir tudo que altera a resposta — perfil do usuário, idioma, versão da base de conhecimento. Cache que ignora o perfil entrega a resposta de um cliente para outro, que é o pior defeito possível.

O Redis é a escolha natural aqui, com a configuração de memória e política de despejo do artigo sobre Redis em produção — e allkeys-lru é a política certa, porque toda entrada é recalculável.

O semântico, onde mora o ganho

A ideia: gerar o vetor da pergunta, procurar a mais próxima já respondida, e reaproveitar se a distância for pequena o bastante.

def buscar_semantico(pergunta, contexto, limiar=0.93):
    v = modelo.embed(pergunta)
    candidatos = indice.buscar(v, k=1, filtro={
        "perfil": contexto["perfil"],
        "versao_base": contexto["versao_base"],
    })
    if candidatos and candidatos[0].similaridade >= limiar:
        return candidatos[0].resposta, candidatos[0].similaridade
    return None, 0

O filtro por perfil e versão é obrigatório, pelo mesmo motivo do cache exato. Não basta a similaridade alta se o contexto for outro.

Repare que a busca de vetor tem custo — a geração do embedding mais a consulta ao índice. Ela precisa ser bem mais barata que a chamada ao modelo, senão o cache não compensa. Com embedding local e índice em memória, essa conta fecha com folga.

O limiar é a decisão que importa

Escolher o número que separa "é a mesma pergunta" de "é outra" define o comportamento inteiro.

LimiarEfeito
0,98Quase só reformulação trivial. Seguro, pega pouco
0,93 a 0,95Bom equilíbrio para atendimento
0,90Pega bastante, começa a errar
Abaixo de 0,88Respostas trocadas com frequência

Esses valores dependem do modelo de embedding e do domínio. O jeito de calibrar é empírico e leva pouco tempo: pegue pares de perguntas reais, marque quais deveriam compartilhar resposta, e veja qual limiar separa os dois grupos.

Vale conhecer o caso que mais engana: perguntas com negação. "Posso cancelar depois de 30 dias?" e "não posso cancelar depois de 30 dias?" têm similaridade altíssima e respostas opostas. Se o seu domínio tem muito disso, o limiar precisa ser mais alto, ou a negação precisa ser tratada separadamente.

O que nunca deve ser cacheado

Esta lista evita a maior parte dos incidentes:

  • Resposta que depende de dado do cliente. Status de pedido, saldo, histórico. Muda por pessoa e por minuto.
  • Qualquer coisa com valor ou prazo específico daquele caso.
  • Conversa com contexto acumulado. A mesma pergunta na quinta mensagem significa outra coisa que na primeira.
  • Resposta que o próprio modelo marcou como incerta.
  • Interação que terminou com o cliente insatisfeito. Cachear resposta ruim é multiplicá-la.

A separação prática: cacheie o que vem de documentação — política, prazo padrão, como fazer. Não cacheie o que vem de consulta ao sistema. Se a resposta envolveu chamada de ferramenta, provavelmente não deve entrar no cache.

Invalidação, que é onde se erra

Cache guarda respostas construídas a partir da sua base. Quando a base muda, as respostas ficam erradas — e continuam sendo servidas com convicção.

A abordagem que funciona é versionar a base e incluir a versão na chave:

VERSAO_BASE = "2026-08-10-r3"    # muda a cada reindexação

Assim, uma atualização invalida tudo automaticamente, sem precisar rastrear quais respostas dependiam de qual documento. O custo é perder o cache inteiro a cada mudança — aceitável, já que ele se reconstrói em horas.

Para invalidação mais fina, registre quais documentos foram usados em cada resposta e remova apenas as afetadas. Vale o trabalho quando a base muda com frequência.

E um TTL sempre, mesmo com versionamento. É a rede de segurança contra o caso em que algo mudou e ninguém atualizou a versão.

Medir o ganho de verdade

registrar({
  "cache": "exato" | "semantico" | "miss",
  "similaridade": s,
  "latencia_ms": ms,
  "tokens_economizados": t,
})

Três números respondem se está funcionando: taxa de acerto por tipo, latência comparada, e tokens economizados.

E um quarto, que é o mais importante e o mais esquecido: a taxa de insatisfação nas respostas vindas do cache. Se ela for maior que a das respostas geradas, o limiar está baixo demais, e você está economizando com prejuízo de qualidade.

Vale também amostrar manualmente algumas dezenas de acertos semânticos por semana, conferindo se a resposta reaproveitada de fato servia. É a mesma disciplina de amostragem do artigo sobre extração de documento.

Quanto isso rende

Depende inteiramente da repetição do seu domínio. Atendimento sobre um catálogo estável tem repetição alta; assistente que responde sobre dado do cliente tem quase nenhuma.

O jeito de estimar antes de implementar é direto: pegue mil perguntas do histórico, gere os vetores, e conte quantas têm outra pergunta anterior acima do limiar. Esse número é o teto do seu ganho, e ele aparece em uma tarde de trabalho.

Se der abaixo de 15%, provavelmente não compensa a complexidade — e o esforço rende mais em reduzir o tamanho do prompt, que é a outra alavanca de custo discutida no artigo sobre API ou modelo próprio.

Se der acima de 40%, o cache é a otimização de melhor retorno disponível, e vale implementar antes de qualquer outra coisa.

Aquecer o cache em vez de esperar

Uma técnica que antecipa boa parte do ganho: em vez de esperar o cache se encher organicamente, pré-popule com as perguntas que você já sabe que virão.

O histórico de atendimento tem essa lista pronta. As cinquenta perguntas mais frequentes, processadas uma vez fora do horário de pico, cobrem uma fatia desproporcional do volume desde o primeiro minuto.

for pergunta in perguntas_frequentes:
    if not buscar_semantico(pergunta, contexto)[0]:
        resposta = gerar(pergunta, contexto)
        guardar_semantico(pergunta, contexto, resposta)

Vale rodar isso depois de cada reindexação da base, junto com a mudança de versão que invalida o cache anterior. Assim a janela de cache frio — quando o custo e a latência sobem — praticamente desaparece.

Um efeito colateral valioso: gerar essas respostas em lote, fora de produção, permite revisá-las antes de entrarem em uso. As perguntas mais frequentes são justamente as que mais vale conferir manualmente, e o cache garante que a versão revisada é a que será servida.

Isso transforma o cache de otimização de custo em mecanismo de qualidade — as respostas mais vistas passam por olho humano uma vez, e valem por milhares de interações.

Onde ele se encaixa na arquitetura

Vale posicionar o cache em relação às outras camadas, porque a ordem importa e é fácil errar.

A sequência que funciona: cache exato primeiro, cache semântico depois, e só então recuperação e geração. Cada camada é mais cara que a anterior, e a primeira que responder encerra o caminho.

Um detalhe que costuma passar: o cache deve ficar antes da recuperação, não depois. Guardar o resultado da busca vetorial e ainda assim chamar o modelo economiza pouco — a geração é a parte cara. O ganho está em pular as duas.

E vale registrar de qual camada veio cada resposta, junto com a instrumentação que já existe por razões de custo. Sem isso, você não consegue distinguir uma queda de latência causada pelo cache de uma causada por menos tráfego — e as duas exigem leituras opostas.

O efeito somado costuma surpreender: em base de conhecimento madura, com cache aquecido e limiar bem calibrado, é comum que a maioria das interações não chegue ao modelo. O sistema fica mais rápido, mais barato e mais previsível ao mesmo tempo — combinação rara em otimização.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta