Como instrumentar LLMs em produção com OpenTelemetry, GenAI semantic conventions, online evals, drift detection e rollback automático — sem depender de palpite
Este é o quinto e último artigo da série LLMOps em Produção. No artigo anterior sobre guardrails runtime, vimos como bloquear, filtrar e moldar respostas antes que cheguem ao usuário. Mas todo guardrails depende de uma decisão anterior: qual versão de prompt, qual versão de modelo, qual rota, qual cache. Essas decisões só podem ser auditadas quando o sistema é observável.
Sem prompt completo, versão de modelo e veredito do eval no mesmo span, você não tem observabilidade generativa. Você tem um log de servidor tradicional fingindo que entende LLM.
gen_ai.*) é o padrão que evita vendor lock-in de telemetria.| Dimensão | API REST tradicional | LLM em produção |
|---|---|---|
| Determinismo | Mesma entrada → mesma saída | Mesma entrada → saídas diferentes |
| Versão | Você controla o deploy | Provedor pode atualizar silenciosamente |
| Custo | Por request, previsível | Por token, variável em ordens de magnitude |
| Qualidade | Status code é suficiente | Exige avaliação semântica |
| Falha comum | Erro 500 | Resposta 200 errada |
OpenTelemetry (OTel) é o padrão aberto de telemetria. A vantagem sobre soluções proprietárias é que você não fica refém de um vendor de observabilidade.
| Atributo | O que registra |
|---|---|
gen_ai.system | Provedor (openai, anthropic, azure) |
gen_ai.request.model | Modelo solicitado |
gen_ai.response.model | Modelo efetivamente usado (pode diferir do request) |
gen_ai.usage.input_tokens | Tokens consumidos na entrada |
gen_ai.usage.output_tokens | Tokens produzidos na saída |
gen_ai.tool.name | Nome da tool chamada |
A distinção entre gen_ai.request.model e gen_ai.response.model é crítica. Você pode pedir claude-sonnet-4 e receber uma snapshot atualizada do provedor. Sem essa separação, drift de modelo fica invisível.
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('llmops.app');
type LLMSpanAttributes = {
promptVersion: string;
modelRequested: string;
modelResponded: string;
temperature: number;
maxTokens: number;
toolsAvailable: string[];
toolsUsed: string[];
route: string;
cacheHit: boolean;
guardrailVerdict: 'pass' | 'block' | 'transform';
};
export async function withLLMSpan<T>(
attributes: LLMSpanAttributes,
fn: () => Promise<T>,
): Promise<T> {
return tracer.startActiveSpan(
'llm.request',
{ attributes: flattenAttributes(attributes) },
async (span) => {
try {
const result = await fn();
span.setAttribute('gen_ai.request.model', attributes.modelRequested);
span.setAttribute('gen_ai.response.model', attributes.modelResponded);
span.setAttribute('llmops.prompt_version', attributes.promptVersion);
span.setAttribute('llmops.route', attributes.route);
span.setAttribute('llmops.cache_hit', attributes.cacheHit);
span.setAttribute('llmops.guardrail_verdict', attributes.guardrailVerdict);
span.setAttribute('llmops.tools_used_count', attributes.toolsUsed.length);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
},
);
}
O ponto-chave é capturar prompt_version. Sem isso, regressão após um git revert de prompt fica indistinguível de regressão de modelo.
llm.request 1.84s
├─ gen_ai.system: anthropic
├─ gen_ai.request.model: claude-sonnet-4-20250514
├─ gen_ai.response.model: claude-sonnet-4-20250514-r2
├─ llmops.prompt_version: summarizer.v3.4.1
├─ llmops.route: long_context_fallback
├─ gen_ai.usage.input_tokens: 4200
├─ gen_ai.usage.output_tokens: 380
├─ llmops.cost_usd: 0.0234
├─ llmops.guardrail_verdict: pass
└─ llmops.eval.verdict: pass (judge_score: 0.82)
| Métrica | O que mede | Threshold típico |
|---|---|---|
| Latência p95 | Cauda longa | Alerta se > 3x baseline |
| Erro rate | Falhas explícitas | Alerta se > 1% |
| Cache hit rate | Efetividade do cache | Investigar se cair > 10 p.p. |
| Guardrail block rate | Bloqueios de segurança | Investigar se subir > 5 p.p. |
| Custo por request | USD por chamada | Budget alert por feature |
| Métrica | O que mede | Threshold típico |
|---|---|---|
| Eval score rolling | Qualidade semântica média | Alerta se cair > 8% vs baseline |
| Hallucination rate | Respostas sem fonte | Alerta se > 3% |
| Judge score distribution | Distribuição de notas | Investigar se moda deslocar |
O drift de modelo funciona assim:
type DriftBaseline = {
promptVersion: string;
modelName: string;
baselineScore: number;
threshold: number; // queda aceitável
windowSize: number; // número mínimo de amostras
};
export function checkDrift(
baseline: DriftBaseline,
recentScores: number[],
): DriftVerdict {
if (recentScores.length < baseline.windowSize) {
return { kind: 'insufficient_samples', samples: recentScores.length };
}
const currentScore =
recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
const delta = baseline.baselineScore - currentScore;
if (delta > baseline.threshold) {
return { kind: 'drift', score: currentScore, dropFrom: baseline.baselineScore, delta };
}
return { kind: 'stable', score: currentScore };
}
Evals offline (CI/staging) protegem contra regressões conhecidas. Evals online (produção) protegem contra regressões desconhecidas. O padrão é amostrar 1% a 5% das requisições e julgar essas.
Conjunto curado (entre 50 e 500 casos) que cobre:
Vieses conhecidos: position bias, self-preference bias, verbosity bias, format bias. Mitigações:
| Métrica | Condição | Severidade |
|---|---|---|
| Eval score rolling | Queda > 8% vs baseline por 15 min | Critical |
| Erro rate | > 2% por 5 min | Critical |
| Latência p95 | > 3x baseline por 10 min | Warning |
| Guardrail block rate | Subida > 10 p.p. por 30 min | Warning |
| Drift delta | > threshold por 1h | Critical |
export function evaluateRollback(
current: Version,
previous: Version,
recentSamples: number[],
baseline: number,
policy: RollbackPolicy,
): RollbackDecision {
if (!previous.isStable) {
return { kind: 'escalate', reason: 'versão anterior não está marcada como estável' };
}
if (recentSamples.length < 20) {
return { kind: 'no_action', reason: 'amostras insuficientes para decidir' };
}
const currentScore = recentSamples.reduce((a, b) => a + b, 0) / recentSamples.length;
const delta = baseline - currentScore;
if (delta <= policy.thresholdDelta) {
return { kind: 'no_action', reason: 'métrica dentro do aceitável' };
}
return {
kind: 'rollback',
from: current,
to: previous,
reason: `${policy.metric} caiu ${delta.toFixed(3)} abaixo do baseline ${baseline}`,
};
}
claude-sonnet-4 não é versão; claude-sonnet-4-20250514 é.gen_ai.* attributes.prompt_version como atributo de span e métrica.gen_ai.* attributes.prompt_version, traceId, user_id, feature_flag.gen_ai.request.model e gen_ai.response.model são capturados separadamente.checkDrift() roda a cada hora contra baseline.A série cobre o ciclo completo: escolher o modelo, rotear, cachear, proteger, observar e reverter. Cada peça alimenta a próxima.
Observabilidade generativa não é luxo de equipe grande. É a única diferença entre um sistema que escala e um sistema que apodrece em silêncio durante meses.
Sem prompt_version no span, você não sabe quem causou a regressão. Sem gen_ai.response.model separado de gen_ai.request.model, você não detecta drift silencioso. Sem online evals, você descobre que o modelo piorou quando o cliente cancela. Sem rollback automático, todo incidente vira reunião de guerra.
A série termina onde deveria começar: toda decisão de produto sobre LLM precisa de evidência operacional. Não de impressão. Não de "parece bom em staging". De traces completos, métricas com baseline, evals rodando em produção e rollback testado.
O LLMOps maduro não é sobre ter o modelo mais caro nem o prompt mais elaborado. É sobre transformar cada requisição em uma unidade auditável de engenharia. O resto é decoração.
Fim da série.
Continue explorando tópicos similares
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.