Blog

Aquecimento de cache de prompt: pagar o prefixo uma vez e reaproveitar

O cache de prompt é a otimização com melhor relação entre esforço e retorno em sistemas com LLM, e é também a que mais gente liga errado. A promessa é simples: o prefixo estável da sua chamada, aquele bloco de instruções, esquema de ferramentas e contexto fixo que se repete em toda requisição, é processado uma vez e reaproveitado nas seguintes por uma fração do preço e com uma fração da latência. O que a promessa não diz é que o cache é posicional e frágil: ele casa por prefixo exato, do primeiro token até o ponto marcado, e qualquer coisa que mude no começo, um carimbo de data e hora, um contador de tokens do usuário, uma lista de ferramentas ordenada por um objeto sem ordem garantida, invalida tudo que vem depois. O resultado típico é uma taxa de acerto de trinta por cento que ninguém percebe, porque a fatura continua caindo o suficiente para parecer que funcionou. Este artigo trata o cache como o que ele é: uma decisão de ordenação do prompt, com custo de escrita, tempo de vida e um problema de aquecimento que só aparece quando o tráfego é irregular.

2026-08-08 / IA Aplicada / 14 min

01

O cache é um prefixo, não um conjunto de blocos

O erro conceitual mais caro é imaginar o cache como um dicionário que guarda pedaços do prompt e os reconhece onde quer que apareçam. Não é isso. O que fica em cache é o estado interno do modelo depois de processar uma sequência de tokens desde a posição zero. Isso significa que o acerto é sempre um prefixo contínuo: se os primeiros oito mil tokens são idênticos aos da chamada anterior e o token oito mil e um difere, você aproveita oito mil e paga integralmente por todo o resto. E significa também que colocar o bloco estável depois de um bloco variável não serve para nada, mesmo que aquele bloco estável tenha vinte mil tokens e nunca mude, porque a variação anterior já quebrou a cadeia.

Daí sai a única regra de ordenação que importa, e ela vale para qualquer provedor: ordene o prompt por frequência de mudança, do mais estável para o mais volátil. Instruções do sistema e definição de ferramentas primeiro, porque mudam em deploy. Contexto de domínio compartilhado depois, porque muda em dias. Dados do cliente em seguida, porque mudam por conversa. Histórico de mensagens e a pergunta do turno atual por último, porque mudam a cada requisição. Essa ordem não é estética, ela é literalmente a diferença entre pagar o prefixo uma vez por dia e pagar a cada chamada.

Ordem que aproveita o cache          Ordem que destrói o cache

[ instrucoes do sistema  ] estavel   [ timestamp da chamada   ] muda sempre
[ definicao de tools     ] estavel   [ instrucoes do sistema  ] estavel
[ contexto de dominio    ] dias      [ definicao de tools     ] estavel
[ perfil do cliente      ] conversa  [ contexto de dominio    ] dias
--------- ponto de cache ---------   [ perfil do cliente      ] conversa
[ historico de mensagens ] turno     [ historico de mensagens ] turno
[ pergunta do turno      ] turno     [ pergunta do turno      ] turno

acerto: tudo antes do marcador       acerto: zero
                                     (o primeiro token ja difere)

Vale notar a assimetria de preço que torna essa decisão interessante. Escrever no cache custa mais do que processar o token normalmente, tipicamente algo em torno de vinte e cinco por cento a mais. Ler do cache custa uma fração pequena do preço normal, na ordem de um décimo. Isso quer dizer que o cache não é gratuito: um prefixo escrito e nunca reaproveitado é prejuízo direto. O ponto de equilíbrio aparece por volta da segunda leitura, e é por isso que a pergunta operacional não é "vale a pena cachear?", é "quantas leituras eu consigo antes de o prefixo expirar?".

02

O que invalida o cache sem ninguém perceber

A taxa de acerto baixa quase nunca vem de uma decisão consciente. Ela vem de detalhes de serialização que ninguém revisou porque parecem inertes. O caso mais comum e mais irritante é a data no prompt do sistema: alguém escreveu "Hoje é 8 de agosto de 2026" para o modelo saber a data atual, e se esse texto for gerado com hora e minuto, o prefixo muda a cada requisição e o cache nunca acerta. O mesmo vale para um identificador de requisição injetado no cabeçalho do prompt, para um contador de mensagens da conversa colocado antes das instruções, e para qualquer coisa que envolva geração aleatória.

