Blog

Guardrails de saída em LLM: validação e recusa segura

A parte perigosa de um sistema com LLM nao e o prompt que entra, e a resposta que sai. O modelo pode devolver JSON quebrado que estoura o parser, inventar um campo que nunca existiu, vazar o CPF que estava no contexto, recusar uma pergunta legitima ou pedir para chamar uma tool destrutiva com argumento errado. Sem uma camada que inspeciona a saida antes de ela chegar ao usuario ou ao banco, cada uma dessas falhas vira bug de producao, incidente de privacidade ou acao irreversivel. Guardrails de saida sao essa camada: um conjunto de validacoes entre o modelo e o mundo que decide se a resposta pode passar, precisa ser reparada ou tem que ser bloqueada. Este artigo mostra como construir essa camada sem transformar o produto num labirinto de if: validacao de schema com reparo e retry, deteccao de recusa e de vazamento, bloqueio de acao perigosa antes da execucao, e a regra de ouro que amarra tudo, sempre ter um fallback seguro. O foco e o minimo que impede a saida ruim de virar dano real.

2026-07-09 / IA Aplicada / 13 min

01

Guardrail de entrada nao e guardrail de saida

A confusao mais comum e achar que validar o prompt de entrada resolve o problema. Nao resolve: sao dois riscos diferentes em momentos diferentes. O guardrail de entrada protege contra o que o usuario manda (prompt injection, pedido abusivo, conteudo proibido) e roda antes do modelo. O guardrail de saida protege contra o que o modelo devolve (formato invalido, alucinacao, vazamento, acao perigosa) e roda depois do modelo, antes de a resposta chegar ao usuario, ao banco ou a uma tool. Uma entrada perfeitamente valida pode gerar uma saida perigosa, porque o modelo e probabilistico e nao garante nada sobre o que produz.

O ponto de instalacao importa: o guardrail de saida fica no caminho de retorno, envelopando a resposta do modelo como um interceptor. Nada que o modelo produz chega ao mundo externo sem passar por ele. A tabela abaixo separa os dois para deixar claro que um nao substitui o outro.

DimensaoGuardrail de entradaGuardrail de saida
Quando rodaAntes de chamar o modeloDepois do modelo, antes do usuario/banco/tool
Protege contraPrompt injection, pedido abusivo, PII na entradaFormato invalido, alucinacao, vazamento, acao perigosa
Acao tipicaRecusar, sanitizar, rotearReparar, fazer retry, bloquear, fallback
Se falharModelo recebe entrada ruimUsuario recebe saida ruim (dano real)

A regra pratica: guardrail de entrada reduz a chance de saida ruim, mas nunca a elimina. A validacao que de fato protege o usuario e a de saida, porque e a ultima antes do dano. Investir so na entrada e trancar a porta da frente e deixar a dos fundos aberta.

02

Validacao de schema: o retry estruturado que conserta em vez de quebrar

O guardrail mais barato e mais rentavel e a validacao de schema. Quando a saida do modelo deveria ser JSON estruturado (para virar chamada de API, registro no banco ou decisao de fluxo), voce nao confia que veio certo, voce valida contra um schema. Mas o passo que separa um sistema fragil de um robusto e o que fazer quando a validacao falha: em vez de estourar um erro para o usuario, voce devolve o erro de validacao ao proprio modelo e pede para ele corrigir. O modelo que produziu o JSON quebrado quase sempre acerta na segunda tentativa quando recebe a mensagem exata do que estava errado.

// guardrails/schema.js
// Valida a saida contra um schema e, se falhar, faz retry com o erro
// como feedback. O modelo conserta o proprio JSON com a mensagem exata.

export async function ensureValidOutput(callModel, { schema, maxRetries = 2 }) {
  let lastError = null;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    // No retry, injeta o erro anterior como instrucao de correcao.
    const raw = await callModel(lastError ? feedbackFor(lastError) : null);

    const parsed = tryParseJson(raw);
    if (!parsed.ok) {
      lastError = { kind: 'parse', detail: parsed.error };
      continue; // JSON quebrado: tenta de novo com o erro de parse
    }

    const validation = schema.validate(parsed.value);
    if (!validation.ok) {
      lastError = { kind: 'schema', detail: validation.errors };
      continue; // schema invalido: tenta de novo listando os campos errados
    }

    return { ok: true, value: parsed.value };
  }

  // Esgotou o retry: NAO propaga saida invalida, sinaliza para o fallback.
  return { ok: false, error: lastError };
}

