跳至内容
人工智能
OpenAI

OpenAI 结构化输出入门

了解如何开始使用 OpenAI 结构化输出,理解其全新语法,并探索其关键应用场景。
Bexruz (Bex) Tuychiev's photo
Bexruz (Bex) Tuychiev
已更新 · 9分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续
继续操作即表示您接受我们的《使用条款》和《隐私政策》,并同意您的数据存储在美国。

2024 年 8 月,OpenAI 在其 API 中宣布了一项强大的新功能——结构化输出(Structured Outputs)。顾名思义,借助该功能,您可以确保大语言模型只按照您指定的格式生成响应。这一能力将大大简化需要精确数据格式的应用构建。

在本教程中,您将学习如何开始使用 OpenAI 结构化输出,理解其新语法,并探索其主要应用。

结构化输出在 AI 应用中的重要性

确定性的响应,或换句话说,格式一致的响应,对于数据录入、信息检索、问答、多步工作流等众多任务至关重要。您可能已经遇到过,即使提示不变,LLM 的输出格式也可能千差万别。

例如,考虑下面这个由 GPT-4o 驱动的简单classify_sentiment 函数:

# 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")

输出:

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.

尽管前两个响应都是相同的单词格式,最后一个却是一整句。如果有下游应用依赖上述代码的输出,它可能会因为只预期单词形式而崩溃。

我们可以通过一些提示工程来修复这个问题,但这是一个耗时且需要反复迭代的过程。即使提示已经非常完善,我们也无法 100% 确保后续请求的响应会守规矩。除非我们使用结构化输出:

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")

输出:

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"}

借助新函数 classify_sentiment_with_structured_outputs,所有响应都遵循相同格式。

这种强制语言模型输出为固定格式的能力意义重大,可为您节省无数提示工程时间,或避免依赖其他开源工具。

OpenAI 结构化输出入门

本节我们将以情感分析函数为例,拆解结构化输出。

环境配置

前提条件

在开始之前,请确保您具备以下条件:

  • 系统已安装 Python 3.7 或更高版本。
  • OpenAI API 密钥。您可以在 OpenAI 官网注册获取。

设置 OpenAI API

1. 安装 OpenAI Python 包:打开终端,运行以下命令安装或将 OpenAI Python 包更新至最新版本:

$ pip install -U openai

2. 配置 API 密钥:您可以将 API 密钥设置为环境变量,或直接在代码中设置。若作为环境变量,请运行:

$ export OPENAI_API_KEY='your-api-key'

3. 验证安装:创建一个简单的 Python 脚本来验证安装:

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

运行脚本以确保一切设置正确。您应当能在终端看到模型的响应。

除了 OpenAI 包,您还需要 Pydantic 库来为结构化输出定义并校验 JSON 架构。使用 pip 安装:

$ pip install pydantic

完成以上步骤后,您的环境就已就绪,可以使用 OpenAI 的结构化输出功能。

使用 Pydantic 定义输出架构

要使用结构化输出,您需要用 Pydantic 模型定义期望的输出结构。Pydantic 是一个用于数据校验与配置管理的 Python 库,允许您用类型注解来定义数据模型。随后,这些模型可用于强制 OpenAI 模型的输出结构。

以下是为评论情感分类器指定格式的 Pydantic 模型示例:

from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
   sentiment: Literal["positive", "negative", "neutral"]

在本例中:

  • SentimentResponse 是定义期望输出结构的 Pydantic 模型。
  • 该模型包含一个字段 sentiment,其取值仅能为三个字面量之一:“positive”“negative”或“neutral”。

当我们在 OpenAI API 请求中传入该模型后,输出将只能是我们提供的这些词之一。

下面看看如何实现。

使用 parse 助手方法

要在 OpenAI 请求中强制执行我们的 Pydantic 架构,我们只需将其传给对话补全 API 的response_format 参数。大致如下:

response = client.beta.chat.completions.parse(
   model=MODEL,
   messages=[...],
   response_format=SentimentResponse
)

您会注意到,我们使用的是 client.beta.chat.completions.parse 方法,而非client.chat.completions.create。 .parse() 是为结构化输出专门编写的对话补全 API 新方法。

