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.
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
- Anthropic documentation — Documentação oficial da Anthropic
- RAGAS library — Avaliação automática de RAG
- LangSmith — Plataforma de observabilidade de LLM
- OpenTelemetry for AI — Padrões de telemetria para IA