Skip to content

Structured outputs no Bedrock: a forma é garantida, o conteúdo não

· 6 min de leitura

tags: llms, architecture, ai-agents

Em 20 de outubro, a AWS anunciou structured outputs para os modelos Claude no Amazon Bedrock: você passa um JSON schema e a resposta tem a garantia de segui-lo. A promessa é que não é mais preciso parsing próprio, retentativas nem pós-processamento.

Rodamos muitas chamadas estruturadas pelo Bedrock, todas com o mesmo padrão: pedir JSON no prompt, validar com Zod e reparar ou pedir de novo quando não encaixa. Então o anúncio soava como um convite para apagar uma camada de código. Antes de apagar qualquer coisa, medimos.

Versão curta: funciona, não é um substituto direto, e por enquanto mantemos o caminho antigo como padrão. Este post conta o que encontramos, sem os detalhes da nossa carga.

Antes do GA: "suportado", até o schema crescer

Antes do anúncio, o structured output nativo no Bedrock com os modelos Claude novos era irregular. Schemas pequenos passavam. Um schema de extração realista falhava com um erro assim:

Schemas contains too many parameters with union types (76 ...) limit: 16

Cada campo opcional ou nullable conta como uma união (string | null, que é o que .nullable() gera no Zod). Um schema de extração em que a maioria dos campos pode faltar chega muito rápido ao limite de 16. Na prática, "não suportado" queria dizer "rejeitado assim que o schema é de verdade". Foi por isso que tínhamos passado para prompt mais validação.

O SDK tem opinião própria

Chamamos o Bedrock pelo AI SDK da Vercel (@ai-sdk/amazon-bedrock). Ele tem uma opção structuredOutputMode com três valores: outputFormat (nativo), jsonTool (uma tool forçada) e auto. Três detalhes importam mais do que a opção em si:

  • auto não escolhe o modo nativo para os Claude mais novos. Na versão 5.0.121, o provider mantém uma lista dos modelos Claude em que confia para o modo nativo. As gerações novas ficam de fora, e um comentário no código ainda diz que o Bedrock as rejeita. O GA saiu, mas o SDK ainda não acompanhou.
  • Ele não sabe que um application inference profile é Claude. Se você chama os modelos por ARNs de perfis, que são IDs opacos, auto não sabe que está falando com a Anthropic. É preciso passar modelFamily: 'anthropic' ou forçar o modo.
  • Ele tira restrições do seu schema. Antes de enviar, o SDK transforma maxLength, maximum e maxItems em dicas na descrição. O modelo lê "seja curto" em vez de um limite rígido, e passa do ponto. Um schema com .max() então falha na sua própria validação com uma resposta que o Bedrock considerou válida.

Forçar o modo nativo fica assim:

const { output } = await generateText({
  model: bedrock(profileArn),
  output: Output.object({ schema }),
  providerOptions: {
    bedrock: { structuredOutputMode: 'outputFormat' },
  },
});

E por causa do terceiro ponto, você continua precisando de uma etapa de reparo: algo que corte os campos longos demais antes que o seu validador rejeite o objeto inteiro. O modo nativo não nos deixou apagar esse código. Só o mudou de hook.

Como medimos

Pegamos inputs reais de várias operações estruturadas, de classificações curtas a relatórios longos e aninhados, e rodamos cada uma de três formas:

  1. o caminho atual (JSON no prompt mais validação),
  2. o mesmo caminho atual de novo,
  3. structured outputs nativo.

A segunda rodada é a importante. A saída de um LLM varia entre rodadas mesmo sem mudar nada, então uma diferença entre dois modelos não diz nada até você compará-la com a diferença entre um modelo e ele mesmo. Chamamos isso de piso de ruído. Todas as comparações abaixo são contra ele.

Mesmo modelo e mesmo reasoning effort nos três braços. A única coisa que mudava era como o schema chegava ao modelo.

