Como combinar cache exato, cache semântico e memória de conversa para reduzir latência e custo sem perder correção em sistemas de LLM em produção

Este é o terceiro artigo da série LLMOps em Produção. No artigo anterior sobre roteamento de modelos LLM, vimos como escolher entre modelos quality, balanced e budget. Aqui entramos no próximo bloco de otimização: depois que o roteador escolhe o modelo, ainda existe uma fronteira grande entre o prompt e o dólar gasto. Essa fronteira é preenchida por cache e memória — dois mecanismos que parecem fazer a mesma coisa, mas resolvem problemas diferentes.
O problema operacional é conhecido. Você sobe um chatbot em produção e descobre nas primeiras 48 horas que:
Cache mal feito não reduz custo: troca custo de inferência por custo de retrabalho, churn e respostas erradas em produção.
| Dimensão | Cache exato | Cache semântico |
|---|---|---|
| Chave | Hash do prompt (system + user normalizados) | Embedding + busca por similaridade |
| Hit perfeito | Strings idênticas | Paráfrases, sinônimos, reordenação |
| Custo de lookup | O(1) — hash table (Redis) | O(log n) a O(n) — vector index (HNSW, IVF) |
| Falso positivo | Praticamente zero | Possível se threshold baixo |
| Quando usar | Prompts determinísticos, tool calls | FAQ, suporte, perguntas com variação linguística |
Antes de construir cache próprio, use o que o provider já oferece. A Anthropic mantém prompt caching e a Google tem context caching para Gemini.
| Mecanismo | O que cacheia | O que economiza | Muda a resposta? |
|---|---|---|---|
| Cache de saída (exato/semântico) | Resposta final | Input + output tokens + latência | Sim — devolve idêntico |
| Prompt cache do provider | KV da atenção sobre o prefixo | Tempo de processamento do prefixo | Não — ainda executa inferência |
// ✅ Bom: separa prefixo cacheável de partes variáveis
const cachedPrefix = [
{
role: 'system',
content: SYSTEM_PROMPT_FIXED, // instruções, regras, few-shot — nunca muda
},
];
const variableSuffix = [
{
role: 'system',
content: `Data atual: ${new Date().toISOString()}`, // muda a cada dia
},
...history,
{ role: 'user', content: userQuestion },
];
function pushToWindow(memory: SlidingMemory, msg: ChatMessage): ChatMessage[] {
memory.messages.push(msg);
if (memory.messages.length > memory.maxMessages) {
memory.messages = memory.messages.slice(-memory.maxMessages);
}
return memory.messages;
}
async function recall(memory: ConversationMemory, query: string): Promise<MemoryFact[]> {
const queryEmbedding = await embed(query);
const scored = memory.facts.map((fact) => ({
fact,
score: cosine(queryEmbedding, fact.embedding),
}));
return scored
.filter((s) => s.score >= RECALL_THRESHOLD)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map((s) => s.fact);
}
| Vector store | Modelo | Quando escolher |
|---|---|---|
| pgvector | Extensão Postgres | Já tem Postgres; volume até ~1M vetores |
| Pinecone | SaaS gerenciado | Não quer operar infra; volume alto |
| Qdrant | Open source / SaaS | Quer self-host ou híbrido; payload filtering rico |
function cosine(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error(`dimensões incompatíveis: ${a.length} vs ${b.length}`);
}
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dot / denom;
}
Recomendação prática: comece em 0.95 e só abaixe se tiver evals medindo taxa de falso positivo.
async function semanticLookup(
store: VectorStore,
key: SemanticKey,
): Promise<CachedAnswer | null> {
const candidates = await store.search({
vector: key.embedding,
filter: { tenantId: key.tenantId }, // filtro obrigatório
topK: 5,
});
const hit = candidates.find((c) => c.score >= key.threshold);
return hit ? hit.payload : null;
}
async function cachedCompletion(
req: CompletionRequest,
deps: {
exact: ExactCache;
semantic: SemanticStore;
model: (req: CompletionRequest) => Promise<string>;
embed: (text: string) => Promise<number[]>;
ttlSeconds: number;
},
): Promise<CompletionResponse> {
const start = Date.now();
// 1. cache exato
const exactKey = `exact:${req.tenantId}:${hashPrompt(req)}`;
const exactHit = await deps.exact.get(exactKey);
if (exactHit) {
return { text: exactHit, fromCache: 'exact', latencyMs: Date.now() - start };
}
// 2. cache semântico
const semHit = await semanticLookup(deps.semantic, req.tenantId, req.user);
if (semHit && semHit.modelVersion === req.model) {
return { text: semHit.answer, fromCache: 'semantic', latencyMs: Date.now() - start };
}
// 3. miss — chama o modelo
const text = await deps.model(req);
// 4. popula ambos os caches com TTL e versão
await deps.exact.set(exactKey, text, deps.ttlSeconds);
await deps.semantic.upsert({
tenantId: req.tenantId,
prompt: req.user,
answer: text,
embedding: await deps.embed(req.user),
modelVersion: req.model,
ttlSeconds: deps.ttlSeconds,
});
return { text, fromCache: 'miss', latencyMs: Date.now() - start };
}
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
system: [
{
type: 'text',
text: SYSTEM_PROMPT_FIXED,
cache_control: { type: 'ephemeral' }, // marca prefixo para cache
},
],
messages: variableMessages,
});
Trate modelVersion como parte da chave e invalide entradas antigas no rollout.
Use namespace por domínio (domain: 'refund-policy') — invalida tudo daquele domínio sem mexer no resto.
A resposta "não atendemos domingo" passa a estar errada quando a loja passa a atender domingo. Sem invalidação, o cache continua servindo a resposta antiga até o TTL expirar.
| Tipo de conteúdo | TTL sugerido | Justificativa |
|---|---|---|
| FAQ estática | 24h a 7d | Muda raramente; cache longo é seguro |
| Política de produto | 1h a 24h | Pode mudar a qualquer momento |
| Resposta com dados do usuário | Não cachear | Dados pessoais mudam; cache vira erro |
| Conteúdo PII | Não cachear sem anonimização | Risco regulatório |
temperature > 0): cria inconsistência.| Métrica | O que responde |
|---|---|
| Hit rate exato | Quantas chamadas pegas pelo hash? |
| Hit rate semântico | Quantas pelo embedding? |
| Falso positivo (após amostragem) | Quantos hits semânticos estavam errados? |
| Latência p50/p99 por caminho | Cache realmente é mais rápido que o modelo? |
| Custo economizado | Dólares preservados por semana |
A métrica de falso positivo é a mais negligenciada e a mais importante para cache semântico.
cache_control.cache_control aplicado no prefixo estável.tenantId.modelVersion é parte da chave; invalide em rollout.Cache e memória são as duas maiores alavancas de custo e latência em sistemas de LLM em produção — e também duas das formas mais fáceis de servir a resposta errada com aparência de eficiência.
A disciplina básica é separar as camadas: cache exato para o determinístico, cache semântico para o paráfraseado, prompt cache do provider para o prefixo, memória de curto prazo para a sessão, memória de longo prazo para o usuário.
A regra que vale repetir: hit rate não é objetivo. Objetivo é resposta correta com menor custo.
No próximo artigo da série, sobre guardrails em runtime para LLM, vamos fechar o ciclo: mesmo com roteamento correto, cache eficiente e memória bem desenhada, ainda é preciso validar entrada, saída e efeitos colaterais em tempo de execução.
Produtos gratuitos e pagos para transformar ideias em uma base que você consegue executar.
13 produtos disponíveisContinue explorando tópicos similares

Observabilidade generativa não é log com texto bonito. É prompt completo, versão do modelo, tools usadas, latência, custo e veredito do eval no mesmo span. Fechamento da série LLMOps em Produção.
Depois de dois dias no workshop da Tech Leads Club, uma síntese prática sobre contexto, specs, harness, verificação, LLMOps e o novo papel do engenheiro.

Como desenhar o sistema completo de um banco digital de ponta a ponta — do ledger de partidas dobradas e idempotência financeira até event sourcing, sagas, antifraude e os trade-offs de consistência do Teorema CAP. Guia hiperdetalhado, com diagramas e TypeScript real.
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.