Saída JSON de IA não é autorização para executar uma ação. Antes de chamar uma ferramenta, a aplicação precisa separar três verificações: o texto pode ser interpretado como JSON, sua estrutura atende ao contrato e a operação faz sentido para aquele usuário e para o estado atual dos dados.
Um documento pode passar pelas duas primeiras e ainda mandar fechar um chamado de outro cliente. Para tornar essa diferença observável, montamos um experimento sintético: um executor simulado só registra uma chamada quando todas as etapas passam. Nenhum modelo, API paga ou sistema de atendimento é acionado.
Neste artigo
O contrato pequeno do experimento
A proposta recebida é fechar um chamado já resolvido. O modelo pode sugerir o identificador e uma justificativa, mas não escolhe quem é o usuário autenticado nem qual cliente ele representa. Essas informações vêm do contexto confiável da aplicação.
O schema usa o dialeto JSON Schema 2020-12, validado com a biblioteca Python jsonschema 4.25.1. O schema fica sob controle do programa, não vem junto de uma resposta livre do modelo:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"action": {"const": "close_ticket"},
"ticket_id": {"type": "integer", "minimum": 1},
"reason": {"type": "string", "minLength": 1, "maxLength": 200}
},
"required": ["action", "ticket_id", "reason"],
"additionalProperties": false
}
Declarar properties não torna os campos obrigatórios; essa função é de required. Também não proíbe campos extras sem uma regra como additionalProperties: false. Essas distinções estão na referência de objetos do JSON Schema. Neste contrato, const limita a ação a um único valor, mais restrito do que uma lista de ações livres.
Parsing e schema têm trabalhos diferentes
No parsing, a entrada é limitada a 4.096 bytes neste ensaio. Chaves repetidas e constantes como NaN são rejeitadas deliberadamente. A documentação do módulo JSON do Python explica que o comportamento padrão aceita algumas dessas situações e mantém o último valor de nomes repetidos. A política precisa ser explícita, não presumida.
Depois, Draft202012Validator confere os dados contra o schema. Seu contrato de validação permite verificar o próprio schema e obter erros da instância. Não corrigimos silenciosamente "7" para 7, nem removemos campos extras antes de validar: neste exemplo, uma resposta fora do contrato é rejeitada.
Só então consultamos o estado confiável dos chamados. O número 7 pertence ao cliente de teste demo-a e está resolvido; o 8 pertence a outro cliente; o 9 ainda está aberto. Os identificadores são fixtures inventadas para o experimento, não dados de usuários.
O bloqueio imediatamente antes da ação
Esta é a etapa de negócio do teste, executada após parsing e schema. trusted_tenant não é extraído do JSON sugerido pelo modelo:
ticket = TICKETS.get(data["ticket_id"])
if (
not ticket
or ticket["tenant"] != trusted_tenant
or ticket["status"] != "resolved"
):
return "business"
calls.append({"ticket_id": data["ticket_id"], "action": "close_ticket"})
return "accepted"
calls é uma lista local usada como contador, não uma integração real. A ação permitida é definida pelo código; não há eval, comando shell ou escolha de função a partir de texto livre. A justificativa não decide permissão e não é executada.
Treze entradas, um executor simulado
O teste foi executado em Python 3.14.2. Para cada caso, o contador é zerado antes da avaliação. A conclusão é sobre esse contrato e essas fixtures, não sobre taxa de erro de um modelo.
| Entrada | Etapa que decide | Chamadas simuladas |
|---|---|---|
| Ação permitida, chamado 7, justificativa presente | Aceita | 1 |
| Texto JSON incompleto | Parsing | 0 |
| Justificativa ausente | Schema | 0 |
Identificador como string "7" |
Schema | 0 |
Identificador booleano true |
Schema | 0 |
Ação delete_ticket |
Schema | 0 |
| Campo extra tentando informar outro cliente | Schema | 0 |
| Chamado 8, de outro cliente | Negócio | 0 |
| Chamado 9, ainda aberto | Negócio | 0 |
| Chamado 10, inexistente | Negócio | 0 |
Duas chaves ticket_id no mesmo objeto |
Parsing | 0 |
Valor NaN |
Parsing | 0 |
| Entrada maior que 4.096 bytes | Limite anterior ao parsing | 0 |
Para reproduzir o caso de maior risco, mantenha o mesmo schema e compare estas duas propostas com as fixtures descritas:
{"action":"close_ticket","ticket_id":7,"reason":"Solicitação resolvida"}
{"action":"close_ticket","ticket_id":8,"reason":"Solicitação resolvida"}
As duas atendem ao schema. A segunda é bloqueada pela propriedade do chamado, consultada fora da resposta de IA. Se o seu teste apenas verifica is_valid, esse erro fica invisível.
O que o teste não garante
A justificativa pode ser falsa ou inadequada mesmo tendo entre 1 e 200 caracteres. Um número inteiro positivo pode apontar para um objeto que mudou de estado. E uma operação válida pode ser repetida indevidamente. Schema não confirma fatos, intenção do usuário, permissão, atualidade ou idempotência.
Em produção, conferir o estado e gravar a mudança precisam respeitar concorrência. Entre uma consulta e o fechamento, outro processo pode reabrir o chamado ou alterar sua propriedade. Use uma operação condicionada ao estado e à autorização atuais, dentro do mecanismo transacional adequado. O mock não simula essa disputa, portanto não pode ser apresentado como prova de segurança de um serviço completo.
Também não exponha automaticamente mensagens internas e dados do chamado ao pedir uma nova tentativa ao modelo. Registre a categoria do erro com dados minimizados e limite o número de correções. Repetir até obter uma estrutura válida não transforma uma ação proibida em permitida.
Como aproveitar isso em uma automação existente
Comece por uma única ação de baixo risco e escreva os exemplos que devem produzir zero chamadas. Inclua pelo menos um objeto estruturalmente válido que a aplicação precisa recusar. Mantenha o executor simulado nos testes; a primeira evidência desejada é que os casos recusados não chegam à ferramenta.
Esse controle complementa os contratos de API com OpenAPI e JSON Schema. Já os evals para prompts ajudam a comparar a qualidade das sugestões quando modelo ou instruções mudam. Aqui a decisão ocorre depois da sugestão: qualquer que seja o modelo, a aplicação continua responsável por aceitar ou recusar a ação.