Pular para o conteúdo

Certificação Claude Developer Foundations · parte 11 de 20

Saídas estruturadas e uso estrito de ferramentas

Como obter respostas em formato que o código lê sem adivinhar, com a saída estruturada e o uso estrito de ferramentas, e o que cada um garante.

Publicada em · 25 min de leitura

Tamanho da fonte:
Largura da coluna:

Quando a resposta do modelo vai para um programa, e não para uma pessoa, ela precisa ter um formato que o código consiga ler sem adivinhar. Até aqui, a forma mais comum de conseguir isso é pedir no prompt: “responda só com JSON”. Funciona na maioria das vezes, e essa maioria é justamente o problema, porque a falha acontece no pior momento, em produção, numa resposta que começou com “Claro! Aqui está” ou com uma vírgula sobrando. A API da Anthropic oferece dois recursos oficiais que trocam esse pedido por uma garantia: a saída estruturada, que controla o formato da resposta final, e o uso estrito de ferramentas, que controla os argumentos que o modelo escreve quando pede uma ferramenta. Esta aula explica o que cada um garante, o que não garante, como usá-los juntos e quais limites a documentação impõe. Ela depende de Prompt e contexto, especialmente da seção 20 (contrato de saída), e de 4.1 Implementação de ferramentas, onde o ciclo de tool_use é explicado. A mecânica da chamada à API está em 1.3 Mecânica da API Claude.

A documentação consultada tem data de 18 de outubro de 2026. Os nomes de campos foram conferidos nas páginas oficiais e também nos tipos do SDK Python anthropic versão 1.15.0, instalado nessa data. Artigos de terceiros citam uma versão beta antiga, com um cabeçalho próprio e um parâmetro com outro nome. Essa versão já não é o caminho atual, e a seção 5 explica por quê.

Mapa da aula

SeçãoO que responde
1. O problema de ler texto livrePor que o código quebra quando a resposta muda de forma
2. Dois recursos, dois alvosPara que serve a saída estruturada e para que serve o uso estrito
3. Como a garantia funcionaO que é amostragem restrita e por que ela impede o erro antes de acontecer
4. Uso combinadoComo colocar os dois recursos no mesmo pedido
5. Limites e casos de falhaModelos, schemas aceitos, recusa, corte por max_tokens e incompatibilidades
6. Pedir no prompt contra usar o recursoA mesma tarefa nos dois jeitos, com a saída real
7. Código executadoUm laço com o SDK real apontado para um servidor local, e o que ele prova
8. Erros comuns e boas práticasArmadilhas que aparecem quando a garantia é tomada por completa
9. Prática, glossário e fontesExercícios, termos definidos e documentação consultada

1. o problema de ler texto livre

Imagine um classificador de chamados de suporte. O código espera um objeto com categoria e urgencia, e cada campo tem um conjunto fechado de valores. Se o modelo responde "urgente" em vez de "alta", a aplicação não sabe onde colocar o chamado. Se responde com um texto de abertura antes do JSON, o json.loads falha logo na primeira posição. Se o JSON chega cortado porque a resposta bateu no limite de tokens, a falha aparece só na leitura seguinte. Cada uma dessas falhas exige um tratamento diferente, e nenhuma delas aparece nos testes feitos com dez exemplos bem-comportados.

O prompt pode reduzir essas falhas, mas não as elimina, porque o modelo é um gerador de texto e não um serializador. Ele escolhe palavras com base no que é plausível, e “plausível” nem sempre coincide com “válido segundo um esquema”. A pergunta que esta aula responde é como tirar essa decisão da esfera da plausibilidade e colocá-la no mecanismo que gera a resposta.

2. dois recursos, dois alvos

Os dois recursos resolvem problemas parecidos, mas atuam em lugares diferentes da resposta, e confundi-los é o primeiro erro comum.

A saída estruturada controla o texto final que o modelo devolve quando termina de responder. Você passa um esquema JSON no campo output_config.format, com type igual a "json_schema" e o esquema dentro de schema. A resposta passa a seguir esse esquema. Um caso típico é extrair dados de um e-mail: nome, endereço, plano de interesse e se a pessoa pediu demonstração. O código que recebe esses dados quer exatamente esses quatro campos, com os tipos certos, e nada a mais.

O uso estrito de ferramentas controla os argumentos de uma chamada de ferramenta. Quando o modelo decide chamar uma função, ele escreve um bloco tool_use com name e input. Com strict: true na definição da ferramenta, o input passa a seguir o input_schema. Um caso típico é uma função de busca de voos, que precisa de um destino em texto, uma data no formato AAAA-MM-DD e um número de passageiros entre 1 e 6. Sem o modo estrito, o modelo pode mandar "2" onde o campo é número ou omitir a data.

