Blog

Observabilidade de LLM: tracing, custo e qualidade

Sistema com LLM em produção sem observabilidade é uma caixa preta que você paga sem entender. A resposta piorou? Não sabe dizer. A fatura triplicou? Não sabe qual rota. O bot ficou lento? Não sabe se é a fila, o modelo ou a tool. Diferente de um serviço tradicional, onde bastam latência e taxa de erro, um LLM tem três eixos que precisam ser observados juntos: latência (quanto demora), custo (quanto gasta em tokens) e qualidade (se a resposta presta). Observar só um deles engana: um modelo mais barato pode alucinar mais, um mais rápido pode custar o dobro. Este artigo mostra como instrumentar as três dimensões sem virar um projeto de plataforma: o modelo de tracing certo para LLM, como calcular custo por chamada de verdade, como medir qualidade em produção sem gabarito, o que logar sem vazar dado sensível e quais alertas evitam a surpresa no fim do mês. O foco é o mínimo que torna o sistema operável.

2026-07-08 / IA Aplicada / 13 min

01

Por que os três pilares tradicionais não bastam

A observabilidade clássica se apoia em logs, métricas e traces, e mira latência, throughput e taxa de erro. Isso responde "o serviço está de pé?", mas não responde nenhuma das perguntas que importam num sistema com LLM. Uma chamada pode retornar 200 OK, dentro do SLA de latência, e ainda assim ter alucinado a resposta, recusado indevidamente ou gasto três vezes mais tokens do que o esperado. O sucesso HTTP não diz nada sobre o sucesso semântico.

Um LLM precisa de três eixos observados em conjunto, porque eles se movem em direções opostas. Trocar de modelo para reduzir custo pode derrubar a qualidade. Encurtar o prompt para reduzir latência pode remover contexto e aumentar o retrabalho. Cada decisão mexe nos três ao mesmo tempo, e sem medir os três você otimiza um número e degrada outro sem perceber. A tabela abaixo mostra o que cada eixo exige que a observabilidade tradicional não entrega.

EixoO que medeSinal que importaPor que APM clássico não pega
LatênciaTempo por fase: fila, prompt, modelo, tools, streamingPercentil p95 por rota, não a médiaNão separa tempo de modelo de tempo de tool
CustoTokens de entrada e saída por chamada, convertidos em moedaCusto por rota e por usuário, tendência diáriaNão existe o conceito de token no APM padrão
QualidadeA resposta está correta, útil e no formato esperadoTaxa de alucinação, recusa, formato inválidoHTTP 200 não significa resposta boa

A regra prática: nunca olhe um eixo isolado. Um dashboard de LLM útil mostra latência, custo e qualidade lado a lado por rota, para que qualquer mudança revele o trade-off imediatamente. Melhorar a média sem olhar o p95, ou baixar custo sem olhar qualidade, é trocar um problema visível por um invisível.

02

O modelo de tracing certo para LLM

O tracing distribuído resolve a pergunta "onde o tempo foi gasto?", e num sistema com LLM a resposta quase nunca é óbvia. Uma única requisição do usuário pode disparar um retrieval, uma ou mais chamadas ao modelo, várias tools e um passo de pós-processamento. Sem span por fase, tudo vira um bloco único de latência e você não sabe se o gargalo é o modelo, a busca vetorial ou a API externa que a tool chamou. O modelo mental é o mesmo do tracing de microserviço, mas os atributos do span são específicos de LLM.

// observability/trace.js
// Envelopa uma chamada de LLM em um span com atributos especificos:
// tokens, custo, modelo e fase. O span vira a unidade que voce agrega depois.

export async function tracedCompletion(tracer, { route, model }, call) {
  const span = tracer.startSpan('llm.completion', {
    attributes: { route, model },
  });

  try {
    const res = await call(); // chama o provedor (ex.: Anthropic)

    // Atributos que tornam o span util para custo e qualidade,
    // nao so para latencia.
    span.setAttributes({
      'llm.tokens.input': res.usage.input_tokens,
      'llm.tokens.output': res.usage.output_tokens,
      'llm.cost.usd': estimateCost(model, res.usage),
      'llm.finish_reason': res.stop_reason, // end_turn, max_tokens, tool_use...
      'llm.cache.read': res.usage.cache_read_input_tokens ?? 0,
    });
    span.setStatus({ code: 'OK' });
    return res;
  } catch (err) {
    // Erro do provedor tambem e um sinal de qualidade e custo.
    span.recordException(err);
    span.setStatus({ code: 'ERROR', message: err.message });
    throw err;
  } finally {
    span.end(); // fecha o span mesmo em erro, senao o trace fica quebrado
  }
}

