Blog

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

A parte perigosa de um sistema com LLM não é o prompt que entra, é 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 legítima ou pedir para chamar uma tool destrutiva com argumento errado. Sem uma camada que inspeciona a saída antes de ela chegar ao usuário ou ao banco, cada uma dessas falhas vira bug de produção, incidente de privacidade ou ação irreversível. Guardrails de saída são essa camada: um conjunto de validações 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: validação de schema com reparo e retry, detecção de recusa e de vazamento, bloqueio de ação perigosa antes da execução, e a regra de ouro que amarra tudo, sempre ter um fallback seguro. O foco é o mínimo que impede a saída ruim de virar dano real.

2026-07-09 / IA Aplicada / 13 min

01

Guardrail de entrada não é guardrail de saída

A confusão mais comum é achar que validar o prompt de entrada resolve o problema. Não resolve: são dois riscos diferentes em momentos diferentes. O guardrail de entrada protege contra o que o usuário manda (prompt injection, pedido abusivo, conteúdo proibido) e roda antes do modelo. O guardrail de saída protege contra o que o modelo devolve (formato inválido, alucinação, vazamento, ação perigosa) e roda depois do modelo, antes de a resposta chegar ao usuário, ao banco ou a uma tool. Uma entrada perfeitamente válida pode gerar uma saída perigosa, porque o modelo é probabilístico e não garante nada sobre o que produz.

O ponto de instalação importa: o guardrail de saída 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 não substitui o outro.

DimensãoGuardrail de entradaGuardrail de saída
Quando rodaAntes de chamar o modeloDepois do modelo, antes do usuário/banco/tool
Protege contraPrompt injection, pedido abusivo, PII na entradaFormato inválido, alucinação, vazamento, ação perigosa
Ação típicaRecusar, sanitizar, rotearReparar, fazer retry, bloquear, fallback
Se falharModelo recebe entrada ruimUsuário recebe saída ruim (dano real)

A regra prática: guardrail de entrada reduz a chance de saída ruim, mas nunca a elimina. A validação que de fato protege o usuário é a de saída, porque é a última antes do dano. Investir só na entrada é trancar a porta da frente e deixar a dos fundos aberta.

02

Validação de schema: o retry estruturado que conserta em vez de quebrar

O guardrail mais barato e mais rentável é a validação de schema. Quando a saída do modelo deveria ser JSON estruturado (para virar chamada de API, registro no banco ou decisão de fluxo), você não confia que veio certo, você valida contra um schema. Mas o passo que separa um sistema frágil de um robusto é o que fazer quando a validação falha: em vez de estourar um erro para o usuário, você devolve o erro de validação ao próprio 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 diferença. Primeiro, o retry tem limite: duas tentativas cobrem a esmagadora maioria dos casos, e insistir além disso só queima token e latência num modelo que não vai convergir. Segundo, quando o retry esgota, o retorno não é a saída inválida, é um sinal de falha que o fallback vai tratar. Propagar JSON quebrado depois de duas tentativas é trocar um erro controlado por um erro em produção.

03

Detectar recusa indevida e recusa legítima

Nem toda recusa é um problema, e nem toda resposta fluida está correta. O modelo pode recusar uma pergunta perfeitamente legítima ("não posso ajudar com isso") por excesso de zelo, e pode responder com confiança algo que deveria ter recusado. O guardrail de recusa mede esse eixo: ele classifica se a resposta é uma recusa e decide se aquela recusa faz sentido no contexto. Recusa em cima de pedido válido é degradação de produto, o usuário bateu numa parede sem motivo. Ausência de recusa em pedido perigoso é risco, o modelo passou por cima de um limite que deveria respeitar.

  • Detectar a recusa: procure os padrões de recusa da sua stack ("não posso", "não consigo ajudar", "isso vai contra") e trate como um sinal classificável, não como texto qualquer.
  • Classificar o contexto: a pergunta era legítima? Se sim, uma recusa é falha, e o caminho é re-perguntar com prompt ajustado ou rotear para humano, não devolver a parede ao usuário.
  • Medir a taxa de recusa: recusa subindo de repente costuma ser prompt quebrado ou guardrail agressivo demais, e só aparece se você conta as recusas como métrica.
  • Não suprimir recusa legítima: quando o modelo recusa algo que deve mesmo recusar, o guardrail confirma e registra, ele não força uma resposta que abriria um buraco de segurança.