Os dois recursos podem aparecer juntos no mesmo pedido, e cada um vale para a parte da resposta que lhe cabe. A seção 4 mostra isso em código. Antes disso, convém entender o mecanismo que faz a garantia valer.

Este é o pedido da extração de contato, copiado da documentação e com o campo que importa destacado na forma de chave:

{
  "model": "claude-opus-5-5",
  "max_tokens": 1024,
  "messages": [{"role": "user", "content": "Extract the key information from this email: ..."}],
  "output_config": {
    "format": {
      "type": "json_schema",
      "schema": {
        "type": "object",
        "properties": {
          "name": {"type": "string"},
          "email": {"type": "string"},
          "plan_interest": {"type": "string"},
          "demo_requested": {"type": "boolean"}
        },
        "required": ["name", "email", "plan_interest", "demo_requested"],
        "additionalProperties": false
      }
    }
  }
}

O exemplo da documentação usa claude-opus-5-5. Na aula, o código usa claude-haiku-5-5, que também aparece na lista de modelos suportados da página de saídas estruturadas.

3. como a garantia funciona

Para entender a garantia, é preciso separar dois momentos. No primeiro, a API transforma o esquema em uma gramática, uma descrição formal de quais sequências de tokens são válidas. No segundo, durante a geração, o modelo escolhe o próximo token entre os candidatos possíveis. Um token é um pedaço de texto, às vezes uma palavra inteira, às vezes uma sílaba ou um sinal de pontuação. Em cada passo, a amostragem restrita remove da lista os tokens que levariam a uma saída fora da gramática. A documentação chama isso de grammar-constrained sampling, e o efeito prático é que um token inválido nunca chega a ser escrito.

Essa diferença explica por que a garantia é mais forte do que a validação depois da geração. Um validador confere o texto pronto e, se ele estiver errado, precisa de uma nova tentativa. A amostragem restrita não deixa o erro surgir. Por isso a documentação diz que a resposta segue o esquema, e não que ela tem boa chance de segui-lo. Repare, porém, que a garantia cobre a forma e não o conteúdo. Um campo "email" com a string "ola" é uma string válida, e o esquema não tem como saber que aquilo não é um endereço.

O escopo da gramática também tem bordas. A própria documentação informa que ela se aplica apenas à saída direta do modelo, e não às chamadas de ferramenta, aos resultados de ferramenta nem às tags de pensamento. Quando o pensamento está ativo, a gramática reinicia entre as seções, e o modelo pode raciocinar livremente antes da resposta estruturada. A consequência prática é que a garantia de output_config.format protege o texto final, e a garantia de strict protege a entrada da ferramenta. Nenhum dos dois protege o que vem antes.

No uso estrito, a documentação dá duas garantias explícitas: a entrada da ferramenta segue o input_schema, e o name é sempre um nome válido entre as ferramentas fornecidas ou as de servidor. Ela não garante que o valor faça sentido no mundo real, que a ferramenta seja chamada em primeiro lugar, nem que a execução do seu código termine bem. O exemplo do passageiro mostra a diferença entre forma e sentido. Com o schema da seção 2, a entrada {"destino": "Atlântida", "data_ida": "2026-06-01", "passageiros": 2} é perfeitamente válida. O destino é uma string, e a data está no formato. A aplicação precisa conferir se a cidade existe no seu sistema, e esse teste acontece depois da garantia.

Há ainda a questão de quando o modelo chama a ferramenta. Por padrão, tool_choice é auto, o que quer dizer que o modelo decide sozinho se usa alguma ferramenta. O strict não muda essa decisão. Ele só garante que, quando a chamada acontecer, os argumentos estarão corretos. A seção 5 trata das opções que forçam a chamada.

4. uso combinado

A página de saídas estruturadas afirma que os dois recursos podem ser usados independentemente ou juntos no mesmo pedido. A combinação faz sentido num fluxo comum: o modelo consulta uma ferramenta para buscar dados e, na resposta final, devolve um resumo com um formato fixo que o restante do sistema consome.

O código da seção 7, no cenário 3, faz exatamente isso. O pedido leva tools com buscar_voo marcada como strict: true e leva também output_config.format com um esquema de resumo. A primeira resposta do modelo é um bloco tool_use com a entrada {"destino": "Lisboa", "data_ida": "2026-06-01", "passageiros": 2}. A aplicação executa a função, devolve o tool_result com o mesmo tool_use_id e envia a segunda requisição, que traz os mesmos dois campos. A resposta final chega como texto JSON, e o código lê esse texto com json.loads. A saída real do cenário mostra que as duas requisições saíram com output_config e tools juntos.

