Skip to content
This page has been auto-translated and may contain errors.View in English

Saída estruturada

docs.scrimba.com

Primeiro, a forma do problema. Saída estruturada é a prática de fazer um modelo retornar dados que seu código pode ler diretamente, um campo em que você pode se ramificar em vez de um parágrafo que você tem que analisar manualmente. Este capítulo é sobre tornar isso confiável.

Até agora o modelo entrega texto, o que é bom quando uma pessoa o lê. Mas software não pode fazer muito com um parágrafo. Para agir sobre a resposta de um modelo em código, você precisa dela como dados: um campo que você pode ler, um valor que você pode ramificar, um registro que você pode salvar.

A mudança é pequena mas muda tudo a jusante. Uma vez que o modelo retorna { "sentiment": "negative" } em vez de uma frase sobre como o cliente parece insatisfeito, seu código pode rotear o ticket, atualizar um painel ou enviar um alerta. A parte interessante é como você faz o modelo produzir dados limpos quando tudo que ele faz é prever texto, e a resposta revela uma alavanca que você não sabia que tinha.

O modelo retorna texto. Seu código precisa de dados: um campo para ler, um valor para ramificar, um registro para armazenar. Este capítulo é a ponte entre os dois, e o movimento é parar de esperar que o modelo formate as coisas bem e começar a restringir o que ele pode emitir.

Há uma alavanca real aqui, não um truque de prompting. O mesmo loop de amostragem de como LLMs funcionam pode ser direcionado para que a saída seja forçada à forma que você quer, o que transforma um gerador de texto nebuloso em um componente no qual o resto do seu sistema pode confiar.

Um modelo que retorna prosa é um componente que você não pode compor. No momento em que sua saída alimenta outro sistema, você precisa de dados com uma forma conhecida, e você precisa que os modos de falha de obter esses dados sejam aqueles que você pode ver e tratar.

Este capítulo é sobre restringir a geração em um schema e o que isso custa em latência, o que não compra para você (valores corretos), e como falha nas extremidades. A maquinaria por baixo é a mesma que impulsiona uso de ferramentas: uma chamada de ferramenta é saída estruturada com uma assinatura de função no lugar do schema.

Peça por JSON

O primeiro instinto é pedir no prompt: "responda como JSON", o formato de texto simples que o software usa para passar dados, escrito como campos e valores entre chaves. Tente e geralmente funciona, que é a armadilha. O modelo ainda não está fazendo nada além de prever texto provável, e às vezes o texto provável é um code fence, ou um amigável "Claro, aqui vai!" antes dos dados, ou um comentário à direita depois. Qualquer um deles quebra o código que lê o JSON. Pedir gentilmente influencia a previsão em direção ao JSON; não força.

Então os provedores lhe dão uma alavanca real. Ativar modo JSON com response_format faz mais do que pedir: restringe o modelo para que a sintaxe retorne como JSON válido.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": 'Extraia a cidade e a temperatura. Responda como JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "Está em torno de 18 graus em Brasília agora."},
    ],
    response_format={"type": "json_object"},  # restringe a saída à sintaxe JSON válida
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "Brasília" 18

response_format={"type": "json_object"} faz a saída ser um JSON válido, então json.loads não vai engasgar em prosa estranha. Você ainda descreve os campos que quer no prompt, porque modo JSON promete JSON válido, não os campos particulares que você tinha em mente. Ele elimina "não é JSON" de forma alguma, não "JSON com a forma errada". Uma coisa que ainda não pode prometer: se a resposta for cortada no meio, você pode receber meio objeto JSON, então é sintaxe confiável, não uma garantia dura em todos os casos.

JunoPeça por JSON Pedir por JSON no prompt sozinho é instável, porque o modelo só prevê texto provável e pode envolvê-lo em fences ou conversas que quebram a análise. Definir response_format para modo JSON restringe a sintaxe para que retorne como JSON válido. Não promete que os campos correspondam ao que você pediu, e uma resposta cortada no meio ainda pode chegar como meio objeto, então descreva a forma no prompt e verifique o que chega.

O reflexo é escrever "responda como JSON" no prompt, onde JSON é o formato de dados de campos e valores que o software passa. Geralmente funciona, que é exatamente o problema: geralmente não é um contrato. O modelo prevê texto provável, e o texto provável às vezes inclui um code fence, um preâmbulo "Claro!", ou uma nota à direita, tudo que quebra json.loads.

