OpenAI の Structured Outputs を試す
使用可能なモデル
使い方 | 使用可能なモデル |
---|---|
Function Calling |
gpt-4-0613 / gpt-3.5-turbo-0613 以降のツールをサポートする全てのモデル |
response_format パラメータ |
gpt-4o-2024-08-06 / gpt-4o-mini-2024-07-18
|
なお、2024/08/08時点では、AzureOpenAIはPlayGroundのみと思われる。
公式ドキュメントに従ってやってみる。Colaboratoryで。
事前準備
!pip install openai
!pip freeze | grep -i openai
openai==1.40.1
from google.colab import userdata
import os
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
Structured Output
Pythonで。
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "イベント情報を抽出して。日本語で。"},
{"role": "user", "content": "アリスとボブは金曜日に科学フェアに行きます。"},
],
response_format=CalendarEvent,
)
event = completion.choices[0].message.parsed
print(event)
OpenAI().beta.chat.completions.parse
ってのがStructured Outputで出力するためのメソッドで、記述の通りベータっぽい。で、出力時のスキーマをPydandicクラスで定義、それをresponse_format
で渡すと、そのスキーマにあわせて出力される。
name='科学フェア' date='金曜日' participants=['アリス', 'ボブ']
結果は辞書で取り出せる
event.dict()
{'name': '科学フェア', 'date': '金曜日', 'participants': ['アリス', 'ボブ']}
いくつかの例が上げられている
CoT
CoTによる数学の計算
from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint
client = OpenAI()
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": "あなたは親切な数学の家庭教師です。ステップバイステップで、ユーザが問題を解くのを導いてください。"},
{"role": "user", "content": "8x + 7 = -23 はどうやって解けばいいの?"}
],
response_format=MathReasoning,
)
math_reasoning = completion.choices[0].message.parsed
pprint(math_reasoning.dict())
{'final_answer': 'x = -\\frac{15}{4}',
'steps': [{'explanation': 'まず、8x + 7 = -23 の方程式を解くためには、x '
'の項を片側に、定数項を反対側に分ける必要があります。これを行うために、等式の両側から7を引きます。',
'output': '8x + 7 - 7 = -23 - 7'},
{'explanation': '7を引くことで、方程式は8x = -23 - 7になります。',
'output': '8x = -30'},
{'explanation': '次に、xを解くために、両側を8で割ります。',
'output': '8x / 8 = -30 / 8'},
{'explanation': '左側はxを残し、右側は分数になります。この段階で、分数の単純化が可能です。',
'output': 'x = -30 / 8'},
{'explanation': '分数 -30/8 は約分できます。 30と8の最大公約数4を使って約分します。',
'output': 'x = -15 / 4'},
{'explanation': 'したがって、方程式 8x + 7 = -23 の解は x = -\\frac{15}{4} である。',
'output': 'x = -\\frac{15}{4}'}]}
構造化データの抽出
論文を構造化データとして抽出。論文データを持ってくるのが面倒だったので、参考程度に。やってることは冒頭の例と基本的に同じ。
from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint
client = OpenAI()
class ResearchPaperExtraction(BaseModel):
title: str
authors: list[str]
abstract: str
keywords: list[str]
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "あなたは構造化データ抽出のエキスパートである。研究論文から構造化されていないテキストを与えられ、それを与えられた構造に変換しなければならない。"},
{"role": "user", "content": "[ここに論文のテキストを入れる]"}
],
response_format=ResearchPaperExtraction,
)
research_paper = completion.choices[0].message.parsed
pprint(research_paper.dict())
UIの生成
ユーザー情報を入力するフォームのUIを生成する。なるほど、Pydanticのこういう使い方はぜんぜん頭になかった。
from enum import Enum
from typing import List
from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint
client = OpenAI()
class UIType(str, Enum):
div = "div"
button = "button"
header = "header"
section = "section"
field = "field"
form = "form"
class Attribute(BaseModel):
name: str
value: str
class UI(BaseModel):
type: UIType
label: str
children: List["UI"]
attributes: List[Attribute]
UI.model_rebuild() # これは再帰型を有効にするために必要
class Response(BaseModel):
ui: UI
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "あなたは日本語UIジェネレーターAIです。ユーザーの入力をUIに変換します。"},
{"role": "user", "content": "ユーザーのプロフィールを入力するフォームを作成して。"}
],
response_format=Response,
)
ui = completion.choices[0].message.parsed
pprint(ui.dict())
{'ui': {'attributes': [{'name': 'method', 'value': 'post'},
{'name': 'action', 'value': '/submit-profile'}],
'children': [{'attributes': [{'name': 'type', 'value': 'text'},
{'name': 'placeholder',
'value': '名前を入力してください'}],
'children': [],
'label': '名前',
'type': <UIType.field: 'field'>},
{'attributes': [{'name': 'type', 'value': 'email'},
{'name': 'placeholder',
'value': 'メールアドレスを入力してください'}],
'children': [],
'label': 'メールアドレス',
'type': <UIType.field: 'field'>},
{'attributes': [{'name': 'type', 'value': 'tel'},
{'name': 'placeholder',
'value': '電話番号を入力してください'}],
'children': [],
'label': '電話番号',
'type': <UIType.field: 'field'>},
{'attributes': [{'name': 'type', 'value': 'text'},
{'name': 'placeholder',
'value': '住所を入力してください'}],
'children': [],
'label': '住所',
'type': <UIType.field: 'field'>},
{'attributes': [{'name': 'type', 'value': 'submit'}],
'children': [],
'label': '送信',
'type': <UIType.button: 'button'>}],
'label': 'ユーザープロフィールフォーム',
'type': <UIType.form: 'form'>}}
モデレーション
入力内容のモデレーション
from enum import Enum
from typing import Optional
from pydantic import BaseModel
from openai import OpenAI
from pprint import pprint
client = OpenAI()
class Category(str, Enum):
violence = "violence"
sexual = "sexual"
self_harm = "self_harm"
class ContentCompliance(BaseModel):
is_violating: bool
category: Optional[Category]
explanation_if_violating: Optional[str]
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "ユーザーの入力が特定のガイドラインに違反しているかどうかを判断し、違反している場合はそれを説明してください。"},
{"role": "user", "content": "面接の準備はどうすればいいでしょうか?"}
],
response_format=ContentCompliance,
)
compliance = completion.choices[0].message.parsed
pprint(compliance.dict())
{'category': None, 'explanation_if_violating': None, 'is_violating': False}
JSONモードとの違い
- Structured Outputは指定されたスキーマに必ず従って出力される。JSONモードはJSONで出力するがスキーマは保証されない。
- Structured Outputは、現時点で最新のgpt-4o、gpt-4o-mini 以降のモデルでしか使えない
- Function Callingで使う場合、JSONモードは常に有効になるが、Structured Outputは関数定義に
strict: true
が定義された場合のみ - Parallel Function Callingは、JSONモードではサポート、Structured Outputではサポートされていない
Structured Outputs の使い分け
Structured Outputsは2つの使い方がある
- function callingで使う
-
response_format
パラメータで使う
どちらを使うべきか?はざっくり以下とまとめられていた
- モデルをシステム内のツール、関数、データなどに接続する場合は、function callingを使用する。
- モデルがユーザーに応答するときの出力を構造化したいのであれば、構造化されたresponse_formatを使うべき
JSONモードだとフォーマットは破綻しないがスキーマが期待通りにはならない、function callingの場合は本来的にはツールとの連携がメインの目的だが、スキーマを指定できるので、これを使って出力制御だけに使う、みたいなやり方があったのが、単に出力制御が目的だけならresponse_format
パラメータを使えばいいという感じ。
Function Callingで使う場合はこちら
単に関数定義に"strict": True
をつけるだけっぽい。
tools = [
{
"type": "function",
"function": {
"name": "get_delivery_date",
"description": "顧客の注文の配達日を取得する。例えば、顧客から「私の荷物はどこですか」と尋ねられたときなど、配達日を知る必要がある場合に呼ぶ。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "顧客の注文ID",
},
},
"required": ["order_id"],
"additionalProperties": False,
},
},
"strict": True,
}
]
SDKの場合はresponse_format
でスキーマを指定すると、内部で"strict": True
を付与してる模様。
シンプルに使えて良い。
ドキュメントには他にもいろいろ書いてあるので、じっくり読んでみたいと思う。
やればやるほど、Pydanticをきちんとやっておく必要性を痛感している。前にGetting Startedやってみただけ、あとは雰囲気で使ってるだけ・・・改めてやってみるつもり。