Repare em quem garante o quê. O strict cuida da entrada da ferramenta, que é uma chamada de função. O output_config.format cuida do texto final, que é o que o usuário ou o sistema seguinte lê. A divisão segue a da seção 3: cada recurso atua na parte da resposta que a gramática alcança.

5. limites e casos de falha

Nenhum recurso de formato funciona para qualquer modelo e qualquer esquema. A documentação impõe limites que, quando desconhecidos, transformam a garantia em erro de requisição ou em resposta incompleta.

Modelos suportados

A lista de modelos da página de saídas estruturadas, consultada em 18 de outubro de 2026, inclui claude-haiku-5-5, além de Opus 5.5, Sonnet 5.5, Fable 5.1, Mythos 5.1 e várias versões anteriores. A página de uso estrito não traz uma lista própria no trecho lido, e diz que usa o mesmo pipeline de compilação das saídas estruturadas. Antes de depender de um modelo específico, confira a lista atual na documentação, porque ela muda com os lançamentos.

Schemas aceitos

Alguns recursos de JSON Schema não são aceitos e resultam em erro 400. A página lista estes casos: esquemas recursivos, $ref externo, restrições numéricas como minimum, maximum e multipleOf, restrições de tamanho de string como minLength e maxLength, restrições de array além de minItems igual a 0 ou 1, e additionalProperties com qualquer valor diferente de false. Nas expressões regulares, não são aceitos backreferences, lookahead, lookbehind, \b e quantificadores {n,m} com faixas grandes.

Os aceitos incluem enum com strings, números, booleanos ou nulos, const, anyOf, allOf sem $ref, $ref interno, default, required, formatos como date-time, date, email, uri e uuid, e minItems com 0 ou 1. Um detalhe importante: os helpers do SDK, como o transform_schema() do Python, removem as restrições não suportadas e passam a descrição dela para o campo. Eles ainda validam a resposta contra o esquema original no seu código. Quem escreve minLength direto no esquema, sem usar o helper, recebe o 400.

Limites de quantidade no uso estrito

A página de saídas estruturadas informa limites por requisição: no máximo 20 ferramentas com strict: true, no máximo 24 parâmetros opcionais somados entre as ferramentas estritas e os esquemas de saída, e no máximo 16 parâmetros com tipos união (anyOf ou arrays de tipos). Em catálogos grandes, esses números aparecem antes do que se imagina.

Recusa

Quando o modelo recusa uma resposta por razões de segurança, o stop_reason vem como "refusal". A documentação diz que essa recusa chega como HTTP 200, e não como erro, e que ela é cobrada pelos tokens gerados. Como a recusa tem precedência sobre o esquema, o conteúdo pode não corresponder ao formato pedido. O campo stop_details identifica a categoria da política acionada e vem null nos demais casos. Não verifiquei o formato completo de stop_details numa resposta real de recusa, então a aula não descreve sua estrutura interna. A página de recusas também informa que uma recusa de Haiku 5.5, entre outros modelos, pode em geral ser atendida por nova tentativa com outro modelo Claude.

Corte por max_tokens