O detalhe que a maioria esquece: registrar o finish_reason. Um pico de respostas terminando em max_tokens indica prompt ou saída mal dimensionados, custo desnecessário e resposta cortada, tudo invisível se você só olha latência. E propagar um traceId único do início da requisição até a resposta final amarra retrieval, modelo e tools no mesmo trace, permitindo abrir uma reclamação de usuário e ver a árvore inteira daquela conversa.

03

Custo: o eixo que ninguém mede até a fatura chegar

Custo de LLM é a métrica mais fácil de ignorar e a mais cara de ignorar. Ele não aparece no APM, não dispara erro, e cresce silenciosamente até a fatura do fim do mês. A base é simples: cada chamada tem tokens de entrada e de saída, cada um com um preço por milhão de tokens que difere por modelo. O erro comum é medir custo agregado da conta inteira, quando o que importa é custo por rota e por usuário, porque é ali que você descobre qual funcionalidade está cara e qual usuário está abusando.

// observability/cost.js
// Calcula o custo de uma chamada a partir dos tokens e da tabela de precos.
// Precos sao por milhao de tokens; entrada e saida tem valores distintos.
// Tokens lidos do cache custam uma fracao do preco de entrada.

const PRICING = {
  // valores ilustrativos, por milhao de tokens (input, output, cacheRead)
  'fast':     { input: 0.8,  output: 4.0,  cacheRead: 0.08 },
  'balanced': { input: 3.0,  output: 15.0, cacheRead: 0.30 },
};

export function estimateCost(model, usage) {
  const p = PRICING[model];
  if (!p) return 0; // modelo desconhecido: nao chuta, sinaliza depois

  const input = usage.input_tokens ?? 0;
  const output = usage.output_tokens ?? 0;
  const cached = usage.cache_read_input_tokens ?? 0;

  // Tokens cacheados nao pagam preco de entrada cheio: contam a parte.
  const billableInput = Math.max(input - cached, 0);

  const usd =
    (billableInput / 1_000_000) * p.input +
    (cached / 1_000_000) * p.cacheRead +
    (output / 1_000_000) * p.output;

  return Number(usd.toFixed(6)); // precisao suficiente para somar milhares
}

Com o custo por chamada calculado e anexado ao span, o resto é agregação: some por rota para achar a funcionalidade cara, por usuário para achar abuso, por dia para ver a tendência. O prompt cache é o maior alavanca de custo em prompts longos e repetidos (system prompt fixo, contexto reaproveitado), e só aparece se você medir os tokens de cache separado. Sem observar custo por dimensão, a única alavanca que sobra é cortar features depois do susto.

04

Qualidade em produção: medir sem gabarito

Qualidade é o eixo mais difícil, porque em produção você raramente tem a resposta certa para comparar. Diferente do eval offline, onde existe um dataset com gabarito, em produção a resposta acabou de ser gerada e ninguém sabe se está correta. A saída é medir sinais indiretos de qualidade que não precisam de gabarito, combinados: nenhum é definitivo sozinho, mas juntos desenham um retrato confiável de degradação.

  • Validação de formato: se a resposta deveria ser JSON com um schema, valide e conte quantas falham. Taxa de formato inválido subindo é degradação mensurável sem gabarito.
  • Taxa de recusa: quantas respostas foram "não posso ajudar" ou similar. Recusa subindo pode ser prompt quebrado, guardrail agressivo ou mudança de modelo.
  • finish_reason por max_tokens: respostas cortadas por limite de saída são qualidade degradada e custo desperdiçado ao mesmo tempo.
  • Sinais do usuário: retentativa na mesma sessão, reformulação da pergunta, thumbs down, escalada para humano. São o eval humano de graça, se você os captura.
  • LLM como juiz amostrado: rode um modelo avaliador sobre uma amostra do tráfego real (1 a 5 por cento), pontuando fidelidade e utilidade, para ter um número contínuo de qualidade sem avaliar tudo.

