Pular para o conteúdo
IAIntermediário

Guardrails de saída: validando a resposta do LLM

Implemente guardrails de saída para LLM: schema com Pydantic, retry com feedback do erro, detecção de dados pessoais (CPF, cartão) e fallback seguro.

Por Equipe IAUAI Estudos · 10 de agosto de 2026 · 9 min de leitura

Nesta página

Aqui você vai aprender a colocar uma camada de validação entre o LLM e o resto do seu sistema: conferir o formato com Pydantic, tentar de novo com o erro como feedback, bloquear vazamento de dados pessoais e cair num caminho seguro quando nada funciona.

O problema real

Você pede ao modelo para extrair os dados de uma nota fiscal e devolver JSON. Na primeira vez, perfeito. Na segunda, a chave vem como preço em vez de preco. Na terceira, o valor vem como "R$ 1.234,56" em texto e seu cálculo quebra. Você escreve "retorne SEMPRE JSON válido" em maiúsculas e melhora, mas não resolve.

O prompt é um pedido. Quem garante é o código. Guardrails de saída são verificações que rodam depois que o modelo responde e antes de você usar a resposta.

Três camadas

  1. Formato: a resposta tem o esquema esperado? (Pydantic, saída estruturada.)
  2. Conteúdo: contém algo que não pode sair, como CPF ou cartão?
  3. Recuperação: se falhou, tentar de novo com feedback ou cair num fallback.

1. Saída estruturada do provedor

Vários provedores já impõem um esquema durante a geração, o que elimina quase todo JSON quebrado. Com o SDK da OpenAI, por exemplo, você passa um modelo Pydantic e recebe o objeto já parseado:

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class NotaFiscal(BaseModel):
    numero: str
    valor: float
    vencimento: str  # AAAA-MM-DD
    fornecedor: str

def extrair_nota(texto: str, modelo: str) -> NotaFiscal:
    resposta = client.chat.completions.parse(
        model=modelo,  # use um modelo com suporte a saída estruturada
        messages=[
            {"role": "system", "content": "Extraia os dados da nota fiscal. Datas em AAAA-MM-DD, valor como número."},
            {"role": "user", "content": texto},
        ],
        response_format=NotaFiscal,
    )
    return resposta.choices[0].message.parsed

Outros provedores têm recursos equivalentes (esquema JSON na requisição ou tool use forçado), com nomes e limites diferentes, então confira a documentação do seu. Mesmo com saída estruturada, o esquema só garante a forma: um valor negativo ou uma data no passado distante continuam válidos para ele. Por isso a validação de negócio no seu código continua necessária.

2. Validação e retry portáteis

Quando o provedor não impõe esquema, ou você usa um modelo local, valide por conta própria e devolva o erro ao modelo. O exemplo usa o SDK da Anthropic, mas o padrão é o mesmo em qualquer um.

import json
import os
import re
from datetime import date

import anthropic
from pydantic import BaseModel, Field, ValidationError, field_validator

client = anthropic.Anthropic()
MODELO = os.environ["EXTRATOR_MODEL"]

class NotaFiscal(BaseModel):
    numero: str
    valor: float = Field(gt=0)
    vencimento: date
    fornecedor: str = Field(min_length=2)

    @field_validator("vencimento")
    @classmethod
    def vencimento_plausivel(cls, v: date) -> date:
        if v.year < 2000 or v.year > date.today().year + 5:
            raise ValueError("vencimento fora de uma faixa plausível")
        return v

SISTEMA = (
    "Extraia os dados da nota fiscal e responda só com JSON neste formato: "
    '{"numero": "texto", "valor": 0.0, "vencimento": "AAAA-MM-DD", "fornecedor": "texto"}. '
    "Use ponto como separador decimal. Não escreva nada fora do JSON."
)

def extrair_com_retry(texto: str, tentativas: int = 3) -> NotaFiscal | None:
    mensagens = [{"role": "user", "content": texto}]
    for _ in range(tentativas):
        r = client.messages.create(
            model=MODELO, max_tokens=400, system=SISTEMA, messages=mensagens
        )
        bruto = r.content[0].text
        try:
            achado = re.search(r"\{.*\}", bruto, re.DOTALL)
            if not achado:
                raise ValueError("nenhum JSON encontrado")
            return NotaFiscal(**json.loads(achado.group(0)))
        except (ValueError, ValidationError, TypeError) as erro:
            mensagens += [
                {"role": "assistant", "content": bruto},
                {"role": "user", "content": f"A resposta falhou na validação: {erro}. Corrija e responda só com o JSON."},
            ]
    return None

