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, 0O 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.
| Limiar | Efeito |
|---|---|
| 0,98 | Quase só reformulação trivial. Seguro, pega pouco |
| 0,93 a 0,95 | Bom equilíbrio para atendimento |
| 0,90 | Pega bastante, começa a errar |
| Abaixo de 0,88 | Respostas 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çãoAssim, 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.