Modo JSON é a primeira alavanca real. Definir response_format para um tipo de objeto JSON restringe a geração para que a sintaxe seja JSON válido, não prosa que parece ser.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": 'Extraia a cidade e a temperatura. Responda como JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "Está em torno de 18 graus em Brasília agora."},
    ],
    response_format={"type": "json_object"},  # restringe a saída à sintaxe JSON válida
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "Brasília" 18

A forma de response_format aqui é da OpenAI; outros provedores expõem a mesma ideia sob seu próprio campo, então importe o conceito, não a chave literal. Modo JSON restringe a sintaxe, nunca a forma do campo. Você descreve os campos no prompt, e o modelo é livre para renomeá-los, descartá-los ou aninhá-los diferentemente enquanto ainda emite JSON válido.

Há mais uma brecha: modo JSON promete JSON bem-formado apenas se a resposta terminar. Acerte seu teto max_tokens no meio-objeto e você fica com saída truncada e inanalisável, então a análise ainda precisa de uma guarda.

JunoPeça por JSON Modo JSON restringe a sintaxe para que o modelo pare de envolver dados em fences e conversas, mas nunca fixa os campos, então o modelo ainda pode renomeá-los ou descartá-los. A chave response_format é a forma da OpenAI; outros provedores soletram diferentemente. E a promessa só se mantém para uma resposta terminada, então uma resposta truncada por max_tokens chega inanalisável. Guarde a análise de qualquer forma.

"Responda como JSON" no prompt é uma sugestão, e sugestões falham sob carga: um code fence aqui, um preâmbulo ali, um comentário à direita quando o modelo se sente conversador, cada um uma exceção de json.loads em produção. Modo JSON (aqui response_format={"type": "json_object"}) substitui a sugestão por uma restrição na sintaxe gerada, então os bytes que retornam são analisados como JSON.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": 'Extraia a cidade e a temperatura. Responda como JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "Está em torno de 18 graus em Brasília agora."},
    ],
    response_format={"type": "json_object"},  # restringe a saída à sintaxe JSON válida
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "Brasília" 18

Seja preciso sobre o que isso restringe. Modo JSON fixa sintaxe, não schema, e apenas em uma resposta completa. O modelo ainda escolhe nomes de campos, pode omitir um campo obrigatório, e pode aninhar como quiser.

A garantia é condicional na geração terminar: se o modelo encontra max_tokens no meio-objeto, você fica com uma string truncada que não é JSON válido, o que significa que o modo de falha único que modo JSON não remove é aquele que seu caminho de retry mais precisa tratar. A chave response_format mostrada é da OpenAI; o equivalente em outros provedores vive sob um nome diferente, então trate o conceito como portável e o campo literal como não. Para garantias de forma você precisa de um schema, que é a próxima seção.

JunoPeça por JSON Modo JSON restringe a sintaxe em uma resposta completa, nada mais: o modelo ainda nomeia, omite e aninha campos como quiser, e uma geração morta por max_tokens lhe entrega saída truncada e inanalisável. Esse caso de truncamento é aquele que seu caminho de erro tem que possuir, porque o modo não vai. A forma response_format é da OpenAI, então importe a ideia, não a chave. Para forma, você quer um schema.

Bloqueie a forma com um schema

Modo JSON mantém a sintaxe válida, mas o modelo ainda pode renomear um campo, descartar um, ou aninhar coisas diferentemente. Para remover essa margem de manobra, dê-lhe um schema: uma descrição exata dos campos e tipos que a saída deve ter, um formulário que o modelo é obrigado a preencher. Com strict: true, a saída é então garantida corresponder a essa forma.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Extraia os detalhes de contato da mensagem."},
        {"role": "user", "content": "Oi, sou Marina Silva, me contacte em marina@example.com."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # força o schema exatamente
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "Marina Silva", "email": "marina@example.com"}

Como um schema pode ser uma garantia dura quando o modelo só prevê tokens? Porque o provedor restringe quais tokens ele pode prever. A cada passo o modelo ainda classifica todos os possíveis próximos tokens, mas o sistema remove aqueles que quebrariam seu schema, e o modelo escolhe do que ficou.