Se a resposta bate no limite de max_tokens, o stop_reason vem como "max_tokens", e a página avisa que a saída pode estar incompleta e não corresponder ao esquema. Quando o corte acontece no meio de um bloco tool_use, a documentação de paradas diz que é preciso refazer a requisição com um max_tokens maior para obter a chamada completa. A seção 7 reproduz esse caso: o JSON cortado em {"name": "John Smi não é consertado. A aplicação pede de novo com mais espaço.

Incompatibilidades

Citations não podem ser usadas junto com output_config.format. Se estiverem ativadas, a API retorna 400, porque as citações exigem blocos de citação intercalados com o texto, o que conflita com a gramática. A técnica de prefill, que começa a resposta do assistente com um trecho escrito por você, também é incompatível com saídas em JSON. O bloco de ferramentas de computador e o de navegador não aceitam strict: true, e a requisição que tenta usá-lo é rejeitada.

Escolha forçada de ferramenta

A forma de forçar uma chamada é tool_choice com any ou com tool. A página de definição de ferramentas diz que o suporte depende do modelo. Em Opus 5.5, Sonnet 5.5, Fable 5.1 e Mythos 5.1, essas duas escolhas retornam 400. O Haiku 5.5 não está nessa lista, e a documentação diz que ele aceita a escolha forçada, com a ressalva de que a chamada forçada pula o bloco de pensamento. A página recomenda, nos modelos que recusam a escolha forçada, usar auto com strict: true. Nos modelos que aceitam, any com strict: true garante que uma das ferramentas seja chamada e que os argumentos sigam o esquema.

Essa nuance corrige uma simplificação que aparece em 4.1 Implementação de ferramentas, que diz que “em alguns modelos mais recentes” a escolha forçada dá erro. A afirmação está certa, mas a restrição é por modelo, e não por geração de modelos. Ao configurar, é preciso o nome do modelo, não só a família.

Cache e custo

Mudar output_config.format invalida o cache de prompt da conversa. A gramática compilada fica em cache por até 24 horas desde o último uso, e a documentação diz que ela é invalidada quando muda a estrutura do esquema ou o conjunto de ferramentas da requisição. Mudar só name ou description não invalida. Mudar tool_choice invalida os blocos de mensagem em cache. Na prática, o esquema deve ser tratado como parte estável do prefixo, a mesma regra que a seção 17 de Prompt e contexto aplica às ferramentas.

Dados sensíveis

As duas páginas dizem que o recurso é elegível a HIPAA, mas que informações de saúde protegidas não devem aparecer em nomes de propriedade, valores de enum, valores de const ou expressões regulares do esquema. Esses dados ficam em cache separado das mensagens e não recebem as mesmas proteções. Dados de saúde só devem aparecer no conteúdo das mensagens.

Versão antiga

Artigos antigos mostram o parâmetro output_format com um cabeçalho beta structured-outputs-2025-11-13. Segundo a página atual, o output_format foi substituído por output_config.format, não é mais preciso o cabeçalho beta, e o output_format está descontinuado. O SDK Python na versão 1.0 ou superior lança TypeError se o output_format={...} for passado a client.beta.messages.create() ou a count_tokens(). O SDK também tem um método messages.parse() que aceita um modelo Pydantic no argumento output_format e o funde em output_config. Não executei esse método contra a API, só conferi pela assinatura no SDK instalado.

A figura abaixo resume o que a aplicação faz com a resposta depois que ela chega. O primeiro losango é o stop_reason, que decide se a resposta é dado, se é uma chamada de ferramenta, se foi recusada ou se foi cortada. Só o caminho end_turn chega ao json.loads, e as falhas de validação caem no mesmo erro tratado.

4 01 decisao resposta
figura 1 · 4 01 decisao resposta

O guia de leitura da figura começa pelo topo, com a resposta da API e o stop_reason. O caminho max_tokens leva a uma nova requisição com mais tokens, e não a uma tentativa de consertar o JSON. O caminho refusal vai para um caminho de recusa, e o conteúdo não é tratado como dado. O caminho tool_use passa pela pergunta sobre o valor, que é a parte que o strict não cobre. O caminho end_turn passa por três testes em sequência: o JSON é lido, os campos e tipos batem e a regra de negócio confere. Qualquer falha termina no erro tratado, que devolve uma mensagem específica ao modelo ou encaminha o caso para revisão.

6. pedir no prompt contra usar o recurso

A diferença entre os dois jeitos fica mais clara num exemplo curto. O mesmo e-mail é processado de duas formas. Na primeira, o pedido diz “responda somente com JSON” e nada mais. Na segunda, o pedido leva output_config.format.

No primeiro caso, a resposta simulada vem assim, porque o modelo começa com uma frase educada antes do bloco de código, e o código falha:

requisição enviada: {"model": "claude-haiku-5-5", "max_tokens": 512}
falha ao ler: Expecting value na posição 0

O erro diz que o parser esperava um valor JSON na posição 0 e encontrou o “C” de “Claro!”. A falha é previsível para quem conhece o comportamento do modelo, mas o código não tem como saber, antes de tentar, se a resposta vem limpa.

No segundo caso, a requisição sai com o campo do esquema, e a resposta final é lida sem tratamento de texto extra. Repare que o campo de formato está ausente na primeira requisição. O código da seção 7 mostra os dois casos lado a lado, e a leitura com json.loads só acontece quando o stop_reason indica resposta completa.

Existe ainda um nível intermediário. Descrever o formato e dar um exemplo no prompt, como na seção 5 de Prompt e contexto, é melhor do que nada, mas não tem garantia. O recurso oficial é o nível mais forte para respostas em texto, e o uso estrito é o nível mais forte para argumentos de ferramenta.

7. código executado

O código abaixo roda com o SDK anthropic na versão 1.15.0, num ambiente virtual isolado, com python -I. O cliente é o SDK de verdade, apontado para um servidor HTTP local que devolve respostas roteirizadas e guarda as requisições recebidas. Assim, o JSON que aparece na saída é exatamente o que a biblioteca enviou, e os nomes dos campos foram testados contra o caminho real de serialização. O modelo não tomou nenhuma decisão. As respostas foram escritas por mim, na ordem em que o cenário as pede. A chamada à API real não foi feita, então a aceitação do esquema pelo servidor, o comportamento do modelo e os 400 descritos na seção 5 não foram verificados.

"""Saídas estruturadas e uso estrito de ferramentas, com o SDK real apontado para um servidor local.

O SDK monta e envia o JSON de verdade. O servidor só devolve respostas roteirizadas
e guarda as requisições que recebeu, para que a aula mostre o que saiu da biblioteca.
Nenhuma chamada chega à API da Anthropic.
"""
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

import anthropic

FILA = []        # respostas a devolver, na ordem em que chegam
CAPTURADAS = []  # corpos das requisições recebidas


class Servidor(BaseHTTPRequestHandler):
    def do_POST(self):
        tamanho = int(self.headers["content-length"])
        CAPTURADAS.append(json.loads(self.rfile.read(tamanho)))
        dados = json.dumps(FILA.pop(0)).encode()
        self.send_response(200)
        self.send_header("content-type", "application/json")
        self.send_header("content-length", str(len(dados)))
        self.end_headers()
        self.wfile.write(dados)

    def log_message(self, *args):
        pass


def mensagem(content, stop_reason="end_turn"):
    return {
        "id": "msg_simulada",
        "type": "message",
        "role": "assistant",
        "model": "claude-haiku-5-5",
        "content": content,
        "stop_reason": stop_reason,
        "stop_sequence": None,
        "usage": {"input_tokens": 10, "output_tokens": 5},
    }


def texto(t):
    return {"type": "text", "text": t}


def uso(id_, nome, entrada):
    return {"type": "tool_use", "id": id_, "name": nome, "input": entrada}


servidor = ThreadingHTTPServer(("127.0.0.1", 0), Servidor)
threading.Thread(target=servidor.serve_forever, daemon=True).start()

# Chave falsa e endereço local: o SDK não precisa de ANTHROPIC_API_KEY nem de rede.
cliente = anthropic.Anthropic(
    api_key="chave-simulada",
    base_url=f"http://127.0.0.1:{servidor.server_port}",
    max_retries=0,
)
MODELO = "claude-haiku-5-5"
EMAIL = (
    "Extraia as informações: John Smith (john@example.com) quer o plano Enterprise "
    "e pediu uma demonstração para a próxima terça às 14h."
)

ESQUEMA_CONTATO = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "email": {"type": "string"},
        "plan_interest": {"type": "string"},
        "demo_requested": {"type": "boolean"},
    },
    "required": ["name", "email", "plan_interest", "demo_requested"],
    "additionalProperties": False,
}

