IA LLMOps MLOps Observabilidade

LLMOps na prática: como gerenciar prompts, versionar modelos e monitorar outputs em produção

LLMOps aplicado: versionamento de prompts com hash, LLM-as-judge, rastreamento de custo por feature, A/B testing e stack completa para produção.

N
Neryx Digital Architects
5 de junho de 2026
14 min de leitura
270 profissionais leram
Categoria: Arquitetura Público: Times de plataforma e operação Etapa: Decisão

Você colocou seu primeiro produto com LLM em produção. O demo funcionou, o cliente aprovou, o deploy subiu. Passaram duas semanas e o sistema começa a se comportar de forma diferente — respostas mais longas do que deveriam, alucinações que não apareciam antes, custo de API triplicando sem explicação clara.

O problema não é o modelo. O problema é que você não tem visibilidade nenhuma do que está acontecendo dentro do seu sistema de IA.

Isso é o que LLMOps resolve — e este artigo vai direto ao ponto: como implementar cada peça.

O que é LLMOps e o que o diferencia do MLOps tradicional

LLMOps é o conjunto de práticas, ferramentas e processos para operar Large Language Models em produção com confiabilidade, rastreabilidade e custo controlado. É uma evolução do MLOps — mas com desafios fundamentalmente diferentes.

Em MLOps clássico, você versiona datasets e pesos de modelo. Em LLMOps, o modelo em si raramente muda — você está consumindo um modelo externo via API. O que muda constantemente são os prompts, o contexto injetado, a lógica de orquestração e, eventualmente, o próprio modelo do provider. Qualquer uma dessas mudanças pode degradar a qualidade do output de forma silenciosa.

Os desafios específicos de LLMs em produção:

  • Outputs não-determinísticos: o mesmo prompt gera respostas diferentes em cada chamada; testes unitários tradicionais não capturam regressões
  • Avaliação subjetiva: diferente de um classificador binário, não há uma métrica de acurácia objetiva para geração de texto livre
  • Custo variável e imprevisível: tokens de entrada + saída determinam o custo; prompts mal calibrados com few-shot excessivo viram fatura alta
  • Drift de qualidade silencioso: o provider pode atualizar o modelo subjacente sem aviso; seu prompt pode parar de funcionar de um dia para o outro
  • Latência imprevisível: um timeout ou throttling no provider derruba sua feature inteira se não houver fallback

Versionamento de system prompts com hash de conteúdo

O maior erro que equipes cometem é tratar prompts como configuração informal — uma string hardcoded no código ou num campo de texto editável diretamente em produção. Prompts são código. Precisam de versionamento semântico, revisão via PR e deploy controlado.

Armazenamento em YAML com hash automático

Armazene cada prompt como um arquivo YAML no repositório. Cada mudança passa pelo mesmo processo de PR e code review que qualquer outra mudança de código:

# prompts/suporte/classificar-ticket.yaml
version: "1.4.2"
model: claude-3-5-sonnet-20241022
temperature: 0.1
max_tokens: 512

system: |
  Você é um especialista em suporte técnico de software.
  Classifique o ticket em uma das categorias abaixo:
  - BUG_CRITICO: sistema fora do ar ou perda de dados
  - BUG_MENOR: funcionalidade degradada mas workaround existe
  - DUVIDA_USO: usuário não sabe usar uma feature existente
  - SOLICITACAO_MELHORIA: pedido de nova funcionalidade
  - FORA_DE_ESCOPO: não relacionado ao produto

  Responda APENAS no formato:
  CATEGORIA: justificativa de uma linha

examples:
  - input: "O sistema não deixa eu fazer login desde ontem"
    output: "BUG_CRITICO: autenticação completamente indisponível"
  - input: "Como exporto o relatório em PDF?"
    output: "DUVIDA_USO: feature existe mas usuário não encontrou"

O loader de prompts lê o arquivo e gera um hash SHA-256 do conteúdo completo. Esse hash vai como metadado em cada chamada ao LLM, permitindo rastrear exatamente qual versão do prompt gerou qual output:

import * as yaml from "js-yaml";
import * as fs from "fs";
import * as crypto from "crypto";

interface PromptConfig {
  version: string;
  model: string;
  temperature: number;
  max_tokens: number;
  system: string;
  examples?: { input: string; output: string }[];
}

interface LoadedPrompt extends PromptConfig {
  hash: string; // primeiros 8 chars do SHA-256 do arquivo completo
  filePath: string;
}

