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.