IA aplicada

LLM Observability em produção: monitorando qualidade, custo e comportamento de modelos em 2026

Modelos de linguagem exigem métricas específicas: latência de inference, custo por token, qualidade de resposta e drift comportamental.

12/03/202610 min de leituraIA
LLM Observability em produção: monitorando qualidade, custo e comportamento de modelos em 2026

Resumo executivo

Modelos de linguagem exigem métricas específicas: latência de inference, custo por token, qualidade de resposta e drift comportamental.

Ultima atualizacao: 12/03/2026

Introdução: A caixa preta de IA

Deployar um modelo de linguagem em produção é apenas o começo. Monitorar seu comportamento, garantir qualidade consistente, controlar custos e detectar drift comportamental são desafios muito mais complexos. Diferente de APIs tradicionais, LLMs são não-determinísticos por natureza, e isso exige uma abordagem completamente nova de observabilidade.

Em 2026, empresas maduras não apenas "confiam" em seus modelos — elas mensuram, iteram e ajustam continuamente. A observabilidade de LLM tornou-se uma disciplina própria, com métricas específicas, ferramentas especializadas e práticas operacionais distintas.

O que monitorar em um sistema LLM

Três pilares de observabilidade

┌─────────────────────────────────────────────────────────────────────┐
│                       LLM OBSERVABILITY                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │
│  │   QUALIDADE   │  │    CUSTO     │  │ COMPORTAMENTO│       │
│  │              │  │              │  │              │       │
│  │ - Precisão   │  │ - Tokens/s   │  │ - Latência   │       │
│  │ - Relevância │  │ - Custo/$     │  │ - Tokens     │       │
│  │ - Satisfação │  │ - Throughput │  │ - Erros      │       │
│  │ - Utilidade  │  │ - Cache hit  │  │ - Failures   │       │
│  └──────────────┘  └──────────────┘  └──────────────┘       │
│                                                                   │
└─────────────────────────────────────────────────────────────────────┘

Métricas de qualidade

1. Relevância e precisão

typescript// Avaliação de relevância com LLM-as-a-judge
interface QualityAssessment {
  query: string;
  response: string;
  relevanceScore: number; // 0-1
  precisionScore: number; // 0-1
  hallucinationScore: number; // 0-1 (lower is better)
}

async function assessQuality(
  query: string,
  response: string,
  context?: string
): Promise<QualityAssessment> {
  const assessmentPrompt = `
Você é um avaliador de qualidade de respostas de LLM.

Query: ${query}

Response: ${response}

${context ? `Context: ${context}` : ''}

Avalie a resposta em três dimensões:
1. Relevância (0-1): A resposta aborda diretamente a query?
2. Precisão (0-1): A informação é factualmente correta?
3. Alucinação (0-1): A resposta afirma informações não suportadas?

Retorne apenas JSON com formato:
{
  "relevanceScore": 0.9,
  "precisionScore": 0.8,
  "hallucinationScore": 0.1
}
  `;

  const response = await llmClient.complete({
    messages: [{ role: 'user', content: assessmentPrompt }],
    model: 'claude-sonnet-4-6-20250214',
    temperature: 0.1 // Baixa temperatura para consistência
  });

  return JSON.parse(response.content);
}

2. Satisfação do usuário

typescript// Coleta de feedback explícito e implícito
interface UserFeedback {
  requestId: string;
  userId: string;
  explicitRating?: number; // 1-5 stars
  implicitMetrics: {
    copiedToClipboard?: boolean;
    acceptedSuggestion?: boolean;
    timeToAccept?: number; // ms
    followUpQuery?: boolean;
    rephrasedQuery?: boolean;
  };
}

async function logFeedback(feedback: UserFeedback) {
  await analytics.track('llm_feedback', {
    ...feedback,
    timestamp: Date.now()
  });

  // Calcula score de satisfação combinado
  const satisfactionScore = calculateSatisfactionScore(feedback);

  await metrics.gauge('llm.satisfaction.score', satisfactionScore, {
    tags: {
      userId: feedback.userId,
      requestId: feedback.requestId
    }
  });
}

