Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Calcular o custo real de um pipeline de LLM considerando tokens de input, output, cache hit e cache miss
Implementar Prompt Caching da Anthropic com cache_control: ephemeral em system prompts e documentos de referência
Monitorar uso de tokens por request com a classe MetricasUso e identificar onde o custo está concentrado
Aplicar estratégias de otimização: seleção dinâmica de modelo, caching de prefixo, compressão de contexto
Interpretar as métricas de cache_creation_input_tokens vs cache_read_input_tokens para avaliar efetividade do cache
Por que isso importa
O custo de API de LLM não é fixo — varia 100x dependendo de como você usa os modelos. Uma equipe que processa 10 milhões de tokens por dia com Claude Sonnet sem otimização paga cerca de $30/dia ($900/mês). Com Prompt Caching ativo em system prompts longos que repetem, esse mesmo volume pode cair para $12/dia — 60% de redução.
Em 2024, a Anthropic reportou que equipes usando Prompt Caching economizaram em média 73% em tokens de sistema. Para um pipeline que usa um system prompt de 8.000 tokens (documentação técnica, regras de negócio, exemplos) em cada request, o caching elimina o custo desses 8.000 tokens em todas as chamadas subsequentes dentro da janela de 5 minutos.
Mas o custo não é só de tokens. É também de latência. Um request com 10.000 tokens de contexto sem caching leva 800ms de TTFT (time to first token) em Claude Sonnet. O mesmo request com cache hit leva 200ms — 4x mais rápido. Em aplicações interativas, isso é a diferença entre UX aceitável e UX que faz o usuário desistir.
Esta unidade te ensina a medir, entender e otimizar ambas as dimensões: custo e latência.
Conceitos Fundamentais
A Anatomia do Custo de um Request LLM
Todo request tem exatamente dois componentes de custo:
Custo total = (tokens_input × preço_input) + (tokens_output × preço_output)Para Claude Sonnet 4.6 (preços de 2025): - Input: $3.00 por 1M tokens - Output: $15.00 por 1M tokens
FinOps: Quatro Preços Distintos e Alavancas de Custo
Uma chamada de API de LLM tem até quatro linhas de custo separadas: input (tokens do prompt), output (tokens gerados — tipicamente 3–5× mais caro que input), cache_creation (criação de cache do prefixo — 1.25× o preço de input), cache_read (leitura de cache — 0.1× o preço de input). Alavancas de redução de output: defina max_tokens explicitamente, peça respostas concisas no prompt, prefira saída estruturada (JSON com campos curtos) a texto livre. Break-even do cache: se o prefixo é lido mais de ~2 vezes dentro do TTL, o cache já pagou o custo de criação. Batch API: 50% de desconto para jobs assíncronos com até 24h de latência — ideal para cargas offline de alto volume.
Por que output é 5x mais caro? Geração de tokens é mais custosa computacionalmente que processamento de input. O modelo precisa executar forward pass para cada token gerado sequencialmente.
Implicação prática: Reduza tokens de output quando possível. “Responda em JSON com campos X, Y, Z” é mais eficiente que “Explique detalhadamente X, Y e Z”. Para tarefas de classificação, max_tokens=50 é suficiente — não use 500.
O Que É Prompt Caching
Fundamento: KV-Cache da Atenção
A cada token do prompt, o modelo calcula vetores Key (K) e Value (V) para aquele token — são eles que os outros tokens consultam na atenção. Se o prefixo do prompt não mudou, esses K e V já foram calculados na requisição anterior e podem ser reutilizados. O prompt caching funciona assim: o provedor guarda os K e V do prefixo em memória por alguns minutos. Na requisição seguinte com o mesmo prefixo, pula o recálculo e cobra menos. Consequência de design: qualquer alteração nos tokens antes do ponto de cache invalida o cache inteiro — por isso o padrão é colocar o conteúdo estático (system prompt, documentos de referência) no início e o conteúdo dinâmico (pergunta do usuário) no fim.
Prompt Caching é um mecanismo da Anthropic onde partes do prompt que não mudam entre requests são processadas uma vez e o KV-cache (Key-Value cache) da atenção é guardado por 5 minutos.
Sem caching:
Request 1: [system: 8000 tokens] + [user: 50 tokens] → paga 8050 tokens de input
Request 2: [system: 8000 tokens] + [user: 60 tokens] → paga 8060 tokens de input
Request 3: [system: 8000 tokens] + [user: 45 tokens] → paga 8045 tokens de input
Total 3 requests: 24.155 tokens de inputCom caching:
Request 1: [system: 8000 tokens] + [user: 50 tokens]
→ cache_creation: 8000 tokens (paga 25% a mais na criação)
→ tokens efetivos: 8050 tokens input + overhead de criação
Request 2: [system: cache hit] + [user: 60 tokens]
→ cache_read: 8000 tokens (paga 10% do preço normal!)
→ tokens efetivos: 50 (user) + 480 (8000 × 6% custo do cache read)
Request 3: [system: cache hit] + [user: 45 tokens]
→ similar ao request 2Resultado: requests 2 e 3 são ~90% mais baratos em input. O breakeven é atingido depois de 2 requests com cache hit.
Preços de Cache da Anthropic
Cache creation: 1.25× o preço normal de input
Cache read: 0.1× o preço normal de input (10% = 90% de desconto!)Para Claude Sonnet ($3/1M input): - Cache creation: $3.75/1M tokens (25% mais caro que normal) - Cache read: $0.30/1M tokens (90% mais barato que normal)
Quando compensa usar caching? Se você usa o mesmo prefixo em N requests dentro de 5 minutos:
Sem cache: N × 8000 × $3/1M = N × $0.024
Com cache: 8000 × $3.75/1M (criação) + (N-1) × 8000 × $0.30/1M (leituras)
= $0.030 + (N-1) × $0.0024
Break-even: N onde com_cache < sem_cache
$0.030 + (N-1) × $0.0024 < N × $0.024
$0.030 - $0.0024 < N × ($0.024 - $0.0024)
$0.0276 < N × $0.0216
N > 1.28
→ Com 2 ou mais requests no mesmo cache TTL, já economizaO Header cache_control
Para ativar caching em um bloco do sistema, você adiciona cache_control: {'type': 'ephemeral'} ao conteúdo que quer cachear. O modelo cria um “checkpoint” no KV-cache até aquele ponto.
# Estrutura para system com cache
system = [
{
'type': 'text',
'text': 'Seu system prompt longo aqui...',
'cache_control': {'type': 'ephemeral'} # marca o ponto de cache
}
]O cache é criado até o último bloco marcado com cache_control. Blocos depois do marcador não são cacheados — geralmente o conteúdo dinâmico (a pergunta do usuário) fica fora do cache.
Estratégias de Otimização Além do Caching
1. Seleção dinâmica de modelo por complexidade
def escolher_modelo(complexidade: str) -> str:
return {
'simples': 'claude-haiku-4-5-20251001', # classificação, extração simples
'media': 'claude-haiku-4-5-20251001', # summarização curta
'complexa': 'claude-sonnet-4-6', # raciocínio multi-step
'critica': 'claude-opus-4-8', # análise de alta qualidade
}.get(complexidade, 'claude-haiku-4-5-20251001')Autocompletar de código, classificação de sentimentos, extração de campos simples: Haiku (12x mais barato que Sonnet).
2. Compressão de contexto Para histórico de conversa longo, em vez de passar todo o histórico:
# INEFICIENTE: passa 50 turnos de histórico
messages = historico_completo + [novo_turno]
# EFICIENTE: sumariza histórico antigo, mantém últimos 5 turnos verbatim
resumo = sumarizar_historico(historico_antigo)
messages = [
{'role': 'user', 'content': f'Contexto anterior: {resumo}'},
{'role': 'assistant', 'content': 'Entendido.'},
] + ultimos_5_turnos + [novo_turno]3. Batch API (OpenAI e Anthropic) Para processamento não-urgente (análise de documentos, geração de relatórios): - Anthropic Message Batches API: 50% de desconto, até 24h de latência - OpenAI Batch API: 50% de desconto, até 24h de latência
Processar 1000 análises de documentos com batch em vez de requests individuais reduz custo pela metade.
4. Streaming para percepção de latência Com streaming, o usuário vê tokens chegando progressivamente — a latência percebida é o TTFT (200-500ms), não o tempo total de resposta (2-10s). Para interfaces conversacionais, streaming sempre.
Aprofundamento Técnico
O Response de Uso do Anthropic
Quando você faz um request com caching ativo, o response inclui:
response.usage = {
'input_tokens': 50, # tokens não-cacheados (user message)
'output_tokens': 120, # tokens gerados
'cache_creation_input_tokens': 8000, # tokens que criaram cache (primeira vez)
'cache_read_input_tokens': 0, # tokens lidos do cache (= 0 na criação)
}
# No segundo request (cache hit):
response.usage = {
'input_tokens': 60,
'output_tokens': 115,
'cache_creation_input_tokens': 0, # não criou novo cache
'cache_read_input_tokens': 8000, # 8000 tokens lidos do cache a 10% do preço
}Como calcular o custo real com cache:
def custo_usd(uso: dict, modelo: str = 'claude-sonnet-4-6') -> float:
precos = {
'claude-sonnet-4-6': {
'input': 3.0 / 1_000_000,
'output': 15.0 / 1_000_000,
'cache_creation': 3.75 / 1_000_000,
'cache_read': 0.30 / 1_000_000,
},
'claude-haiku-4-5-20251001': {
'input': 0.25 / 1_000_000,
'output': 1.25 / 1_000_000,
'cache_creation': 0.30 / 1_000_000, # 25% mais caro
'cache_read': 0.025 / 1_000_000, # 10% do preço normal
},
}
p = precos[modelo]
return (
uso.get('input_tokens', 0) * p['input'] +
uso.get('output_tokens', 0) * p['output'] +
uso.get('cache_creation_input_tokens', 0) * p['cache_creation'] +
uso.get('cache_read_input_tokens', 0) * p['cache_read']
)Implementando MetricasUso
A classe MetricasUso do starter serve para acumular estatísticas ao longo de múltiplos requests:
from dataclasses import dataclass, field
@dataclass
class MetricasUso:
"""Acumula métricas de múltiplos requests para análise."""
total_requests: int = 0
input_tokens: int = 0
output_tokens: int = 0
cache_creation_input_tokens: int = 0
cache_read_input_tokens: int = 0
def atualizar(self, usage) -> None:
"""Adiciona uso de um response ao acumulado."""
self.total_requests += 1
self.input_tokens += usage.input_tokens
self.output_tokens += usage.output_tokens
# Campos de cache podem não existir em respostas sem caching
self.cache_creation_input_tokens += getattr(usage, 'cache_creation_input_tokens', 0)
self.cache_read_input_tokens += getattr(usage, 'cache_read_input_tokens', 0)
def custo_total_usd(self, modelo: str = 'claude-sonnet-4-6') -> float:
return custo_usd({
'input_tokens': self.input_tokens,
'output_tokens': self.output_tokens,
'cache_creation_input_tokens': self.cache_creation_input_tokens,
'cache_read_input_tokens': self.cache_read_input_tokens,
}, modelo)
def eficiencia_cache(self) -> float:
"""Percentual dos tokens de input que vieram do cache."""
total_input = self.input_tokens + self.cache_read_input_tokens
if total_input == 0:
return 0.0
return self.cache_read_input_tokens / total_input
def resumo(self) -> dict:
return {
'requests': self.total_requests,
'tokens': {
'input': self.input_tokens,
'output': self.output_tokens,
'cache_created': self.cache_creation_input_tokens,
'cache_read': self.cache_read_input_tokens,
},
'eficiencia_cache': f"{self.eficiencia_cache():.1%}",
'custo_estimado_usd': round(self.custo_total_usd(), 6),
}O Parâmetro system com Cache no Anthropic SDK
A forma mais comum de usar caching é em system prompts longos que repetem entre requests:
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT_LONGO = """
Você é um especialista em análise de código Python.
REGRAS DE ANÁLISE:
1. Sempre identifique complexidade ciclomática
2. Verifique nomenclatura de variáveis segundo PEP8
3. Analise potencial de Memory Leaks
4. Verifique tratamento de exceções
...
[mais 5000 palavras de regras e exemplos]
"""
def analisar_codigo(codigo: str, metricas: MetricasUso) -> str:
response = client.messages.create(
model='claude-sonnet-4-6',
max_tokens=500,
system=[
{
'type': 'text',
'text': SYSTEM_PROMPT_LONGO,
'cache_control': {'type': 'ephemeral'}, # cacheia este bloco
}
],
messages=[
{'role': 'user', 'content': f'Analise este código:\n\n```python\n{codigo}\n```'}
]
)
metricas.atualizar(response.usage)
return response.content[0].textNa primeira chamada: cache_creation_input_tokens = len(SYSTEM_PROMPT_LONGO em tokens). Na segunda chamada dentro de 5 min: cache_read_input_tokens = mesmo número, com 90% de desconto.
Exemplos Anotados
Exemplo 1: Comparação Com e Sem Caching
import anthropic
import time
client = anthropic.Anthropic()
# System prompt longo que simula documentação técnica (normalmente 2000-8000 tokens)
SYSTEM = """Você analisa código Python. Regras:
1. Complexidade: identifique loops aninhados, recursão excessiva
2. Nomenclatura: PEP8 — snake_case para variáveis, PascalCase para classes
3. Imports: sem imports não usados, sem star imports
4. Exceptions: nunca use except sem tipo específico
5. Type hints: funções públicas devem ter type hints
[... imagine mais 4000 tokens de regras aqui ...]"""
CODIGOS = [
"def soma(a, b):\n return a+b",
"def calc(x,y,z):\n result=x*y+z\n return result",
"class minha_classe:\n def __init__(self):\n pass",
]
# === SEM CACHE ===
print("=== Sem cache ===")
total_input_sem_cache = 0
for codigo in CODIGOS:
r = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=200,
system=SYSTEM, # string simples, sem cache_control
messages=[{'role': 'user', 'content': f'Analise:\n{codigo}'}]
)
# Cada request paga o system prompt inteiro
total_input_sem_cache += r.usage.input_tokens
print(f" Input tokens: {r.usage.input_tokens}")
print(f"Total input tokens: {total_input_sem_cache}")
# Output esperado: ~(system_tokens + user_tokens) × 3
# === COM CACHE ===
print("\n=== Com cache ===")
total_input_com_cache = 0
cache_criado = False
for i, codigo in enumerate(CODIGOS):
r = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=200,
system=[
{
'type': 'text',
'text': SYSTEM,
'cache_control': {'type': 'ephemeral'}, # cacheia o system
}
],
messages=[{'role': 'user', 'content': f'Analise:\n{codigo}'}]
)
# Primeiro request: cache_creation_input_tokens > 0
# Requests seguintes: cache_read_input_tokens > 0 (muito mais barato)
criacao = getattr(r.usage, 'cache_creation_input_tokens', 0)
leitura = getattr(r.usage, 'cache_read_input_tokens', 0)
if i == 0:
print(f" Request 1 — Cache creation: {criacao} tokens")
else:
print(f" Request {i+1} — Cache read: {leitura} tokens (90% desconto)")
# Custo efetivo: input normal + cache_read × 0.1
tokens_efetivos = r.usage.input_tokens + leitura * 0.1
total_input_com_cache += tokens_efetivos
print(f"\nCusto efetivo com cache: {total_input_com_cache:.1f} token-equivalentes")
# vs total_input_sem_cache — diferença visível no 2º e 3º requestExemplo 2: LLMWrapper Completo com Caching e Métricas
import os
import anthropic
from dataclasses import dataclass, field
client = anthropic.Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY'))
@dataclass
class MetricasUso:
total_requests: int = 0
input_tokens: int = 0
output_tokens: int = 0
cache_creation_input_tokens: int = 0
cache_read_input_tokens: int = 0
def atualizar(self, usage) -> None:
self.total_requests += 1
self.input_tokens += usage.input_tokens
self.output_tokens += usage.output_tokens
# getattr com default 0 para compatibilidade sem caching
self.cache_creation_input_tokens += getattr(usage, 'cache_creation_input_tokens', 0) or 0
self.cache_read_input_tokens += getattr(usage, 'cache_read_input_tokens', 0) or 0
def custo_haiku_usd(self) -> float:
"""Custo com preços de Haiku 4.5."""
return (
self.input_tokens * 0.25 / 1_000_000 +
self.output_tokens * 1.25 / 1_000_000 +
self.cache_creation_input_tokens * 0.30 / 1_000_000 + # 1.25× normal
self.cache_read_input_tokens * 0.025 / 1_000_000 # 0.1× normal
)
def economia_cache_usd(self) -> float:
"""Quanto foi economizado pelo cache (vs pagar preço normal)."""
economia_por_token_cache = (0.25 - 0.025) / 1_000_000 # diferença de preço
return self.cache_read_input_tokens * economia_por_token_cache
class LLMWrapper:
"""Wrapper com caching de system prompt e rastreamento de métricas."""
def __init__(self, system: str, modelo: str = 'claude-haiku-4-5-20251001'):
self.system = system
self.modelo = modelo
self.metricas = MetricasUso()
def chamar(self, prompt: str, max_tokens: int = 300) -> str:
"""Chama o LLM com system cacheado."""
response = client.messages.create(
model=self.modelo,
max_tokens=max_tokens,
# TODO 1: system com cache_control
# system=[{'type': 'text', 'text': self.system, 'cache_control': {'type': 'ephemeral'}}]
system=self.system, # sem cache por enquanto
messages=[{'role': 'user', 'content': prompt}]
)
# TODO 2: atualizar self.metricas com response.usage
# self.metricas.atualizar(response.usage)
return response.content[0].text
def relatorio(self) -> dict:
return {
'total_requests': self.metricas.total_requests,
'custo_usd': round(self.metricas.custo_haiku_usd(), 6),
'economia_cache_usd': round(self.metricas.economia_cache_usd(), 6),
'cache_read_tokens': self.metricas.cache_read_input_tokens,
}
# Teste: mesmo sistema com 5 perguntas diferentes
SYSTEM = """Você é um especialista em Python. Responda de forma objetiva.
Regra: sempre inclua exemplo de código na resposta.
Regra: máximo 3 frases de explicação.
[imagine um system prompt longo de 2000+ tokens aqui]"""
wrapper = LLMWrapper(SYSTEM)
perguntas = [
"Como criar uma dataclass em Python?",
"Quando usar @classmethod vs @staticmethod?",
"Como funciona o GIL do Python?",
"O que é um context manager?",
"Como usar type hints em funções?",
]
for p in perguntas:
resposta = wrapper.chamar(p)
print(f"Q: {p[:50]}...")
print(f"R: {resposta[:100]}...")
print()
print("=== Relatório de Uso ===")
import json
print(json.dumps(wrapper.relatorio(), indent=2))Padrões e Armadilhas
Padrões Corretos
Padrão 1: Coloque system prompts estáticos como primeiros blocos cacheáveis
# CORRETO: parte estática antes, parte dinâmica depois
system = [
{
'type': 'text',
'text': REGRAS_ESTATICAS_LONGAS, # 8000 tokens que não mudam
'cache_control': {'type': 'ephemeral'}, # ← cacheia até aqui
},
{
'type': 'text',
'text': f'Contexto do usuário: {user_context}', # dinâmico, não cacheado
}
]Padrão 2: Medir eficiência do cache antes de confiar nele
# Sempre logar cache_creation vs cache_read para saber se está funcionando
if getattr(response.usage, 'cache_read_input_tokens', 0) > 0:
print("Cache HIT ✓")
elif getattr(response.usage, 'cache_creation_input_tokens', 0) > 0:
print("Cache CREATED — próximo request será mais barato")
else:
print("Sem cache — verifique se cache_control foi aplicado corretamente")Padrão 3: Usar getattr com default 0 para compatibilidade
# O campo cache_creation_input_tokens pode não existir em alguns responses
# (sem caching ativo ou versões antigas do SDK)
criacao = getattr(response.usage, 'cache_creation_input_tokens', 0) or 0
leitura = getattr(response.usage, 'cache_read_input_tokens', 0) or 0Armadilhas
⚠️ Armadilha 1: Cache com TTL de 5 minutos — não use para dados altamente variáveis
# PROBLEMA: se o "contexto fixo" muda a cada 4 minutos,
# você paga cache_creation em cada request (25% mais caro que sem cache)
system = [
{
'type': 'text',
'text': f'Relatório atual: {relatorio_muda_toda_hora}',
'cache_control': {'type': 'ephemeral'}, # criado mas nunca lido do cache
}
]
# CORRETO: só coloca no cache o que é verdadeiramente estático⚠️ Armadilha 2: Não confundir input_tokens com tokens totais de input
response.usage.input_tokens # tokens NÃO cacheados (dinâmicos)
response.usage.cache_read_input_tokens # tokens lidos do cache
# Tokens de input TOTAIS = input_tokens + cache_read_input_tokens
# Para calcular custo, use cada campo com seu preço específico — não some e aplique preço único
tokens_totais_nao_cacheados = response.usage.input_tokens
tokens_do_cache = response.usage.cache_read_input_tokens
# Cada um tem preço diferente!⚠️ Armadilha 3: cache_control em messages em vez de system
# ERRADO: tentando cachear na mensagem de usuário (não suportado da mesma forma)
messages = [
{'role': 'user', 'content': [
{'type': 'text', 'text': doc_longo, 'cache_control': {'type': 'ephemeral'}}
]}
]
# O cache de mensagens funciona mas tem semântica diferente — sistema é mais efetivo
# para contexto que repete entre requests⚠️ Armadilha 4: Não rastrear métricas e descobrir o custo pela fatura
# SEM monitoramento: fatura do mês vem R$ 5000 e você não sabe por quê
response = client.messages.create(...)
# COM monitoramento: você vê o problema em tempo real
response = client.messages.create(...)
logger.info('llm_cost', extra={
'input_tokens': response.usage.input_tokens,
'output_tokens': response.usage.output_tokens,
'cache_read_tokens': getattr(response.usage, 'cache_read_input_tokens', 0),
'custo_estimado_usd': calcular_custo(response.usage),
})⚠️ Armadilha 5: Esperar que o cache funcione com system como string simples
# NÃO cria cache — sistema como string não tem cache_control
system='Você é um assistente especialista...'
# CRIA cache — sistema como lista de blocos com cache_control
system=[{'type': 'text', 'text': '...', 'cache_control': {'type': 'ephemeral'}}]Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter tem 2 TODOs que implementam caching e métricas:
TODO 1 — Em LLMWrapper.chamar(), o parâmetro system está passando como string simples. Substitua por lista de blocos com cache_control. A estrutura é:
system=[{
'type': 'text',
'text': self.system,
'cache_control': {'type': 'ephemeral'}
}]Isso marca o system prompt inteiro como cacheável. Seção de referência: Conceitos Fundamentais → “O Header cache_control” e Aprofundamento Técnico → “O Parâmetro system com Cache”.
TODO 2 — Ainda em chamar(), após receber o response, chame self.metricas.atualizar(response.usage). Isso passa o objeto usage do response para a classe MetricasUso que acumula input_tokens, output_tokens, cache_creation_input_tokens e cache_read_input_tokens. Seção de referência: Aprofundamento Técnico → “Implementando MetricasUso”.
Após implementar, o main() do starter faz 5 perguntas com o mesmo wrapper e imprime o relatório de métricas. Na primeira chamada você verá cache_creation_input_tokens > 0. Nas chamadas seguintes (dentro de 5 minutos), verá cache_read_input_tokens > 0 e o custo acumulado mostrará a economia.
Experimento adicional: compare o custo de 5 requests COM caching vs sem caching comentando o cache_control. A diferença em tokens pequenos não é dramática, mas imagine escalar para 10.000 requests/dia com um system prompt de 8.000 tokens — o caching economiza $21.600/mês nesses volumes.
Agora você está pronto para o lab.