课程
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 结构化输出的最佳实践
在使用结构化输出时,有若干最佳实践与建议需要牢记。本节将概述其中一些。
- 使用 Pydantic 模型定义输出架构,因为它提供了定义期望输出结构的简洁且类型安全的方式。
- 保持架构简洁且具体,以获得最准确的结果。
- 使用合适的数据类型(
str、int、float、bool、List、Dict)来准确表示数据。 - 对枚举使用
Literal类型,为字段定义允许的具体取值。 - 处理模型拒答。当使用新的
.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 在响应中的灵活性,并要求谨慎设计架构,以在结构与所需细节层级之间取得平衡。
我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。