O erro clássico é tratar recusa como falha sempre, e reescrever o prompt até o modelo responder qualquer coisa. Isso quebra a recusa legítima e transforma o guardrail num vetor de ataque. A postura certa é distinguir: recusa indevida se conserta, recusa legítima se respeita. O guardrail não existe para forçar resposta, existe para garantir que a decisão de responder ou não esteja alinhada com o contexto.

04

Vazamento de dado sensível na saída

Um risco silencioso: o contexto do modelo carrega dado sensível (CPF, telefone, dado de outro usuário que entrou por engano no retrieval) e o modelo repete esse dado na resposta. A entrada estava sob controle, mas a saída vaza. O guardrail de vazamento inspeciona a resposta antes de entregar e bloqueia ou redige o que não deveria sair. É a mesma lógica da redação de log, mas aqui o alvo é o que chega ao usuário final, então a barra é mais alta: em log você redige para não persistir, na saída você redige ou bloqueia para não 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 decisão entre redigir e bloquear é o que separa um guardrail útil de um perigoso. Mascarar um email num texto que ainda faz sentido é razoável. Mas quando a resposta inteira gira em torno de um dado que vazou por engano (o modelo confundiu o pedido de um usuário com o cadastro de outro), redigir deixa passar uma resposta sem sentido e potencialmente incriminadora. Nesse caso, bloquear e cair no fallback é mais seguro do que entregar algo mutilado.

05

Bloquear ação perigosa antes de executar

O guardrail mais crítico é o que fica entre o modelo e uma ação com efeito colateral. Quando o LLM decide chamar uma tool (cancelar pedido, emitir reembolso, apagar registro, disparar mensagem em massa), a saída do modelo deixa de ser texto e vira comando. Um argumento alucinado, um id errado ou um valor fora de faixa não geram uma resposta ruim, geram uma ação irreversível. Aqui a saída do modelo é uma proposta de ação, e o guardrail é a aprovação: valida os argumentos, checa limites e políticas, e só então 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 princípios sustentam esse guardrail. Negar por padrão: uma ação que não está na tabela de políticas não executa, porque o modelo pode inventar um nome de tool que você nunca definiu. E escalar em vez de simplesmente falhar: reembolso acima do limite ou disparo em massa não viram erro seco, viram um pedido de aprovação humana. O guardrail não existe só para dizer não, existe para rotear a decisão para quem tem autoridade quando o modelo está fora da alçada.

06

Fallback seguro: a regra de ouro

Todo guardrail acima compartilha a mesma regra de ouro: quando algo dá errado, a saída nunca é a resposta ruim, é sempre um fallback seguro. Schema que não valida depois do retry, recusa que não se conserta, vazamento crítico, ação fora da política, todos convergem para o mesmo lugar, uma resposta controlada que você escreveu, não uma que o modelo alucinou. O anti-padrão é deixar a falha vazar como erro técnico (stack trace, 500, JSON quebrado na tela) ou, pior, deixar a saída ruim passar porque o guardrail só logou e não bloqueou.

Caminho da saída do modelo pelos guardrails

  resposta do modelo
        |
        v
  [ schema válido? ] --não--> retry (até 2x) --falhou--> FALLBACK
        | sim
        v
  [ é recusa? ] --sim--> legítima? --não--> re-perguntar / humano
        | não (ou legítima)
        v
  [ vaza dado sensível? ] --crítico--> BLOQUEIA --> FALLBACK
        |                --redige--> segue com texto redigido
        v
  [ é ação? ] --fora da política--> BLOQUEIA --> aprovação humana
        | dentro da política
        v
  entrega ao usuário / executa a ação

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