FERRAMENTA_VOO = {
    "name": "buscar_voo",
    "description": (
        "Consulta voos disponíveis para um destino em uma data. Use quando o cliente "
        "pedir passagens. Não reserva nem cobra nada."
    ),
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "destino": {"type": "string", "description": "Cidade de destino."},
            "data_ida": {"type": "string", "format": "date", "description": "Data no formato AAAA-MM-DD."},
            "passageiros": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6]},
        },
        "required": ["destino", "data_ida"],
        "additionalProperties": False,
    },
}

DESTINOS_CONHECIDOS = {"Lisboa", "Paris", "Tóquio"}


def resumo_requisicao(corpo):
    """Mostra só os campos que a aula discute, como o SDK os enviou."""
    saida = {"model": corpo["model"], "max_tokens": corpo["max_tokens"]}
    if "output_config" in corpo:
        saida["output_config"] = corpo["output_config"]
    if "tools" in corpo:
        saida["tools"] = [{"name": t["name"], "strict": t.get("strict")} for t in corpo["tools"]]
    return json.dumps(saida, ensure_ascii=False)


print("== cenário 1: saída em JSON pelo recurso oficial")
FILA.append(mensagem([texto('{"name": "John Smith", "email": "john@example.com", '
                            '"plan_interest": "Enterprise", "demo_requested": true}')]))
r = cliente.messages.create(
    model=MODELO,
    max_tokens=512,
    messages=[{"role": "user", "content": EMAIL}],
    output_config={"format": {"type": "json_schema", "schema": ESQUEMA_CONTATO}},
)
print("requisição enviada:", resumo_requisicao(CAPTURADAS[-1]))
print("stop_reason:", r.stop_reason)
dado = json.loads(r.content[0].text)
print("campos lidos:", dado)


