Curso
Em agosto de 2024, a OpenAI anunciou um novo recurso poderoso na sua API — Structured Outputs. Com esse recurso, como o nome sugere, você garante que os LLMs gerem respostas somente no formato que você especificar. Essa capacidade facilita muito a criação de aplicações que exigem formatação de dados precisa.
Neste tutorial, você vai aprender como começar com Structured Outputs da OpenAI, entender sua nova sintaxe e explorar suas principais aplicações.
Desenvolver aplicativos de IA
Importância de Structured Outputs em aplicações de IA
Respostas determinísticas — ou seja, com formatação consistente — são cruciais para tarefas como entrada de dados, recuperação de informações, perguntas e respostas, fluxos de trabalho em múltiplas etapas e por aí vai. Você provavelmente já viu LLMs gerarem saídas em formatos bem diferentes, mesmo com o mesmo prompt.
Por exemplo, considere esta função simples classify_sentiment alimentada pelo GPT-4o:
# List of hotel reviews
reviews = [
"The room was clean and the staff was friendly.",
"The location was terrible and the service was slow.",
"The food was amazing but the room was too small.",
]
# Classify sentiment for each review and print the results
for review in reviews:
sentiment = classify_sentiment(review)
print(f"Review: {review}\nSentiment: {sentiment}\n")
Saída:
Review: The room was clean and the staff was friendly.
Sentiment: Positive
Review: The location was terrible and the service was slow.
Sentiment: Negative
Review: The food was amazing but the room was too small.
Sentiment: The sentiment of the review is neutral.
Embora as duas primeiras respostas estejam no mesmo formato de uma única palavra, a última é uma frase inteira. Se alguma outra aplicação a jusante dependesse da saída do código acima, ela quebraria por esperar uma resposta de uma única palavra.
Dá para corrigir isso com um pouco de prompt engineering, mas é um processo demorado e iterativo. Mesmo com um prompt muito bem feito, não há 100% de garantia de que as respostas manterão o formato esperado em chamadas futuras. A menos, claro, que a gente use Structured Outputs:
def classify_sentiment_with_structured_outputs(review):
"""Sentiment classifier with Structured Outputs"""
...
# Classify sentiment for each review with Structured Outputs
for review in reviews:
sentiment = classify_sentiment_with_structured_outputs(review)
print(f"Review: {review}\nSentiment: {sentiment}\n")
Saída:
Review: The room was clean and the staff was friendly.
Sentiment: {"sentiment":"positive"}
Review: The location was terrible and the service was slow.
Sentiment: {"sentiment":"negative"}
Review: The food was amazing but the room was too small.
Sentiment: {"sentiment":"neutral"}
Com a nova função classify_sentiment_with_structured_outputs, as respostas ficam todas no mesmo formato.
Essa capacidade de obrigar modelos de linguagem a seguir um formato rígido é relevante — e economiza muitas horas de prompt engineering ou a dependência de outras ferramentas open source.
Começando com Structured Outputs da OpenAI
Nesta seção, vamos detalhar Structured Outputs usando o exemplo do analisador de sentimento.
Configurando o ambiente
Pré-requisitos
Antes de começar, garanta que você tenha:
- Python 3.7 ou superior instalado.
- Uma chave da OpenAI API. Você pode obtê-la cadastrando-se no site da OpenAI.
Configurando a OpenAI API
1. Instale o pacote OpenAI para Python: Abra o terminal e rode o comando abaixo para instalar ou atualizar o pacote para a versão mais recente:
$ pip install -U openai
2. Defina sua chave de API: Você pode configurar a chave como variável de ambiente ou diretamente no código. Para defini-la como variável de ambiente, rode:
$ export OPENAI_API_KEY='your-api-key'
3. Verifique a instalação: Crie um script Python simples para validar a instalação:
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello!"}
],
max_tokens=5
)
>>> print(response.choices[0].message.content.strip())
Hello! How can I
Execute o script para checar se está tudo certo. Você verá a resposta do modelo impressa no terminal.
Além do pacote OpenAI, você vai precisar da biblioteca Pydantic para definir e validar esquemas JSON para Structured Outputs. Instale com pip:
$ pip install pydantic
Com esses passos, seu ambiente está pronto para usar o recurso Structured Outputs da OpenAI.
Definindo um esquema de saída com Pydantic
Para usar Structured Outputs, você precisa definir a estrutura esperada da saída com modelos Pydantic. O Pydantic é uma biblioteca de validação de dados e gerenciamento de configurações em Python que permite definir modelos usando anotações de tipo. Esses modelos fazem o modelo da OpenAI respeitar a estrutura especificada nas respostas.
Veja um exemplo de modelo Pydantic para especificar o formato do nosso classificador de sentimento de avaliações:
from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
Neste exemplo:
SentimentResponseé um modelo Pydantic que define a estrutura esperada da saída.- O modelo tem um único campo
sentiment, que só pode assumir um dos três valores literais: "positive", "negative" ou "neutral".
Ao enviar esse modelo como parte das requisições à OpenAI, as saídas serão apenas uma das palavras que definimos.
Vamos ver como.
Usando o helper parse
Para impor nosso esquema Pydantic nas requisições à OpenAI, basta passá-lo ao parâmetro response_format da API de chat completions. Em linhas gerais, fica assim:
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[...],
response_format=SentimentResponse
)
Note que, em vez de usar client.chat.completions.create, usamos o método client.beta.chat.completions.parse. O .parse() é um método novo da Chat Completions API, criado especificamente para Structured Outputs.
Agora, vamos juntar tudo reescrevendo o classificador de sentimento com Structured Outputs. Primeiro, fazemos os imports necessários, definimos o modelo Pydantic, o prompt de sistema e um template de prompt:
from openai import OpenAI
from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
client = OpenAI()
MODEL = "gpt-4o-mini"
SYSTEM_PROMPT = "You are a sentiment classifier assistant."
PROMPT_TEMPLATE = """
Classify the sentiment of the following hotel review as positive, negative, or neutral:\n\n{review}
"""
Em seguida, escrevemos uma nova função usando o helper .parse():
# Function to classify sentiment using OpenAI's chat completions API with structured outputs
def classify_sentiment_with_structured_outputs(review):
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": PROMPT_TEMPLATE.format(review=review)},
],
response_format=SentimentResponse
)
return response.choices[0].message
A linha importante na função é response_format=SentimentResponse, que é o que de fato habilita Structured Outputs.
Vamos testar em uma das avaliações:
# List of hotel reviews
reviews = [
"The room was clean and the staff was friendly.",
"The location was terrible and the service was slow.",
"The food was amazing but the room was too small.",
]
result = classify_sentiment_with_structured_outputs(reviews[0])
>>> print(result.content)
{"sentiment":"positive"}
Aqui, result é um objeto de mensagem:
>>> type(result)
openai.types.chat.parsed_chat_completion.ParsedChatCompletionMessage[SentimentResponse]
Além do atributo .content, que recupera a resposta, ele tem o atributo .parsed, que retorna as informações analisadas como uma classe:
>>> result.parsed
SentimentResponse(sentiment='positive')
Como você pode ver, temos uma instância da classe SentimentResponse. Isso significa que podemos acessar o sentimento como string, em vez de dicionário, usando o atributo .sentiment:
>>> result.parsed.sentiment
'positive'
Aninhando modelos Pydantic para definir esquemas complexos
Em alguns casos, você pode precisar de estruturas de saída mais complexas, com dados aninhados. O Pydantic permite aninhar modelos, possibilitando criar esquemas detalhados para vários cenários. Isso é especialmente útil com dados hierárquicos ou quando você precisa impor uma estrutura específica a saídas complexas.
Vamos considerar um exemplo em que precisamos extrair informações detalhadas do usuário, incluindo nome, dados de contato e uma lista de endereços. Cada endereço deve incluir rua, cidade, estado e CEP. Isso requer mais de um modelo Pydantic para montar o esquema correto.
Passo 1: definir os modelos Pydantic
Primeiro, definimos os modelos Pydantic para endereço e informações do usuário:
from pydantic import BaseModel
from typing import List
# Define the Pydantic model for an address
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
# Define the Pydantic model for user information
class UserInfo(BaseModel):
name: str
email: str
phone: str
addresses: List[Address]
Neste exemplo:
Addressé um modelo Pydantic que define a estrutura de um endereço.UserInfoé um modelo Pydantic que inclui uma lista de objetosAddress, além de campos para nome, e-mail e telefone.
Passo 2: usar os modelos aninhados nas chamadas à API
Depois, usamos esses modelos Pydantic aninhados para impor a estrutura de saída em uma chamada à OpenAI API:
SYSTEM_PROMPT = "You are a user information extraction assistant."
PROMPT_TEMPLATE = """ Extract the user information from the following text:\n\n{text}"""
# Function to extract user information using OpenAI's chat completions API with structured outputs
def extract_user_info(text):
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": PROMPT_TEMPLATE.format(text=text)},
],
response_format=UserInfo
)
return response.choices[0].message
# Example text containing user information
text = """John DoeEmail: john.doe@example.comPhone: 123-456-7890Addresses:- 123 Main St, Springfield, IL, 62701- 456 Elm St, Shelbyville, IL, 62702"""
# Extract user information and print the results
user_info = extract_user_info(text)
O texto de exemplo está totalmente sem espaçamento e difícil de ler. Vamos ver se o modelo se sai bem. Vamos usar a biblioteca json para "embelezar" a resposta:
import json
data = json.loads(user_info.content)
pretty_response = json.dumps(data, indent=2)
print(pretty_response)
{
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "123-456-7890",
"addresses": [
{
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip_code": "62701"
},
{
"street": "456 Elm St",
"city": "Shelbyville",
"state": "IL",
"zip_code": "62702"
}
]
}
Como você vê, o modelo capturou corretamente as informações de um único usuário e dois endereços, conforme o esquema que fornecemos.
Em resumo, ao aninhar modelos Pydantic, você define esquemas complexos que lidam com dados hierárquicos e impõem estruturas específicas para saídas elaboradas.
Function calling com Structured Outputs
Um dos recursos mais difundidos dos modelos de linguagem mais recentes é o function calling (também chamado de tool calling). Essa capacidade permite conectar modelos de linguagem a funções definidas pelo usuário, dando a eles acesso ao mundo externo.
Alguns exemplos comuns:
- Buscar dados em tempo real (por exemplo, clima, preços de ações, placares esportivos)
- Fazer cálculos ou análise de dados
- Consultar bancos de dados ou APIs
- Gerar imagens ou outras mídias
- Traduzir textos entre idiomas
- Controlar dispositivos de casa inteligente ou sistemas IoT
- Executar lógica de negócios ou fluxos de trabalho personalizados
Não vamos entrar em detalhes sobre como o function calling funciona aqui, mas você pode ler nosso tutorial de OpenAI Function Calling.
O que importa saber é que, com Structured Outputs, usar function calling com modelos da OpenAI fica muito mais simples. Antes, as funções passadas aos modelos da OpenAI exigiam escrever esquemas JSON complexos, detalhando cada parâmetro com dicas de tipo. Aqui vai um exemplo:
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
}
Embora a função get_current_weather tenha só dois parâmetros, o schema JSON fica enorme e sujeito a erro se escrito manualmente.
Com Structured Outputs, isso é resolvido usando novamente modelos Pydantic:
from pydantic import BaseModel
from typing import Literal
def get_weather(location: str, unit: str, condition: str):
# Implementation details...
pass
class WeatherData(BaseModel):
location: str
unit: Literal["celsius", "fahrenheit"]
condition: Literal["sunny", "cloudy", "rainy", "snowy"]
Primeiro, você escreve a função e sua lógica. Depois, a define novamente com um modelo Pydantic especificando os parâmetros de entrada esperados.
Em seguida, para converter o modelo Pydantic em um schema JSON compatível, chame pydantic_function_tool:
>>> import openai
>>> openai.pydantic_function_tool(WeatherData)
{'type': 'function',
'function': {'name': 'WeatherData',
'strict': True,
'parameters': {'properties': {'location': {'title': 'Location',
'type': 'string'},
'unit': {'enum': ['celsius', 'fahrenheit'],
'title': 'Unit',
'type': 'string'},
'condition': {'enum': ['sunny', 'cloudy', 'rainy', 'snowy'],
'title': 'Condition',
'type': 'string'}},
'required': ['location', 'unit', 'condition'],
'title': 'WeatherData',
'type': 'object',
'additionalProperties': False}}}
Veja como usar essa ferramenta em uma requisição:
import openai
client = OpenAI()
tools = [openai.pydantic_function_tool(WeatherData)]
messages = [
{
"role": "system",
"content": "You are a helpful customer support assistant. Use the supplied tools to assist the user.",
},
{
"role": "user",
"content": "What is the weather in Tokyo?",
}
]
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=tools
)
tool_call = response.choices[0].message.tool_calls[0]
>>> tool_call
ChatCompletionMessageToolCall(id='call_QnZZ0DmNN2cxw3bN433JQNIC', function=Function(arguments='{"location":"Tokyo","unit":"celsius","condition":"sunny"}', name='WeatherData'), type='function')
Passamos o modelo Pydantic, em formato JSON compatível, no parâmetro tools da Chat Completions API. Depois, dependendo da consulta, o modelo decide chamar (ou não) a ferramenta.
Como nossa pergunta acima é “What is the weather in Tokyo?”, vemos uma chamada em tool_calls do objeto de mensagem retornado.
Lembre-se: o modelo não chama a função get_weather; ele gera os argumentos para ela com base no esquema Pydantic que fornecemos:
arguments = json.loads(tool_call.function.arguments)
>>> arguments
{'location': 'Tokyo', 'unit': 'celsius', 'condition': 'sunny'}
Cabe a nós chamar a função com os argumentos fornecidos:
some_result = get_weather(**arguments)
Se você quer que o modelo gere os argumentos e chame a função automaticamente, você está procurando um agente de IA.
Temos um tutorial de LangChain Agents separado, caso tenha interesse.
Boas práticas ao usar Structured Outputs da OpenAI
Ao usar Structured Outputs, vale seguir algumas recomendações importantes. Nesta seção, destacamos algumas delas.
- Use modelos Pydantic para definir esquemas de saída — é uma forma limpa e com tipagem segura de definir a estrutura esperada.
- Mantenha os esquemas simples e específicos para obter resultados mais precisos.
- Use tipos de dados apropriados (
str,int,float,bool,List,Dict) para representar seus dados com fidelidade. - Use tipos
Literalpara enums e defina valores permitidos específicos para os campos. - Trate recusas do modelo. Ao usar o novo método
.parse(), os objetos de mensagem têm um novo atributo.refusalque indica recusa:
text = """John DoeEmail: john.doe@example.comPhone: 123-456-7890Addresses:- 123 Main St, Springfield, IL, 62701- 456 Elm St, Shelbyville, IL, 62702"""
user_info = extract_user_info(text)
if user_info.refusal:
print(user_info.refusal)
else:
print(user_info.content)
Saída:
{"name":"John Doe","email":"john.doe@example.com","phone":"123-456-7890","addresses":[{"street":"123 Main St","city":"Springfield","state":"IL","zip_code":"62701"},{"street":"456 Elm St","city":"Shelbyville","state":"IL","zip_code":"62702"}]}
6. Forneça descrições claras e objetivas para cada campo nos seus modelos Pydantic para melhorar a precisão das saídas do modelo:
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str = Field(..., description="The person's full name")
age: int = Field(..., description="The person's age in years")
occupation: str = Field(..., description="The person's current job or profession")
Essas práticas ajudam muito a tirar o máximo proveito de Structured Outputs nas suas aplicações.
Conclusão
Neste tutorial, você aprendeu a começar com um novo recurso da OpenAI API: Structured Outputs. Vimos como esse recurso faz os modelos de linguagem produzirem saídas no formato que especificamos. Você também viu como usá-lo em conjunto com function calling e conferiu algumas boas práticas para aproveitar ao máximo o recurso.
Aqui estão algumas fontes relacionadas para aprofundar seu entendimento:
Obtenha uma das melhores certificações de IA
Structured Outputs: perguntas frequentes
Como os modelos Pydantic funcionam com Structured Outputs?
Modelos Pydantic são usados para definir o schema da estrutura de saída desejada, que é então passada para a OpenAI API para impor o formato da resposta.
Structured Outputs pode ser usado com function calling?
Sim. Structured Outputs pode ser usado com function calling para simplificar a definição de parâmetros de função e saídas esperadas.
Quais são os benefícios de usar Structured Outputs?
Os benefícios incluem formatos de resposta consistentes, menos necessidade de pós-processamento, mais confiabilidade em aplicações de IA e integração mais fácil com sistemas existentes.
Existem limitações ao usar Structured Outputs?
Apesar de poderoso, Structured Outputs pode limitar a flexibilidade das respostas do modelo e exigir um design de schema cuidadoso para equilibrar estrutura e nível de detalhamento desejado nas saídas.
Sou criador de conteúdo em ciência de dados há mais de 2 anos e um dos perfis com maior alcance no Medium. Gosto de escrever artigos detalhados sobre IA e ML, com uma pitada de sarcasmo — porque alguém precisa deixar o assunto menos monótono. Já publiquei mais de 130 artigos e um curso na DataCamp, com outro em andamento. Meu conteúdo já alcançou mais de 5 milhões de visualizações, e 20 mil pessoas passaram a me seguir no Medium e no LinkedIn.





