廣瀬製紙株式会社

Employees' Blog

OpenAI API+Pythonで構造化出力をする
Structured Outputsとpydantic

公開日:2025.05.26 更新日:2025.05.26
なんらかの構造をイメージしたイラスト

記事の目的

こんにちは。廣瀬製紙株式会社 稼働率向上PJチームのA.Mです。
前回、Node.jsでOpenAI APIを使う場合の、構造化出力の方法を記事にしました。

今回はPythonでOpenAI APIを使う場合の記事にしたいと思います。

正直なところ、公式ドキュメントを見ればだいたい分かる内容なのですが、「Optional」を使ってパラメータを任意にする方法、「Field」を使ってパラメータのより詳細の指示を与える方法、は書いてなさそうだったので(他のページを探したらありそうですが)、本記事にて記録を残しておくことにしました。

サンプルコード

今回のコードは、ChatGPTに4コママンガの構成を考えてもらい出力してもらう、というものになります。


import os
from openai import OpenAI
from pydantic import BaseModel, Field
from dotenv import load_dotenv
from typing import Optional

# モデル定義
class CharacterInfo(BaseModel):
    role: str
    appearance: str
    color: str = Field(description="キャラクターの服装の色や外見の色。")


class Character(BaseModel):
    role: str
    expression: str
    words: Optional[str] = Field(
        description="必要に応じて日本語のセリフを簡潔に出力。()を使った補足などは不要。"
    )


class ComicStrip(BaseModel):
    story: str = Field(description="全体のストーリーを詳細に書くこと")
    characters: list[CharacterInfo]
    composition1: str
    background1: str
    characters1: list[Character]
    composition2: str
    background2: str
    characters2: list[Character]
    composition3: str
    background3: str
    characters3: list[Character]
    composition4: str
    background4: str
    characters4: list[Character]


# プロンプト生成関数
def prompt() -> ComicStrip:
    load_dotenv()
    api_key = os.getenv("OPENAI_API_KEY")
    client = OpenAI(api_key=api_key)

    theme = "犬と家族の幸せ"

    completion = client.beta.chat.completions.parse(
        model="gpt-4o-2024-08-06",
        messages=[
            {
                "role": "system",
                "content": "あなたは優秀な4コママンガのアシスタントです。",
            },
            {
                "role": "user",
                "content": f"「{theme}」というテーマで、4コママンガの構成を考えてください。起承転結(導入→予兆→事件→結末)を意識して。出力は日本語で行うこと。",
            },
        ],
        response_format=ComicStrip,
    )

    response = completion.choices[0].message

    if response.refusal:
        print(response.refusal)
    else:
        return response.parsed

解説

必要なライブラリ等は事前にインストールする必要があります。APIキーはdotenvを使って渡しているので、.envファイルを作ってください。

最も重要なのはCharacterInfo、Character、ComicStripという3つのクラスで、ここでモデルを定義しています。

全体の構成としては、CharacterInfoで登場人物のキャラクターの役割と外見、服装などの色を定義し、ComicStripで4コママンガ全体の構成を定義しつつ、CharacterInfoを参照しています。

CharacterInfoの他にCharacterが存在するのは、こちらは各コマにおけるキャラクターの表情、セリフを定義するためです。
Characterは呼び出し元(ComicStrip)ではリスト形式になっており、任意のキャラクターたちを登場させられる仕様になっています。

words: Optional[str] のように書けばそのパラメータは任意になります。また = Field(description=””) とつけることで、パラメータのより詳細な内容をLLMに伝えることができます。

今回は4コママンガでコマ数が4で固定になっていますが、任意のコマ数に変更できるようにしたり、書き方が少し冗長だと感じるようであれば別でクラスを作って参照させれば良いです。

出力結果

story = (
    '晴れた日に家族と犬が庭でボール遊びを楽しんでいた。'
    'しかし母の呼びかけに気づいた犬は、開いた門から通りへ飛び出してしまう。'
    '心配した家族は声をからしながら探し回るが、夕方には犬が小さな花をくわえて無事に帰還。'
    '改めて家族の幸せを感じるエピソード。'
)

characters = [
    CharacterInfo(
        role='父',
        appearance='30代男性、短髪',
        color='青いシャツ'
    ),
    CharacterInfo(
        role='母',
        appearance='30代女性、長い髪',
        color='赤いシャツ'
    ),
    CharacterInfo(
        role='子供',
        appearance='小学生、ボーイッシュ',
        color='黄色いシャツ'
    ),
    CharacterInfo(
        role='犬',
        appearance='小型犬、白と茶色の毛',
        color='白と茶'
    ),
]

composition1 = (
    '画面いっぱいに庭でボール遊びをする家族と犬の全身を収めたワイドショット'
)
background1 = (
    '晴れた日の芝生の庭、青空と木のフェンス'
)
characters1 = [
    Character(
        role='父',
        expression='笑顔',
        words='今日もいい天気だね!'
    ),
    Character(
        role='子供',
        expression='元気',
        words='ボールポーン!'
    ),
    Character(
        role='母',
        expression='微笑み',
        words='わんこ楽しそう!'
    ),
    Character(
        role='犬',
        expression='興奮',
        words='ワン!'
    ),
]

composition2 = (
    '中間距離のミディアムショットで、母が呼びかけるシーン'
)
background2 = (
    '同じ庭、背後に少し開いた門とフェンス'
)
characters2 = [
    Character(
        role='母',
        expression='手を挙げて呼ぶ',
        words='お昼ごはんよー'
    ),
    Character(
        role='子供',
        expression='振り返り',
        words='はーい'
    ),
    Character(
        role='犬',
        expression='キョロキョロ',
        words=None
    ),
]

composition3 = (
    '門の外の通りまで追いかける犬と、庭から慌てて見守る家族の対比ショット'
)
background3 = (
    '住宅街の通り、向こうに猫が走る'
)
characters3 = [
    Character(
        role='犬',
        expression='全速力',
        words=None
    ),
    Character(
        role='子供',
        expression='慌て',
        words='わっ!行っちゃった!'
    ),
    Character(
        role='父',
        expression='焦り',
        words='危ない!'
    ),
]

composition4 = (
    '夕暮れの玄関前で、花をくわえて帰ってきた犬と喜ぶ家族を捉えたあたたかいラストショット'
)
background4 = (
    '薄暗くなりかけた玄関ポーチ、足元に花びら'
)
characters4 = [
    Character(
        role='犬',
        expression='満足そう',
        words=None
    ),
    Character(
        role='母',
        expression='安堵の笑顔',
        words='おかえり〜'
    ),
    Character(
        role='子供',
        expression='涙ぐみ',
        words='よかった!'
    ),
    Character(
        role='父',
        expression='柔和',
        words='大切な家族だね'
    ),
]

このような出力結果が得られました。

出力は Pydantic の ComicStrip オブジェクト(インスタンス)になっているので、必要であれば辞書形式にするなど手を加えてください。

まとめ

OpenAIから一定の構造化されたデータを受け取るにあたって、以前はJSONモードという方式が使われてました。
今はStructured Outputというモードが標準になりましたが、Structured Outputの方法も私の知る限り依然と少し仕様に変更があります。

現在はこのようなPydanticを使った方法が公式ドキュメントにて解説されていますので、そちらも参照してみてください。

本記事によりStructured Outputのイメージが掴みやすくなった、ということであれば幸いです。