print("\n== cenário 2: uso estrito de ferramenta, com valor semanticamente errado")
FILA.append(mensagem([uso("toolu_01", "buscar_voo",
                          {"destino": "Atlântida", "data_ida": "2026-06-01", "passageiros": 2})],
                     stop_reason="tool_use"))
r = cliente.messages.create(
    model=MODELO,
    max_tokens=512,
    tools=[FERRAMENTA_VOO],
    messages=[{"role": "user", "content": "Tem voo para a Atlântida em 1º de junho para 2 pessoas?"}],
)
print("requisição enviada:", resumo_requisicao(CAPTURADAS[-1]))
bloco = next(b for b in r.content if b.type == "tool_use")
print("schema aceitou:", bloco.input)
if bloco.input["destino"] not in DESTINOS_CONHECIDOS:
    print("a aplicação recusa: destino não existe no sistema (o strict não checa isso)")


print("\n== cenário 3: os dois recursos juntos, numa única requisição com laço")
FILA.append(mensagem([uso("toolu_02", "buscar_voo",
                          {"destino": "Lisboa", "data_ida": "2026-06-01", "passageiros": 2})],
                     stop_reason="tool_use"))
FILA.append(mensagem([texto('{"destino": "Lisboa", "voos_encontrados": 3, "resumo": "3 voos no dia 1 de junho"}')]))
formato = {"type": "json_schema", "schema": {
    "type": "object",
    "properties": {
        "destino": {"type": "string"},
        "voos_encontrados": {"type": "integer"},
        "resumo": {"type": "string"},
    },
    "required": ["destino", "voos_encontrados", "resumo"],
    "additionalProperties": False,
}}
mensagens = [{"role": "user", "content": "Quais voos há para Lisboa em 1º de junho, para 2 pessoas?"}]
r = cliente.messages.create(model=MODELO, max_tokens=512, tools=[FERRAMENTA_VOO],
                            output_config={"format": formato}, messages=mensagens)
print("volta 1:", resumo_requisicao(CAPTURADAS[-1]))
mensagens.append({"role": "assistant", "content": r.content})
resultados = [{"type": "tool_result", "tool_use_id": b.id,
               "content": json.dumps({"voos": 3, "data": "2026-06-01"})}
              for b in r.content if b.type == "tool_use"]
mensagens.append({"role": "user", "content": resultados})
r = cliente.messages.create(model=MODELO, max_tokens=512, tools=[FERRAMENTA_VOO],
                            output_config={"format": formato}, messages=mensagens)
print("volta 2:", resumo_requisicao(CAPTURADAS[-1]))
print("resposta final lida:", json.loads(r.content[0].text))


print("\n== cenário 4: max_tokens corta o JSON antes do fim")
FILA.append(mensagem([texto('{"name": "John Smi')], stop_reason="max_tokens"))
FILA.append(mensagem([texto('{"name": "John Smith", "email": "john@example.com", '
                            '"plan_interest": "Enterprise", "demo_requested": true}')]))
limite = 64
r = cliente.messages.create(model=MODELO, max_tokens=limite,
                            messages=[{"role": "user", "content": EMAIL}],
                            output_config={"format": {"type": "json_schema", "schema": ESQUEMA_CONTATO}})
print("primeira resposta:", r.stop_reason, "| texto:", r.content[0].text)
if r.stop_reason == "max_tokens":
    # Não tente consertar o JSON: o problema é o limite, então pede de novo com mais espaço.
    limite *= 4
    r = cliente.messages.create(model=MODELO, max_tokens=limite,
                                messages=[{"role": "user", "content": EMAIL}],
                                output_config={"format": {"type": "json_schema", "schema": ESQUEMA_CONTATO}})
print("segunda resposta:", r.stop_reason, "| max_tokens enviado:", CAPTURADAS[-1]["max_tokens"])
print("lido:", json.loads(r.content[0].text))


print("\n== cenário 5: recusa, tratada antes de qualquer parse")
FILA.append(mensagem([], stop_reason="refusal"))
r = cliente.messages.create(model=MODELO, max_tokens=512,
                            messages=[{"role": "user", "content": EMAIL}],
                            output_config={"format": {"type": "json_schema", "schema": ESQUEMA_CONTATO}})
if r.stop_reason == "refusal":
    print("recusa detectada pelo stop_reason; o conteúdo não é lido como dado")


print("\n== cenário 6: pedir JSON só no prompt, sem o recurso oficial")
FILA.append(mensagem([texto(
    'Claro! Aqui está a extração:\n```json\n{"name": "John Smith", "email": "john@example.com"}\n```'
)]))
r = cliente.messages.create(
    model=MODELO,
    max_tokens=512,
    messages=[{"role": "user", "content": EMAIL + " Responda somente com JSON."}],
)
print("requisição enviada:", resumo_requisicao(CAPTURADAS[-1]))
try:
    json.loads(r.content[0].text)
