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.
Productos gratuitos y de pago para convertir ideas en una base que puedas lanzar.
11 productos disponiblesContinúa explorando temas 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.

Time AI-native escreve código que a IA consegue ler, gerar e validar. Significa design system como contexto, API como contrato zod/OpenAPI, tool definitions MCP e domain logic legível. Aqui vai o padrão com diagramas, exemplos TypeScript e anti-patterns.

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.
Una lista de 47 puntos para encontrar errores, riesgos de seguridad y problemas de rendimiento antes del lanzamiento.
Templates probados en producción, usados por desarrolladores. Ahorra semanas de setup en tu próximo proyecto.