Fonte da invalidaçãoPor que passa despercebidaCorreção
Data e hora completa no prompt do sistemaParece um dado estático, mas muda a cada segundoTruncar para o dia, ou mover a data para o fim do prompt
Ferramentas serializadas a partir de um objetoA ordem das chaves parece estável até um deploy mudá-laOrdenar as ferramentas explicitamente por nome antes de serializar
JSON com indentação variável ou chaves fora de ordemO conteúdo é igual, mas a sequência de tokens não éSerializar de forma canônica: chaves ordenadas, espaçamento fixo
Identificador de requisição ou de sessão no inícioFoi colocado para rastreio e ninguém pensou no cacheManter fora do prompt, no metadado da chamada
Nome do usuário ou saudação personalizada no topoTorna o prefixo único por pessoa, matando o compartilhamentoMover o bloco personalizado para depois do ponto de cache
Rotação de modelo entre chamadasO cache é por modelo, e o roteador troca sem avisarContabilizar o cache por modelo e evitar rotação no caminho quente

A defesa contra isso não é revisão de código, é um teste. O prefixo estável precisa ter uma verificação automatizada que monta o prompt duas vezes, em momentos diferentes, com entradas diferentes, e compara o hash da parte que deveria ser idêntica. Se o hash diverge, o teste falha e alguém descobre no pull request em vez de descobrir na fatura três semanas depois.

// Montagem do prompt com prefixo estável verificável.
// A ordenação canônica das ferramentas e a ausência de qualquer valor
// derivado do relógio são o que garante o acerto entre chamadas.

const serializarFerramentas = (ferramentas) =>
  [...ferramentas]
    .sort((a, b) => a.name.localeCompare(b.name))
    .map((f) => JSON.stringify(f, Object.keys(f).sort()))
    .join('\n');

// Tudo que entra aqui muda no máximo em deploy ou em janela de horas.
// Nada de relógio, identificador de requisição ou dado do cliente.
export const montarPrefixoEstavel = ({ instrucoes, ferramentas, glossario }) =>
  [
    instrucoes.trim(),
    '## Ferramentas disponíveis',
    serializarFerramentas(ferramentas),
    '## Glossário do domínio',
    glossario.trim(),
  ].join('\n\n');

export const montarChamada = ({ prefixoEstavel, perfilCliente, historico, pergunta }) => ({
  system: [
    // O bloco marcado é o que o provedor guarda; ele precisa ser byte a byte
    // igual ao da chamada anterior para haver acerto.
    { type: 'text', text: prefixoEstavel, cache_control: { type: 'ephemeral' } },
    // Depois do marcador entra tudo que varia por conversa e por turno.
    { type: 'text', text: perfilCliente },
  ],
  messages: [...historico, { role: 'user', content: pergunta }],
});
// Teste que trava a estabilidade do prefixo. Falha no pull request
// quando alguém injeta relógio, aleatoriedade ou ordem não determinística.

import { createHash } from 'node:crypto';
import { montarPrefixoEstavel } from './prompt.js';

const hash = (texto) => createHash('sha256').update(texto).digest('hex');

test('o prefixo estável não muda entre montagens', () => {
  const entrada = {
    instrucoes: INSTRUCOES,
    // Mesma coleção em ordens diferentes: a serialização canônica
    // precisa produzir exatamente a mesma sequência de tokens.
    ferramentas: [...FERRAMENTAS].reverse(),
    glossario: GLOSSARIO,
  };

  const primeiro = hash(montarPrefixoEstavel({ ...entrada, ferramentas: FERRAMENTAS }));
  const segundo = hash(montarPrefixoEstavel(entrada));

  expect(segundo).toBe(primeiro);
});

test('o prefixo estável não contém marcas de tempo', () => {
  const prefixo = montarPrefixoEstavel({
    instrucoes: INSTRUCOES,
    ferramentas: FERRAMENTAS,
    glossario: GLOSSARIO,
  });

  expect(prefixo).not.toMatch(/\d{2}:\d{2}:\d{2}/);
  expect(prefixo).not.toMatch(/\d{4}-\d{2}-\d{2}T/);
});

03

O problema do aquecimento: tempo de vida contra ritmo do tráfego