现在,我们把所有内容整合起来,用结构化输出来重写评论情感分类器。首先进行必要的导入,定义 Pydantic 模型、系统提示和提示模板:

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}
"""

然后,编写一个使用 .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

此函数中的关键一行是 response_format=SentimentResponse,它真正启用了结构化输出。

我们在其中一个评论上测试一下:

# 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"}

这里,result 是一个消息对象:

>>> type(result)
openai.types.chat.parsed_chat_completion.ParsedChatCompletionMessage[SentimentResponse]

除了用于获取响应的 .content 属性外,它还有一个 .parsed 属性,以类的形式返回解析后的信息:

>>> result.parsed
SentimentResponse(sentiment='positive')

如您所见,我们获得了 SentimentResponse 类的一个实例。这意味着我们可以通过 .sentiment 属性以字符串形式访问情感,而不是通过字典:

>>> result.parsed.sentiment
'positive'

嵌套 Pydantic 模型以定义复杂架构

在某些情况下,您可能需要定义更复杂的输出结构,涉及嵌套数据。Pydantic 允许模型相互嵌套,从而创建可应对多种用例的精细架构。这在处理分层数据或需要为复杂输出强制特定结构时尤为有用。

我们来看一个示例,需要提取详细的用户信息,包括姓名、联系方式以及一组地址。每个地址应包含街道、城市、州和邮编字段。这就需要多个 Pydantic 模型来构建正确的架构。

步骤 1:定义 Pydantic 模型

首先,为地址和用户信息定义 Pydantic 模型:

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]

在本例中:

  • Address 是定义地址结构的 Pydantic 模型。
  • UserInfo 是包含 Address 对象列表的 Pydantic 模型,同时还包含用户姓名、邮箱和电话字段。

步骤 2:在 API 调用中使用嵌套的 Pydantic 模型

接下来,在 OpenAI API 调用中使用这些嵌套的 Pydantic 模型来强制输出结构:

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)

该示例文本完全不可读,且关键信息之间没有空格。看看模型是否能成功解析。我们将使用 json 库来美化响应:

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"
   }
 ]
}

如您所见,模型根据我们提供的架构,正确捕捉了一位用户的信息及其两个独立地址。

总之,通过嵌套 Pydantic 模型,您可以定义能够处理分层数据的复杂架构,并为复杂输出强制特定结构。

结合结构化输出进行函数调用

新一代语言模型广泛具备函数调用(也称为工具调用)能力。该能力允许您将语言模型连接到用户自定义函数,从而让模型能够访问外部世界。

常见示例包括:

  • 检索实时数据(如天气、股价、体育比分)
  • 执行计算或数据分析
  • 查询数据库或 API
  • 生成图像或其他媒体
  • 进行文本翻译
  • 控制智能家居设备或物联网系统
  • 执行自定义业务逻辑或工作流

这里我们不展开函数调用的细节,您可阅读我们的 OpenAI 函数调用教程。

需要了解的是,有了结构化输出,使用 OpenAI 模型进行函数调用会更轻松。过去,传给 OpenAI 模型的函数需要手写复杂的 JSON 架构,为每个参数提供类型注解。如下所示:

{
   "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"],
       },
   }
}

尽管 get_current_weather 函数只有两个参数,其 JSON 架构仍会非常庞大且手写易出错。

结构化输出通过再次使用 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"]

首先编写函数本身及其逻辑。然后,再用 Pydantic 模型定义一次,指定期望的输入参数。

接着,通过调用 pydantic_function_tool 将 Pydantic 模型转换为兼容的 JSON 架构:

>>> 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}}}

以下展示如何在请求中使用该工具:

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')

我们将 Pydantic 模型以兼容的 JSON 格式传给对话补全 API 的 tools 参数。随后,模型会根据查询决定是否调用该工具。

由于上述示例中的查询是“东京的天气如何?”,我们在返回的消息对象的 tool_calls 中看到了一个调用。

请记住,模型并不会调用 get_weather 函数,而是基于我们提供的 Pydantic 架构为其生成参数:

arguments = json.loads(tool_call.function.arguments)
>>> arguments
{'location': 'Tokyo', 'unit': 'celsius', 'condition': 'sunny'}

接下来由我们使用提供的参数来调用函数:

some_result = get_weather(**arguments)

如果您希望模型既生成函数参数又同时调用函数,那么您需要的是一个 AI 代理。

我们还有一篇单独的 LangChain 代理教程,如果您感兴趣可以参考。

使用 OpenAI 结构化输出的最佳实践

在使用结构化输出时,有若干最佳实践与建议需要牢记。本节将概述其中一些。

  1. 使用 Pydantic 模型定义输出架构,因为它提供了定义期望输出结构的简洁且类型安全的方式。
  2. 保持架构简洁且具体,以获得最准确的结果。
  3. 使用合适的数据类型(str、int、float、bool、List、Dict)来准确表示数据。
  4. 对枚举使用 Literal 类型,为字段定义允许的具体取值。
  5. 处理模型拒答。当使用新的 .parse() 方法时,消息对象新增了 .refusal 属性用于指示拒答:
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)

输出:

{"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. 为 Pydantic 模型中的每个字段提供清晰简洁的描述,以提升模型输出的精准度:

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")

这些实践将极大提升您在应用中高效使用结构化输出的效果。

结语

在本教程中,我们学习了如何开始使用 OpenAI API 的一项新功能:结构化输出。我们看到该功能如何强制语言模型以我们指定的格式产出结果,了解了其与函数调用的组合用法,并探索了若干最佳实践以充分发挥其价值。

以下相关资源可帮助您加深理解:

Structured Outputs 常见问答

Pydantic 模型如何与结构化输出配合工作?

Pydantic 模型用于定义期望输出结构的架构,然后将其传递给 OpenAI API 来强制响应格式。

结构化输出可以与函数调用一起使用吗?

可以。结构化输出可与函数调用结合使用,从而简化定义函数参数和期望输出的过程。

使用结构化输出有哪些好处?

优势包括响应格式一致、减少后处理需求、提升 AI 应用的可靠性,以及更易与现有系统集成。

使用结构化输出是否存在限制?

尽管功能强大,结构化输出可能会限制 AI 在响应中的灵活性,并要求谨慎设计架构,以在结构与所需细节层级之间取得平衡。


Bexruz (Bex) Tuychiev's photo
Author
Bexruz (Bex) Tuychiev
LinkedIn

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题
人工智能
OpenAI

热门 OpenAI 课程

课程

使用 OpenAI API

3 小时
183.4K
用 OpenAI API 开始开发 AI 驱动的应用。 了解支撑 ChatGPT 等热门 AI 应用的功能。
查看详情开始课程

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续
继续操作即表示您接受我们的《使用条款》和《隐私政策》,并同意您的数据存储在美国。
查看更多