function feedbackFor(err) {
  return err.kind === 'parse'
    ? `Sua resposta anterior nao era JSON valido: ${err.detail}. Responda apenas com JSON.`
    : `Sua resposta nao respeitou o schema. Corrija estes campos: ${JSON.stringify(err.detail)}.`;
}

Dois detalhes fazem a diferenca. Primeiro, o retry tem limite: duas tentativas cobrem a esmagadora maioria dos casos, e insistir alem disso so queima token e latencia num modelo que nao vai convergir. Segundo, quando o retry esgota, o retorno nao e a saida invalida, e um sinal de falha que o fallback vai tratar. Propagar JSON quebrado depois de duas tentativas e trocar um erro controlado por um erro em producao.

03

Detectar recusa indevida e recusa legitima

Nem toda recusa e um problema, e nem toda resposta fluida esta correta. O modelo pode recusar uma pergunta perfeitamente legitima ("nao posso ajudar com isso") por excesso de zelo, e pode responder com confianca algo que deveria ter recusado. O guardrail de recusa mede esse eixo: ele classifica se a resposta e uma recusa e decide se aquela recusa faz sentido no contexto. Recusa em cima de pedido valido e degradacao de produto, o usuario bateu numa parede sem motivo. Ausencia de recusa em pedido perigoso e risco, o modelo passou por cima de um limite que deveria respeitar.

  • Detectar a recusa: procure os padroes de recusa da sua stack ("nao posso", "nao consigo ajudar", "isso vai contra") e trate como um sinal classificavel, nao como texto qualquer.
  • Classificar o contexto: a pergunta era legitima? Se sim, uma recusa e falha, e o caminho e re-perguntar com prompt ajustado ou rotear para humano, nao devolver a parede ao usuario.
  • Medir a taxa de recusa: recusa subindo de repente costuma ser prompt quebrado ou guardrail agressivo demais, e so aparece se voce conta as recusas como metrica.
  • Nao suprimir recusa legitima: quando o modelo recusa algo que deve mesmo recusar, o guardrail confirma e registra, ele nao forca uma resposta que abriria um buraco de seguranca.

O erro classico e tratar recusa como falha sempre, e reescrever o prompt ate o modelo responder qualquer coisa. Isso quebra a recusa legitima e transforma o guardrail num vetor de ataque. A postura certa e distinguir: recusa indevida se conserta, recusa legitima se respeita. O guardrail nao existe para forcar resposta, existe para garantir que a decisao de responder ou nao esteja alinhada com o contexto.

04

Vazamento de dado sensivel na saida

Um risco silencioso: o contexto do modelo carrega dado sensivel (CPF, telefone, dado de outro usuario que entrou por engano no retrieval) e o modelo repete esse dado na resposta. A entrada estava sob controle, mas a saida vaza. O guardrail de vazamento inspeciona a resposta antes de entregar e bloqueia ou redige o que nao deveria sair. E a mesma logica da redacao de log, mas aqui o alvo e o que chega ao usuario final, entao a barra e mais alta: em log voce redige para nao persistir, na saida voce redige ou bloqueia para nao expor.

// guardrails/leak.js
// Verifica se a resposta contem dado sensivel que nao deveria sair.
// Em vez de so mascarar, decide entre passar, redigir ou bloquear.

const SENSITIVE = [
  { name: 'cpf',   re: /\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b/g },
  { name: 'card',  re: /\b(?:\d[ -]?){13,16}\b/g },
  { name: 'email', re: /[\w.+-]+@[\w-]+\.[\w.-]+/g },
];

export function checkLeak(output, { allowed = [] }) {
  const found = [];
  for (const rule of SENSITIVE) {
    if (allowed.includes(rule.name)) continue; // ex.: email do proprio usuario pode
    if (rule.re.test(output)) found.push(rule.name);
  }

  if (found.length === 0) return { action: 'pass', output };

  // Dado sensivel que o usuario nao deveria ver: bloqueia, nao arrisca.
  // Dado que so precisa ser ocultado: redige e deixa passar.
  const critical = found.some((f) => f === 'cpf' || f === 'card');
  if (critical) return { action: 'block', leaked: found };

  const redacted = SENSITIVE.reduce(
    (acc, r) => (allowed.includes(r.name) ? acc : acc.replace(r.re, `[${r.name.toUpperCase()}]`)),
    output,
  );
  return { action: 'redact', output: redacted, leaked: found };
}