Note que json.JSONDecodeError e ValidationError herdam de ValueError, então o except cobre os dois. O limite de tentativas importa: cada retry repete o histórico inteiro, então o custo cresce. Duas ou três tentativas bastam; se o modelo não acerta por três vezes, o problema costuma estar no prompt ou na entrada.

3. Dados pessoais na saída

Antes de exibir ou registrar a resposta, procure o que nunca deveria aparecer. Expressões regulares pegam o formato, mas formato não é prova: validar o dígito verificador do CPF e o algoritmo de Luhn do cartão derruba muitos falsos positivos.

import re

CPF = re.compile(r"\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b")
CARTAO = re.compile(r"\b(?:\d[ -]?){13,19}\b")
EMAIL = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b")

def cpf_valido(texto: str) -> bool:
    d = [int(c) for c in texto if c.isdigit()]
    if len(d) != 11 or len(set(d)) == 1:
        return False
    for n in (9, 10):
        soma = sum(d[i] * (n + 1 - i) for i in range(n))
        if (soma * 10 % 11) % 10 != d[n]:
            return False
    return True

def luhn_valido(texto: str) -> bool:
    d = [int(c) for c in texto if c.isdigit()][::-1]
    total = sum(d[0::2]) + sum(sum(divmod(x * 2, 10)) for x in d[1::2])
    return len(d) >= 13 and total % 10 == 0

def mascarar(texto: str) -> str:
    texto = CPF.sub(lambda m: "[CPF]" if cpf_valido(m.group()) else m.group(), texto)
    texto = CARTAO.sub(lambda m: "[CARTAO]" if luhn_valido(m.group()) else m.group(), texto)
    return EMAIL.sub("[EMAIL]", texto)

print(mascarar("CPF 529.982.247-25, cartão 4111 1111 1111 1111, [email protected]"))
# CPF [CPF], cartão [CARTAO], [EMAIL]

Essa checagem é uma rede de proteção, não a defesa principal. Nomes, endereços e dados em texto livre passam por ela. A defesa principal é não colocar o dado no contexto do modelo sem necessidade, tema de segurança em aplicações com LLM. E vale lembrar da LGPD: o log com dado pessoal sem máscara também é tratamento de dados.

4. Fallback

Se as tentativas acabaram, decida de antemão o que acontece. Opções, da mais simples à mais cara:

  • Devolver um erro claro ao chamador e registrar o caso para revisão.
  • Mandar o item para uma fila de revisão humana.
  • Usar um extrator mais simples (regex para o campo mais importante), aceitando que cobre menos casos.

Evite fallbacks elaborados. Um regex de 300 linhas para imitar o modelo vira outro sistema para manter. Na maioria dos casos, fila de revisão humana com o motivo da falha é a opção mais sensata.

Armadilhas comuns

  • Achar que o prompt é a validação. Ele melhora a taxa de acerto, mas só o código garante.
  • Retry sem limite. Duas ou três tentativas, depois fallback.
  • Logar a saída crua antes de mascarar.
  • Validar só o formato. Um JSON perfeito pode trazer valor absurdo, e por isso os validadores de negócio (gt=0, faixa de data) existem.
  • Aplicar a mesma rigidez a texto livre. Em respostas abertas, valide o que importa (tamanho, termos proibidos, dados pessoais), não um esquema.

Quando relaxar

Em tarefas abertas como rascunho de texto, regras demais atrapalham. Se a latência é crítica, prefira saída estruturada nativa e um único passo de validação, sem retry. E meça com evals quanto a camada realmente reduz de erro antes de aumentar a complexidade.

Próximos passos

Para dar ferramentas ao modelo com argumentos validados, veja function calling na prática. Para treinar a escrita de padrões de detecção, use o laboratório de regex e o jogo Firewall Regex.

Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria

Aprenda jogando

Firewall Regex

Bloqueie payloads maliciosos sem barrar usuário legítimo. Falso positivo custa caro.

Jogar Firewall Regex

Teste seu conhecimento

Teste o que você aprendeu sobre guardrails de saída

Pergunta 1 de 5

Qual é a ideia central do artigo sobre prompt e validação?

Um conteúdo prático por quinzena

Deixe o seu e-mail para receber um conteúdo prático de IA e cloud a cada quinzena. Sem spam, e você pede a exclusão quando quiser.

Continue lendo