A técnica que mais rende é o LLM como juiz amostrado: você não avalia cem por cento do tráfego (caro e lento), avalia uma fatia representativa e trata o resultado como uma métrica de qualidade que sobe e desce ao longo do tempo. Combinado com validação de formato e sinais do usuário, isso transforma qualidade de "acho que piorou" em uma linha no dashboard que dispara alerta quando cai. Nenhum sinal é perfeito, mas o conjunto é o suficiente para saber que algo mudou antes do cliente reclamar.

05

O que logar sem vazar dado sensível

Logar prompt e resposta é o que torna a depuração possível: sem ver o que entrou e o que saiu, todo bug de qualidade vira adivinhação. Mas prompt de produção carrega dado do usuário (nome, telefone, documento, histórico), e jogar isso cru no log é um incidente de privacidade esperando para acontecer. O equilíbrio é logar o suficiente para depurar, com redação de dados sensíveis antes de persistir, e retenção curta para o conteúdo bruto.

// observability/redact.js
// Redige dados sensiveis do prompt/resposta ANTES de logar.
// Mantem o texto legivel para depurar, sem persistir PII crua.

const PATTERNS = [
  { name: 'email', re: /[\w.+-]+@[\w-]+\.[\w.-]+/g, tag: '[EMAIL]' },
  { name: 'phone', re: /\b\d{2}[\s-]?\d{4,5}[\s-]?\d{4}\b/g, tag: '[PHONE]' },
  { name: 'cpf',   re: /\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b/g, tag: '[CPF]' },
  { name: 'card',  re: /\b(?:\d[ -]?){13,16}\b/g, tag: '[CARD]' },
];

export function redact(text) {
  if (typeof text !== 'string') return text;
  return PATTERNS.reduce((acc, p) => acc.replace(p.re, p.tag), text);
}

// O log guarda metadados sempre, e o conteudo redigido com retencao curta.
export function buildLogRecord({ traceId, route, model, usage, prompt, output }) {
  return {
    traceId,
    route,
    model,
    tokens: { input: usage.input_tokens, output: usage.output_tokens },
    // Conteudo redigido: util para depurar, seguro para reter por pouco tempo.
    prompt: redact(prompt),
    output: redact(output),
  };
}

A separação que importa: metadados (tokens, custo, latência, rota, finish_reason) são baratos e seguros, então guarde por muito tempo para análise de tendência. Conteúdo bruto (prompt e resposta) é caro e sensível, então redija sempre e retenha por pouco (dias, não meses), o suficiente para depurar o incidente recente. Nunca logue chave de API, token de sessão ou credencial, e trate o log de LLM com o mesmo cuidado de qualquer store de dado pessoal, porque é exatamente isso que ele é.

06

Alertas que evitam a surpresa

Dashboard você olha quando lembra; alerta te avisa quando você não está olhando. O objetivo dos alertas de LLM é o mesmo dos guardrails: transformar um problema silencioso (custo subindo, qualidade caindo) em um evento acionável antes de virar prejuízo ou reclamação. O erro é alertar só em erro técnico (5xx, timeout) e ignorar os sinais que são únicos de LLM.

  1. Custo por dia acima do orçamento esperado, ou custo por rota subindo mais de X por cento semana a semana: pega abuso, loop e regressão de prompt antes da fatura.
  2. Taxa de formato inválido ou de recusa acima da linha de base: sinal direto de que o prompt ou o modelo mudou de comportamento.
  3. p95 de latência por rota estourando o SLA, separando tempo de modelo de tempo de tool, para saber onde agir.
  4. Proporção de respostas terminando em max_tokens subindo: prompt ou limite de saída mal dimensionados, gerando custo e resposta cortada.
  5. Score do juiz amostrado caindo abaixo do limiar: a métrica de qualidade contínua que dispara antes do cliente perceber.