Se o schema diz que o próximo campo deve ser email, tokens que iniciariam qualquer outro campo são tirados da mesa antes do modelo escolher. Os tokens fora da forma são removidos, então o modelo não pode se desviar da forma.

Há um efeito mais suave que vale a pena saber. Os nomes de campo que você escolhe se tornam parte do que o modelo lê ao prever o valor, então um nome claro guia uma melhor resposta. Solicitado a preencher tempC, o modelo se inclina para um número em Celsius; solicitado a preencher value, tem muito menos a considerar. Nomear campos claramente é parte instrução, parte schema.

JunoBloqueie a forma com um schema Um schema é o formulário que o modelo tem que preencher, e com strict: true ele fica com os campos e tipos exatos, porque o sistema remove qualquer token que quebraria a forma antes do modelo escolher. Essa é aplicação, não um pedido educado. Nomeie seus campos claramente também, já que o nome do campo é contexto que o modelo lê ao prever o valor. Levou-me um tempo para confiar que um nome como tempC faz trabalho real.

Modo JSON fixa a sintaxe mas deixa o modelo livre para renomear, descartar ou re-aninhar campos. Um schema fecha essa lacuna: uma descrição exata de campos e tipos, e com strict: true a saída é garantida corresponder a ele.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Extraia os detalhes de contato da mensagem."},
        {"role": "user", "content": "Oi, sou Marina Silva, me contacte em marina@example.com."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # força o schema exatamente
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "Marina Silva", "email": "marina@example.com"}

O mecanismo é decodificação restrita: poda de token aplicada ao passo de amostragem. A cada posição o modelo produz sua classificação usual sobre todos os tokens, então o sistema mascara qualquer token que faria a saída parar de corresponder ao schema, e o modelo amostra dos sobreviventes. Se a gramática diz que o próximo token deve abrir o campo email, tokens que iniciariam qualquer outra coisa são zerados antes da amostragem. O modelo não pode se desviar da forma porque os movimentos fora da forma são removidos do tabuleiro, não desencorajados no prompt.

Dois manuseios práticos. Nomes de campo são instrução, então nomeie-os para o valor que quer. tempC direciona o modelo para Celsius onde value o deixa adivinhando, e uma descrição em um campo direciona ainda mais. E a mesma restrição força um enum (uma lista fixa de valores permitidos) ou um intervalo de número, que é o que torna a classificação confiável: na posição do rótulo, apenas seus rótulos listados sobrevivem à máscara, então o modelo não pode cunhar uma categoria que não está na lista.

JunoBloqueie a forma com um schema Com strict: true, decodificação restrita mascara todo token que quebraria o schema antes do modelo amostrar, então a forma é forçada, não solicitada. Dependa disso: nomes de campo e descrições são instruções, então nomeie tempC não value e o modelo preenche melhor. A mesma máscara força um enum, que é por que a classificação funciona, o modelo não pode inventar um rótulo que não está na sua lista.

Modo JSON lhe dá bytes analisáveis; um schema lhe dá uma forma conhecida. Com strict: true a saída é garantida corresponder ao conjunto de campos e tipos que você declara, que é a diferença entre dados que você pode indexar cegamente e dados que você tem que provar defensivamente.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Extraia os detalhes de contato da mensagem."},
        {"role": "user", "content": "Oi, sou Marina Silva, me contacte em marina@example.com."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)

O mecanismo é decodificação restrita, e você quer entender a partir do zero porque seus custos não são grátis. O modelo emite uma probabilidade para todo token em seu vocabulário a cada passo. Decodificação restrita compila seu schema em uma gramática (um conjunto de regras para quais sequências de tokens são legais), então a cada passo constrói uma máscara que define a probabilidade de todo token ilegal para zero, então a amostragem pode apenas pousar em um token que mantém a saída válida até agora. A forma é forçada por construção, não por pedir.

Aquela compilação não é grátis, e se mostra como latência. A primeira chamada contra um novo schema paga um custo único para construir a gramática, então latência de cold-start em um schema novo é maior que estado estável. Mantenha seus schemas estáveis e reutilizados em vez de gerados por requisição, ou você paga aquela compilação repetidamente.

