OpenAI / GPT-4o:Python 返回结构化 / JSON 输出

在调用 OpenAI (比如:GPT-4o )接口时,希望返回的结果是能够在后续任务中自动化处理的结构化 / JSON 输出。GPT 版本:++gpt-4o-2024-08-06++,提供了这样的功能。

目标:从非结构化输入到结构化数据(比如:JSON)。

目录

[1. 结构化输出](#1. 结构化输出)

[1.1 简介](#1.1 简介)

[1.2 优点](#1.2 优点)

[2. 接口](#2. 接口)

[2.1 官方代码](#2.1 官方代码)

[2.2 Pydantic](#2.2 Pydantic)

[2.2.1 简介](#2.2.1 简介)

[2.2.2 示例](#2.2.2 示例)

[2.2.3 特点](#2.2.3 特点)

[2.3 Python 代码](#2.3 Python 代码)

[3. 异常](#3. 异常)

[3.1 ValidationError](#3.1 ValidationError)

[3.2 解决](#3.2 解决)

[3.3 例子](#3.3 例子)

[3.3.1 Prompt](#3.3.1 Prompt)

[3.3.2 Pydantic](#3.3.2 Pydantic)

[3.3.3 API](#3.3.3 API)

[3.3.4 数据验证](#3.3.4 数据验证)


1. 结构化输出

1.1 简介

来源:Introducing Structured Outputs in the API | OpenAI

Introducing Structured Outputs in the API

We are introducing Structured Outputs in the API---model outputs now reliably adhere to developer-supplied JSON Schemas.

在 API 中引入结构化输出

我们在 API 中引入了结构化输出 --- 模型输出现在可靠地遵循开发人员提供的 JSON 架构。

来源:Structured Outputs - OpenAI API

JSON is one of the most widely used formats in the world for applications to exchange data.

Structured Outputs is a feature that ensures the model will always generate responses that adhere to your supplied JSON Schema, so you don't need to worry about the model omitting a required key, or hallucinating an invalid enum value.

在 Pydantic 中,可以通过 Optional 或者 default 参数来设置可选字段和默认值。

结构化输出是一项功能,可确保模型始终生成符合 您提供的 JSON 模式的响应,因此您++不必担心模型会遗漏必需的键或产生无效枚举值的幻觉++。

1.2 优点

  • Reliable type-safety: No need to validate or retry incorrectly formatted responses
  • Explicit refusals: Safety-based model refusals are now programmatically detectable
  • Simpler prompting: No need for strongly worded prompts to achieve consistent formatting

2. 接口

2.1 官方代码

官方的文档指出:

In addition to supporting JSON Schema in the REST API, the OpenAI SDKs for Python and JavaScript also make it easy to define object schemas using Pydantic and Zod respectively.

除了在 REST API 中支持 JSON Schema 之外,OpenAI 的 Python 和 JavaScript SDK 还可以轻松使用 Pydantic和 Zod 分别定义对象模式

表明,对于 Python 程序,可选的方法有两种:一种是 JSON Schema ,另一种是使用 Pydantic

2.2 Pydantic

2.2.1 简介

PydanticPython使用最广泛的数据验证库。

2.2.2 示例

这里先展示一个 Pydantic 官方文档给的示例。

python 复制代码
from datetime import datetime

from pydantic import BaseModel, PositiveInt


class User(BaseModel):
    id: int  
    name: str = 'John Doe'  
    signup_ts: datetime | None  
    tastes: dict[str, PositiveInt]  


external_data = {
    'id': 123,
    'signup_ts': '2019-06-01 12:22',  
    'tastes': {
        'wine': 9,
        b'cheese': 7,  
        'cabbage': '1',  
    },
}

user = User(**external_data)  

print(user.id)  
#> 123
print(user.model_dump())  
"""
{
    'id': 123,
    'name': 'John Doe',
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
}
"""

这里,类 User 继承自 pydantic.BaseModel ,其定义了四个属性:id , name , signup_ts , tastes ,同时定义了他们的数据类型

后续定义了一个 Pythondict 型变量 external_data赋值

user = User(**external_data) 中,将 dict字典解包 的方式传递给类 User ,得到对象 user

需要注意的是,如果 external_data 中有++多余的字段或类型不匹配++ ,Pydantic会抛出相应的错误。如下展示官方提供的示例。

python 复制代码
# continuing the above example...

from datetime import datetime
from pydantic import BaseModel, PositiveInt, ValidationError


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None
    tastes: dict[str, PositiveInt]


external_data = {'id': 'not an int', 'tastes': {}}  

try:
    User(**external_data)  
except ValidationError as e:
    print(e.errors())
    """
    [
        {
            'type': 'int_parsing',
            'loc': ('id',),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'not an int',
            'url': 'https://errors.pydantic.dev/2/v/int_parsing',
        },
        {
            'type': 'missing',
            'loc': ('signup_ts',),
            'msg': 'Field required',
            'input': {'id': 'not an int', 'tastes': {}},
            'url': 'https://errors.pydantic.dev/2/v/missing',
        },
    ]
    """

这里报异常:ValidationError 。由于 external_data 中的 id 类型不是 int ,且缺少了 signup_ts。故异常为两个方面。

2.2.3 特点

***++个人感觉++ ,PydanticJava的类有很多相似之处,尤其是在数据模型和验证方面:

  • 数据结构定义

Java 中,通常通过类(Class )来定义数据结构,其中属性由类成员变量表示。

Pydantic 也是通过 Python 的类来定义数据模型,属性通常是类的字段(Field)

  • 类型约束

Java 是强类型语言,类的成员变量通常都有明确的类型(如 int , String等)。

Pydantic 也允许在类定义时指定字段的类型,并且在创建实例时进行类型检查和验证。

  • 数据验证

Java中,可以通过构造函数、setter 方法或其他工具进行输入数据的验证。

Pydantic 内置了强大的数据验证功能,它会根据你定义的类型自动进行验证并在++必要时提供详细的错误信息++。

  • 默认值和可选值

Java类中,可以通过构造函数或者设置默认值来定义可选字段。

Pydantic 中,可以通过 Optional 或者 default参数来设置可选字段和默认值。

2.3 Python 代码

这里直接粘贴官方代码。

python 复制代码
from pydantic import BaseModel
from openai import OpenAI

api_key = '你的KEY'
base_url = '你的URL'

client = OpenAI(api_key=api_key, base_url=base_url)

class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "You are a helpful math tutor. Guide the user through the solution step by step."},
        {"role": "user", "content": "how can I solve 8x + 7 = -23"}
    ],
    response_format=MathReasoning,
)

math_reasoning = completion.choices[0].message.parsed

# Example response 
"""
{
  "steps": [
    {
      "explanation": "Start with the equation 8x + 7 = -23.",
      "output": "8x + 7 = -23"
    },
    {
      "explanation": "Subtract 7 from both sides to isolate the term with the variable.",
      "output": "8x = -23 - 7"
    },
    {
      "explanation": "Simplify the right side of the equation.",
      "output": "8x = -30"
    },
    {
      "explanation": "Divide both sides by 8 to solve for x.",
      "output": "x = -30 / 8"
    },
    {
      "explanation": "Simplify the fraction.",
      "output": "x = -15 / 4"
    }
  ],
  "final_answer": "x = -15 / 4"
}
"""

这里需要注意的是,调用的 API 接口是 client.beta .chat.completions.parse ,接口参数中有一个 response_format =MathReasoning ,其中赋值的是自定义继承pydantic.BaseModel 的类。

官方给出两种形式的结构化输出:function callingjson_schema 。前者就是这里的自定义类,后者是 JSON。具体使用哪种应需求选择。

  • If you are connecting the model to tools, functions, data, etc. in your system, then you should use function calling

  • If you want to structure the model's output when it responds to the user, then you should use a structured response_format

  • 如果要将模型连接到系统中的工具、函数、数据等,则应使用函数调用

  • 如果你想在响应用户时构建模型的输出,那么你应该使用结构化的 response_format

说白了,就是 GPT-4o 返回的结果是需要后续程序调用,则选 function calling ,如果直接返回给用户,则提供 JSON格式。

3. 异常

3.1 ValidationError

但实际操作时,执行官方提供的代码,我个人遇到了不可解决的问题。

bash 复制代码
pydantic_core._pydantic_core.ValidationError: 1 validation error for MathReasoning
  Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value="To solve the equation \\...ation or more examples!", input_type=str]
    For further information visit https://errors.pydantic.dev/2.8/v/json_invalid

即,提示解析的 ValidationError

3.2 解决

经多次尝试,仍提示数据验证异常。不清楚是什么原因导致,OpenAI 社区也有同样的问题:Official example MathResponse raise invalid json - API - OpenAI Developer Forum。但还没看到有效的解决方案。

我的解决方案是,采用 json_object 格式来获取 OpenAI response ,然后再交于自定义的 MathReasoning 进行数据验证。但这++需要增加额外的 Prompt,且 Prompt必须严格给出 JSON 格式的示例,并强制要求 GPT-4o 返回 JSON 格式++。官方就此给出了重要说明:

  • When using JSON mode, you ++must always++ instruct the model to ++produce JSON++ via some message in the conversation , for example ++via your system message++. If you don't include an explicit instruction to generate JSON, the model may generate an unending stream of whitespace and the request may run continually until it reaches the token limit. To help ensure you don't forget, the API will throw an error if the string "JSON" does not appear somewhere in the context.

  • JSON mode will not guarantee the output matches any specific schema, only that it is valid and parses without errors. You should ++use Structured Outputs++ to ensure it matches your schema , or if that is not possible, you should use a validation library and ++potentially retries++ to ensure that the output matches your desired schema.

  • 使用 JSON 模式时,您**++必须始终++指示模型通过对话中的某些消息生成 JSON** ,例如通过您的++系统消息++。如果您不包含生成 JSON 的明确指令,则模型可能会生成无休止的空格流,并且请求可能会持续运行,直到达到令牌限制。为了帮助确保您不会忘记,如果字符串 "JSON" 未出现在上下文中的某个位置,API 将引发错误。

  • ++JSON 模式不保证输出与任何特定架构匹配,只保证它是有效的并且解析没有错误++ 。++您应该使用结构化输出来确保它与您的架构匹配,或者如果无法匹配,则应使用验证库并可能重试,以确保输出与所需的架构匹配。++

此时,需要修改调用的 API 接口,同时修改参数 response_format={"type": "json_object"} ,++切记++ 得额外输入带有强制要求输出 JSONPrompt

3.3 例子

这里展示我实际生产中的一个例子。

3.3.1 Prompt

给一个 JSON 的例子,且强调返回 JSON格式。

bash 复制代码
Background:
XXX.

Task Description:
Given a text pair, such as '{"left": "XXX", "right": "XXX"}'. 

Step 1: First, XXX. 

Step 2: Then, XXX. 

Step 3: Finally, XXX.

Example:
input:
{
  "left": "XXX",
  "right": "XXX"
}

output (must be in JSON format):
{
  "relation": {
    "type": "XXX",
    "subtype": "XXX",
    "description": "XXX"
  },
  "left": {
    "type": "XXX",
    "content": "XXX",
    "explanation": "XXX"
  },
  "right": {
    "type": "XXX",
    "content": "XXX",
    "explanation": "XXX"
  },
  "category": "XXX"
}

Note: You have to return the correct JSON format.
3.3.2 Pydantic

自定义的结构化数据类,可以有层次结构,详细见官方文档:https://platform.openai.com/docs/guides/structured-outputs/supported-schemas

python 复制代码
from pydantic import BaseModel


class Relation(BaseModel):
    type: str
    subtype: str
    description: str


class Detail(BaseModel):
    type: str
    content: str
    explanation: str


class Annotation(BaseModel):
    relation: Relation
    left: Detail
    right: Detail
    category: str
3.3.3 API

API 的参数额外输入 prompt ,同时修改 response_format={"type": "json_object"}

python 复制代码
prompt = 'XXX. Note: You have to return the correct JSON format.'
content = 'XXX'


completion = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {
            "role": "system",
            "content": "You are a helpful assistant designed to output JSON."
        },
        {
            "role": "user",
            "content": prompt
        },
        {
            "role": "user",
            "content": content
        }
    ],
    response_format={"type": "json_object"},
)
response = completion.choices[0].message.content
3.3.4 数据验证

上述返回的 responsestr 类型的 JSON 。首先需要使用 json.loads(response) 转换为 JSON对象。

然后使用自定义类 Annotation3.3.2 中定义的) 验证 JSON 是否合规,即符合 Annotation 类定义的数据结构以及子结构

如果无异常,则可通过对象.属性方法获取对应的值。

python 复制代码
import json


try:
    row: json = json.loads(response)
except json.decoder.JSONDecodeError as e:
    print(e)

try:
    annotation: Annotation = Annotation.model_validate(row)
except ValidationError as e:
    print(e)

print(annotation.relation.type)

个人实践中,错误率1% 左右,可按照 3.2 中官方的重要说明中讲的,进行++多次重试++,我的经验是重试一次即可。

相关推荐
WANGWUSAN666 分钟前
Python高频写法总结!
java·linux·开发语言·数据库·经验分享·python·编程
40岁的系统架构师24 分钟前
1 JVM JDK JRE之间的区别以及使用字节码的好处
java·jvm·python
觅远1 小时前
python实现word转html
python·html·word
悠然的笔记本2 小时前
python2和python3的区别
python
西猫雷婶2 小时前
python学opencv|读取图像(十六)修改HSV图像HSV值
开发语言·python·opencv
lovelin+v175030409662 小时前
智能电商:API接口如何驱动自动化与智能化转型
大数据·人工智能·爬虫·python
赵谨言2 小时前
基于python+django的外卖点餐系统
经验分享·python·毕业设计
孤独的履行者2 小时前
入门靶机:DC-1的渗透测试
数据库·python·网络安全
CodeClimb3 小时前
【华为OD-E卷-最左侧冗余覆盖子串 100分(python、java、c++、js、c)】
java·python·华为od
深度学习lover3 小时前
<项目代码>YOLO Visdrone航拍目标识别<目标检测>
python·yolo·目标检测·计算机视觉·visdrone航拍目标识别