Um usuário relatou que nosso plugin de tradução estava retornando o mesmo resultado em cache para cada requisição, independentemente da entrada. Investigamos e descobrimos algo pior: 95% de todos os hits de semantic cache em nossa plataforma eram falsos positivos. 199 requisições de tradução diferentes, 198 corpos de requisição únicos, uma resposta em cache servida para todos eles.
Se você se preocupa com o estado de agentes de longa duração e o tratamento de requisições em produção, este post combina bem com Por que seu agente de AI continua perdendo a memória, o guia de chatbot com uma única chave e o guia de rate limiting de API de AI.
O Relatório de Bug
O relatório era simples: "Desativei o semantic cache, mas cada tradução retorna o mesmo resultado."
Três IDs de requisição, três segmentos de tradução diferentes, respostas em cache idênticas. Os corpos das requisições variavam de 1.564 a 8.676 bytes. O ID da resposta em cache era o mesmo em todos eles: chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF.
Primeira suspeita: as configurações de cache do usuário não estavam sendo aplicadas. Isso acabou sendo um bug de sincronização de data-source separado (o painel de administração escrevia em uma tabela, o API gateway lia de outra). Mas corrigir isso resolveu apenas metade do problema. Mesmo com o cache ativado e funcionando corretamente, o semantic cache estava correspondendo a requisições que nunca deveriam coincidir.
Os Dados de Produção
Extraímos 24 horas de dados de hits de cache do ClickHouse. Os números eram ruins.
| Modelo | Total de Requisições | Hits de Cache | Requisições Únicas | Respostas Únicas | Taxa de Hit |
|---|---|---|---|---|---|
| gpt-4.1-nano | 200 | 199 | 198 | 1 | 99.5% |
| glm-4.6-thinking | 100 | 38 | 13 | 1 | 38% |
| gpt-5-nano | 31 | 29 | 28 | 2 | 93.5% |
| gpt-oss-120b | 18 | 17 | 17 | 1 | 94.4% |
| qwen3-vl-flash | 17 | 16 | 16 | 1 | 94.1% |
198 requisições de tradução únicas, todas retornando a mesma resposta única em cache. Isso não é um cache. É uma função quebrada que retorna uma constante.
Cada modelo afetado compartilhava duas características: todas as requisições vinham de um único usuário e todas usavam um template de system prompt fixo com conteúdo de usuário variável.
Por que Embeddings falham em entradas estruturadas
O plugin de tradução envia requisições como esta:
System: "Act as a translation API. Output a single raw JSON object only.
Input: {"targetLanguage":"<lang>","title":"...","segments":[...]}"
User: {"targetLanguage":"zh","title":"Product Page",
"description":"Translate product descriptions",
"tone":"formal",
"segments":[{"text":"actual varying content here"}]}
O system prompt é idêntico em todas as requisições. A mensagem do usuário é um objeto JSON onde targetLanguage, title, description e tone são fixos. Apenas segments[].text muda.
Quando nosso semantic cache extrai texto para embedding, ele concatena o system prompt e a mensagem do usuário. O template fixo representa cerca de 80% do texto. O modelo de embedding (all-mpnet-base-v2, 768 dimensões) comprime isso em um vetor onde a estrutura do template domina. O conteúdo real da tradução mal move o ponteiro.
Resultado: a similaridade de cosseno entre "traduzir 'Hello world'" e "traduzir 'The quarterly financial report shows a 15% increase in revenue'" excede 0.95. Nosso threshold é 0.95. Cada requisição de tradução corresponde à primeira entrada em cache.
Vasculhando os logs, encontramos três maneiras pelas quais isso falha:
O plugin de tradução é o pior caso. Chaves e valores JSON fixos abafam os segmentos de tradução reais. gpt-4.1-nano e gpt-5-nano foram atingidos por isso.
Um assistente de sumarização de contexto teve uma variação do mesmo problema. Seu system prompt era tão longo que o conteúdo do usuário (variando de 5KB a 47KB) mal era registrado no embedding. Foi assim que o glm-4.6-thinking acabou retornando o mesmo resumo para cada conversa.
O terceiro padrão foi mais sutil. Para gpt-oss-120b e qwen3-vl-flash, os primeiros 500 caracteres de cada requisição eram idênticos byte a byte. O conteúdo variável vinha depois, mas o embedding já era dominado pelo prefixo compartilhado.
O que a pesquisa diz
Este não é um problema novo. Artigos recentes o quantificaram.
O projeto vCache da UC Berkeley descobriu que hits de cache corretos e incorretos têm "distribuições de similaridade altamente sobrepostas". O threshold ideal varia de 0.71 a 1.0 entre diferentes entradas em cache. Nenhum número único funciona. A correção deles: aprender um threshold separado por entrada de cache, o que reduziu as taxas de erro em 6x enquanto dobrou as taxas de hit. (vCache, 2025)
Fica pior quando você mistura tipos de query. Um estudo de cache ciente de categoria mostrou que um threshold de 0.80 produz 15% de correspondências falsas em queries de código (sort_ascending vs sort_descending), enquanto o mesmo threshold perde paráfrases válidas em queries conversacionais. Um threshold, dois modos de falha. (Category-Aware Semantic Caching, 2025)
Bancos também enfrentam isso. Um estudo de caso da InfoQ documentou um sistema RAG onde "Posso pular o pagamento do meu empréstimo este mês" coincidiu com "O que acontece se eu perder um pagamento de empréstimo" com 88.7% de similaridade. Intenção diferente, mesma resposta em cache. Eles começaram com uma taxa de falso positivo de 99% e precisaram de quatro rodadas de otimização para chegar a 3.8%. (InfoQ Banking Case Study, 2025)
O problema mais profundo: embeddings medem se dois prompts são semanticamente semelhantes, não se a mesma resposta pode responder a ambos. Essa lacuna é onde vivem os hits de cache falsos. (Efficient Prompt Caching via Embedding Similarity, 2024)
Cada artigo que encontramos concorda em uma coisa: a similaridade de embedding por si só não é suficiente. Você precisa de uma camada de verificação.
A correção em duas camadas
Construímos duas defesas. A primeira remove o ruído do template antes do embedding. A segunda verifica os hits após a correspondência.
Camada 2: Extração de conteúdo para Embeddings
Antes de gerar um embedding, agora detectamos entrada estruturada (JSON) e extraímos apenas o conteúdo significativo e variável.
A lógica:
- Verifica se o conteúdo da mensagem começa com
{ou[ - Se for analisado como JSON, coleta recursivamente todos os valores de folha de string
- Filtra valores curtos (20 caracteres ou menos), pois geralmente são campos de configuração como
"zh","formal"ou"Product Page" - Se o texto extraído for muito curto ou vazio, volta para o texto original
function extractContentForEmbedding(text: string): string {
const extracted = tryExtractJsonContent(text);
return extracted && extracted.length > 20 ? extracted : text;
}
Isso se aplica tanto ao system prompt quanto à mensagem do usuário. Para o plugin de tradução, o embedding agora representa "Hello world" em vez de um blob JSON de 2KB. Para o assistente de sumarização, ele extrai a conversa real do wrapper do template.
O threshold de 20 caracteres foi escolhido empiricamente:
"zh"(2 chars): filtrado. Valor de configuração."formal"(6 chars): filtrado. Valor de configuração."Product Page"(12 chars): filtrado. Campo de template."Translate product descriptions"(31 chars): mantido. Conteúdo significativo."The quarterly financial report..."(40+ chars): mantido. Conteúdo real da tradução.
Camada 3: Verificação de Fingerprint
Após um hit de semantic cache, comparamos um hash do texto extraído da requisição atual com o hash armazenado na entrada em cache. Se eles não coincidirem, o hit é rejeitado.
// Na escrita do cache
entry.metadata.textHash = fnv1aHash(extractedText);
// Na leitura do cache, após encontrar uma correspondência de similaridade
if (entry.metadata.textHash !== undefined) {
if (entry.metadata.textHash !== fnv1aHash(currentExtractedText)) {
// Falso positivo: semanticamente similar, mas conteúdo diferente
metrics.recordFingerprintRejection();
return null;
}
}
O hash usa o texto extraído (pós-Camada 2), não a entrada bruta. Duas requisições com wrappers de template diferentes, mas conteúdo real idêntico, ainda coincidem. Conteúdo diferente, hash diferente, rejeitado.
Entradas de cache antigas sem textHash pulam a verificação (compatibilidade reversa). Elas expiram naturalmente via TTL.
Usamos FNV-1a (32 bits) para o hash. Rápido, determinístico, e uma taxa de colisão de ~1 em 4 bilhões é aceitável para verificar um único hit de cache.
Por que não apenas aumentar o Threshold?
Nosso threshold já é 0.95. Aumentá-lo não ajuda. O problema é que entradas estruturalmente semelhantes produzem pontuações de similaridade acima de 0.95, não importa o que o conteúdo real diga.
Os dados do vCache confirmam isso: as distribuições de similaridade de hits corretos e incorretos se sobrepõem tanto que nenhum ponto de corte único os separa. Aumente o threshold para 0.99 e você matará hits de cache legítimos para paráfrases sem eliminar os falsos positivos de requisições pesadas em templates.
Corrija a entrada, verifique a saída. Não mexa no threshold.
Resultados
Com ambas as camadas implantadas:
| Métrica | Antes | Depois |
|---|---|---|
| Falsos positivos gpt-4.1-nano | 198/199 | 0 |
| Parcela de falsos positivos em todos os hits de cache | ~95% | <5% |
| Taxa de hit de cache legítimo | Inalterada | Inalterada |
| Latência adicionada por requisição | 0 | <1ms (JSON parse + FNV hash) |
A Camada 2 sozinha teria corrigido o plugin de tradução. A Camada 3 é a rede de segurança para casos em que a extração de JSON não separa totalmente o conteúdo, ou para entradas estruturadas que não são JSON.
Conclusões
Se você estiver executando um semantic cache em produção:
Monitore a diversidade de respostas. Se um modelo tem 100% de taxa de hit de cache e 1 resposta única, você tem um problema de falso positivo. Query:
SELECT model, uniqExact(substring(response_body, 1, 200)) as unique_responses, count() as total FROM request_logs WHERE cache_hit = true GROUP BY model.Entrada estruturada destrói o embedding ingênuo. Qualquer requisição com um template fixo (APIs JSON, wrappers de system prompt, tarefas de preenchimento de formulários) produzirá pontuações de similaridade artificialmente altas. Pré-processe antes do embedding.
Uma camada de verificação não é opcional. Todo semantic cache de produção na literatura de pesquisa possui uma. A questão é se você usa uma verificação de hash leve, um reranker cross-encoder ou uma chamada de verificação completa de LLM. Escolha com base no seu orçamento de latência.
Thresholds globais são um compromisso, não uma solução. Diferentes tipos de query precisam de diferentes thresholds. Se você não puder fazer thresholds por categoria ou por entrada, pelo menos adicione o pré-processamento de entrada para normalizar a qualidade do embedding entre as categorias.
O semantic caching pode reduzir de 30% a 70% os custos de API de LLM. Mas sem o pré-processamento de entrada e a verificação de hits, você está servindo respostas obsoletas e chamando isso de ganho de desempenho.
LemonData fornece acesso unificado a mais de 300 modelos de AI com cache integrado, roteamento e otimização de custos. Experimente grátis com $1 de crédito.