A decisao entre redigir e bloquear e o que separa um guardrail util de um perigoso. Mascarar um email num texto que ainda faz sentido e razoavel. Mas quando a resposta inteira gira em torno de um dado que vazou por engano (o modelo confundiu o pedido de um usuario com o cadastro de outro), redigir deixa passar uma resposta sem sentido e potencialmente incriminadora. Nesse caso, bloquear e cair no fallback e mais seguro do que entregar algo mutilado.

05

Bloquear acao perigosa antes de executar

O guardrail mais critico e o que fica entre o modelo e uma acao com efeito colateral. Quando o LLM decide chamar uma tool (cancelar pedido, emitir reembolso, apagar registro, disparar mensagem em massa), a saida do modelo deixa de ser texto e vira comando. Um argumento alucinado, um id errado ou um valor fora de faixa nao geram uma resposta ruim, geram uma acao irreversivel. Aqui a saida do modelo e uma proposta de acao, e o guardrail e a aprovacao: valida os argumentos, checa limites e politicas, e so entao deixa executar.

// guardrails/action.js
// A saida do modelo (tool call) e uma PROPOSTA. O guardrail valida
// argumentos e politica antes de deixar a acao rodar de verdade.

const POLICIES = {
  refund: { maxAmount: 500, requiresOrderId: true },
  bulkMessage: { maxRecipients: 100 },
};

export function authorizeAction(action) {
  const policy = POLICIES[action.name];
  if (!policy) return { ok: false, reason: 'acao desconhecida: nega por padrao' };

  // Argumento fora da faixa vira bloqueio, nunca execucao.
  if (policy.maxAmount != null && action.args.amount > policy.maxAmount) {
    return { ok: false, reason: `valor ${action.args.amount} acima do limite ${policy.maxAmount}` };
  }
  if (policy.requiresOrderId && !action.args.orderId) {
    return { ok: false, reason: 'reembolso sem orderId: exige revisao humana' };
  }
  if (policy.maxRecipients != null && action.args.recipients?.length > policy.maxRecipients) {
    return { ok: false, reason: 'disparo acima do limite: rotear para aprovacao' };
  }

  return { ok: true }; // dentro da politica: pode executar
}

Dois principios sustentam esse guardrail. Negar por padrao: uma acao que nao esta na tabela de politicas nao executa, porque o modelo pode inventar um nome de tool que voce nunca definiu. E escalar em vez de simplesmente falhar: reembolso acima do limite ou disparo em massa nao viram erro seco, viram um pedido de aprovacao humana. O guardrail nao existe so para dizer nao, existe para rotear a decisao para quem tem autoridade quando o modelo esta fora da alcada.

06

Fallback seguro: a regra de ouro

Todo guardrail acima compartilha a mesma regra de ouro: quando algo da errado, a saida nunca e a resposta ruim, e sempre um fallback seguro. Schema que nao valida depois do retry, recusa que nao se conserta, vazamento critico, acao fora da politica, todos convergem para o mesmo lugar, uma resposta controlada que voce escreveu, nao uma que o modelo alucinou. O anti-padrao e deixar a falha vazar como erro tecnico (stack trace, 500, JSON quebrado na tela) ou, pior, deixar a saida ruim passar porque o guardrail so logou e nao bloqueou.

Caminho da saida do modelo pelos guardrails

  resposta do modelo
        |
        v
  [ schema valido? ] --nao--> retry (ate 2x) --falhou--> FALLBACK
        | sim
        v
  [ e recusa? ] --sim--> legitima? --nao--> re-perguntar / humano
        | nao (ou legitima)
        v
  [ vaza dado sensivel? ] --critico--> BLOQUEIA --> FALLBACK
        |                --redige--> segue com texto redigido
        v
  [ e acao? ] --fora da politica--> BLOQUEIA --> aprovacao humana
        | dentro da politica
        v
  entrega ao usuario / executa a acao

  FALLBACK = resposta segura escrita por voce, nunca erro cru na tela

O fallback certo depende do contexto: numa resposta de texto, e uma mensagem honesta ("nao consegui gerar uma resposta confiavel agora, vou te transferir"); numa chamada de tool, e nao executar e escalar; num fluxo automatico, e parar e alertar em vez de seguir com dado suspeito. O que ele nunca e: um erro tecnico jogado na cara do usuario ou uma saida invalida que passou porque ninguem bloqueou. A diferenca entre um sistema que degrada com dignidade e um que quebra feio esta inteira nessa decisao.