function calculateSatisfactionScore(feedback: UserFeedback): number {
  let score = 0;
  let factors = 0;

  if (feedback.explicitRating !== undefined) {
    score += feedback.explicitRating / 5; // Normaliza para 0-1
    factors += 1;
  }

  if (feedback.implicitMetrics.copiedToClipboard) {
    score += 0.8; // Copiar indica alta satisfação
    factors += 1;
  }

  if (feedback.implicitMetrics.acceptedSuggestion) {
    score += 0.7;
    factors += 1;
  }

  return factors > 0 ? score / factors : 0;
}

3. Avaliação automática com RAGAS

typescript// Usando RAGAS para avaliação automática
import { RagasEvaluator } from 'ragas';

const evaluator = new RagasEvaluator({
  model: 'claude-sonnet-4-6-20250214',
  apiKey: process.env.ANTHROPIC_API_KEY
});

async function evaluateRAGPipeline(
  query: string,
  retrievedDocs: string[],
  generatedResponse: string,
  groundTruth?: string
) {
  const metrics = await evaluator.evaluate({
    question: query,
    context: retrievedDocs,
    answer: generatedResponse,
    ground_truth: groundTruth
  });

  // Métricas calculadas automaticamente
  return {
    faithfulness: metrics.faithfulness, // A resposta é fiel ao contexto?
    answerRelevancy: metrics.answer_relevancy, // A resposta é relevante?
    contextPrecision: metrics.context_precision, // O contexto é preciso?
    contextRecall: metrics.context_recall, // O contexto é completo?
    contextEntityRecall: metrics.context_entity_recall, // Entidades recuperadas?
    answerSimilarity: groundTruth ? metrics.answer_similarity : null // Similaridade com resposta correta
  };
}

Métricas de custo

1. Custo por token

typescript// Rastreamento detalhado de custos
interface TokenUsage {
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  cost: number;
}

interface ModelPricing {
  model: string;
  promptCostPer1K: number;
  completionCostPer1K: number;
}

const MODEL_PRICING: Record<string, ModelPricing> = {
  'claude-sonnet-4-6-20250214': {
    model: 'claude-sonnet-4-6-20250214',
    promptCostPer1K: 0.003, // $0.003 por 1K prompt tokens
    completionCostPer1K: 0.015 // $0.015 por 1K completion tokens
  },
  'claude-opus-4-6-20250214': {
    model: 'claude-opus-4-6-20250214',
    promptCostPer1K: 0.015,
    completionCostPer1K: 0.075
  }
};

function calculateTokenCost(
  model: string,
  promptTokens: number,
  completionTokens: number
): TokenUsage {
  const pricing = MODEL_PRICING[model];

  if (!pricing) {
    throw new Error(`Unknown model: ${model}`);
  }

  const promptCost = (promptTokens / 1000) * pricing.promptCostPer1K;
  const completionCost = (completionTokens / 1000) * pricing.completionCostPer1K;
  const totalCost = promptCost + completionCost;

  return {
    promptTokens,
    completionTokens,
    totalTokens: promptTokens + completionTokens,
    cost: totalCost
  };
}

// Middleware para rastreamento automático
export async function trackLLMRequest<T>(
  model: string,
  request: () => Promise<{ promptTokens: number; completionTokens: number; result: T }>
): Promise<{ result: T; usage: TokenUsage }> {
  const startTime = Date.now();

  const { promptTokens, completionTokens, result } = await request();

  const latency = Date.now() - startTime;
  const usage = calculateTokenCost(model, promptTokens, completionTokens);

  // Registra métricas
  await metrics.histogram('llm.request.duration', latency, {
    tags: {
      model,
      operation: 'inference'
    }
  });

  await metrics.gauge('llm.request.tokens.prompt', promptTokens, { tags: { model } });
  await metrics.gauge('llm.request.tokens.completion', completionTokens, { tags: { model } });
  await metrics.gauge('llm.request.cost', usage.cost, { tags: { model } });

  return { result, usage };
}

