antonio leandro

claude, na prática

Structured outputs

tutorial · 01 · o básico · Anthropic · · ~45 min de leitura do original

a tese

json válido não é problema de prompt: o schema vira grammar compilado que restringe o sampling, e o preço deixa de ser retry e passa a ser limite de complexidade do schema

o que fica

  1. A garantia de conformidade vem de sampling restrito por um grammar compilado a partir do schema, não de instrução no prompt.
  2. O grammar compilado fica em cache por 24 horas desde o último uso, então a primeira requisição com um schema novo paga a latência da compilação.
  3. A garantia cai em dois casos previstos: refusal e max_tokens — nos dois o texto pode sair fora do schema, com status 200 e tokens cobrados.
  4. Cada parâmetro opcional aproximadamente dobra uma parte do espaço de estados do grammar, e por isso o limite de 24 opcionais vale para o request inteiro, somando todos os schemas strict.
  5. Propriedades required saem antes das optional no output, independentemente da ordem em que você as declarou no schema.
  6. O schema é cacheado separado das mensagens e não recebe as proteções de HIPAA, então PHI não pode aparecer em nome de campo, enum, const ou pattern.

o problema

Pedir json a um modelo sempre funcionou até a hora que não funciona. O prompt descreve o formato, o modelo obedece na maioria das vezes, e a minoria vira erro de JSON.parse() em produção: uma vírgula sobrando, um campo obrigatório ausente, um número que veio como string, uma chave inventada que o schema não previa. Nada disso é raro o bastante para ignorar nem frequente o bastante para ser fácil de reproduzir.

A resposta padrão era defensiva: parse com try/catch, validação contra o schema, retry quando falha, talvez um segundo modelo para consertar a saída do primeiro. Isso é código que existe só porque a fronteira entre o modelo e o resto do sistema é texto livre. Custa latência, custa token e continua sendo probabilístico — o retry pode falhar de novo. Para um pipeline que roda milhões de extrações, “quase sempre válido” é uma taxa de erro, não uma garantia.

a ideia

Em vez de pedir o formato, restringir o formato. O schema que você manda deixa de ser descrição e vira regra de decodificação: em cada passo de geração, os tokens que quebrariam o schema são eliminados antes da amostragem. Se a gramática permite apenas true ou false naquela posição, não existe caminho para o modelo escrever outra coisa.

A feature tem duas metades independentes, que podem ser usadas juntas no mesmo request. output_config.format controla o que Claude fala: a resposta sai como json conforme o seu schema, no bloco de texto. strict: true numa ferramenta controla como Claude chama suas funções: nome e input validados contra o input_schema. A primeira resolve extração e formatação de resposta; a segunda resolve o loop agêntico, onde um argumento malformado quebra a chamada seguinte.

como funciona

O schema é compilado num artefato de gramática. Compilação custa tempo, então o resultado fica em cache por 24 horas a partir do último uso: a primeira chamada com um schema inédito é mais lenta, as seguintes não. O cache é invalidado se a estrutura do schema muda ou se o conjunto de ferramentas do request muda; mexer só em name ou description não invalida.

Junto com o grammar, a api injeta um system prompt adicional explicando o formato esperado. Isso aparece na sua conta de input tokens, e trocar output_config.format invalida o prompt cache daquela thread. O grammar vale só para a saída direta do modelo — chamadas de ferramenta, resultados de ferramenta e blocos de thinking ficam de fora, e o estado da gramática reseta entre as seções, então o modelo pensa livre e só a resposta final sai amarrada.

Do lado do cliente, os sdks fazem uma tradução que vale entender antes de confiar nela. Python (Pydantic), TypeScript (Zod), Ruby e PHP transformam seu schema: removem restrições não suportadas, empurram a informação dessas restrições para a description do campo, forçam additionalProperties: false em todo objeto, filtram formatos de string e validam a resposta contra o schema original. Ou seja: um minimum: 100 do Pydantic não chega ao modelo como restrição de gramática — chega como a frase “Must be at least 100” e como uma checagem local depois. C# e Go fazem o mesmo quando o schema é derivado de um tipo nativo.

o que isso custou

A gramática precisa ser finita e compilável, e é aí que a lista de exclusões nasce. Schemas recursivos não passam. Restrições numéricas (minimum, maximum, multipleOf) e de string (minLength, maxLength) não passam. $ref externo não passa. Array só aceita minItems com valor 0 ou 1. Regex funciona para o caso simples, mas backreference, lookahead e limite de palavra não. Uso indevido devolve 400.

Há teto de complexidade explícito: 20 ferramentas strict por request, 24 parâmetros opcionais somados entre todos os schemas, 16 parâmetros com tipo em união. Além disso existem limites internos sobre o tamanho da gramática compilada, mais um timeout de compilação de 180 segundos — dá para respeitar cada limite da tabela e ainda assim receber “Schema is too complex for compilation”. A recomendação da própria doc é reduzir opcionais, achatar aninhamento e marcar como strict só as ferramentas onde a violação dói.

E a garantia tem exceções declaradas. Se o modelo recusa, stop_reason vem como refusal, o status é 200, você paga os tokens e a saída pode não bater com o schema. Se bate no max_tokens, o json sai truncado. Capitalização de enum e const não é garantida — compare sem diferenciar caixa. Propriedades required aparecem antes das optional, então a ordem do seu schema não é a ordem do output. Citations e prefilling são incompatíveis com json outputs. E PHI não pode entrar no schema: ele é cacheado separado das mensagens e não recebe as mesmas proteções.

onde isso aparece hoje

O par natural é Implementing tool use: strict tool use é a mesma máquina aplicada ao input_schema, e usar as duas metades juntas é o desenho recomendado para workflow agêntico — ferramenta chamada com parâmetro válido, resposta final em json parseável. Combina com Streaming Messages, com contagem de tokens sem compilar, e com Batch processing, onde a extração estruturada em escala ainda pega os 50% de desconto. Duas fricções para lembrar: Citations devolve 400 se você ligar as duas coisas, porque intercalar bloco de citação com texto briga com a gramática, e mudar o formato derruba o Prompt caching da conversa.

lido na íntegra por pipeline de llm, revisado por antonio leandro antes de publicar ·