export function loadPrompt(promptPath: string): LoadedPrompt {
  const raw = fs.readFileSync(promptPath, "utf-8");
  const config = yaml.load(raw) as PromptConfig;
  const hash = crypto
    .createHash("sha256")
    .update(raw)
    .digest("hex")
    .slice(0, 8);

  return { ...config, hash, filePath: promptPath };
}

// Uso no serviço:
const prompt = loadPrompt("./prompts/suporte/classificar-ticket.yaml");
console.log(`Prompt v${prompt.version} hash=${prompt.hash}`);
// Saída: Prompt v1.4.2 hash=a3f8b2c1

Registro de versões no banco de dados para rollback rápido

Para equipes que precisam de rollback sem redeploy — útil quando um novo prompt causa degradação em produção e você precisa voltar em segundos, não em minutos de pipeline:

-- Tabela de versionamento com suporte a rollback instantâneo
CREATE TABLE prompt_versions (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  prompt_key  VARCHAR(255) NOT NULL,  -- 'suporte/classificar-ticket'
  version     VARCHAR(50)  NOT NULL,
  content_hash VARCHAR(64) NOT NULL,
  system_prompt TEXT NOT NULL,
  model       VARCHAR(100) NOT NULL,
  temperature NUMERIC(3,2) NOT NULL,
  max_tokens  INTEGER      NOT NULL,
  is_active   BOOLEAN      DEFAULT FALSE,
  activated_at TIMESTAMPTZ,
  created_at  TIMESTAMPTZ  DEFAULT NOW(),
  created_by  VARCHAR(255),
  UNIQUE(prompt_key, version)
);

-- Só um prompt pode estar ativo por key
CREATE UNIQUE INDEX idx_one_active_per_key
  ON prompt_versions (prompt_key)
  WHERE is_active = TRUE;

-- Rollback: desativa o atual, ativa o anterior
UPDATE prompt_versions SET is_active = FALSE
  WHERE prompt_key = 'suporte/classificar-ticket' AND is_active = TRUE;

UPDATE prompt_versions SET is_active = TRUE, activated_at = NOW()
  WHERE prompt_key = 'suporte/classificar-ticket' AND version = '1.3.8';

Avaliação de output com LLM-as-judge

Como você avalia se a resposta do seu LLM é boa? Para classificação com resposta esperada conhecida, você compara diretamente. Mas para geração livre — rascunho de e-mail, análise técnica, resposta de suporte — não há ground truth automático.

A técnica mais eficaz é LLM-as-judge: usar um modelo mais econômico para avaliar o output do modelo de produção segundo critérios específicos ao seu domínio. Funciona em escala porque você pode amostrar 10% do tráfego sem impacto na latência do usuário.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

interface JudgeResult {
  score: number; // média de 1-5
  criteria: {
    relevance: number;  // responde ao que foi perguntado?
    accuracy: number;   // informações corretas?
    clarity: number;    // linguagem clara e profissional?
    completeness: number; // resposta completa?
  };
  reasoning: string;
  flags: string[]; // lista de problemas; vazio se nenhum
}

async function judgeOutput(
  userInput: string,
  systemContext: string,
  assistantOutput: string
): Promise {
  const judgePrompt = `Você é um avaliador de qualidade de respostas de IA para sistemas de suporte técnico.

Avalie a resposta nos critérios abaixo (nota 1-5 cada):
- relevance: responde diretamente ao que o usuário perguntou?
- accuracy: as informações são factualmente corretas?
- clarity: linguagem clara, profissional, sem jargão desnecessário?
- completeness: a resposta está completa ou deixou lacunas importantes?

Contexto do sistema: ${systemContext}
Input do usuário: ${userInput}
Resposta avaliada: ${assistantOutput}

Responda SOMENTE em JSON válido:
{
  "score": ,
  "criteria": { "relevance": N, "accuracy": N, "clarity": N, "completeness": N },
  "reasoning": "",
  "flags": [""]
}`;

  const response = await client.messages.create({
    model: "claude-3-5-haiku-20241022", // modelo econômico para avaliação em escala
    max_tokens: 512,
    messages: [{ role: "user", content: judgePrompt }],
  });

  const text = response.content[0];
  if (text.type !== "text") throw new Error("Judge retornou tipo inesperado");

  return JSON.parse(text.text) as JudgeResult;
}

// Exemplo de uso com amostragem:
async function maybeEvaluate(
  request: LLMRequest,
  response: string,
  sampleRate = 0.1
): Promise {
  if (Math.random() > sampleRate) return; // avaliar apenas ~10%

  const result = await judgeOutput(
    request.userInput,
    request.systemContext,
    response
  );

  await saveEvaluation({
    promptVersion: request.promptVersion,
    promptHash: request.promptHash,
    feature: request.feature,
    score: result.score,
    flags: result.flags,
    evaluatedAt: new Date(),
  });
}