// Uso
const { result, usage } = await trackLLMRequest(
  'claude-sonnet-4-6-20250214',
  async () => {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-6-20250214',
      max_tokens: 1024,
      messages: [{ role: 'user', content: '...' }]
    });

    return {
      promptTokens: response.usage.input_tokens,
      completionTokens: response.usage.output_tokens,
      result: response.content
    };
  }
);

console.log(`Cost: $${usage.cost.toFixed(4)}`);

2. Otimização de custo com cache semântico

typescript// Cache semântico para reduzir custos
import { embeddingModel } from './embeddings';
import { vectorStore } from './vector-store';

interface SemanticCacheEntry {
  query: string;
  queryEmbedding: number[];
  response: string;
  cachedAt: number;
  hits: number;
}

const semanticCache = new Map<string, SemanticCacheEntry>();
const SIMILARITY_THRESHOLD = 0.95;

async function getCachedResponse(query: string): Promise<string | null> {
  const queryEmbedding = await embeddingModel.embed(query);

  // Busca entradas similares no cache
  for (const [key, entry] of semanticCache) {
    const similarity = cosineSimilarity(queryEmbedding, entry.queryEmbedding);

    if (similarity > SIMILARITY_THRESHOLD) {
      // Cache hit
      entry.hits++;

      await metrics.increment('llm.cache.hit', {
        tags: { type: 'semantic' }
      });

      // Registra economia de custo
      const estimatedSavings = estimateCostSavings(entry);
      await metrics.gauge('llm.cache.savings', estimatedSavings);

      return entry.response;
    }
  }

  await metrics.increment('llm.cache.miss', {
    tags: { type: 'semantic' }
  });

  return null;
}

async function cacheResponse(query: string, response: string): Promise<void> {
  const queryEmbedding = await embeddingModel.embed(query);

  const entry: SemanticCacheEntry = {
    query,
    queryEmbedding,
    response,
    cachedAt: Date.now(),
    hits: 0
  };

  semanticCache.set(query, entry);
}

function cosineSimilarity(a: number[], b: number[]): number {
  const dotProduct = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
  const magnitudeA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
  const magnitudeB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));

  return dotProduct / (magnitudeA * magnitudeB);
}

// Integração com requisição LLM
async function getLLMResponseWithCache(query: string): Promise<string> {
  // Tenta cache primeiro
  const cached = await getCachedResponse(query);
  if (cached) {
    return cached;
  }

  // Cache miss: faz requisição
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6-20250214',
    max_tokens: 1024,
    messages: [{ role: 'user', content: query }]
  });

  const result = response.content[0].text;

  // Caches a resposta
  await cacheResponse(query, result);

  return result;
}

Métricas de comportamento

1. Detecção de drift comportamental

typescript// Detecção de drift usando distribuições de embeddings
import { EmbeddingModel } from './embeddings';

interface ResponseDistribution {
  model: string;
  date: string;
  embeddings: number[][];
  mean: number[];
  covariance: number[][];
}

async function detectBehavioralDrift(
  currentResponses: string[],
  historicalDistributions: ResponseDistribution[]
): Promise<{ drifted: boolean; score: number }> {
  const currentEmbeddings = await Promise.all(
    currentResponses.map(r => embeddingModel.embed(r))
  );

  const currentDistribution = calculateDistribution(currentEmbeddings);

  let maxDriftScore = 0;

  for (const historical of historicalDistributions) {
    const driftScore = calculateDrift(currentDistribution, historical);
    maxDriftScore = Math.max(maxDriftScore, driftScore);
  }

  return {
    drifted: maxDriftScore > 0.7, // Threshold ajustável
    score: maxDriftScore
  };
}