except json.JSONDecodeError as erro:
    print("falha ao ler:", erro.msg, "na posição", erro.pos)

A saída real, sem edição, foi esta:

== cenário 1: saída em JSON pelo recurso oficial
requisição enviada: {"model": "claude-haiku-5-5", "max_tokens": 512, "output_config": {"format": {"type": "json_schema", "schema": {"type": "object", "properties": {"name": {"type": "string"}, "email": {"type": "string"}, "plan_interest": {"type": "string"}, "demo_requested": {"type": "boolean"}}, "required": ["name", "email", "plan_interest", "demo_requested"], "additionalProperties": false}}}}
stop_reason: end_turn
campos lidos: {'name': 'John Smith', 'email': 'john@example.com', 'plan_interest': 'Enterprise', 'demo_requested': True}

== cenário 2: uso estrito de ferramenta, com valor semanticamente errado
requisição enviada: {"model": "claude-haiku-5-5", "max_tokens": 512, "tools": [{"name": "buscar_voo", "strict": true}]}
schema aceitou: {'destino': 'Atlântida', 'data_ida': '2026-06-01', 'passageiros': 2}
a aplicação recusa: destino não existe no sistema (o strict não checa isso)

== cenário 3: os dois recursos juntos, numa única requisição com laço
volta 1: {"model": "claude-haiku-5-5", "max_tokens": 512, "output_config": {"format": {"type": "json_schema", "schema": {"type": "object", "properties": {"destino": {"type": "string"}, "voos_encontrados": {"type": "integer"}, "resumo": {"type": "string"}}, "required": ["destino", "voos_encontrados", "resumo"], "additionalProperties": false}}}, "tools": [{"name": "buscar_voo", "strict": true}]}
volta 2: {"model": "claude-haiku-5-5", "max_tokens": 512, "output_config": {"format": {"type": "json_schema", "schema": {"type": "object", "properties": {"destino": {"type": "string"}, "voos_encontrados": {"type": "integer"}, "resumo": {"type": "string"}}, "required": ["destino", "voos_encontrados", "resumo"], "additionalProperties": false}}}, "tools": [{"name": "buscar_voo", "strict": true}]}
resposta final lida: {'destino': 'Lisboa', 'voos_encontrados': 3, 'resumo': '3 voos no dia 1 de junho'}