Rastreamento de custo por feature

Custo de LLM em produção é notoriamente difícil de prever. A granularidade que importa é custo por feature, não custo total da conta — porque quando o custo explode, você precisa saber qual feature é responsável e qual mudança de prompt causou o spike.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

// Preços em USD por 1M tokens (atualizar quando o provider mudar)
const TOKEN_COSTS: Record = {
  "claude-3-5-sonnet-20241022": { input: 3.0, output: 15.0 },
  "claude-3-5-haiku-20241022":  { input: 0.8, output: 4.0 },
  "claude-opus-4-5":            { input: 15.0, output: 75.0 },
};

interface LLMTrace {
  traceId: string;
  feature: string;
  promptVersion: string;
  promptHash: string;
  model: string;
  inputTokens: number;
  outputTokens: number;
  costUsd: number;
  latencyMs: number;
  success: boolean;
  timestamp: Date;
}

export async function trackedLLMCall(params: {
  feature: string;
  promptVersion: string;
  promptHash: string;
  systemPrompt: string;
  messages: Anthropic.MessageParam[];
  model?: string;
}): Promise<{ response: Anthropic.Message; trace: LLMTrace }> {
  const model = params.model ?? "claude-3-5-sonnet-20241022";
  const traceId = crypto.randomUUID();
  const start = Date.now();

  const response = await client.messages.create({
    model,
    max_tokens: 1024,
    system: params.systemPrompt,
    messages: params.messages,
  });

  const latencyMs = Date.now() - start;
  const costs = TOKEN_COSTS[model] ?? { input: 0, output: 0 };
  const costUsd =
    (response.usage.input_tokens / 1_000_000) * costs.input +
    (response.usage.output_tokens / 1_000_000) * costs.output;

  const trace: LLMTrace = {
    traceId,
    feature: params.feature,
    promptVersion: params.promptVersion,
    promptHash: params.promptHash,
    model,
    inputTokens: response.usage.input_tokens,
    outputTokens: response.usage.output_tokens,
    costUsd,
    latencyMs,
    success: response.stop_reason === "end_turn",
    timestamp: new Date(),
  };

  // Fire-and-forget para não adicionar latência
  sendTrace(trace).catch((err) =>
    console.error("Falha ao enviar trace:", err)
  );

  return { response, trace };
}

async function sendTrace(trace: LLMTrace): Promise {
  await fetch("/api/internal/llm-traces", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(trace),
  });
}

Com esse rastreamento, você consegue responder perguntas como: "Qual feature consome mais tokens por request?", "O custo médio da feature X aumentou após a mudança de prompt?", "Qual percentual do custo mensal é imputável ao agente de classificação?"

Alertas de degradação de qualidade

Drift de qualidade é quando a performance do LLM piora gradualmente e você não percebe até que o problema já é grave. O monitoramento precisa ser proativo.

# prometheus/rules/llmops.yaml
groups:
  - name: llmops_quality
    rules:
      # Score de qualidade caiu 15% em relação à média de 7 dias
      - alert: LLMQualityDegradation
        expr: |
          avg_over_time(llm_judge_score_avg{feature="suporte_classificacao"}[1h])
          <
          avg_over_time(llm_judge_score_avg{feature="suporte_classificacao"}[7d] offset 1h) * 0.85
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Qualidade do LLM caiu mais de 15% na feature {{ $labels.feature }}"
          description: "Score atual: {{ $value }}"

      # Custo por request dobrou em relação à baseline
      - alert: LLMCostSpike
        expr: |
          rate(llm_cost_usd_total[1h])
          >
          rate(llm_cost_usd_total[7d] offset 1h) * 2.0
        for: 15m
        labels:
          severity: critical
        annotations:
          summary: "Custo de LLM 2x acima da baseline"

      # Latência P95 acima de 5 segundos
      - alert: LLMHighLatency
        expr: |
          histogram_quantile(0.95, rate(llm_latency_ms_bucket[5m])) > 5000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P95 de latência do LLM acima de 5s"

      # Taxa de outputs com flags de problema acima de 20%
      - alert: LLMHighFlagRate
        expr: |
          rate(llm_evaluation_flags_total[1h])
          /
          rate(llm_evaluation_total[1h]) > 0.20
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Taxa de flags de qualidade acima de 20%"

A/B testing de prompts em produção

Antes de fazer rollout de um novo prompt para 100% do tráfego, valide em produção com tráfego real. O roteamento deve ser determinístico por usuário — o mesmo usuário sempre cai na mesma variante durante o experimento, evitando experiência inconsistente.

