IA LLMOps Produção MLOps Engenharia

LLMOps: como colocar aplicações de IA em produção de verdade

Guia prático de LLMOps: versionamento de prompts, guardrails, avaliação de outputs, monitoramento de custo e observabilidade para aplicações com LLMs em.

N
Neryx Digital Architects
10 de março de 2026
16 min de leitura
260 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

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.