Um detalhe operacional fecha o ciclo: todo acionamento de fallback e um evento que voce quer contar. Fallback subindo e o sinal mais direto de que o modelo, o prompt ou o schema mudaram de comportamento, e conecta o guardrail a observabilidade, o assunto do artigo relacionado. Guardrail sem metrica de acionamento e uma rede de seguranca que voce nao sabe se esta segurando alguem.

07

Montar a camada sem virar labirinto de if

A armadilha e espalhar validacao por todo o codigo em ifs soltos, ate ninguem saber mais qual regra roda quando. A camada de guardrails deve ser uma esteira ordenada e centralizada: a saida do modelo entra por uma ponta, passa pelos guardrails na ordem certa, e sai validada pela outra, ou cai no fallback. Cada guardrail e uma funcao pequena e testavel isoladamente; a esteira apenas os encadeia. O caminho e adicionar por ordem de risco.

  1. Comece pela validacao de schema com retry: e o mais barato, o mais frequente e o que mais evita bug bobo de parser em producao.
  2. Adicione o fallback seguro logo em seguida: sem ele, os outros guardrails so trocam um erro por outro; com ele, toda falha tem destino controlado.
  3. Ligue o guardrail de acao antes de qualquer tool com efeito colateral: aqui o custo de errar e irreversivel, entao ele nao e opcional.
  4. Instrumente o guardrail de vazamento onde a resposta contem dado do usuario: quanto mais sensivel o dominio, mais cedo ele entra.
  5. Coloque a deteccao de recusa por ultimo e conecte tudo a metricas: acionamento de cada guardrail vira linha no dashboard, e o baseline revela quando algo mudou.

A diferenca entre um sistema com LLM que da confianca e um que assusta esta em quem controla a saida. Sem guardrails, e o modelo, probabilistico e sem garantia, que decide o que chega ao usuario e ao banco. Com guardrails, o modelo propoe e a sua camada dispoe: valida, repara, bloqueia ou cai no fallback, mas nunca deixa a saida ruim virar dano. Poucas centenas de linhas de guardrail bem colocadas separam um piloto que voce nao poe na frente do cliente de um produto que aguenta producao.

FAQ

Perguntas frequentes

Guardrail de saida nao e a mesma coisa que validar o prompt de entrada?

Nao. Sao dois controles em momentos diferentes. O guardrail de entrada roda antes do modelo e protege contra o que o usuario manda (prompt injection, pedido abusivo). O guardrail de saida roda depois do modelo e protege contra o que ele devolve (formato invalido, alucinacao, vazamento, acao perigosa). Uma entrada perfeitamente valida pode gerar uma saida perigosa, porque o modelo e probabilistico. A validacao que de fato protege o usuario e a de saida, porque e a ultima antes do dano chegar ao banco, a uma tool ou a tela.

Por que fazer retry em vez de so retornar erro quando o schema falha?

Porque o modelo que produziu o JSON quebrado quase sempre acerta na segunda tentativa quando recebe a mensagem exata do que estava errado. Retornar erro na primeira falha desperdicaria uma correcao facil e barata. A chave e o retry ser estruturado (voce injeta o erro de validacao como feedback) e limitado (duas tentativas cobrem quase tudo; insistir alem disso so queima token). E, quando o retry esgota, o retorno nao e a saida invalida, e um sinal de falha que cai no fallback seguro, nunca JSON quebrado propagado para o usuario.

O guardrail deve sempre bloquear quando encontra dado sensivel na saida?

Nao sempre; depende do dado e do papel dele na resposta. Se e um dado que so precisa ser ocultado e a resposta continua fazendo sentido sem ele, redigir (mascarar) e suficiente. Mas se o dado e critico (CPF, cartao) ou se a resposta inteira gira em torno de um dado que vazou por engano, redigir deixaria passar algo sem sentido ou incriminador, entao o certo e bloquear e cair no fallback. A regra: redija quando ocultar preserva a resposta, bloqueie quando o vazamento contamina a resposta toda.

Guardrails de saida sao o que impede a resposta ruim de virar dano real

Validacao de schema com retry, deteccao de recusa e vazamento, bloqueio de acao perigosa e fallback seguro sao o minimo para que nada que o modelo produz chegue ao usuario ou ao banco sem passar por um controle. Posso desenhar essa camada no seu produto, encadeada e observavel, do schema ao fallback, integrada ao seu stack.