additionalProperties: false e uma lista completa required não são decoração: são o que deixam o sistema rejeitar campos extras ou faltantes imediatamente, e em provedores que suportam recusa orientada por schema, eles também são o que dão ao modelo um modo limpo de retornar uma recusa estruturada em vez de um objeto malformado. Largue-os e você amplia o que conta como válido, que é o oposto de por que você alcançou modo strict.

Algumas realidades de produção. Suporte estrito é irregular: alguns provedores forçam uma gramática real, outros a aproximam ou suportam apenas um subconjunto de JSON Schema (não pattern, aninhamento limitado), então teste o que seu provedor realmente honra em vez de assumir o spec. Saída parcial e streaming é o caso desconfortável: enquanto tokens fazem stream, você mantém um objeto sintaticamente incompleto, então ou buffer até conclusão antes de analisar ou use um analisador incremental que tolera uma meia-estrutura construída. E isso é a mesma maquinaria que uso de ferramentas: um schema de argumentos de uma função é restrito de forma idêntica, então tudo aqui se transfere diretamente para tornar chamadas de ferramentas confiáveis.

JunoBloqueie a forma com um schema Decodificação restrita compila seu schema em uma gramática e zera todo token ilegal antes da amostragem, então a forma é forçada por construção. Aquela compilação custa latência na primeira chamada contra um novo schema, então reutilize schemas em vez de cunhar um por requisição. Mantenha additionalProperties: false e uma lista required completa, eles são o que força rejeição e, em alguns provedores, uma recusa estruturada limpa. Suporte estrito varia, streaming lhe entrega meio um objeto, e a mesma máscara impulsiona args de chamada de ferramenta, então o que você aprende aqui compensa em dobro.

Valide mesmo assim

Um schema restringe a forma, mas trate a saída como dados do mundo externo de qualquer forma. A forma pode ser válida enquanto o conteúdo está errado: um email extraído que é na verdade um typo, um número que o modelo adivinhou, um campo deixado em branco porque a entrada não continha. E a chamada ainda pode falhar de formas comuns, como ser cortada por max_tokens e chegar como JSON truncado. Analise defensivamente: uma forma válida não é um valor correto.

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
        if not data.get("name") or not data.get("email"):
            return None  # presente mas vazio
        return data
    except json.JSONDecodeError:
        return None  # não é JSON válido (por exemplo, uma resposta truncada)

contact = parse_contact(response.choices[0].message.content)
if not contact:
    pass  # trate a falha: tente novamente, pergunte novamente, ou mostre um erro amigável

Esta é a lição de alucinação de como LLMs funcionam em uma nova roupagem: a forma estar certa não torna os valores corretos. Um schema garante que você fica com um campo name; não garante que o nome está correto. Um try/except ao redor da análise mais uma verificação dos valores é um seguro barato contra a resposta que é bem-formada e ainda errada.

JunoValide mesmo assim Um schema garante a forma, nunca a verdade dos valores, então um registro válido pode ainda manter um valor errado ou vazio, e uma chamada pode ainda chegar truncada. Envolva a análise em try/except e verifique os valores antes de confiar neles. Decida de antemão o que acontece quando a validação falha, uma tentativa novamente ou um erro amigável, então não é uma luta depois.

Modo strict garante forma, não conteúdo. A saída pode corresponder ao schema e ainda estar errada: um email aparentemente plausível que é um typo, um número inventado para preencher um campo obrigatório, uma string vazia onde a entrada continha nada. E a análise pode falhar imediatamente quando uma resposta trunca em max_tokens. Então você valida, toda vez.

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "unparseable"  # truncado ou malformado

    if not data.get("name") or not data.get("email"):
        return None, "empty_field"  # presente mas em branco
    if "@" not in data["email"]:
        return None, "bad_email"  # forma ok, valor não
    return data, None

contact, error = parse_contact(response.choices[0].message.content)
if error == "unparseable":
    pass  # tente novamente uma vez, provável truncamento: aumente max_tokens ou encurte a entrada
elif error:
    pass  # registre a falha em nível de valor e volte

