Todo bot que atende cliente acaba dizendo alguma coisa que não devia. Promete um prazo que a empresa não cumpre, dá um desconto que não existe, opina sobre política, orienta sobre remédio, confirma um pedido que não foi feito.

E a reação padrão é sempre a mesma: acrescentar mais uma frase ao prompt. "Nunca prometa prazos." Funciona nas primeiras cem conversas. Na conversa 101 alguém escreve de um jeito diferente e o modelo promete o prazo.

🔑 A frase que organiza tudo

O prompt é uma preferência forte, não um controle. Ele muda a probabilidade de um comportamento; não a elimina. Tudo que precisa de garantia tem que estar em código, fora do modelo.

As quatro camadas

CamadaOnde ageGarante?
1. EntradaAntes de chamar o modeloSim — é código
2. InstruçãoNo prompt de sistemaNão — é preferência
3. SaídaDepois da resposta, antes de mostrarSim — é código
4. AçãoEntre a decisão do modelo e o efeito realSim, e é a que mais importa

Camada 1 — filtrar a entrada

Barato, rápido e resolve o grosso. Antes de gastar tokens, cheque:

import re

LIMITE_CARACTERES = 4000

def triagem_entrada(texto, historico):
    if len(texto) > LIMITE_CARACTERES:
        return "recusa", "mensagem_muito_longa"

    if len(historico) > 40:
        return "recusa", "conversa_longa_demais"      # reinicie o contexto

    # dados sensíveis que você não quer nem receber
    if re.search(r"\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b", texto):
        return "mascarar", "cpf_detectado"

    if re.search(r"\b(?:\d[ -]*?){13,16}\b", texto):
        return "mascarar", "possivel_cartao"

    return "seguir", None

Mascarar em vez de recusar é quase sempre a escolha certa: o cliente que mandou o CPF não fez nada de errado e não pode ficar sem atendimento. Substitua por um marcador antes de enviar ao modelo e, se precisar do dado, resolva no seu código.

Um classificador de conteúdo pequeno pode entrar aqui também, para assunto proibido. Rodando localmente numa GPU modesta, ele processa milhares de mensagens por segundo e custa quase nada por mensagem.

Camada 2 — a instrução, com expectativas certas

A instrução não garante nada, mas melhora bastante a taxa. O que funciona melhor que uma lista de proibições:

Camada 3 — inspecionar a saída

Aqui você recupera a garantia. A resposta já existe, você tem o texto e pode aplicar regra determinística antes de qualquer humano ver:

PROIBIDOS = [
    (r"\b\d+\s*(dias|dias úteis|horas)\b", "promessa_de_prazo"),
    (r"\b(\d{1,2})\s*%\s*de\s*desconto", "desconto_nao_autorizado"),
    (r"garanto|prometo|com certeza vai", "promessa_absoluta"),
    (r"(cancelei|estornei|liberei)\b", "afirmou_acao_nao_executada"),
]

def triagem_saida(resposta, contexto):
    for padrao, motivo in PROIBIDOS:
        if re.search(padrao, resposta, re.I):
            return "bloquear", motivo

    # o bot afirmou algo que não estava em nenhuma fonte recuperada?
    if contexto["usa_rag"] and "[doc" not in resposta and contexto["exige_fonte"]:
        return "bloquear", "sem_citacao"

    return "liberar", None

Quando bloquear, não mostre erro. Troque por uma resposta segura: "prefiro te passar para um atendente para confirmar isso". O usuário tem uma experiência levemente pior; a empresa não tem um problema.

⚠️ Registre todo bloqueio, com o texto original

O registro dos bloqueios é a fonte mais rica que você vai ter. Ele mostra exatamente onde o prompt está falhando e quais casos precisam virar exemplo no conjunto de avaliação.

Se a taxa de bloqueio passar de 2 ou 3%, o problema não é o filtro: é o prompt ou o modelo. Filtro que bloqueia demais vira atendimento humano disfarçado.

Camada 4 — separar o que o modelo decide do que o sistema faz

Esta é a camada que evita as manchetes. A regra é simples e não tem exceção:

✅ A regra do efeito

O modelo nunca executa. Ele propõe. Toda ação com efeito no mundo — estornar, cancelar, enviar, aprovar, apagar, mudar cadastro — passa por código que valida permissão, limite e estado antes de acontecer.

LIMITES = {"estorno": 200.00, "desconto": 0.10}

def executar(acao, usuario, pedido):
    if acao["tipo"] not in LIMITES:
        return recusar("acao_desconhecida")

    if not usuario.pode(acao["tipo"]):
        return recusar("sem_permissao")

    if acao["valor"] > LIMITES[acao["tipo"]]:
        return fila_de_aprovacao(acao, usuario)     # humano decide

    if pedido.status != "entregue":
        return recusar("estado_invalido")

    return aplicar(acao)      # só aqui algo acontece de verdade

Com essa camada, mesmo que alguém convença o modelo a "aprovar um estorno de R$ 40 mil", nada acontece: o código recusa. O modelo pode ser enganado; a regra de negócio, não.

O que cada camada custa

CamadaCusto por mensagemLatência acrescentada
Regras de entradaZero< 1 ms
Classificador local (GPU compartilhada)Frações de centavo~20 ms
Instrução no prompt~200 tokens (R$ 0,49/1M)Zero
Regras de saídaZero< 1 ms
Segundo modelo revisando a saídaDobra o custo+1 a 3 s

Repare que as camadas que garantem são justamente as gratuitas. O segundo modelo como revisor é a opção mais cara e a menos confiável das cinco — use só para julgamento subjetivo que regra não alcança, e nunca como única defesa.

Teste as camadas antes de abrir para o público

API por token para o assistente e GPU por hora para o classificador local, no mesmo saldo em reais.

Criar conta →

O caso especial de quem dá conselho regulado

Se o seu bot toca em saúde, direito, investimento ou benefício, some duas coisas:

Comece por aqui

  1. Liste as cinco frases que a sua empresa não pode dizer. Cinco, não cinquenta.
  2. Escreva a regra de saída para cada uma. Uma tarde de trabalho.
  3. Ponha teto e permissão em toda ação com efeito. Um dia.
  4. Ligue o registro de bloqueios e olhe a lista uma vez por semana.
  5. Cada bloqueio recorrente vira caso no seu conjunto de avaliação.

Conclusão

Contenção de bot não se faz escrevendo regra em prompt — se faz cercando o modelo com código. Filtro na entrada, instrução clara com saída de emergência, inspeção da saída e, acima de tudo, a separação entre o que o modelo propõe e o que o sistema executa.

O teste mental que vale: se um usuário mal-intencionado convencer o modelo a fazer a pior coisa possível, o que acontece? Se a resposta for "nada, o código recusa", você está bem. Se for "depende do prompt", você tem trabalho a fazer.

Continue: prompt injection · medir com evals · observabilidade