Peça a um modelo para responder em JSON e na maioria das vezes
json.loads() funciona sem problemas. As falhas são as que custam uma
sessão de debug: um comentário de uma linha dentro do objeto, um code fence
de markdown envolvendo tudo, uma chave escrita userEmail quando seu código
espera user_email. Um prompt mais estrito (“responda APENAS com o objeto
JSON, sem nenhum outro texto”) torna esses casos mais raros. Não os torna
impossíveis, porque o modelo ainda pode ignorar o pedido. A saída
estruturada remove o pedido da equação: você entrega à API um JSON Schema,
que restringe a amostragem de tokens do próprio modelo para que a resposta
tenha que corresponder a ele.
JSON mode contra saída estruturada: o que cada um garante
O “JSON mode” (response_format: {"type": "json_object"} na API antiga da
OpenAI, e o equivalente na maioria dos outros provedores) garante uma única
coisa: a saída é interpretável como JSON. Não diz nada sobre quais chaves
existem, que tipo cada uma tem, ou se um campo do qual você depende está
mesmo presente. Um modelo em JSON mode pode devolver {"result": "yes"}
quando seu código espera {"status": "approved", "confidence": 0.91}, e
nada avisa que algo deu errado.
A saída estruturada (a OpenAI chama de Structured Outputs, a Anthropic chama de JSON outputs) é uma garantia diferente: a resposta corresponde ao JSON Schema que você fornece, com as chaves obrigatórias presentes e o tipo correto em cada uma. O function calling resolve um problema adjacente: fazer o modelo pedir a seu código que execute algo. Os dois se confundem com facilidade porque ambos passam um esquema. O que muda é o que o esquema molda:
| Recurso | O que garante | Para que serve |
|---|---|---|
JSON mode (json_object) | Apenas sintaxe JSON válida | Fallback legado, sem esquema a cumprir |
| Saída estruturada (schema strict) | O JSON corresponde exatamente ao seu esquema | Moldar a resposta para o usuário ou seu código |
| Function calling / tool use | Nome e argumentos do tool correspondem ao seu esquema | O modelo decide invocar seu código |
Um pipeline em produção costuma usar os dois últimos juntos. O tool use strict evita que as chamadas de função de um agente cheguem malformadas, e um esquema strict na resposta final tipa o resumo devolvido ao usuário.
Como o constrained decoding força a correspondência
O modelo nunca compara sua resposta com seu esquema depois de gerá-la. O esquema muda o que ele tem permissão para gerar, um token por vez.
A camada de serving compila seu JSON Schema em uma gramática: uma máquina de estados que rastreia, em cada posição da saída, quais tokens ainda levariam a algo válido. Antes de cada token ser amostrado, todo token do vocabulário que a gramática descarta naquela posição tem seu logit empurrado para menos infinito, ficando com probabilidade zero depois do softmax. O modelo amostra do que resta, e as probabilidades relativas entre os tokens sobreviventes são preservadas. Uma chave escrita errada, uma chave de fechamento no lugar errado, uma string onde o esquema exige um número: uma vez que a gramática os proíbe, esses tokens deixam de ser opções.
Essa etapa de compilação tem um custo. Construir uma gramática para um novo esquema exige trabalho real, então a primeira requisição contra aquele esquema é mais lenta que as seguintes. A API da Anthropic mantém a gramática compilada em cache por 24 horas após o último uso, e chamadas posteriores com o mesmo esquema pulam a compilação.
O mecanismo também explica o limite. O constrained decoding fixa apenas a
forma, nada mais. A gramática sabe quais tokens são sintaticamente
legais, não qual deles é verdadeiro, então o modelo ainda pode preencher um
campo "email" válido com um endereço inventado.
Como obter JSON conforme o esquema na OpenAI, Claude e Ollama
A Responses API da OpenAI recebe o esquema em text.format, com strict
ativado:
| |
O Claude recebe o esquema em output_config.format, não em uma definição
de tool:
| |
O JSON volta no conteúdo de texto da resposta, não em um bloco de tool call. O workaround antigo (definir um tool falso, forçar o modelo a chamá-lo, ler os argumentos) ainda funciona em modelos que só suportam tool use. Onde existe saída estruturada nativa, você não precisa disso: o decoder impõe o esquema diretamente, em vez de esperar que os argumentos do tool coincidam por acaso.
Um modelo executado localmente com Ollama
recebe o esquema pelo campo format de /api/chat:
| |
O mecanismo é o mesmo. O runtime llama.cpp do Ollama constrói uma
gramática a partir do esquema e mascara os tokens do mesmo jeito que as
APIs hospedadas.
Quais recursos de JSON Schema os provedores suportam
Cada provedor impõe um subconjunto de JSON Schema, e os subconjuntos diferem o bastante para quebrar um esquema copiado de uma API para outra.
A OpenAI exige que todo objeto leve "additionalProperties": false com
cada uma de suas propriedades listada em required — não há como marcar um
campo como realmente opcional, então modele-o como required e nullable. A
Anthropic é mais permissiva: uma propriedade fora de required é
genuinamente opcional, e as propriedades required aparecem primeiro na
saída, seguidas das opcionais presentes. A Anthropic também suporta
$ref/$defs locais, anyOf e const, mas rejeita esquemas recursivos,
limites numéricos como minimum e maximum, e restrições de tamanho de
string como maxLength. Os exemplos
da OpenAI incluem um $ref: "#" recursivo para saída em árvore, algo que a
documentação da Anthropic exclui explicitamente.
Quando seu esquema precisa de uma restrição que o provedor não suporta,
mova-a para a description do campo (“deve ser um inteiro positivo menor
que 100”) e verifique você mesmo depois do parsing. Os helpers dos SDKs de
ambas as plataformas fazem isso automaticamente quando você constrói o
esquema a partir de um modelo Pydantic ou Zod em vez de escrever JSON
Schema na mão.
Como validar a saída estruturada depois do parsing
Uma forma válida não é uma resposta correta. O esquema garante que
severity seja um dos seus quatro valores de enum. Se “blocking” é a
decisão certa para este ticket é outra pergunta. Trate o objeto
parseado como você trataria o envio de um formulário: input a validar,
não verdade estabelecida.
| |
O Instructor envolve a OpenAI, a
Anthropic e a maioria dos outros provedores em um ciclo de validação e
retry. Você passa a ele um modelo Pydantic. Em uma falha de validação, ele
devolve o erro ao modelo e pergunta de novo, até um limite de retry que
você define, em vez de lançar um ValidationError na primeira falha.
Também é o caminho prático para conseguir uma garantia de esquema em um
provedor ou modelo mais antigo sem strict mode nativo: o ciclo de retry
substitui o enforcement no nível do decoder, e cada falha custa uma viagem
de ida e volta extra.
Os validadores de campo pegam valores malformados em uma única resposta. Para verificar se as respostas estão corretas em centenas de entradas, e se uma mudança no prompt as piorou, você precisa de um eval suite.
Quando a saída estruturada falha de qualquer forma
O strict mode não cobre esses casos. Verifique-os antes de sair caçando um bug no seu próprio código:
- Recusas. Um modelo pode se recusar a responder, e o texto da recusa
não corresponde ao seu esquema. Verifique
stop_reason(ou o equivalente) antes do parsing, não depois que o parsing lançar um erro. - Truncamento. Atingir
max_tokensno meio de um objeto produz JSON incompleto. A gramática controla qual token vem a seguir, não quanto espaço resta para terminar. Aumente o limite ou reduza o esquema. - Caminhos não forçados. JSON mode, uma biblioteca de retry em um
modelo sem strict mode, ou um provedor que ignora silenciosamente uma
palavra-chave do esquema que não suporta podem todos devolver
"Blocking"quando seu enum diz"blocking". Se alguma requisição do seu sistema passar por um desses caminhos, normalize os valores na entrada. - Tamanho do esquema. Um esquema grande com muitos ramos
anyOfadiciona tokens a cada requisição, além do atraso de compilação para um esquema novo. Mantenha os esquemas tão simples e pequenos quanto a tarefa permitir.
A garantia do provedor é real, mas seu código de parsing não deve assumir que ela se manteve.
JSON mode, schema strict ou function calling: qual usar
Use o JSON mode simples só contra um modelo ou caminho de SDK sem suporte a esquema, como fallback e não como padrão. Use um JSON Schema strict quando estiver moldando uma resposta para seu próprio código ou um usuário: extração, classificação, um resumo tipado. Use o function calling quando for o modelo a decidir se executa algo. Ative o strict mode também na definição do tool, para que uma chamada malformada falhe na API em vez de dentro do seu código.
Se tudo que você precisa é um rótulo de um enum curto, compare também com um modelo system one. Quando a saída é restrita a um conjunto conhecido de respostas, um modelo pode escolher uma em vez de gerá-la, e as trocas de latência e custo diferem o bastante para você medir no seu próprio tráfego.
Para todo o resto, o padrão é um esquema strict mais um validador no resultado parseado. O esquema mantém a forma correta. Verificar se o conteúdo está correto continua sendo seu trabalho.