function calculateDistribution(embeddings: number[]): {
  mean: number[];
  covariance: number[][];
} {
  const dimensions = embeddings[0].length;
  const mean = new Array(dimensions).fill(0);

  // Calcula média
  for (const emb of embeddings) {
    for (let i = 0; i < dimensions; i++) {
      mean[i] += emb[i] / embeddings.length;
    }
  }

  // Calcula covariância
  const covariance: number[][] = [];
  for (let i = 0; i < dimensions; i++) {
    covariance[i] = [];
    for (let j = 0; j < dimensions; j++) {
      let cov = 0;
      for (const emb of embeddings) {
        cov += (emb[i] - mean[i]) * (emb[j] - mean[j]);
      }
      covariance[i][j] = cov / embeddings.length;
    }
  }

  return { mean, covariance };
}

function calculateDrift(
  current: { mean: number[]; covariance: number[][] },
  historical: { mean: number[]; covariance: number[][] }
): number {
  // Wasserstein distance simplificado
  let distance = 0;

  for (let i = 0; i < current.mean.length; i++) {
    distance += Math.pow(current.mean[i] - historical.mean[i], 2);
  }

  return Math.sqrt(distance);
}

2. Detecção de alucinações

typescript// Detecção de alucinações usando citations
interface CitationCheck {
  response: string;
  citedSources: string[];
  uncitedClaims: string[];
  hallucinationProbability: number;
}

async function detectHallucinations(
  response: string,
  retrievedContext: string[]
): Promise<CitationCheck> {
  // Extrai afirmações da resposta
  const claims = await extractClaims(response);

  const citedSources: string[] = [];
  const uncitedClaims: string[] = [];

  for (const claim of claims) {
    const isSupported = await checkClaimSupport(claim, retrievedContext);

    if (isSupported.supported) {
      citedSources.push(...isSupported.sources);
    } else {
      uncitedClaims.push(claim);
    }
  }

  const hallucinationProbability = uncitedClaims.length / claims.length;

  return {
    response,
    citedSources,
    uncitedClaims,
    hallucinationProbability
  };
}

async function checkClaimSupport(
  claim: string,
  context: string[]
): Promise<{ supported: boolean; sources: string[] }> {
  const prompt = `
Determine if the following claim is supported by the provided context.

Claim: ${claim}

Context:
${context.map((c, i) => `[${i + 1}] ${c}`).join('\n')}

Respond with JSON:
{
  "supported": true/false,
  "sources": [1, 2, ...], // indices of supporting sources
  "confidence": 0-1
}
  `;

  const response = await llmClient.complete({
    messages: [{ role: 'user', content: prompt }],
    model: 'claude-sonnet-4-6-20250214',
    temperature: 0
  });

  return JSON.parse(response.content);
}

Arquitetura de observabilidade

Pipeline completo de monitoramento

typescript// Sistema centralizado de observabilidade LLM
class LLMObservabilitySystem {
  private qualityMetrics: Map<string, QualityAssessment> = new Map();
  private costMetrics: Map<string, TokenUsage> = new Map();
  private behaviorMetrics: Map<string, ResponseDistribution> = new Map();

  async trackRequest(requestId: string, config: {
    query: string;
    response: string;
    model: string;
    promptTokens: number;
    completionTokens: number;
    latency: number;
    context?: string[];
    userId?: string;
  }) {
    // Rastreia custo
    const costUsage = calculateTokenCost(
      config.model,
      config.promptTokens,
      config.completionTokens
    );
    this.costMetrics.set(requestId, costUsage);

    // Avalia qualidade
    const quality = await assessQuality(
      config.query,
      config.response,
      config.context?.join('\n')
    );
    this.qualityMetrics.set(requestId, quality);

    // Registra métricas de comportamento
    await this.trackBehavior(requestId, config);

    // Calcula métricas compostas
    const overallScore = this.calculateOverallScore(
      quality,
      costUsage,
      config.latency
    );

    // Alerta se score cair abaixo do threshold
    if (overallScore < 0.7) {
      await this.alertPoorPerformance(requestId, overallScore);
    }

    return { quality, costUsage, overallScore };
  }

