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:

  1. O cache é por prefixo exato. Um caractere diferente antes do breakpoint e você paga uma escrita nova.
  2. Você pode ter até 4 breakpoints por requisição.
  3. 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.
  4. 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.
  5. 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:

  1. Tools + instruções base: iguais para todos os clientes, mudam só no deploy.
  2. Contexto do tenant: serviços, horários e políticas do estabelecimento. Muda raramente e é igual em todas as conversas daquele tenant.
  3. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. Comece a logar cache_read_input_tokens e cache_creation_input_tokens antes de mudar qualquer coisa. Sem esses números, você não sabe se a mudança ajudou.
  2. 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.
  3. 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.
  4. Troque a janela deslizante de um turno por cortes em lote.
  5. 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.