Prompt caching é o recurso da API do Claude que guarda o processamento de um prefixo do prompt (tools, system e começo da conversa) e cobra cerca de 10% do preço normal de input quando a próxima chamada repete esse prefixo exatamente igual. Num agente de IA em Laravel, o ganho depende menos do cache_control em si e mais de como você ordena o conteúdo. O que muda pouco vai no começo, o que muda a cada turno vai no fim, e nada dinâmico pode vazar para dentro do prefixo cacheado.
Aprendi isso montando agentes de atendimento no WhatsApp, do tipo que consulta agenda no Horalis ou responde dúvidas sobre cobranças. O padrão se repete: um system prompt grande com regras de negócio, uma dúzia de tools, o contexto do estabelecimento e uma conversa que cresce a cada mensagem. Sem cache, você paga por esses milhares de tokens fixos em toda chamada, e o loop de tool calling faz várias chamadas por mensagem do cliente.
Como funciona o prompt caching da API do Claude?
A API monta o prompt sempre na mesma ordem: tools, depois system, depois messages. Você marca um bloco com cache_control: {"type": "ephemeral"}. Esse bloco vira um breakpoint, ou seja, um ponto de corte que diz à API para cachear tudo do início até ali.
Algumas regras práticas:
- O cache é por prefixo exato. Um caractere diferente antes do breakpoint e você paga uma escrita nova.
- Você pode ter até 4 breakpoints por requisição.
- O TTL padrão é de 5 minutos e é renovado a cada leitura. Existe a opção de 1 hora (
"ttl": "1h"), com escrita mais cara. - Há um tamanho mínimo de prefixo para cachear, que varia por modelo (1024 tokens nos Sonnet, mais nos Haiku). Abaixo disso a API simplesmente não cacheia e não reclama.
- O cache é isolado por organização na Anthropic, então outro cliente da API nunca lê o seu prefixo.
A cobrança, em relação ao preço base de input do modelo:
| Tipo de token | Multiplicador |
|---|---|
| Input normal (depois do último breakpoint) | 1x |
| Escrita de cache, TTL 5 min | 1,25x |
| Escrita de cache, TTL 1 h | 2x |
| Leitura de cache | 0,1x |
A conta do break-even é simples. Com TTL de 5 minutos, uma escrita mais uma leitura custa 1,35x, contra 2x sem cache. Um único acerto já paga. Com TTL de 1 hora, escrita mais uma leitura dá 2,1x, então você precisa de pelo menos dois acertos dentro da hora. Para simular isso com o seu volume, a calculadora de custo de IA compara os modelos com e sem prompt caching.
Onde colocar os breakpoints num agente de IA?
O erro mais comum é tratar breakpoint como "cachear esta seção". Ele é um marcador de prefixo. Você não precisa de um por seção, e sim de um em cada ponto onde a frequência de mudança muda.
Num agente multi-tenant, as camadas costumam ser estas:
- Tools + instruções base: iguais para todos os clientes, mudam só no deploy.
- Contexto do tenant: serviços, horários e políticas do estabelecimento. Muda raramente e é igual em todas as conversas daquele tenant.
- Histórico da conversa: cresce a cada turno.
Isso dá três breakpoints. Um no fim das instruções base, que já cobre as tools porque elas vêm antes no prefixo. Outro no fim do contexto do tenant. E um na última mensagem. Pôr um breakpoint separado na última tool seria desperdício, já que tools e instruções base mudam juntas.
Em Laravel, chamando a API direto com o Http:::
final class AgendaAgentClient { public function __construct( private PromptRepository $prompts, private ToolRegistry $registry, ) {} public function send(Tenant $tenant, array $history): array { $payload = [ 'model' => config('services.anthropic.model'), 'max_tokens' => 1024, 'tools' => $this->tools(), 'system' => [ [ 'type' => 'text', 'text' => $this->prompts->base(), 'cache_control' => ['type' => 'ephemeral'], ], [ 'type' => 'text', 'text' => $this->prompts->tenantContext($tenant, today()), 'cache_control' => ['type' => 'ephemeral'], ], ], 'messages' => $this->withCacheOnLastMessage($history), ]; $response = Http::withHeaders([ 'x-api-key' => config('services.anthropic.key'), 'anthropic-version' => '2023-06-01', ]) ->timeout(60) ->retry(2, 500) ->post('https://api.anthropic.com/v1/messages', $payload) ->throw(); $this->recordUsage($tenant, $response->json('usage', [])); return $response->json(); } }
O breakpoint da conversa vai no último bloco da última mensagem. Se o conteúdo for string, é preciso convertê-lo em array de blocos, porque cache_control só existe em bloco:
private function withCacheOnLastMessage(array $messages): array { $last = array_key_last($messages); $content = $messages[$last]['content']; if (is_string($content)) { $content = [['type' => 'text', 'text' => $content]]; } $content[array_key_last($content)]['cache_control'] = ['type' => 'ephemeral']; $messages[$last]['content'] = $content; return $messages; }
Isso funciona bem no loop de tool calling. Quando a última mensagem é um tool_result, o breakpoint cai nele. Na próxima iteração, todo o histórico anterior é lido do cache. A API também procura acertos em posições anteriores ao breakpoint, dentro de uma janela de cerca de 20 blocos. Por isso mover o breakpoint para a nova última mensagem a cada turno continua aproveitando o prefixo da chamada anterior.
O que quebra o prompt caching sem você perceber?
O cache raramente falha de forma visível. A resposta chega certa, só que mais cara. Os vilões que mais encontro:
| Causa | Por que quebra | Correção |
|---|---|---|
| Data e hora no system prompt | O prefixo muda a cada minuto | Granularidade de dia no system; hora exata na mensagem do usuário |
| Tools em ordem não determinística | Tools são o início do prefixo, e qualquer troca invalida tudo | Ordenar por nome antes de serializar |
| Tools diferentes por plano ou tenant | O prefixo global vira um por tenant | Mesmo conjunto para todos; checar permissão na execução |
| Nome do cliente nas instruções base | O primeiro breakpoint deixa de ser compartilhado | Mover para o bloco do tenant ou para a conversa |
| Janela deslizante de memória | Cortar a mensagem mais antiga muda o prefixo inteiro da conversa | Cortar em lotes, não um turno por vez |
| Deploy de prompt novo | Prefixo novo, escrita em todos os tenants | Esperado; só não faça rollout que alterna versões por requisição |
Vale detalhar três delas.
Tools determinísticas. Se o registry monta as tools a partir de classes descobertas pelo container, a ordem pode variar entre workers ou deploys. Ordene sempre:
private function tools(): array { return collect($this->registry->all()) ->sortBy(fn (Tool $tool) => $tool->name()) ->map(fn (Tool $tool) => $tool->definition()) ->values() ->all(); }
Tools por plano. É tentador esconder a tool emitir_segunda_via de quem não tem o plano. Mas as tools ficam no topo do prefixo, então cada combinação de tools vira um cache separado. Prefiro mandar o mesmo conjunto para todos e, na execução, devolver um tool_result com is_error: true explicando que o recurso não está disponível no plano. O modelo lida bem com isso e o prefixo global continua compartilhado.
Memória e janela de contexto. Se você limita o histórico às últimas N mensagens e remove uma a cada turno, a primeira mensagem do array muda sempre, e o cache da conversa nunca acerta. A estratégia que funciona é deixar a conversa crescer até um teto e, ao atingi-lo, resumir ou cortar um bloco grande de uma vez. Você paga uma escrita no corte e volta a acertar nos turnos seguintes. Detalhei as estratégias de janela, resumo e fatos em como gerenciar a memória de um agente de IA no Laravel, e o prompt caching é mais um motivo para preferir cortes em lote.
Como medir se o prompt caching está funcionando?
O objeto usage da resposta separa os tokens em três campos de input:
cache_read_input_tokens: lidos do cache, cobrados a 0,1x.cache_creation_input_tokens: escritos no cache nesta chamada, cobrados a 1,25x ou 2x.input_tokens: o que veio depois do último breakpoint, cobrado a 1x.
Registre os três por chamada:
private function recordUsage(Tenant $tenant, array $usage): void { $read = $usage['cache_read_input_tokens'] ?? 0; $write = $usage['cache_creation_input_tokens'] ?? 0; $input = $usage['input_tokens'] ?? 0; Log::info('claude.usage', [ 'tenant_id' => $tenant->id, 'input' => $input, 'cache_write' => $write, 'cache_read' => $read, 'output' => $usage['output_tokens'] ?? 0, 'hit_ratio' => round($read / max(1, $read + $write + $input), 3), ]); }
A métrica que importa é a taxa de acerto: leitura sobre o total de input. Num agente com prompt base grande e conversa de vários turnos, ela deve ficar alta. Se aparecer cache_write alto em todas as chamadas, algum item da tabela acima está mexendo no prefixo. Se cache_write e cache_read vierem zerados, o prefixo provavelmente está abaixo do mínimo do modelo. Se você já contabiliza tokens por cliente, como em medir tokens e limitar o custo de IA por cliente no Laravel, lembre de precificar cada campo com seu multiplicador. Somar tudo como input normal superestima o custo de quem tem bom acerto.
Como garantir em teste que o prefixo continua estável?
Como a quebra de cache é silenciosa, transformo a regra em teste. Com Http::fake, faço duas chamadas com conversas diferentes para o mesmo tenant e confiro que tools e system saíram idênticos:
it('mantém o prefixo cacheável estável entre chamadas', function () { Http::fake([ 'api.anthropic.com/*' => Http::response([ 'content' => [['type' => 'text', 'text' => 'ok']], 'stop_reason' => 'end_turn', 'usage' => ['input_tokens' => 10, 'output_tokens' => 2], ]), ]); $tenant = Tenant::factory()->create(); $client = app(AgendaAgentClient::class); $client->send($tenant, [['role' => 'user', 'content' => 'oi']]); $this->travel(3)->minutes(); $client->send($tenant, [['role' => 'user', 'content' => 'quero marcar amanhã']]); $prefixes = Http::recorded()->map( fn (array $pair) => md5(json_encode([$pair[0]['tools'], $pair[0]['system']])) ); expect($prefixes->unique())->toHaveCount(1); });
O travel pega justamente o caso de alguém colocar now()->format('H:i') no system prompt. Esse teste já barrou mais de uma regressão desse tipo em revisão.
Quando o prompt caching não compensa?
O cache não ajuda em todos os casos:
- Tráfego esparso com TTL de 5 minutos. Um tenant que recebe uma mensagem a cada meia hora paga 1,25x na escrita e nunca lê. O prefixo global, compartilhado por todos os tenants, quase sempre está quente; o do tenant pequeno, não. Avalie se o bloco do tenant merece breakpoint próprio ou se é melhor deixá-lo como input normal.
- Rajadas em paralelo. O cache só fica disponível depois que a primeira resposta começa a ser gerada. Se você dispara 50 jobs simultâneos com o mesmo prefixo, como numa classificação em massa, vários deles escrevem o mesmo cache. Dá para mandar uma chamada na frente para aquecer e só então liberar o resto. Para volume offline, a Batch API costuma ser a ferramenta certa.
- TTL de 1 hora sem volume. Só vale quando você tem certeza de pelo menos duas leituras por hora no mesmo prefixo e o intervalo entre elas passa de 5 minutos, como um agente interno usado ao longo do expediente.
- Rollout de prompt alternando versões. Se metade das requisições usa a versão A e metade a B, você mantém dois caches. É aceitável durante um rollout gradual, como descrevi em versionar prompts no Laravel. Só não sorteie a versão por requisição: fixe por tenant ou por conversa.
Conclusão: o que fazer amanhã no seu agente
Se você já tem um agente em produção, o caminho com melhor retorno é este:
- Comece a logar
cache_read_input_tokensecache_creation_input_tokensantes de mudar qualquer coisa. Sem esses números, você não sabe se a mudança ajudou. - Reordene o prompt por frequência de mudança: tools e instruções base, depois o contexto do tenant, depois a conversa. Coloque um breakpoint no fim de cada camada e outro na última mensagem.
- Varra o prefixo atrás de conteúdo dinâmico: hora, nome do cliente, tools condicionais e ordem não determinística. Tire tudo isso do prefixo ou leve para depois do último breakpoint.
- Troque a janela deslizante de um turno por cortes em lote.
- Escreva o teste de prefixo estável para que ninguém desfaça isso sem querer daqui a três meses.
O cache_control é uma linha de código. O trabalho de verdade é disciplina de ordenação, e ela continua rendendo enquanto o agente estiver no ar.