Algumas semanas atrás, meu agent de pesquisa começou a custar quatro vezes mais do que tinha custado na semana anterior. Mesma funcionalidade, mesmo tráfego, mesmo modelo. O resultado parecia bom. A fatura, não.
Passei uma tarde adicionando instruções print antes de admitir o óbvio: eu não tinha ideia do que o agent estava realmente fazendo em qualquer execução específica. Ele fazia um número variável de chamadas de ferramentas, tentava novamente em falhas que nunca registrei, e ocasionalmente caía em um loop onde relia o mesmo documento três vezes antes de desistir. Nada disso aparecia em lugar nenhum, exceto na fatura.
O culpado acabou sendo um caminho de repetição. Uma ferramenta instável estava expirando, o agent tentava novamente silenciosamente com o contexto completo reenviado a cada vez, e uma execução "bem-sucedida" estava fazendo nove chamadas ao modelo em vez de duas. Só o encontrei porque finalmente anotei o que cada etapa estava fazendo. Aquela tarde me custou mais do que um mês de qualquer ferramenta de rastreamento.
A versão curta: um agent é um loop que você não controla completamente, e você não pode melhorar um loop que não consegue observar. Você precisa capturar, por execução, o que aconteceu em cada etapa, quantos tokens foram consumidos, quanto custou, quanto tempo levou e se falhou. Você não precisa de uma plataforma para começar. Você precisa de um log.
O que capturar de verdade
Esqueça o tooling por um momento. A questão é: quando uma execução dá errado, o que você gostaria de saber? Trabalhe de trás para frente e você terá a lista.
Para cada execução de agent, quero um trace — a execução inteira — feito de spans, um por etapa. Cada span deve conter:
- Que tipo de etapa foi — uma chamada ao modelo, uma chamada de ferramenta, uma recuperação, a invocação do agent de nível superior.
- Uso de tokens — tokens de entrada e tokens de saída, separadamente. Eles custam valores diferentes e revelam coisas diferentes.
- Custo — derivado dos tokens e do preço do modelo. Calcule uma vez, armazene no span.
- Latência — hora de início e fim. Execuções lentas e execuções caras geralmente são problemas diferentes.
- Chamadas de ferramentas — qual ferramenta, quais argumentos, o que retornou (ou como falhou).
- Resultado — a etapa teve êxito, falhou ou tentou novamente, e por quê.
- Pontuação Eval — se você pontuar a execução (deveria), anexe a pontuação para correlacionar qualidade com custo.
Esse último ponto é o que as pessoas pulam, e é o que mais compensa. Um trace diz o que aconteceu; uma pontuação eval diz se foi importante. Juntá-los permite fazer a única pergunta que importa: a versão cara é realmente melhor? Frequentemente não é.
Note o que não está na lista: texto completo do prompt e da resposta em cada span por padrão. Capturar o conteúdo das mensagens é útil para depuração e arriscado para privacidade e custo de armazenamento. Capturo isso por trás de um flag que posso ativar para uma execução específica, não sempre ativo para tudo.
As convenções GenAI do OpenTelemetry como linha de base
Antes de inventar seus próprios nomes de campo, veja o que já existe. O OpenTelemetry — o mesmo padrão neutro de fornecedor que você usaria para qualquer rastreamento de backend — tem um conjunto de convenções semânticas GenAI: um vocabulário acordado para nomear as coisas que acabei de listar.
A parte útil são os nomes de atributos. Um span de chamada ao modelo recebe gen_ai.request.model (qual modelo), gen_ai.usage.input_tokens e gen_ai.usage.output_tokens (a divisão de tokens), gen_ai.response.finish_reasons (por que parou — stop, tool_calls, etc.), e gen_ai.provider.name para marcar o provedor. Há até atributos para contabilidade de cache de prompt — gen_ai.usage.cache_read.input_tokens e gen_ai.usage.cache_creation.input_tokens — que importam muito quando o cache entra em cena, porque a entrada em cache pode ser uma ordem de magnitude mais barata. Os nomes dos spans seguem o formato {gen_ai.operation.name} {gen_ai.request.model}, e as convenções no nível do agent definem operações como invoke_agent (a execução completa), chat (uma única chamada ao modelo) e execute_tool (uma invocação de ferramenta). (OTel GenAI spans, OTel GenAI observability blog)
Dois avisos honestos. Primeiro, em meados de 2026, essas convenções ainda estão marcadas com status Development — estão se estabilizando, mas não estão congeladas, então os nomes de atributos ainda podem mudar. Recentemente migraram para seu próprio repositório, o que indica que estão sendo levadas a sério e que ainda estão em movimento. Segundo, você não precisa adotar a maquinaria OTel para se beneficiar. Mesmo que escreva um log JSON simples, use esses nomes de campo. No dia em que superar seu log caseiro e migrar para um backend real, seus dados já falam o idioma. É a compatibilidade futura mais barata que você jamais comprará.
O log JSONL caseiro que resolve 80% do problema
Aqui está a configuração que realmente uso para projetos pequenos. Um arquivo JSONL de somente acréscimo, um objeto por span, nomes de campo no estilo OTel. Sem serviço, sem SDK, sem conta.
import json, time, uuid
PRICES = { # USD per token; see your provider's pricing page
"claude-haiku-4.5": {"in": 1.00/1e6, "out": 5.00/1e6},
"claude-sonnet-4.6": {"in": 3.00/1e6, "out": 15.00/1e6},
}
def log_span(trace_id, kind, model, usage, t0, status, **extra):
p = PRICES.get(model, {"in": 0, "out": 0})
cost = usage["in"] * p["in"] + usage["out"] * p["out"]
record = {
"trace_id": trace_id,
"span_id": uuid.uuid4().hex[:12],
"gen_ai.operation.name": kind, # invoke_agent | chat | execute_tool
"gen_ai.request.model": model,
"gen_ai.usage.input_tokens": usage["in"],
"gen_ai.usage.output_tokens": usage["out"],
"cost_usd": round(cost, 6),
"latency_ms": int((time.time() - t0) * 1000),
"status": status, # ok | error | retry
**extra, # feature, tool_name, eval_score...
}
with open("traces.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
Cada etapa no seu loop de agent chama log_span com um trace_id compartilhado. Marque cada execução com a funcionalidade que serviu (feature="research", feature="summarize"). Agora você pode responder perguntas reais com uma linha sobre o arquivo:
# cost per feature, last run-set
jq -s 'group_by(.feature)[] | {feature: .[0].feature,
usd: (map(.cost_usd) | add)}' traces.jsonl
É isso. Custo por execução, atribuição por funcionalidade, latência, contagens de tentativas, e — se você escrever sua pontuação eval no span — qualidade versus custo, tudo de um arquivo que você pode ler com jq, grep ou DuckDB. O loop de tentativas que quadruplicou minha fatura teria aparecido aqui como linhas status: "retry" empilhadas sob um trace_id, três minutos de análise em vez de uma tarde de adivinhações.
A tabela de preços é a única coisa que você deve manter honesta. Os tokens de saída custam atualmente cerca de 5 vezes os tokens de entrada nos níveis atuais do Claude, e as tarifas que usei acima eram de $1/$5 por milhão para Haiku 4.5 e $3/$15 para Sonnet 4.6 quando verifiquei — mas as tarifas mudam e descontos de cache/lote movem muito o número real, então trate qualquer tabela como um instantâneo e verifique a página do seu provedor. (Claude API pricing breakdown)
Atribuição de custo de tokens: a pergunta que realmente vale
É aqui que a disciplina comprova seu valor. Uma vez que cada span carrega um custo e uma tag de funcionalidade, você pode parar de adivinhar sobre sua economia unitária.
Executei isso no meu próprio projeto e descobri que uma funcionalidade "gratuita" — um auto-resumo que disparava em cada upload de documento — representava 60% dos meus gastos com modelos, usada por talvez um décimo dos meus usuários. Não era um bug. Era uma decisão de produto que tomei às cegas. Ver o custo por funcionalidade transformou isso em uma decisão que eu poderia tomar intencionalmente: tornei-a opt-in, e a fatura caiu pela metade sem reclamações.
Esse é o ponto central da atribuição. O gasto total é um número sobre o qual você não pode agir. "A funcionalidade X custa $0,40 por execução e a funcionalidade Y custa $0,02" é um número sobre o qual você pode. E quando você combina com pontuações eval, obtém a versão mais afiada: custo por execução boa. Se o caminho caro pontua igual ao barato, o caminho caro é só um vazamento com uma descrição bonita.
Construir versus comprar: quando o log JSONL não é suficiente
O log caseiro é genuinamente suficiente por muito tempo. Deixa de ser suficiente quando você quer coisas que um arquivo não consegue fornecer barato: uma UI para navegar por um trace complexo e bagunçado, instrumentação automática para não chamar log_span manualmente em todo lugar, execuções eval conectadas ao CI, ou compartilhar traces com alguém que não vive em um terminal.
Nesse ponto, as ferramentas hospedadas valem a pena ser vistas, e o resumo honesto é que elas se agrupam pelo que fazem melhor, não por qual é "a melhor":
- Langfuse captura o prompt, resposta, uso de tokens, latência e custo por trace, tem licença MIT e é auto-hospedável — o próximo passo natural de um arquivo JSONL se você quer uma UI mas quer manter seus dados. (Foi adquirida pela ClickHouse no início de 2026, o que vale saber se a estabilidade do fornecedor importa para você.)
- Arize Phoenix é nativo do OpenTelemetry — construído sobre instrumentação OTel e OpenInference — e roda localmente via Docker, com uma sólida biblioteca open-source de métricas eval. Se você adotou as convenções OTel acima, este é o backend de menor fricção.
- LangSmith é o mais profundo se você já está no LangChain/LangGraph, e menos se não estiver.
- Braintrust inclina-se para eval-primeiro, com portões de CI que podem bloquear um merge quando a qualidade regride — útil quando a disciplina de entrega importa mais do que dashboards.
Não vou coroar nenhum. A regra de decisão que uso: se quero um dashboard, olho para Langfuse ou Phoenix; se quero portões eval no CI, olho para Braintrust; se estou profundo no LangChain, LangSmith. E se ainda não quero nenhum dos dois, fico no arquivo JSONL, porque o arquivo já responde as perguntas que tenho hoje. Vale também notar que os principais agents de codificação — Claude Code, Codex, Copilot — agora exportam telemetria OTel eles mesmos, então parte disso você obtém sem escrever nenhuma cola.
Quando isso é exagero
Deixa eu ser o que diz: para um protótipo novíssimo com três usuários e uma funcionalidade, rastreamento completo é procrastinação disfarçada de rigor. Se você consegue ler cada execução a olho nu e sua fatura mensal é um erro de arredondamento, registre as contagens de tokens e o custo, pule o resto e vá construir a coisa.
O limite honesto são dois sinais: quando uma fatura te surpreende, ou quando uma falha se esconde de você. A primeira vez que não conseguir explicar um pico de custo, ou a primeira vez que um agent falhar de um jeito que você não consegue reproduzir, você cruzou a linha — e a solução é uma tarde adicionando log_span, não uma semana erguendo uma plataforma.
Comece com o arquivo. Use os nomes de campo OTel para que o hack de hoje seja o caminho de migração de amanhã. Marque cada span com uma funcionalidade e um custo. Compre uma ferramenta no dia em que o arquivo parar de responder suas perguntas — não antes. O objetivo nunca foi observabilidade por si mesma. Foi conseguir olhar para uma execução que deu errado e saber, em três minutos, exatamente para onde foi o dinheiro.