Achado 1: a forma é garantida, o conteúdo não

Este é o que importa. Com um schema aninhado, um resumo em texto mais um array de avaliações por item, o modo nativo muitas vezes devolveu JSON válido mas oco: o resumo vazio, o array vazio, todas as chaves obrigatórias presentes. Aconteceu em mais ou menos duas de cada três rodadas. Aconteceu também com e sem reasoning effort, então não era um problema do orçamento de thinking.

Nada falhou. O schema dizia summary: string, e "" é uma string. O Zod aceitou, e a chamada parecia mais rápida e mais barata. A economia de tokens e de latência era o conteúdo que faltava.

Essa é a armadilha dos schemas garantidos: eles mudam a falha de "deu erro" para "parecia estar certo". Se você for pelo modo nativo, coloque um mínimo em cada campo que precisa trazer conteúdo (.min(1) nas strings e nos arrays que nunca deveriam vir vazios), para que uma resposta oca falhe na validação e seja pedida de novo em vez de chegar a um usuário.

Achado 2: onde claramente ajudou

Um schema foi para o outro lado. Pelo caminho atual, uma parte considerável das respostas falhava na validação (cerca de um quarto), quase sempre por problemas de estrutura que o reparo não conseguia resolver. Com o modo nativo, todas as respostas foram válidas, e a latência mediana caiu para mais ou menos a metade.

Então não é que o nativo seja pior. Garantir a forma vale muito nos schemas em que o modelo erra a forma com frequência. Vale muito pouco nos que o modelo já acerta, e pode atrapalhar onde valores vazios passam como válidos.

Achado 3: no resto, empate

Em classificações curtas, comparações e relatórios médios, o nativo e o de texto diferiram mais ou menos o mesmo que o de texto dele mesmo. Uma operação ficou um pouco mais lenta no nativo e teve um par de falhas que nunca vimos pelo texto. Outra saiu levemente pior em concordância. As amostras eram pequenas demais para chamar qualquer uma das duas de tendência.

Dois custos menores: o caminho nativo somou uns cem tokens de input por chamada, e ainda não medimos como tirar o schema do prompt afeta o prompt caching.

Notas à margem sobre os Claude novos

Nada disto é específico de structured outputs, mas apareceu em todas as rodadas:

  • Eles pensam por padrão, dentro do seu orçamento de saída. Os tokens de thinking contam para maxOutputTokens. Um orçamento calibrado para um modelo anterior pode ser gasto em thinking e voltar com o JSON cortado no meio, ou vazio. Defina o reasoning effort explicitamente e dê folga ao orçamento.
  • Eles escrevem mais. Para a mesma tarefa vimos várias vezes os tokens de saída da geração anterior. Revise todos os limites fixos no código.
  • Eles rejeitam parâmetros de sampling fora do padrão, como temperature, quando o thinking está ativo. Se o seu código manda temperature: 0 para ser determinístico, essa requisição falha ou é refeita sem ele. O determinismo tem que vir de outro lugar.

O que estamos fazendo

Por enquanto, prompt mais validação continua sendo o padrão. Structured outputs nativo passa a ser algo ativado por schema. Um schema se qualifica quando o caminho de texto tem uma taxa real de falhas de validação, os campos que precisam trazer conteúdo têm mínimos, e uma rodada contra o piso de ruído mostra que ajuda.

A camada de reparo fica de qualquer jeito, porque o SDK continua tirando as restrições que deveria fazer cumprir.

Vamos revisar de novo à medida que o SDK aprender sobre os modelos novos e o caminho nativo amadurecer. O recurso é real e o GA é uma boa notícia. Só que ainda não significa o que "sem pós-processamento" parece prometer.

Se você está prestes a apagar sua camada de validação por causa deste anúncio, meça antes: o mesmo modelo duas vezes, depois o nativo, com os seus próprios inputs. E olhe o que as respostas válidas trazem, não só se elas parseiam.