O problema real
Seu dashboard mostra que as chamadas de API estão normais. Mas a conta AWS subiu USD 2 mil em um mês. Você avalia o código, está igual. Verificas logs, estão iguais. Aí, três meses depois, descobre que um prompt começou a retornar 50 tokens a mais por requisição. Em 100 mil requisições, são milhões de tokens.
Aplicações com LLM precisam de observabilidade diferente de apps tradicionais. Você não quer saber só se a API respondeu; quer saber quantos tokens gastou, qual foi a latência do primeiro token (TTFT), qual é a taxa de erro semanticamente válido mas errado, etc. Este artigo mostra o que medir e como.
O que medir que é diferente
1. Tokens (input + output)
Cada token custa. Você precisa saber:
# A cada chamada de LLM:
tokens_input = response.usage.prompt_tokens
tokens_output = response.usage.completion_tokens
tokens_total = tokens_input + tokens_output
# Log estruturado
log_entry = {
"timestamp": "2026-08-01T10:00:00Z",
"request_id": "req_12345",
"model": "gpt-4o-mini",
"tokens_input": tokens_input,
"tokens_output": tokens_output,
"tokens_total": tokens_total,
"cost": (tokens_input * 0.00015 + tokens_output * 0.0006) / 1000
}
Meta recomendada:
- Rastreie tokens por modelo, por usuário, por feature
- Compare input vs output (se output cresce, algo está errado)
2. Latência: TTFT (Time To First Token)
É a latência até o primeiro token chegar. Usuários percebem isso imediatamente.
import time
start = time.time()
response = client.chat.completions.create(...)
ttft = (time.time() - start) * 1000 # em ms
log_entry["ttft_ms"] = ttft
log_entry["latency_total_ms"] = (time.time() - start) * 1000
Meta recomendada:
- Mediana < 300ms (aceitável)
- p99 < 1.000ms (limite)
- Se degradar de repente, algo está congestionado (fila, limite de rate)
3. Taxa de recusa (refusal rate)
O modelo recusa responder por questões de segurança/policy.
def check_if_refused(response_text: str) -> bool:
"""Detecta se modelo recusou."""
refusal_phrases = [
"cannot help",
"inappropriate",
"policy violation",
"não posso"
]
return any(phrase in response_text.lower() for phrase in refusal_phrases)
refused = check_if_refused(response.choices[0].message.content)
log_entry["refused"] = refused
Meta recomendada:
- Monitore taxa de recusa por tipo de pergunta
- Se sobe de repente, policy pode ter mudado
- Se usuários reclamam, investigar
4. Taxa de alucinação (hallucination detection)
LLMs inventam coisas. Você pode detectar algumas offline, outras com LLM-as-judge.
Método 1: Detecção simples (quando há verdade ground)
def contains_hallucination(response: str, ground_truth: list[str]) -> bool:
"""Detecta se resposta contém fatos não contidos no contexto."""
from datetime import datetime
# Se resposta menciona uma data não no contexto
# Se resposta cita uma fonte não fornecida
# Se resposta menciona um número não no contexto
# Exemplo simples: procura por "[source:" ou "[citação:"
return "[source:" not in response and ("according to" in response.lower())
hallucinated = contains_hallucination(response_text, documents)
log_entry["hallucinated"] = hallucinated
Método 2: LLM-as-judge (mais preciso, mais caro)
def rate_hallucination_with_llm(response: str, context: str) -> float:
"""Usa outro LLM para avaliar se resposta alucina.
Retorna: 0.0 (sem alucinação) a 1.0 (pura alucinação)
"""
client = OpenAI()
judge_prompt = f"""Leia o CONTEXTO e a RESPOSTA.
A resposta contém informações não mencionadas no contexto?
CONTEXTO:
{context}
RESPOSTA:
{response}
Responda com um número de 0 a 1:
0 = resposta está 100% baseada no contexto
1 = resposta alucina completamente
Apenas o número:"""
judge_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": judge_prompt}],
temperature=0
)
try:
score = float(judge_response.choices[0].message.content.strip())
return min(max(score, 0.0), 1.0)
except:
return 0.5 # fallback
hallucination_score = rate_hallucination_with_llm(response_text, context)
log_entry["hallucination_score"] = hallucination_score
Cuidado: LLM-as-judge é caro (USD 0,05-0,10 por avaliação). Use apenas em amostra (5-10%).
5. Custo por requisição
cost_usd = (
tokens_input * 0.00015 + # OpenAI GPT-4o-mini input
tokens_output * 0.0006 # GPT-4o-mini output
) / 1000
log_entry["cost_usd"] = cost_usd
# Agregue por dia
daily_cost = sum([entry["cost_usd"] for entry in logs if same_day])
Meta recomendada:
- Custo por usuário / por feature
- Monitore custo acumulado vs. orçamento
- Se outlier (USD 10 por requisição), algo errado
6. Correlação: request_id
Tudo conectado:
import uuid
request_id = str(uuid.uuid4())[:12]
# Cada log tem esse ID
log_entry["request_id"] = request_id
# Você consegue fazer uma query: "mostre-me todos os eventos do request_id abc"
# E vê o trace completo
Instrumentando seu código
Wrapper para OpenAI
import json
from datetime import datetime
from openai import OpenAI
class ObservedOpenAI(OpenAI):
"""Wrapper que observa cada chamada."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = self._get_logger()
def _get_logger(self):
"""Retorna logger (implementar com seu serviço)."""
# Você pode usar: CloudWatch, DataDog, Splunk, etc.
class SimpleLogger:
def log(self, entry):
print(json.dumps(entry, default=str))
return SimpleLogger()
def chat_completions_create(self, *args, **kwargs):
"""Override que registra."""
request_id = kwargs.pop("request_id", str(uuid.uuid4())[:12])
import time
start = time.time()
response = super().chat.completions.create(*args, **kwargs)
duration = time.time() - start
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"request_id": request_id,
"model": response.model,
"tokens_input": response.usage.prompt_tokens,
"tokens_output": response.usage.completion_tokens,
"tokens_total": response.usage.prompt_tokens + response.usage.completion_tokens,
"duration_ms": duration * 1000,
"cost_usd": (
response.usage.prompt_tokens * 0.00015 +
response.usage.completion_tokens * 0.0006
) / 1000
}
self.logger.log(log_entry)
return response
# Uso
client = ObservedOpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "olá"}],
request_id="my_req_123"
)
Avaliação online vs offline
Online
Avalia enquanto o usuário usa:
- Real-time
- Baseada em comportamento real
- Problema: pode ser lenta, custa tokens
Exemplo:
# Avalia em produção, 10% das requisições
if random.random() < 0.1:
hallucination_score = rate_hallucination_with_llm(response, context)
Offline
Avalia em lote, depois:
- Mais barato
- Pode ser mais sofisticado
- Problema: não é real-time
Exemplo:
# Uma vez por dia
python evaluate_responses.py --date 2026-08-01
LLM-as-judge: seus limites
LLM-as-judge é útil mas não perfeito.
O que funciona bem:
- Avaliar relevância ("essa resposta responde a pergunta?")
- Avaliar tom ("está professional?")
- Avaliar factualidade contra contexto pequeno
O que NÃO funciona bem:
- Verificar se fato é 100% preciso (requer fact-checking externo)
- Detecção robusta de viés (requer dataset anotado)
- Avaliações numérica calibradas (juiz muda opinião)
Solução: Combine com métrica manual.
def evaluate_batch_with_human_sample(responses: list, sample_size: int = 50):
"""Avalia com LLM, depois humano valida amostra."""
# LLM avalia tudo
scores = [rate_with_llm(r) for r in responses]
# Humano valida 50 aleatorios
human_scores = manual_evaluation(sample(responses, sample_size))
# Calibra scores de LLM
correlation = calculate_correlation(scores[:sample_size], human_scores)
print(f"Correlação LLM vs Humano: {correlation:.2f}")
return scores, correlation
Dashboard de observabilidade
Seu painel deve responder:
| Pergunta | Métrica | Meta |
|---|---|---|
| Quanto estou gastando? | Custo total diário (USD) | < Orçamento |
| Que modelos custam mais? | Custo por modelo | Identificar outliers |
| Como é a latência? | TTFT mediana / p99 | Med < 300ms, p99 < 1s |
| Taxa de erro semanticamente válido? | Hallucination score, recusal rate | < 2% |
| Qual feature custa mais? | Custo por endpoint | Investigar se anormal |
| Está degradando? | Tokens por requisição ao longo do tempo | Alertar se subir 20%+ |
import pandas as pd
# Agregação simples
df = pd.DataFrame(logs)
# Custo por dia
daily = df.groupby(df['timestamp'].dt.date).agg({
'cost_usd': 'sum',
'tokens_total': 'sum',
'request_id': 'count'
}).rename(columns={'request_id': 'num_requests'})
print(daily)
Armadilhas comuns
"Rastreio tudo, 100% das requisições"
Vai poluir seus logs. Rastreie 100% de métricas agrupadas (custo total), mas trace detalhado apenas em amostra.
"LLM-as-judge resolve tudo"
Não resolve. Combine com métricas simples e validação manual.
"Se TTFT é bom, tudo está bom"
Não. TTFT pode ser rápido mas saída ser alucinação. Mede múltiplas dimensões.
"Depois eu configuro observabilidade"
Você esquece. Configure desde o dia 1. Leva 2 horas.
Quando não monitorar
- Protótipo de 1 dia (ok ignorar, mas não em produção)
- Menos de 100 requisições/mês (overhead > benefício)
Próximos passos
- Integre com FinOps para IA para alertas de orçamento.
- Investigate alucinações com RAG na prática como mitigation.
- Melhore estrutura de prompts com Engenharia de prompt em produção.
- Proteja com Segurança em aplicações com LLM.
Checklist: observabilidade pronta
- Registra tokens por requisição
- Calcula custo por requisição
- Mede TTFT
- Detecta taxa de recusa
- Avalia alucinação em amostra (5-10%)
- Correlação com request_id
- Dashboard com 6+ métricas
- Alertas para outliers (custo +50%, TTFT +100%)
- Teste com dados reais
Observabilidade é seu radar. Sem ela, você está voando cego.