import * as crypto from "crypto";

interface PromptVariant {
  id: string;
  version: string;
  weight: number; // 0-100; soma de todas as variantes deve ser 100
}

export function selectVariant(
  userId: string,
  experimentId: string,
  variants: PromptVariant[]
): PromptVariant {
  // Hash determinístico: mesmo userId + experimentId sempre retorna o mesmo bucket
  const seed = `${experimentId}:${userId}`;
  const hash = crypto.createHash("md5").update(seed).digest("hex");
  const bucket = parseInt(hash.slice(0, 8), 16) % 100;

  let cumulative = 0;
  for (const variant of variants) {
    cumulative += variant.weight;
    if (bucket < cumulative) return variant;
  }

  // Fallback para a última variante (nunca deveria chegar aqui)
  return variants[variants.length - 1];
}

// Configuração de um experimento típico: 80/20 split
const experiment = {
  id: "exp-prompt-classificacao-v15",
  variants: [
    { id: "control",   version: "1.4.2", weight: 80 },
    { id: "treatment", version: "1.5.0", weight: 20 },
  ],
};

const variant = selectVariant(userId, experiment.id, experiment.variants);
// Registrar variant.id no trace para análise posterior do experimento

Critérios para promover a variante nova para 100%:

  • Score de LLM-as-judge estatisticamente igual ou superior ao controle
  • Custo por request igual ou menor
  • Latência P95 dentro de 15% da versão anterior
  • Nenhum aumento na taxa de flags de problema
  • Mínimo de 500 avaliações por variante para significância estatística

Stack sugerida para times em diferentes estágios

Time early-stage (0-10k requests/dia): Git + logs estruturados

Prompts em YAML no repositório, logs estruturados com promptVersion e promptHash em cada request, dashboards no Datadog ou CloudWatch. Simples, sem dependência extra, suficiente para detectar regressões grosseiras.

Time em crescimento (10k-1M requests/dia): Langfuse self-hosted

Langfuse é open source, pode ser rodado em Docker, tem SDK para TypeScript e Python, e oferece interface web para visualizar traces, comparar versões de prompt e gerenciar datasets de avaliação. É a melhor opção para quem quer a UX de LangSmith sem enviar dados para terceiros.

Time em escala (>1M requests/dia): stack própria

  • ClickHouse: armazenamento colunar de traces — consultas analíticas sobre bilhões de registros em segundos
  • Prometheus + Grafana: métricas de infra (latência, taxa de erro, custo agregado)
  • Grafana Tempo: distributed tracing para rastrear o fluxo de uma request pelo pipeline
  • Serviço de avaliação dedicado: worker assíncrono que consome requests amostradas e roda o LLM-as-judge em background

Checklist de LLMOps para colocar em produção

Antes do deploy

  • Todos os prompts versionados em arquivo de configuração com semver e hash de conteúdo
  • Dataset de avaliação com mínimo 50 pares input/output esperado por prompt
  • Custo estimado por request documentado e aprovado
  • Fallback definido: retry com backoff, fallback para modelo menor, ou mensagem de erro ao usuário
  • Timeout explícito configurado em cada chamada ao LLM (nunca confiar no default)
  • Testes de regressão passando contra o dataset de avaliação

No deploy e operação contínua

  • Rastreamento ativo: cada chamada gera trace com promptVersion, promptHash, inputTokens, outputTokens, custo, latência
  • Amostragem para avaliação configurada em 5-10% das requests
  • Alertas ativos para degradação de qualidade, spike de custo e latência alta
  • Dashboard com latência P95, custo diário por feature e score de qualidade visíveis
  • Revisão semanal das amostras com score baixo do LLM-as-judge
  • A/B testing obrigatório para toda mudança de prompt — nunca rollout direto para 100%
  • Teste do dataset de avaliação antes de migrar para nova versão do modelo do provider

Conclusão

LLMOps não é overhead burocrático. É a diferença entre um produto de IA que funciona de forma confiável ao longo do tempo e um que começa bem e vai degradando silenciosamente até o cliente parar de usar.

Comece pelo essencial: prompts versionados no Git, rastreamento de custo por feature, um alerta de latência. Adicione LLM-as-judge quando tiver volume suficiente. Implemente A/B testing quando a frequência de mudanças de prompt justificar. Evolua a stack conforme o problema cresce.

O importante é não operar às cegas.

Na Neryx, ajudamos equipes a construir sistemas de IA que funcionam em produção com a disciplina de engenharia que produtos críticos exigem. Se você está colocando LLMs em produção e quer uma revisão da sua arquitetura de observabilidade, fale com a gente.

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.