🪜 Hierarquia de 3 tiers
A invalidação segue uma hierarquia de 3 tiers: uma mudança só
invalida o tier dela e os de baixo. Como a ordem é
tools → system → messages, mexer mais no começo tem raio de dano maior.
O que é
Os três tiers são camadas em cascata. O modelo é a raiz: trocá-lo invalida tudo. Depois vêm tools, system e messages, cada um invalidando a si e ao que vier depois.
Por que aprender
Porque permite calcular o "raio de dano" de qualquer mudança antes de fazê-la: alterar uma mensagem é barato; mexer em tools ou trocar o modelo custa o prefixo inteiro.
Quanto mais no começo (tools) a mudança acontece, maior a cascata. Uma mensagem nova no fim quase não custa.
Conceitos-chave
| Mudança | O que invalida |
|---|---|
| trocar modelo | tudo (tools + system + messages) |
mudar tools | tudo (tools + system + messages) |
mudar system | system + messages |
| mudar mensagem | só messages |
🎚️ tool_choice / thinking on-off
Nem tudo que muda por request quebra o cache. Trocar tool_choice a
cada request, ou ligar/desligar thinking,
mantém o cache de tools+system. São parâmetros que
não entram no prefixo cacheável — não precisa se preocupar com eles.
O que é
tool_choice e thinking são controles de comportamento do request, não bytes do prefixo. Alterá-los não muda o casamento de prefixo.
Por que aprender
Porque evita paranoia. É fácil achar que "qualquer coisa que eu mude por request derruba o cache" — e aí você deixa de usar recursos úteis por medo infundado.
✓ Pode variar à vontade
- ✓
tool_choicediferente por request. - ✓Ligar/desligar
thinking.
✗ Continua derrubando
- ✗Mudar a lista
tools. - ✗Editar o system prompt.
Conceitos-chave
tool_choice não entra no prefixo.
thinking on/off preserva tools+system.
O que derruba é conteúdo do prefixo.
Sem paranoia: nem todo param quebra.
🔭 20-block lookback
Cada breakpoint procura a entrada anterior olhando no máximo 20 blocos pra
trás. Um turno que adiciona mais de 20 blocos — loops com
muitos tool_use/tool_result —
pode fazer o próximo turno errar o cache silenciosamente.
O que é
O lookback é a janela de busca do breakpoint: ele só encontra o prefixo cacheado se ele estiver dentro dos 20 blocos anteriores. Passou disso, ele não acha — mesmo com o cache existindo.
Por que aprender
Porque é a causa "invisível" de miss em agentes que fazem muitas chamadas de ferramenta por turno. Nada mudou no prefixo, mas o breakpoint não alcança a entrada anterior.
Recriação ilustrativa. O que importa: em loops longos, insira um breakpoint intermediário antes de estourar os 20 blocos.
Dica prática
Se seu agente faz muitas chamadas de ferramenta por turno e o cache "some" sem que o prefixo tenha mudado, suspeite do lookback. Um breakpoint intermediário a cada ~15 blocos mantém a entrada dentro da janela.
Conceitos-chave
Lookback = 20 blocos pra trás.
Turno com >20 blocos = risco de miss.
Comum em loops de tool_use/tool_result.
Fix: breakpoint a cada ~15 blocos.
👻 Silent invalidators
Silent invalidators são bytes que mudam no prefixo sem você perceber.
Os clássicos: datetime.now()/Date.now()/uuid
no system; json.dumps sem sort_keys=True;
iterar set; tools montadas por usuário; seção condicional no system
(if flag: system += ...).
O que é
São fontes de não-determinismo que alteram o prefixo a cada request, mesmo com o cache quente. Diferente do write frio (por tempo), aqui o problema é o conteúdo mudando.
Por que aprender
Porque são a causa nº 1 de cache que "nunca funciona". O sintoma é sempre o mesmo: cache_read_input_tokens sempre 0.
✗ Silent invalidators comuns
- ✗
datetime.now()/Date.now()/ uuid no system. - ✗
json.dumpssemsort_keys=True. - ✗Iterar
set(ordem variável). - ✗Tools montadas por usuário.
- ✗Seção condicional no system (
if flag: system += ...).
✓ Correções
- ✓Injete data/hora nas
messages, não no system. - ✓Serialize com
sort_keys=True. - ✓Ordene com
sorted(...)antes de montar. - ✓Congele o system; mova o dinâmico pro fim.
Sintoma
cache_read_input_tokens sempre 0 repetindo o mesmo prefixo. É o carimbo de que tem um silent invalidator ativo.
🌐 Timing concorrente (fan-out)
A entrada de cache só fica legível DEPOIS que a 1ª resposta começa a streamar. Então N requests paralelos com o mesmo prefixo pagam todos preço cheio — nenhum viu o cache do outro ainda.
O que é
No fan-out (disparar vários agentes/requests de uma vez), a gravação do prefixo pelo primeiro request ainda não terminou quando os outros partem. Todos gravam do zero.
Por que aprender
Porque disparar tudo junto parece mais rápido, mas joga fora o cache compartilhado. Uma pequena mudança na ordem do disparo economiza muito.
Fix: mandar 1, esperar o 1º token (o cache fica legível), e só então disparar os N-1 — que agora leem barato.
Conceitos-chave
Entrada legível só após o 1º stream.
N paralelos com mesmo prefixo = todos 1×.
Fix: 1 → esperar 1º token → N-1.
Vale para fan-out / subagentes paralelos.
🧪 Verificar com usage
O instrumento de medição é o objeto usage na resposta:
cache_read_input_tokens e
cache_creation_input_tokens.
Zero em requests repetidos = tem invalidator. Aí você faz
diff dos bytes do prompt entre dois requests iguais para achar o byte
que muda.
O que é
Auditar com usage é comparar os campos de cache entre dois requests que deveriam ser idênticos. Se o read não sobe, algo no prefixo está mudando.
Por que aprender
Porque é a diferença entre chutar e provar. Com o diff de bytes você localiza o silent invalidator exato — a data, o uuid, a chave fora de ordem.
Exemplo copy-run — auditar silent invalidators do pipeline
Objetivo: fazer o Claude Code varrer o código que monta o prompt e listar cada silent invalidator + como corrigir.
Audite o pipeline em <caminho da pasta/arquivo que monta
o prompt>. Procure especificamente, no que alimenta o
PREFIXO (tools + system), por:
- datetime.now() / Date.now() / uuid interpolados no system
- json.dumps(...) SEM sort_keys=True
- iteração de set (ordem não-determinística)
- montagem de `tools` a partir de set/dict sem sorted(...)
- seções condicionais no system (if flag: system += ...)
Para cada ocorrência, aponte:
1. arquivo e linha;
2. por que quebra o cache (muda os bytes do prefixo);
3. a correção (mover o dinâmico para as messages,
sort_keys=True, sorted(...), congelar o system).
Como verificar: depois de aplicar as correções, rode o mesmo pedido duas vezes seguidas e compare o usage. O cache_read_input_tokens deve deixar de ser 0 no 2º request. Se ainda ficar 0, faça o diff dos bytes do prompt entre os dois requests para achar o que mudou.
Conceitos-chave
usage = instrumento de medição.
Read 0 repetido = invalidator ativo.
Diff de bytes localiza a mudança exata.
Provar > chutar: meça sempre.
Resumo do módulo
usage.Próxima trilha:
Trilha 3 — Boas práticas avançadas (cache vs compactação, arquitetura pró-cache, prompts prontos)