Domínio 4 — Prompt Engineering & Structured Output · Lição 4 de 7

Structured outputs: output_config, tool_use e strict

Objetivos de aprendizagem

O caminho atual: output_config.format

Nos modelos atuais, a forma direta de obter JSON sintaticamente válido e aderente a um schema é o parâmetro output_config com um format do tipo json_schema. A API garante que o bloco de texto da resposta contém JSON válido conforme o schema — sem cercas de markdown, sem prosa em volta, sem vírgula sobrando.

import json
import anthropic

client = anthropic.Anthropic()

INVOICE_SCHEMA = {
    "type": "json_schema",
    "schema": {
        "type": "object",
        "properties": {
            "supplier": {"type": "string"},
            "total": {"type": ["number", "null"]},
            "due_date": {"type": ["string", "null"]},
            "category": {
                "type": "string",
                "enum": ["servicos", "produtos", "impostos", "unclear", "other"],
            },
            "category_detail": {"type": ["string", "null"]},
        },
        "required": ["supplier", "total", "due_date", "category", "category_detail"],
        "additionalProperties": False,
    },
}

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    output_config={"format": INVOICE_SCHEMA},
    messages=[{
        "role": "user",
        "content": "Extraia: 'NF 5501 - Consultoria Delta - R$ 8.900,00 - venc. 15/12/2026'",
    }],
)

text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)  # JSON garantidamente válido e aderente ao schema
print(data["supplier"], data["total"])

No SDK Python, o helper client.messages.parse() faz o mesmo com um modelo Pydantic e já devolve a instância validada em response.parsed_output:

from typing import Optional, List
from pydantic import BaseModel
import anthropic

class Contact(BaseModel):
    name: str
    email: Optional[str]
    interests: List[str]
    demo_requested: bool

client = anthropic.Anthropic()

response = client.messages.parse(
    model="claude-opus-5",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "Extraia: Jane Doe (jane@co.com) quer plano Enterprise e uma demo.",
    }],
    output_format=Contact,  # kwarg do helper parse(): recebe o modelo Pydantic
)

contact = response.parsed_output  # instância Contact validada
print(contact.name, contact.demo_requested)

Não confunda: output_format=Contact acima é o argumento do helper parse() do SDK (recebe uma classe Pydantic). Na chamada bruta messages.create(), o parâmetro atual da API é output_config={"format": ...} — um antigo parâmetro top-level output_format da API foi substituído por output_config e aparece em provas apenas como distrator.

O padrão clássico: tool use com JSON schema

Antes dos structured outputs nativos, o padrão consolidado de extração era definir uma ferramenta cujo input_schema é o schema da extração e ler os dados do bloco tool_use da resposta. A "ferramenta" não executa nada — é só um recipiente tipado. Esse padrão continua válido, aparece bastante no exame, e é o único caminho quando você também precisa de ferramentas reais na mesma chamada.

import anthropic

client = anthropic.Anthropic()

extraction_tool = {
    "name": "record_invoice",
    "description": "Registra os dados extraídos de uma nota fiscal.",
    "strict": True,  # garante aderência exata ao input_schema
    "input_schema": {
        "type": "object",
        "properties": {
            "supplier": {"type": "string"},
            "total": {"type": ["number", "null"]},
            "line_items": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "description": {"type": "string"},
                        "amount": {"type": "number"},
                    },
                    "required": ["description", "amount"],
                    "additionalProperties": False,
                },
            },
        },
        "required": ["supplier", "total", "line_items"],
        "additionalProperties": False,
    },
}

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[extraction_tool],
    tool_choice={"type": "any"},  # obriga a chamar uma ferramenta (ver restrição abaixo)
    messages=[{"role": "user", "content": "Extraia: 'ACME - item A R$ 100 - item B R$ 50 - total R$ 150'"}],
)

for block in response.content:
    if block.type == "tool_use":
        print(block.input)  # dict já estruturado conforme o schema