O padrão é uma análise defensiva que distingue modos de falha, porque eles querem tratamento diferente. Um JSONDecodeError geralmente significa truncamento, então a correção é aumentar max_tokens ou reduzir a entrada e tentar novamente. Um valor vazio ou fora do intervalo é uma falha de conteúdo, não uma de formato, então tentar novamente a mesma chamada raramente ajuda; registre e volte.

Separe "não pude analisar" de "analisado mas errado", porque a correção difere. Isso conecta de volta a alucinação: decodificação restrita removeu o modo de falha de saída malformada e deixou o modo valor-errado completamente intacto.

JunoValide mesmo assim Modo strict fixa forma, não conteúdo, então valide toda vez: um registro pode ser bem-formado e manter um typo, um palpite, ou um em branco. Divida as falhas, porque um `JSONDecodeError` geralmente significa truncamento (aumente `max_tokens` ou reduza a entrada e tente novamente) enquanto um valor ruim é uma falha de conteúdo que tentar novamente a mesma chamada não vai consertar. Decodificação restrita deletou o problema de saída malformada e deixou valor-errado intacto.

Decodificação restrita fecha o modo de falha de saída malformada e fecha nada mais. A forma é garantida; o conteúdo não. Um campo obrigatório que a entrada nunca suportou fica preenchido com um palpite confiante, um número pousa fora de qualquer intervalo sensato, um email é analisado como string mas é um typo.

A garantia de formato em si lapsa em truncamento. Então validação não é higiene opcional, é a camada que pega o que o schema estruturalmente não pode.

python
import json

def parse_contact(raw, retry):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "truncated"  # geração incompleta, não um problema de conteúdo

    if not data.get("name") or "@" not in data.get("email", ""):
        return None, "invalid_value"  # forma manteve, valor não
    return data, None

contact, failure = parse_contact(raw, retry=False)
if failure == "truncated":
    pass  # aumente max_tokens ou corte a entrada, depois tente novamente a chamada
elif failure == "invalid_value":
    pass  # caminho de reparo: re-pergunte com a saída ruim alimentada de volta, ou rota para fallback

Três coisas separam um validador que se sustenta de um try/except que engole tudo:

  • Classifique a falha: truncamento é um problema de orçamento consertado por uma tentativa novamente com mais max_tokens, enquanto um valor inválido é um problema de conteúdo que uma tentativa novamente cega vai reproduzir.
  • Construa um caminho de reparo para o caso em nível de valor em vez de apenas um fallback. Alimentar a saída inválida de volta com uma instrução para consertá-la geralmente recupera-a em uma chamada extra, que é mais barata que falhar na requisição, mas limite as tentativas para que uma entrada persistentemente errada não possa fazer loop.
  • Delimite o custo: toda tentativa novamente é outra chamada cobrada e outro round-trip de latência, então um orçamento de tentativa é parte do design, não uma reflexão tardia.

Valide os valores, não apenas a análise. Esta é a história de contenção de alucinação aplicada aos dados estruturados: decodificação restrita lhe dá um envelope limpo e lhe diz nada sobre o que está dentro, então o envelope é exatamente onde as equipes param de validar e se queimam. Validação de schema, verificações de intervalo e semânticas, e um loop de reparo limitado são as três camadas, e elas pegam coisas diferentes: o schema pega forma, as verificações pegam valores sem sentido, o loop de reparo recupera os salváveis.

JunoValide mesmo assim Modo strict deleta saída malformada e deixa valores errados totalmente intactos, então valide conteúdo, não apenas a análise. Classifique a falha: truncamento é uma correção de orçamento (mais max_tokens, tente novamente), um valor inválido é uma falha de conteúdo que uma tentativa novamente cega apenas reproduz, então construa um caminho de reparo que alimenta a saída ruim de volta, e limite as tentativas para que uma entrada ruim não possa fazer loop sua conta. O envelope limpo é exatamente onde as pessoas param de verificar e se queimam.

Dois padrões: classificar e extrair

A maioria do trabalho de saída estruturada é uma de duas formas.

Classificação ordena uma entrada em um de um conjunto fixo de rótulos. Um enum no schema restringe a saída exatamente àqueles rótulos, usando o mesmo truque de poda de token: na posição do rótulo, apenas os valores permitidos podem ser previstos, então o modelo não pode inventar uma categoria que não está em sua lista.

python
# fragmento de schema para classificação
{"type": "string", "enum": ["billing", "technical", "general"]}