Aqui está a parte que dá nome ao artigo e que a maioria das implementações ignora. O prefixo em cache tem tempo de vida curto, tipicamente alguns minutos a partir do último acesso, com opções de vida estendida a um custo de escrita maior. Cada leitura renova o relógio. Isso cria uma dinâmica que depende inteiramente do ritmo do seu tráfego, e não do tamanho do seu prompt: se as chamadas chegam com intervalo menor que o tempo de vida, o prefixo se mantém vivo sozinho e você paga a escrita uma única vez; se o intervalo entre chamadas é maior, cada requisição paga a escrita de novo e você está gastando mais do que gastaria sem cache nenhum.

Perfil de tráfegoO que acontece com o prefixoDecisão
Constante, várias chamadas por minutoSempre vivo, renovado pelo próprio tráfegoNão faça nada: o cache se sustenta sozinho
Rajadas curtas com longos valesMorre no vale e é reescrito a cada rajadaAquecer antes da rajada quando ela é previsível
Comercial diurno, silêncio noturnoMorre toda noite e a primeira chamada do dia é lenta e caraAquecer uma vez antes do pico de abertura
Esparso e imprevisívelQuase sempre frio, escrita raramente amortizadaManter aquecido custa mais que o benefício: desligue o cache
Lote noturno de processamentoVive durante o lote inteiro se a concorrência for controladaAquecer uma vez e disparar o lote em seguida

A conclusão prática é desconfortável para quem gosta de solução geral: manter um prefixo quente com um ping periódico só compensa quando o volume que se beneficia dele é grande o suficiente para pagar as escritas do aquecimento. Um ping a cada quatro minutos, o dia inteiro, são algumas centenas de escritas de prefixo por dia. Se o seu volume de leitura é de dezenas de milhares de chamadas, isso é irrelevante e o aquecimento se paga muitas vezes. Se o seu volume é de algumas centenas de chamadas por dia, o aquecimento custa quase tanto quanto o que ele economiza, e a decisão correta é aquecer apenas antes dos picos conhecidos, ou não aquecer.

// Aquecedor de prefixo. Roda como job periódico e dispara a chamada
// mais barata possível que ainda escreve o prefixo no cache do provedor.

const INTERVALO_MS = 4 * 60 * 1000; // abaixo do TTL nominal, com margem

// A chamada de aquecimento pede o mínimo de saída: o objetivo é escrever
// o prefixo, não obter resposta. Tokens de saída são o item caro.
const aquecer = async (cliente, prefixoEstavel) => {
  const inicio = process.hrtime.bigint();

  const resposta = await cliente.messages.create({
    model: MODELO,
    max_tokens: 1,
    system: [{ type: 'text', text: prefixoEstavel, cache_control: { type: 'ephemeral' } }],
    messages: [{ role: 'user', content: 'ok' }],
  });

  const ms = Number(process.hrtime.bigint() - inicio) / 1e6;
  const { cache_creation_input_tokens: escritos, cache_read_input_tokens: lidos } = resposta.usage;

  // Se o aquecimento escreveu em vez de ler, o prefixo tinha morrido:
  // é o sinal de que o intervalo está acima do TTL real do provedor.
  return { escritos, lidos, ms, expirou: escritos > 0 };
};

export const iniciarAquecimento = (cliente, prefixoEstavel, metricas) => {
  const timer = setInterval(async () => {
    try {
      const r = await aquecer(cliente, prefixoEstavel);
      metricas.registrar('cache.aquecimento', r);
    } catch (erro) {
      // Falha no aquecimento nunca pode derrubar o processo: o pior caso
      // é o prefixo esfriar e a próxima chamada real pagar a escrita.
      metricas.registrar('cache.aquecimento.erro', { mensagem: erro.message });
    }
  }, INTERVALO_MS);

  timer.unref();
  return () => clearInterval(timer);
};

Duas escolhas desse código merecem justificativa. A primeira é o limite de um token na saída: a escrita do prefixo acontece do lado da entrada, então gerar resposta é desperdício puro. A segunda é usar o próprio retorno de uso para descobrir se o prefixo expirou. Quando o aquecimento reporta escrita em vez de leitura, o intervalo escolhido está acima do tempo de vida real, e esse número é um dado observado no seu provedor, na sua região, e não uma constante de documentação para copiar.

04

Quando o prefixo é grande demais para caber numa versão só

