Fazer um chatbot ou um resumidor de documentos funcionar em um Jupyter Notebook é relativamente simples — uma chamada de API e o modelo faz o trabalho. Colocar isso em produção com SLA, custo previsível, qualidade mensurável e capacidade de evoluir sem quebrar usuários é uma disciplina inteiramente diferente. LLMOps (Large Language Model Operations) é o conjunto de práticas que torna isso possível.
O que faz uma aplicação LLM "estar em produção"
Um sistema LLM em produção precisa garantir quatro propriedades simultaneamente: comportamento previsível (o modelo faz o que você quer, não o que você acha que quer), custo controlado (tokens consumidos não explodem com volume), qualidade mensurável (você sabe quando o sistema piorou), e capacidade de evolução (você pode melhorar o sistema sem regredir casos existentes). Sem LLMOps, você tem um protótipo esperando para falhar em escala.
Versionamento de prompts: o Git para seu cérebro do sistema
Prompt é código. Sem versionamento, você não sabe qual versão está em produção, não consegue fazer rollback quando uma mudança piora a qualidade, e não tem base para comparar experimentos:
// Estrutura de arquivo de prompt versionado
// prompts/resumo-contrato/v3.yaml
version: "3.0.0"
name: "resumo-contrato"
description: "Resumo executivo de contratos jurídicos para gestores não técnicos"
model: "claude-3-5-sonnet-20241022"
max_tokens: 800
temperature: 0.1
created_at: "2026-01-15"
author: "danilo@neryx.com.br"
system: |
Você é um assistente jurídico sênior especializado em simplificar contratos
para gestores não-jurídicos. Seu objetivo é extrair as informações essenciais
sem ambiguidade e sem jargão técnico.
Responda SEMPRE no formato JSON com a estrutura:
{
"partes": [...],
"objeto": "...",
"valor_total": "...",
"prazo": "...",
"clausulas_risco": [...],
"resumo_executivo": "..."
}
Nunca inclua texto fora do JSON. Nunca faça inferências — se uma informação
não estiver no texto, use null no campo.
user_template: |
Analise o seguinte contrato e extraia as informações solicitadas:
<contrato>
{{texto_contrato}}
</contrato>
changelog:
- "3.0.0: Adicionado campo clausulas_risco. Formato JSON obrigatório."
- "2.1.0: Instruções de null para campos ausentes (reduziu alucinações 34%)"
- "2.0.0: Reescrita completa — v1 estava gerando parágrafos sem estrutura"
O prompt é lido em runtime e associado à versão do deployment. Você consegue fazer A/B testing entre versões e medir qual produz melhores outputs.
Guardrails: controle de input e output
Guardrails são verificações que ocorrem antes de enviar para o modelo (input) e depois de receber a resposta (output). Eles protegem contra: injeção de prompt, outputs fora do formato esperado, conteúdo indesejado, e custos inesperados por inputs maliciosos.
public class LlmPipeline
{
private readonly ILlmClient _client;
private readonly ILogger<LlmPipeline> _logger;
// Guardrail de input: verifica tamanho, sanitiza e detecta injection
private Result<string> ValidarInput(string input)
{
// Limite de tokens antes de enviar (evita custos explosivos)
if (input.Length > 50_000)
return Result.Failure("Input excede 50.000 caracteres");
// Detecção básica de prompt injection
var padroesSuspeitos = new[]
{
"ignore previous instructions",
"ignore all previous",
"disregard your",
"system: você agora é",
};
if (padroesSuspeitos.Any(p =>
input.Contains(p, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogWarning("Possível tentativa de prompt injection detectada");
return Result.Failure("Input contém instruções não permitidas");
}
return Result.Success(SanitizarInput(input));
}
// Guardrail de output: valida formato e filtra conteúdo
private Result<T> ValidarOutput<T>(string rawOutput) where T : class
{
// Tenta desserializar — modelo pode ter saído do formato
try
{
var resultado = JsonSerializer.Deserialize<T>(rawOutput,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (resultado is null)
return Result.Failure("Output deserializou como null");
// Validação de campos obrigatórios (via FluentValidation ou DataAnnotations)
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateObject(resultado,
new ValidationContext(resultado), validationResults, true))
{
var erros = string.Join(", ", validationResults.Select(v => v.ErrorMessage));
return Result.Failure($"Output inválido: {erros}");
}
return Result.Success(resultado);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Output do LLM não é JSON válido: {Raw}", rawOutput[..Math.Min(200, rawOutput.Length)]);
return Result.Failure("Modelo não retornou JSON no formato esperado");
}
}
public async Task<Result<T>> ExecutarAsync<T>(
PromptTemplate template,
Dictionary<string, string> variaveis,
CancellationToken ct) where T : class
{
// 1. Guardrail de input
var inputValidado = template.Variaveis.Aggregate(
Result<string>.Success(""),
(acc, kv) => acc.IsSuccess ? ValidarInput(variaveis[kv]) : acc);
if (!inputValidado.IsSuccess) return Result.Failure<T>(inputValidado.Error);
// 2. Render do prompt com variáveis
var prompt = template.Render(variaveis);
// 3. Chamada ao modelo com retry e timeout
var rawOutput = await ExecutarComRetryAsync(prompt, template, ct);
// 4. Guardrail de output
return ValidarOutput<T>(rawOutput);
}
}
Avaliação de qualidade: você precisa de métricas
Sem métricas de qualidade, você não sabe se uma mudança de prompt melhorou ou piorou o sistema. As métricas variam por caso de uso, mas há categorias comuns:
// Tipos de avaliação por automação
public enum TipoAvaliacao
{
// Automático: baseado em regras
Estrutural, // JSON válido, campos presentes, formato correto
Factual, // Verifica fatos contra fonte de verdade conhecida
// Semi-automático: LLM avalia LLM (LLM-as-judge)
Relevancia, // O output é relevante para a pergunta?
Fidelidade, // O output é fiel ao contexto fornecido? (anti-alucinação)
Completude, // Todas as informações pedidas estão presentes?
// Manual: humano avalia
Qualidade, // Julgamento subjetivo de qualidade
}
// Exemplo: avaliador de fidelidade usando LLM-as-judge
public class FidelidadeAvaliador
{
public async Task<AvaliacaoResult> AvaliarAsync(
string contexto,
string resposta,
CancellationToken ct)
{
var prompt = $"""
Avalie se a RESPOSTA é fiel ao CONTEXTO fornecido.
CONTEXTO: {contexto}
RESPOSTA: {resposta}
Responda APENAS com JSON:
{{
"score": 0-10,
"fiel": true/false,
"problemas": ["lista de afirmações não suportadas pelo contexto"]
}}
""";
var resultado = await _cliente.CompletarAsync(prompt, ct);
return JsonSerializer.Deserialize<AvaliacaoResult>(resultado)!;
}
}
Construa um conjunto de casos de teste com inputs e outputs esperados — seu "test suite" de LLM. Antes de qualquer mudança de prompt em produção, rode os casos de teste e compare scores. Regressão de qualidade é tão grave quanto regressão de código.
Observabilidade: o que monitorar
LLM em produção precisa de três camadas de observabilidade:
// Trace de uma execução LLM — capture tudo
public record LlmTrace
{
public Guid TraceId { get; init; } = Guid.NewGuid();
public string PromptVersion { get; init; } // qual versão do prompt
public string Modelo { get; init; } // claude-3-5-sonnet, gpt-4o, etc.
public int TokensInput { get; init; }
public int TokensOutput { get; init; }
public decimal CustoEstimado { get; init; } // calcule por modelo
public TimeSpan Latencia { get; init; }
public bool Sucesso { get; init; }
public string? ErroGuardrail { get; init; }
public double? ScoreQualidade { get; init; } // se avaliação automática rodou
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
// NÃO armazene o conteúdo completo do input/output — pode ter dados sensíveis
// Armazene apenas hashes ou amostras anonimizadas para debugging
}
// Métricas a monitorar (OpenTelemetry/Prometheus)
// 1. Custo acumulado por dia/semana/mês (por feature, por usuário se relevante)
// 2. Latência p50/p95/p99 (LLMs têm cauda pesada)
// 3. Taxa de erro de guardrail (input rejeitado vs output inválido)
// 4. Taxa de retry (indica instabilidade do modelo ou prompts mal construídos)
// 5. Score de qualidade médio (se LLM-as-judge estiver implementado)
// 6. Distribuição de tokens — detecta inputs anômalos (possível abuso)
Controle de custo: a variável esquecida
Custo de LLM é proporcional a tokens × preço_por_token. Em aplicações que crescem, isso pode surpreender:
// Estratégias de controle de custo
public class CustoController
{
// 1. Cache semântico: evita chamar o modelo para inputs similares
public async Task<string?> BuscarCacheAsync(string input, CancellationToken ct)
{
var embedding = await _embeddings.GerarAsync(input, ct);
var similar = await _vectorStore.BuscarSimilarAsync(embedding, threshold: 0.95f, ct);
return similar?.Output;
}
// 2. Roteamento de modelo por complexidade
public string SelecionarModelo(string input, TarefaTipo tipo)
{
var tokens = EstimarTokens(input);
return tipo switch
{
TarefaTipo.ClassificacaoSimples when tokens < 500
=> "claude-haiku-3-5", // 40x mais barato que Sonnet
TarefaTipo.ExtrairDados when tokens < 2000
=> "gpt-4o-mini", // bom custo-benefício para extração
TarefaTipo.AnalisarContrato or TarefaTipo.GerarCodigo
=> "claude-sonnet-4-5", // vale o custo para tarefas complexas
_ => "claude-sonnet-4-5"
};
}
// 3. Limite de budget por tenant/usuário
public async Task<bool> VerificarBudgetAsync(string tenantId, CancellationToken ct)
{
var custoHoje = await _metricas.SomarCustoAsync(tenantId, DateTime.Today, ct);
var limite = await _config.GetLimiteDiarioAsync(tenantId, ct);
return custoHoje < limite;
}
}
Versionamento de modelo: lidar com depreciações
Modelos são depreciados. GPT-3.5-turbo já foi substituído múltiplas vezes. Claude 2 foi depreciado. O que funciona hoje pode parar de funcionar em 12 meses. Estratégia para não ser pego de surpresa:
// Configuração de modelo por feature — nunca hardcode no código
// appsettings.json
{
"LlmConfig": {
"Features": {
"ResumirContrato": {
"ModeloAtual": "claude-sonnet-4-5-20250929",
"ModeloFallback": "gpt-4o-2024-11-20",
"MaxTokens": 1000,
"Temperature": 0.1
},
"ClassificarTicket": {
"ModeloAtual": "claude-haiku-4-5-20251001",
"ModeloFallback": "gpt-4o-mini-2024-07-18",
"MaxTokens": 300,
"Temperature": 0
}
}
}
}
Com essa estrutura, trocar o modelo de uma feature é uma mudança de configuração — sem alterar código. O fallback garante disponibilidade durante depreciações ou outages.
Pipeline completo em produção
Unindo tudo em um fluxo de execução:
Request → [Guardrail Input] → [Cache Semântico?]
↓ cache miss ↓ cache hit
[Selecionar Modelo] [Retornar cache]
↓
[Render Prompt vX.Y.Z]
↓
[Chamar LLM com retry]
↓
[Guardrail Output]
↓ falha ↓ sucesso
[Log + Fallback] [Avaliação automática]
↓
[Armazenar trace]
↓
[Cache resultado?]
↓
[Retornar ao cliente]
LLMOps não é MLOps renomeado — modelos de linguagem têm características únicas: respostas probabilísticas, custo variável por request, depreciação de modelos, e avaliação de qualidade não determinística. As práticas acima são a diferença entre um protótipo que impressiona em demo e um sistema de IA que funciona em produção mês a mês.