Extração puxa campos específicos de texto livre, como no exemplo de contato anterior: um nome, uma data, um montante, uma lista de nomes de produtos. Juntos, classificar e extrair cobrem uma grande parte do trabalho real de IA: rotear tickets de suporte, marcar conteúdo, transformar emails em registros, ler recibos. Ambos transformam um gerador de texto nebuloso em um componente confiável no qual seu código pode construir.

JunoDois padrões: classificar e extrair Classificação ordena uma entrada em um de um conjunto fixo de rótulos, bloqueado com um enum para que o modelo não possa inventar uma categoria fora de sua lista. Extração puxa campos nomeados de texto livre em um registro. Ambos transformam um gerador de texto nebuloso em um componente confiável, e entre os dois cobrem uma grande parte dos recursos práticos de IA.

Quase tudo que você constrói com saída estruturada é uma de duas formas, e nomeá-las ajuda você a alcançar o schema certo.

Classificação mapeia uma entrada para um rótulo de um conjunto fixo. O enum carrega o trabalho inteiro: decodificação restrita mascara todo token exceto seus rótulos listados na posição do rótulo, então o modelo retorna um deles ou nada mais.

python
# classificação: um rótulo de um conjunto fechado
{"type": "string", "enum": ["billing", "technical", "general"]}

Extração puxa campos nomeados de texto livre em um registro, os exemplos de contato e ticket. Os dois se compõem: um único schema pode classificar e extrair ao mesmo tempo, que é a maioria dos recursos reais. Conjuntos fechados ficam com um enum; valores abertos ficam com um campo tipado.

A decisão de tornar consciente: quando um valor realmente é um conjunto fixo (status, categoria, prioridade), use um enum para que o modelo não possa desviar, e quando é aberto (um nome, um resumo), use um campo tipado simples e aceite que você está confiando no valor o suficiente para validá-lo. Misturar eles, um enum onde o mundo real tem mais casos, ou um campo livre onde um conjunto fechado teria pegado erros, é onde esses recursos silenciosamente se comportam mal.

JunoDois padrões: classificar e extrair Duas formas cobrem a maioria disso: classificação mapeia para um rótulo de um conjunto fechado (o enum faz a aplicação) e extração puxa campos nomeados em um registro, e um schema pode fazer os dois ao mesmo tempo. Escolha deliberadamente: enum para conjuntos fixos para que o modelo não possa desviar, campo tipado simples para valores abertos que você vai validar. Um enum que está faltando um caso do mundo real é uma fonte silenciosa de rótulos errados.

Saída estruturada colapsa para dois padrões, e a distinção é uma alavanca de design, não apenas vocabulário. Classificação mapeia entrada para um rótulo de um conjunto fechado, forçado por um enum que decodificação restrita reduz a uma escolha dura entre seus valores. Extração puxa campos tipados de texto livre. A maioria dos schemas de produção os combinam.

python
# classificação: decodificação restrita permite apenas esses tokens na posição do rótulo
{"type": "string", "enum": ["billing", "technical", "general"]}

A alavanca é onde você desenha a fronteira do conjunto fechado, porque o enum é uma garantia de correção com uma borda afiada. Um valor que é realmente limitado (um status, uma prioridade) pertence em um enum para que o modelo não possa emitir nada fora da lista, e isso transforma uma geração aberta em uma verificação na qual seu código a jusante pode confiar.

Mas o enum força uma escolha mesmo quando nenhuma se encaixa: se a entrada real não corresponder a qualquer rótulo, o modelo é restrito a escolher o mais próximo errado em vez de dizer que não se encaixa. Um enum sem escapa-hatch transforma "não se encaixa" em um rótulo errado confiante. Então para qualquer classificador enfrentando entrada real bagunçada, adicione um escapa-hatch explícito, um membro other ou unknown, e leia-o como o sinal de rotear para outro lugar.

A mesma cautela se aplica a confiança: se uma decisão depende do rótulo, tenha o modelo também emitir um campo de confiança ou rationale curta, para que uma chamada limítrofe seja visível em vez de lavada em um valor de enum limpo. Esses são os mesmos instintos que uso de ferramentas, onde o modelo escolhe uma ferramenta de um conjunto fechado e você enfrenta o problema idêntico de "e se nenhuma se encaixa".