Sistemas maduros raramente têm um prefixo. Têm um por vertical de atendimento, um por idioma, um por variante de experimento, um por versão do prompt em rollout. Cada combinação é um prefixo distinto no cache, com sua própria escrita e seu próprio tempo de vida. Um sistema com quatro verticais, três idiomas e duas variantes de flag tem vinte e quatro prefixos concorrendo por aquecimento, e a taxa de acerto agregada despenca sem que nenhuma linha de código pareça errada.

  1. Conte quantos prefixos distintos o seu sistema realmente emite hoje, multiplicando as dimensões que variam: vertical, idioma, variante de flag, versão do prompt e modelo.
  2. Meça o volume de chamadas por prefixo, não no agregado, porque a decisão de aquecer é sempre por prefixo.
  3. Corte o que não se paga: prefixos abaixo do volume de equilíbrio devem sair do cache, não entrar na rotina de aquecimento.
  4. Fatore o que é comum a todos eles para um bloco compartilhado no topo, deixando a parte específica depois do primeiro marcador.
  5. Durante um rollout de prompt, aceite conscientemente que existem dois prefixos quentes ao mesmo tempo, e encerre a versão antiga em vez de deixá-la viva indefinidamente.

A fatoração do item quatro é o que mais rende e o que menos gente faz. Se as instruções de tom, as regras de segurança e o glossário geral da empresa são os mesmos em todas as verticais, esse bloco pode ser um prefixo compartilhado por todo o tráfego, aquecido uma vez e reaproveitado por todos. A parte específica da vertical vira um segundo trecho marcado depois dele. Provedores que suportam mais de um ponto de cache por chamada permitem exatamente esse encadeamento, e o ganho é grande porque o bloco compartilhado é o que tem o maior volume de leitura de todo o sistema.

chamada de atendimento (vertical A, idioma pt, variante 1)

[ bloco comum a toda a empresa ] <- marcador 1: compartilhado por todos
[ contexto da vertical A       ] <- marcador 2: compartilhado pela vertical
[ variacao da flag 1           ]
[ perfil do cliente            ]
[ historico + pergunta         ]

chamada de atendimento (vertical B, idioma pt, variante 1)

[ bloco comum a toda a empresa ] <- ACERTO no marcador 1
[ contexto da vertical B       ] <- escrita propria da vertical B
[ variacao da flag 1           ]
[ perfil do cliente            ]
[ historico + pergunta         ]

05

Medir o acerto real, e não a economia aparente

A métrica que quase todo mundo usa para avaliar o cache é a queda no valor da fatura, e ela é enganosa por um motivo simples: a fatura também cai quando o volume cai, quando o histórico fica mais curto, quando alguém troca de modelo. Avaliar cache pela fatura mistura tudo. O dado que importa vem direto do retorno de uso de cada chamada, que separa tokens de entrada normais, tokens escritos no cache e tokens lidos do cache, e é com esses três números que se calcula a taxa de acerto de verdade.

  • Taxa de acerto por prefixo: tokens lidos do cache divididos pela soma de lidos, escritos e entrada normal. Agregar isso no sistema inteiro esconde o prefixo que nunca acerta.
  • Razão entre escritas e leituras por prefixo: acima de um para dois, aquele prefixo está custando mais do que economiza e precisa sair do cache.
  • Tempo até o primeiro token separado por acerto e por falha, porque o ganho de latência do cache é frequentemente maior que o ganho de custo e aparece direto na experiência do cliente.
  • Contagem de prefixos distintos por dia, que é o indicador antecedente de fragmentação: quando esse número sobe sem que alguém tenha criado uma vertical nova, algo está injetando variação no prefixo.
  • Custo de aquecimento por prefixo, comparado ao custo economizado por ele, porque essa razão é a única justificativa defensável para manter o job de ping rodando.
// Contabilidade do cache a partir do retorno de uso da API.
// Um registro por chamada, agrupado pela identidade do prefixo.

export const registrarUso = (metricas, { prefixoId, modelo, usage, ttfbMs }) => {
  const entradaNormal = usage.input_tokens ?? 0;
  const escritos = usage.cache_creation_input_tokens ?? 0;
  const lidos = usage.cache_read_input_tokens ?? 0;
  const total = entradaNormal + escritos + lidos;

  metricas.registrar('llm.cache', {
    prefixoId,
    modelo,
    // A taxa por chamada, agregada depois por prefixo na janela desejada.
    taxaAcerto: total === 0 ? 0 : lidos / total,
    escritos,
    lidos,
    // Separar a latência por acerto é o que revela o ganho de experiência,
    // que costuma ser maior e mais visível que o ganho de custo.
    ttfbMs,
    acertou: lidos > 0,
  });
};