== cenário 4: max_tokens corta o JSON antes do fim
primeira resposta: max_tokens | texto: {"name": "John Smi
segunda resposta: end_turn | max_tokens enviado: 256
lido: {'name': 'John Smith', 'email': 'john@example.com', 'plan_interest': 'Enterprise', 'demo_requested': True}

== cenário 5: recusa, tratada antes de qualquer parse
recusa detectada pelo stop_reason; o conteúdo não é lido como dado

== cenário 6: pedir JSON só no prompt, sem o recurso oficial
requisição enviada: {"model": "claude-haiku-5-5", "max_tokens": 512}
falha ao ler: Expecting value na posição 0

O que a saída mostra: no cenário 1, o SDK enviou output_config.format com type e schema exatamente como foram passados. No cenário 2, o strict: true chegou na definição da ferramenta, e a entrada com “Atlântida” passou pelo schema, o que confirma que forma e sentido são coisas diferentes. No cenário 3, os dois campos aparecem nas duas voltas do laço. No cenário 4, o stop_reason de max_tokens foi o sinal para pedir de novo, e não o conserto do JSON. No cenário 6, a falha de leitura aparece na posição 0, porque o texto começa antes do JSON.

O que a execução não prova: que a API real aceita essas requisições, que o modelo responde com a forma pedida, que os 400 da seção 5 acontecem como descritos, e que a recusa tem a estrutura de stop_details que a documentação sugere. Para verificar isso, o próximo passo é rodar o mesmo script com um cliente real, anthropic.Anthropic() sem base_url, e registrar o usage e o stop_reason de cada chamada antes de ampliar o uso.

8. erros comuns e boas práticas

O primeiro erro é tratar a garantia como validação completa. O esquema garante que o campo destino é uma string, e não que a cidade existe. Quem pula a validação de regra de negócio, porque “a API já valida”, descobre o problema quando um id inventado chega ao banco. A regra prática é a da seção 9 de Prompt e contexto: o esquema garante a forma, e a aplicação continua responsável pelo conteúdo.

O segundo erro é ignorar o stop_reason. Uma resposta cortada por max_tokens pode ter JSON que parece válido até o ponto de corte e, mesmo assim, não corresponder ao esquema. A decisão correta é olhar o stop_reason antes de ler o conteúdo, e tratar o corte como problema de limite, com um max_tokens maior, e não como problema de formato.

O terceiro erro é achar que uma recusa é um erro de rede. Ela chega como HTTP 200, é cobrada e pode vir com conteúdo que não segue o esquema. Se o código só trata exceções e não confere o stop_reason, a recusa passa como resposta normal.

O quarto erro é usar um recurso que não é compatível com outro sem perceber. Citations junto com output_config.format resultam em 400. Prefill também não combina com saídas em JSON. Quando a requisição falha com um desses 400, a causa costuma estar em um recurso que foi ligado em outra parte do código, e não no esquema.

O quinto erro é escrever minLength, maximum ou maxLength direto no esquema e esperar que o modelo respeite o limite. Essas restrições não são aceitas e a resposta é 400. O caminho correto é o helper do SDK, que move a restrição para a descrição do campo, ou validar o limite no código depois da resposta.

O sexto erro é forçar uma ferramenta com tool_choice em modelos que não aceitam a escolha. Em Opus 5.5, Sonnet 5.5, Fable 5.1 e Mythos 5.1, any e tool retornam 400. A alternativa documentada é auto com strict: true e, quando a chamada precisa acontecer, a descrição da ferramenta e a instrução no prompt.

O sétimo erro é mudar o esquema a cada requisição, por exemplo montando um campo diferente conforme o pedido. Cada mudança estrutural invalida a gramática em cache e o cache de prompt da conversa, e o custo aparece na fatura. O esquema deve ser estável, e as variações devem ser tratadas por ferramentas ou campos opcionais dentro de um esquema fixo.

Como boa prática, a aplicação deve registrar o stop_reason de cada chamada e a taxa de falhas de validação. Essa taxa é a métrica que mostra se uma troca de modelo ou uma edição de prompt piorou a extração, antes que o usuário perceba. A seção 20 de Prompt e contexto trata da política de falha em detalhe.

9. prática, glossário e fontes

O primeiro exercício é adicionar ao esquema de contato um campo telefone com minLength: 8. Rode o script com a alteração e veja o que o SDK envia. Depois, escreva em uma nota o que você esperaria da API real, com o código de erro e a razão, e confirme na documentação de erros antes de afirmar qualquer coisa. Esse exercício não tem como ser verificado sem chamada real, então a resposta deve ser uma previsão rotulada.

O segundo exercício é implementar a validação de regra de negócio para a ferramenta buscar_voo: a data não pode estar no passado, e o destino deve estar numa lista de cidades atendidas. Cada falha deve virar um tool_result com is_error e uma mensagem que diga o que corrigir, como na seção 4 de 4.1 Implementação de ferramentas.

O terceiro exercício é escrever um classificador de chamados com um enum de três categorias e um campo de urgência, e medir quantas respostas são recusadas, cortadas ou inválidas num lote de exemplos. A medida importa mais do que a implementação, porque mostra onde a garantia do recurso não basta.

Glossário.

Saída estruturada: recurso que faz a resposta final seguir um esquema JSON, configurado em output_config.format com type igual a "json_schema".

Uso estrito de ferramentas: opção strict: true na definição de uma ferramenta, que faz a entrada gerada pelo modelo seguir o input_schema.

Amostragem restrita: técnica em que a API remove, em cada passo da geração, os tokens que levariam a uma saída fora da gramática. Também chamada de grammar-constrained sampling na documentação.

Gramática: representação formal das sequências válidas de um esquema, compilada pela API e mantida em cache por até 24 horas desde o último uso.

stop_reason: campo da resposta que indica por que o modelo parou. Os valores citados nesta aula são end_turn, tool_use, max_tokens e refusal.

stop_details: campo que, em uma recusa, identifica a categoria da política acionada. A documentação diz que ele é null nos demais casos.

tool_choice: campo que controla se o modelo pode, deve ou não usar ferramentas. Os valores são auto, any, tool e none.

Fontes consultadas em 18 de outubro de 2026, na documentação oficial da Anthropic:

As páginas foram lidas diretamente, com a ferramenta de leitura de páginas. A página de saídas estruturadas tem mais de 100 mil caracteres, e a leitura cobriu tudo, porque o restante foi lido numa segunda chamada. A página de motivos de parada tem mais de 120 mil caracteres, e a parte final, depois de 100 mil, não foi conferida. Os trechos sobre max_tokens e refusal vieram do que foi lido. Os tipos do SDK Python anthropic 1.15.0 foram conferidos pelo código instalado no dia 18 de outubro de 2026.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?