  private async trackBehavior(
    requestId: string,
    config: any
  ) {
    const embedding = await embeddingModel.embed(config.response);

    // Verifica drift
    const historical = Array.from(this.behaviorMetrics.values());
    const { drifted, score } = await detectBehavioralDrift(
      [config.response],
      historical
    );

    if (drifted) {
      await this.alertDrift(requestId, score);
    }

    // Armazena distribuição atual
    this.behaviorMetrics.set(requestId, {
      model: config.model,
      date: new Date().toISOString(),
      embeddings: [embedding],
      mean: embedding,
      covariance: []
    });
  }

  private calculateOverallScore(
    quality: QualityAssessment,
    costUsage: TokenUsage,
    latency: number
  ): number {
    // Ponderação de métricas
    const qualityWeight = 0.5;
    const costWeight = 0.3;
    const latencyWeight = 0.2;

    // Normaliza latência (assumindo 500ms como ideal)
    const normalizedLatency = Math.max(0, 1 - latency / 10000);

    // Normaliza custo (assumindo $0.01 como ideal)
    const normalizedCost = Math.max(0, 1 - costUsage.cost / 0.1);

    // Média ponderada
    const qualityScore = (
      quality.relevanceScore +
      quality.precisionScore +
      (1 - quality.hallucinationScore)
    ) / 3;

    return (
      qualityScore * qualityWeight +
      normalizedCost * costWeight +
      normalizedLatency * latencyWeight
    );
  }

  private async alertPoorPerformance(requestId: string, score: number) {
    await alerts.send({
      severity: 'warning',
      title: 'LLM Performance Alert',
      message: `Request ${requestId} has poor overall score: ${score.toFixed(2)}`,
      metadata: {
        requestId,
        score
      }
    });
  }

  private async alertDrift(requestId: string, score: number) {
    await alerts.send({
      severity: 'critical',
      title: 'LLM Behavioral Drift Detected',
      message: `Request ${requestId} shows behavioral drift: ${score.toFixed(2)}`,
      metadata: {
        requestId,
        driftScore: score
      }
    });
  }
}

Dashboards e visualização

Dashboard de métricas em tempo real

typescript// Integração com Grafana/Prometheus
import { Registry, Counter, Histogram, Gauge } from 'prom-client';

const registry = new Registry();

// Métricas
const llmRequestDuration = new Histogram({
  name: 'llm_request_duration_seconds',
  help: 'Duration of LLM requests',
  labelNames: ['model', 'operation'],
  buckets: [0.1, 0.5, 1, 2, 5, 10]
});

const llmRequestCost = new Gauge({
  name: 'llm_request_cost_dollars',
  help: 'Cost of LLM requests in dollars',
  labelNames: ['model']
});

const llmCacheHitRate = new Gauge({
  name: 'llm_cache_hit_rate',
  help: 'Cache hit rate for LLM requests',
  labelNames: ['cache_type']
});

const llmQualityScore = new Gauge({
  name: 'llm_quality_score',
  help: 'Quality score of LLM responses',
  labelNames: ['metric_type']
});

registry.registerMetric(llmRequestDuration);
registry.registerMetric(llmRequestCost);
registry.registerMetric(llmCacheHitRate);
registry.registerMetric(llmQualityScore);

// Endpoint de métricas para Prometheus
export async function metricsHandler(req: Request): Promise<Response> {
  return new Response(await registry.metrics(), {
    headers: { 'Content-Type': 'text/plain' }
  });
}

Métricas de sucesso

Para validar que seu sistema de observabilidade está funcionando:

  • Tempo para detecção de drift: Objetivo <24 horas após mudança de comportamento
  • Taxa de alertas relevantes: >80% dos alertas devem acionar ação corretiva
  • Custo de observabilidade: <10% do custo total de inferência LLM
  • Cobertura de requisições monitoradas: >95% das requisições em produção

Seu sistema LLM em produção sofre com custos imprevisíveis, qualidade inconsistente ou drift comportamental não detectado? Fale com especialistas da Imperialis sobre observabilidade de LLM, de métricas de qualidade a detecção de drift, para operar modelos em produção com confiança.

Fontes

Leituras relacionadas