Fluxo de observabilidade de uma chamada de LLM

  requisição do usuário
        |
        v
  [ trace inicia: traceId ]
        |
        +--> span: retrieval        120ms
        +--> span: llm.completion    infos: tokens, custo, finish_reason
        |         840ms
        +--> span: tool getStatus    retry=0   210ms
        |
        v
  [ trace fecha ]
        |
        +--> métricas agregadas:  custo/rota   p95/rota   qualidade/rota
        +--> log redigido:        prompt/output (retenção curta)
        +--> alertas:             custo, formato, recusa, latência, juiz

Cada alerta deve apontar para a rota e o trace, não para um número global. "Custo subiu" não ajuda; "custo da rota /suporte subiu 40 por cento, veja o trace abc123" leva direto à causa. Observabilidade só vale quando encurta o caminho do sintoma até a origem, e um alerta sem contexto é só mais um número que a equipe aprende a ignorar.

07

Começar pequeno sem virar projeto de plataforma

A armadilha é achar que observabilidade de LLM exige uma plataforma inteira antes de dar valor. Não exige. Um middleware que envelopa a chamada, calcula custo, mede latência por fase e loga redigido já entrega noventa por cento do valor em poucas centenas de linhas. O caminho é adicionar por camadas, na ordem de retorno.

  • Comece pelo custo por chamada: é o mais barato de instrumentar e o que mais surpreende, porque ninguém sabia o número real por rota.
  • Adicione o span por fase logo depois: separa tempo de modelo de tempo de tool e retrieval, o que torna a latência acionável.
  • Ligue o log redigido cedo, com retenção curta: é o que permite depurar o primeiro bug de qualidade sem virar risco de privacidade.
  • Instrumente os sinais de qualidade baratos (formato inválido, recusa, max_tokens) antes do juiz amostrado, que é mais caro de montar.
  • Ponha os alertas por último, quando já tem linha de base: alertar sem baseline gera ruído, alertar com baseline gera ação.

A diferença entre operar um sistema com LLM e rezá-lo está em enxergar os três eixos juntos: quanto demora, quanto custa e se presta. Quem instrumenta isso cedo descobre a regressão de qualidade em um dashboard e o pico de custo em um alerta; quem deixa para depois descobre os dois no lugar errado, o primeiro na reclamação do cliente e o segundo na fatura.

FAQ

Perguntas frequentes

Preciso de uma plataforma dedicada de observabilidade de LLM?

Não para começar. Um middleware que envelopa a chamada do modelo, calcula custo por tokens, emite span por fase e loga com redação cabe em poucas centenas de linhas e entrega a maior parte do valor. Plataformas dedicadas ajudam quando o volume cresce e você quer visualização de trace pronta e eval integrado, mas adotar uma cedo demais adiciona custo e dependência antes de você entender o que precisa medir. Instrumente os três eixos primeiro; escolha a ferramenta depois, sabendo o que ela resolve.

Como meço qualidade se não tenho a resposta certa em produção?

Com sinais indiretos combinados, nenhum definitivo sozinho: taxa de formato inválido, taxa de recusa, respostas cortadas por max_tokens, sinais do usuário (retentativa, reformulação, thumbs down, escalada) e um LLM como juiz rodando sobre uma amostra do tráfego (1 a 5 por cento). Cada um é um proxy imperfeito, mas o conjunto vira uma métrica de qualidade contínua que sobe e desce ao longo do tempo e dispara alerta quando cai, permitindo detectar degradação antes do cliente reclamar.

Como calculo o custo real de cada chamada?

Cada resposta do provedor traz o número de tokens de entrada e de saída; multiplique cada um pelo preço por milhão de tokens do modelo (entrada e saída têm preços diferentes) e some. Tokens lidos do prompt cache custam uma fração do preço de entrada, então conte-os à parte. Anexe esse custo ao span da chamada e agregue por rota, por usuário e por dia. O erro comum é olhar só o total da conta; o valor está em saber qual rota e qual usuário geram o gasto.

Observabilidade é o que transforma um sistema de LLM de caixa preta em operação

Tracing por fase, custo por chamada, sinais de qualidade e alertas com contexto são o mínimo para operar LLM sem surpresa de fatura nem regressão invisível. Posso instrumentar essas três dimensões no seu produto, do middleware ao dashboard, integradas ao seu stack e prontas para escalar.