// Regra de decisão executável: um prefixo só permanece no cache enquanto
// as leituras amortizarem as escritas na janela observada.
export const deveManterNoCache = ({ escritos, lidos }, fatorEscrita = 1.25, fatorLeitura = 0.1) => {
  const custoComCache = escritos * fatorEscrita + lidos * fatorLeitura;
  const custoSemCache = escritos + lidos;
  return custoComCache < custoSemCache;
};

O último trecho é a versão executável da decisão que costuma ficar implícita numa planilha. Os fatores são os multiplicadores de preço de escrita e de leitura em relação ao token de entrada normal, e valem os do seu provedor, consultados no momento em que você escreve isso. Rodar essa comparação por prefixo, uma vez por semana, é o que impede o cache de virar aquela otimização que todo mundo acha que está ativa e que ninguém consegue provar que ainda compensa.

FAQ

Perguntas frequentes

Por que a minha taxa de acerto do cache é baixa se o meu prompt quase não muda?

Porque o cache casa por prefixo exato de tokens desde a posição zero, e não por semelhança de conteúdo. Basta um token diferente no começo para tudo depois dele ser cobrado integralmente. As causas mais comuns são invisíveis numa revisão rápida: uma data com hora e minuto no prompt do sistema, um identificador de requisição colocado no cabeçalho para rastreio, ferramentas serializadas a partir de um objeto cuja ordem de chaves mudou num deploy, JSON com indentação ou ordenação inconsistente, e nome do cliente numa saudação no topo, que torna o prefixo único por pessoa. A verificação que resolve isso é um teste que monta o prefixo duas vezes com entradas diferentes e compara o hash: se divergir, o pull request falha antes de a fatura contar a história.

Vale a pena manter um job de ping para o prefixo nunca esfriar?

Depende inteiramente do volume que se beneficia do prefixo, e a conta é direta. Um ping a cada quatro minutos gera algumas centenas de escritas de prefixo por dia. Se o tráfego real é de dezenas de milhares de chamadas contra aquele mesmo prefixo, o aquecimento é ruído no custo e se paga muitas vezes em latência e em preço. Se o tráfego é de algumas centenas de chamadas por dia, o aquecimento custa quase tanto quanto economiza e não se justifica: nesse caso, aqueça apenas antes de picos previsíveis, como a abertura do horário comercial ou o início de um lote noturno, ou simplesmente não use cache naquele prefixo. A decisão é sempre por prefixo, nunca no agregado do sistema.

Cache de prompt é a mesma coisa que cache semântico de respostas?

Não, e confundir os dois leva a decisões ruins. O cache de prompt é um mecanismo do provedor que guarda o estado interno do modelo para um prefixo de tokens e cobra mais barato pela releitura dele; a resposta continua sendo gerada do zero a cada chamada, então a saída pode variar e a informação nunca fica velha. O cache semântico é uma camada sua, antes do modelo, que devolve uma resposta já gerada quando a nova pergunta se parece o suficiente com outra anterior, o que economiza a chamada inteira mas traz risco de servir conteúdo desatualizado ou de casar perguntas parecidas com respostas diferentes. Eles são complementares: o cache de prompt reduz o custo das chamadas que acontecem, e o cache semântico reduz o número de chamadas que acontecem.

Cache é ordenação de prompt, e aquecimento é decisão de tráfego

O cache de prompt entrega o que promete, mas só para quem trata o prompt como uma sequência ordenada por frequência de mudança em vez de um punhado de blocos concatenados na ordem em que foram escritos. A diferença entre trinta e noventa por cento de acerto quase nunca está no provedor: está numa data com hora no topo, numa serialização não determinística das ferramentas e numa saudação personalizada antes do ponto de cache. E o aquecimento, que parece a parte sofisticada, é a mais simples de decidir quando você mede por prefixo: se o tráfego mantém o prefixo vivo sozinho, não faça nada; se ele morre em vales previsíveis, aqueça antes do pico; se ele quase nunca acerta, tire aquele prefixo do cache em vez de pagar escrita para ninguém ler.