JunoDois padrões: classificar e extrair Classificação é um conjunto fechado forçado por um enum, extração é campos tipados de texto livre, e o enum é uma garantia com uma borda afiada: força uma escolha mesmo quando nada se encaixa, lavando "não corresponde" em um rótulo errado confiante. Adicione um membro other ou unknown e rota nele, e emita um campo de confiança quando uma decisão depende do rótulo. Mesma forma que escolher uma ferramenta de um conjunto fechado em uso de ferramentas.

Na prática

Transformando um email de suporte bagunçado em um ticket estruturado, combinando classificação e extração em um schema:

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Transforme o email de suporte em um ticket."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado em dobro pela assinatura do mês passado."}

Uma chamada classifica o email de duas formas e extrai um resumo, retornando um registro que seu código pode rotear e armazenar, com a forma garantida e os valores ainda valendo a pena verificar. Até agora tudo flui texto dentro e texto para fora. Depois você dá ao modelo um senso completamente diferente: em Embeddings, texto vira números que você pode comparar por significado, a fundação para busca e para trabalhar com seus próprios documentos.

JunoNa prática Uma chamada com um schema strict pode classificar um email de duas formas e extrair um resumo ao mesmo tempo, retornando um registro que seu código pode rotear e armazenar. A forma é garantida; os valores ainda valem a pena verificar. Uso de ferramentas depende deste mesmo truque depois: os argumentos que um modelo envia para uma ferramenta são saída estruturada também.

Um recurso real geralmente combina ambos os padrões em um schema. Aqui um email de suporte vira um ticket: duas classificações e uma extração em uma única chamada.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Transforme o email de suporte em um ticket."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado em dobro pela assinatura do mês passado."}

Os dois enums são restringidos com dureza, então category e urgency são sempre rotuláveis. O summary é um campo aberto, então é o para validar antes de confiar nele. Aquela divisão, rótulos forçados mais um campo de texto livre verificado, é a forma da maioria dos recursos de extração. Depois, Embeddings dá ao modelo um senso diferente: texto transformado em números que você pode comparar por significado, a fundação para busca e para trabalhar com seus próprios documentos.

JunoNa prática Um schema strict pode classificar de duas formas e extrair um resumo em uma única chamada, retornando um registro que você pode rotear e armazenar. Os enums são forçados então os rótulos são seguros para agir; o summary aberto é o campo para validar. Aquela mistura de rótulos bloqueados e texto livre verificado é a forma cotidiana de um recurso de extração.

A forma de produção cotidiana é uma composição: classificar em alguns eixos e extrair um campo de texto livre, tudo em um schema e uma chamada.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Transforme o email de suporte em um ticket."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado em dobro pela assinatura do mês passado."}

A aplicação é irregular entre campos, e esse é o ponto de design. Os enums são garantias duras, então category e urgency são seguros para rotear sem verificações adicionais. O summary é uma string irrestrita, então é exatamente onde um valor alucinado ou desfocado pode se esconder atrás de um schema limpo, e o campo que ganha uma passagem de validação. Dois rótulos forçados com um campo summary aberto é também o schema mais exposto a truncamento: um resumo longo gerado é o que coloca você em max_tokens e transforma o objeto inteiro inanalisável, então dimensione o orçamento para o campo de texto livre, não os rótulos.

Esta é a mesma maquinaria que uso de ferramentas: uma chamada de ferramenta é um modelo emitindo argumentos estruturados contra um schema de função, restrito de forma idêntica, então a validação, reparo, e instintos de escapa-hatch de enum aqui se transferem completamente para tornar agentes confiáveis. Daqui, Embeddings muda o modelo para um modo diferente: texto como vetores que você compara por significado, a base para busca e para fundamentar respostas em seus próprios documentos.

JunoNa prática Um schema strict, dois enums forçados e um summary aberto: os rótulos são seguros para rotear, o campo de texto livre é onde um valor alucinado se esconde e onde uma geração longa bate `max_tokens` em truncamento, então orce e valide especificamente. Esta é a mesma maquinaria que uso de ferramentas, args restrito contra um schema de função, então todo hábito aqui, validação, reparo, escapa-hatches de enum, carrega direto para agentes.