O fallback certo depende do contexto: numa resposta de texto, é uma mensagem honesta ("não consegui gerar uma resposta confiável agora, vou te transferir"); numa chamada de tool, é não executar e escalar; num fluxo automático, é parar e alertar em vez de seguir com dado suspeito. O que ele nunca é: um erro técnico jogado na cara do usuário ou uma saída inválida que passou porque ninguém bloqueou. A diferença entre um sistema que degrada com dignidade e um que quebra feio está inteira nessa decisão.

Um detalhe operacional fecha o ciclo: todo acionamento de fallback é um evento que você quer contar. Fallback subindo é o sinal mais direto de que o modelo, o prompt ou o schema mudaram de comportamento, e conecta o guardrail à observabilidade, o assunto do artigo relacionado. Guardrail sem métrica de acionamento é uma rede de segurança que você não sabe se está segurando alguém.

07

Montar a camada sem virar labirinto de if

A armadilha é espalhar validação por todo o código em ifs soltos, até ninguém saber mais qual regra roda quando. A camada de guardrails deve ser uma esteira ordenada e centralizada: a saída do modelo entra por uma ponta, passa pelos guardrails na ordem certa, e sai validada pela outra, ou cai no fallback. Cada guardrail é uma função pequena e testável isoladamente; a esteira apenas os encadeia. O caminho é adicionar por ordem de risco.

  1. Comece pela validação de schema com retry: é o mais barato, o mais frequente e o que mais evita bug bobo de parser em produção.
  2. Adicione o fallback seguro logo em seguida: sem ele, os outros guardrails só trocam um erro por outro; com ele, toda falha tem destino controlado.
  3. Ligue o guardrail de ação antes de qualquer tool com efeito colateral: aqui o custo de errar é irreversível, então ele não é opcional.
  4. Instrumente o guardrail de vazamento onde a resposta contém dado do usuário: quanto mais sensível o domínio, mais cedo ele entra.
  5. Coloque a detecção de recusa por último e conecte tudo a métricas: acionamento de cada guardrail vira linha no dashboard, e o baseline revela quando algo mudou.

A diferença entre um sistema com LLM que dá confiança e um que assusta está em quem controla a saída. Sem guardrails, é o modelo, probabilístico e sem garantia, que decide o que chega ao usuário e ao banco. Com guardrails, o modelo propõe e a sua camada dispõe: valida, repara, bloqueia ou cai no fallback, mas nunca deixa a saída ruim virar dano. Poucas centenas de linhas de guardrail bem colocadas separam um piloto que você não põe na frente do cliente de um produto que aguenta produção.

FAQ

Perguntas frequentes

Guardrail de saída não é a mesma coisa que validar o prompt de entrada?

Não. São dois controles em momentos diferentes. O guardrail de entrada roda antes do modelo e protege contra o que o usuário manda (prompt injection, pedido abusivo). O guardrail de saída roda depois do modelo e protege contra o que ele devolve (formato inválido, alucinação, vazamento, ação perigosa). Uma entrada perfeitamente válida pode gerar uma saída perigosa, porque o modelo é probabilístico. A validação que de fato protege o usuário é a de saída, porque é a última antes do dano chegar ao banco, a uma tool ou à tela.

Por que fazer retry em vez de só 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 desperdiçaria uma correção fácil e barata. A chave é o retry ser estruturado (você injeta o erro de validação como feedback) e limitado (duas tentativas cobrem quase tudo; insistir além disso só queima token). E, quando o retry esgota, o retorno não é a saída inválida, é um sinal de falha que cai no fallback seguro, nunca JSON quebrado propagado para o usuário.

O guardrail deve sempre bloquear quando encontra dado sensível na saída?

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

Guardrails de saída são o que impede a resposta ruim de virar dano real

Validação de schema com retry, detecção de recusa e vazamento, bloqueio de ação perigosa e fallback seguro são o mínimo para que nada que o modelo produz chegue ao usuário ou ao banco sem passar por um controle. Posso desenhar essa camada no seu produto, encadeada e observável, do schema ao fallback, integrada ao seu stack.