tool_choice: auto, any e forçado

ValorComportamentoUso típico em extração
{"type": "auto"}O modelo decide se chama ferramenta ou responde em texto.Inadequado quando a saída estruturada é obrigatória — o modelo pode devolver prosa.
{"type": "any"}O modelo é obrigado a chamar alguma ferramenta, mas escolhe qual.Vários schemas de extração e tipo de documento desconhecido: garante saída estruturada e deixa o modelo classificar.
{"type": "tool", "name": "..."}O modelo é obrigado a chamar aquela ferramenta específica.Garantir que uma extração específica rode (ex.: extract_metadata antes das etapas de enriquecimento).
{"type": "none"}Proíbe chamadas de ferramenta.Forçar resposta final em texto mesmo com tools definidas.

Restrição em modelos mais novos: nas gerações topo de linha mais recentes (ex.: Claude Opus 5.5 e Fable 5.1), tool_choice any e forçado ({"type": "tool"}) retornam erro 400 — nesses modelos, prefira output_config.format para saída estruturada garantida. Em claude-opus-5 ambos ainda funcionam normalmente. Existe também disable_parallel_tool_use para limitar a uma chamada por vez.

strict: true — garantia sintática, não semântica

strict: true na definição da ferramenta (e o output_config.format nativo) garantem que a saída adere exatamente ao schema: tipos corretos, campos obrigatórios presentes, nenhuma propriedade extra, JSON sempre parseável. Eliminam a classe inteira de erros de sintaxe.

O que eles não garantem é a semântica dos valores:

Erros semânticos exigem validação pós-extração e retry com feedback (Lição 5) e schemas bem projetados (abaixo).

Design de schema que evita fabricação

Prefill de assistente: padrão obsoleto (erro 400)

Um padrão antigo de forçar JSON era o prefill: terminar messages com uma mensagem assistant parcial (ex.: {"role": "assistant", "content": "{"}) para o modelo "continuar" de dentro do JSON. Nos modelos atuais esse padrão retorna erro 400 — a API rejeita a mensagem assistant parcial ao final. Ele foi substituído por output_config.format e por tool use com strict, que dão garantias reais de schema (o prefill nunca garantiu validade — só induzia o começo). Em questões de prova, prefill aparece como distrator de "solução legada".

Pegadinhas da prova

Resumo em 5 linhas

  1. output_config={"format": {"type": "json_schema", ...}} (ou messages.parse() com Pydantic) é o caminho atual para JSON garantido.
  2. Tool use com input_schema é o padrão clássico de extração: os dados saem no bloco tool_use.
  3. strict: true elimina erros de sintaxe/schema, mas não erros semânticos (somas erradas, campo trocado).
  4. tool_choice: auto pode devolver texto; any obriga alguma ferramenta; forçado obriga uma específica — any/forçado retornam 400 nos modelos topo de linha mais novos.
  5. Campos nullable, enum com "unclear" e "other"+detail evitam fabricação; prefill de assistente é obsoleto (400).

Documentação oficial

Questões de fixação

1. Um pipeline precisa de JSON 100% parseável de cada resposta, sem prosa em volta, usando claude-opus-5 sem ferramentas reais. Qual é a abordagem atual recomendada?

2. Com strict: true numa ferramenta de extração de faturas, a equipe ainda encontra registros em que os line_items não somam o total. Por quê?

3. Um sistema tem 3 ferramentas de extração (nota fiscal, recibo, contrato) e recebe documentos de tipo desconhecido em claude-opus-5. É obrigatório que toda resposta seja uma chamada de ferramenta, mas o modelo deve classificar o tipo. Qual configuração atende?

4. (escolha duas) Documentos de origem nem sempre contêm CNPJ e às vezes a categoria é ilegível. Quais decisões de schema evitam que o modelo fabrique valores?

5. Por que enums de categoria em schemas de extração costumam incluir "other" acompanhado de um campo detail em texto?