Pydanticによる構造化出力
Pydanticモデルを出力スキーマとして定義し、新しいstructured outputs機能を介してAPIに渡して、レスポンスを型付きPythonオブジェクトへ自動的にデシリアライズします。
「Pydanticによる構造化出力」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
LLMの出力にPydanticを使う理由
Pydanticは、Pythonの型ヒントを使ってデータスキーマを定義するPythonのデータ検証ライブラリです。外部ソースから取得したデータの検証とデシリアライズを得意としており、LLMの出力は皆さんが遭遇する中でも特に信頼性の低い外部データソースの一つです。PydanticスキーマとOpenAIのStructured Outputsを組み合わせることで、AIモデルから型安全で検証済みの、さらに自動的にデシリアライズされたレスポンスを取得できます。
data = json.loads(response)を記述してから手動でフィールドを抽出し、型変換する代わりに、すべてのフィールドが正しい型であることを保証された、完全に型付けされたPythonオブジェクトを取得できます。IDEの自動補完や実行時検証も利用できます。これが、プロフェッショナルなAIエンジニアリングチームが構造化抽出を処理する方法です。
基本的なPydanticスキーマの定義
Pydanticモデルは、BaseModelを継承し、Pythonの型アノテーションを使ってフィールドを定義したクラスです。フィールドの型には、Pythonのプリミティブ型、ネスト用の他のPydanticモデル、またはリストやOptional、Unionに使用するtypingモジュールの型を指定できます。
from pydantic import BaseModel, Field
from typing import Optional, List
from enum import Enum
class Sentiment(str, Enum):
positive = 'positive'
negative = 'negative'
neutral = 'neutral'
class ReviewAnalysis(BaseModel):
sentiment: Sentiment
confidence: float = Field(ge=0.0, le=1.0, description='Confidence score 0-1')
key_themes: List[str] = Field(description='Main topics mentioned in the review')
summary: str = Field(max_length=200, description='One-sentence summary')
product_name: Optional[str] = Field(default=None, description='Product mentioned, if any')
would_recommend: Optional[bool] = None
# Pydantic validates types and constraints at instantiation
example = ReviewAnalysis(
sentiment=Sentiment.positive,
confidence=0.95,
key_themes=['fast delivery', 'good quality'],
summary='Customer loves the product and quick shipping.',
product_name='Wireless Headphones',
would_recommend=True
)
print(example.model_dump_json(indent=2))OpenAI Structured OutputsでのPydanticの使用
Pydanticモデルクラスをclient.beta.chat.completions.parse()のresponse_formatパラメーターに直接渡します。SDKがモデルを自動的にJSON Schemaへ変換してAPIに送信し、レスポンスを型付きのPythonオブジェクトにデシリアライズします。
import openai
from pydantic import BaseModel
from typing import List, Optional
from enum import Enum
client = openai.OpenAI()
class Sentiment(str, Enum):
positive = 'positive'
negative = 'negative'
neutral = 'neutral'
class ReviewAnalysis(BaseModel):
sentiment: Sentiment
confidence: float
key_themes: List[str]
summary: str
would_recommend: Optional[bool]
review_text = '''
I bought this laptop for my design work and I am blown away. It handles Photoshop
like a dream, the screen colors are beautiful, and it has not slowed down once in
three months. Battery life could be better but overall highly recommend!
'''
result = client.beta.chat.completions.parse(
model='gpt-4o-mini',
messages=[
{'role': 'system', 'content': 'Analyze the customer review and extract structured information.'},
{'role': 'user', 'content': review_text}
],
response_format=ReviewAnalysis
)
analysis = result.choices[0].message.parsed
print(f'Sentiment: {analysis.sentiment.value}')
print(f'Confidence: {analysis.confidence}')
print(f'Themes: {analysis.key_themes}')
print(f'Recommend: {analysis.would_recommend}')ネストしたPydanticモデル
Pydanticスキーマでは他のPydanticモデルを参照できるため、任意の深さのネストしたStructured Outputsを定義できます。契約書、請求書、履歴書、医療記録などの文書から階層データを抽出するのに適しています。
from pydantic import BaseModel
from typing import List, Optional
class Address(BaseModel):
street: Optional[str]
city: str
country: str
postal_code: Optional[str]
class ContactInfo(BaseModel):
email: Optional[str]
phone: Optional[str]
address: Optional[Address]
class Person(BaseModel):
full_name: str
age: Optional[int]
job_title: Optional[str]
contact: ContactInfo
skills: List[str]
# When you pass Person to response_format, the API generates:
# {
# "full_name": "...",
# "contact": {
# "email": "...",
# "address": { "city": "...", "country": "..." }
# },
# "skills": ["...", "..."]
# }
print('Nested model defined - pass to response_format for extraction')Pydantic Validatorによるフィールド検証
PydanticのValidatorを使うと、単純な型チェックを超えたカスタム検証ロジックを追加できます。信頼度スコアが0から1の範囲内であること、価格が負の値でないこと、日付文字列が正しい形式であることなどを検証できます。LLMが検証に失敗する値を返すと、PydanticはValidationErrorを発生させます。これを捕捉して処理できます。
from pydantic import BaseModel, Field, field_validator
from typing import Optional
import re
class ExtractedContact(BaseModel):
name: str
email: Optional[str] = None
phone: Optional[str] = None
confidence: float = Field(ge=0.0, le=1.0)
@field_validator('email')
@classmethod
def validate_email(cls, v):
if v is not None:
# Basic email format check
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', v):
raise ValueError(f'Invalid email format: {v}')
return v
@field_validator('phone')
@classmethod
def normalize_phone(cls, v):
if v is not None:
# Remove non-digit characters for normalization
digits = re.sub(r'[^0-9+]', '', v)
return digits
return v
try:
contact = ExtractedContact(name='Alice', email='not-an-email', confidence=0.9)
except Exception as e:
print(f'Validation error: {e}')オブジェクトのリストの抽出
文書から同じエンティティの複数のインスタンスを抽出するのは、よくあるパターンです。たとえば、請求書のすべての明細項目、会議の議事録に含まれるすべてのアクションアイテム、ニュース記事に含まれるすべてのエンティティなどです。この場合は、リストフィールドを持つコンテナモデルで元のモデルをラップすると、すっきり処理できます。
import openai
from pydantic import BaseModel
from typing import List
client = openai.OpenAI()
class ActionItem(BaseModel):
task: str
assignee: str
due_date: str # or use datetime with proper parsing
priority: str # high / medium / low
class MeetingNotes(BaseModel):
meeting_title: str
action_items: List[ActionItem]
key_decisions: List[str]
meeting_transcript = '''
Q3 Planning Meeting - June 2025
Decision: Launch new feature in July.
Decision: Extend free trial to 30 days.
Action: Alice to finalize designs by June 30th - High priority.
Action: Bob to write API docs by July 5th - Medium priority.
Action: Carol to set up staging environment by June 28th - High priority.
'''
result = client.beta.chat.completions.parse(
model='gpt-4o-mini',
messages=[
{'role': 'system', 'content': 'Extract structured data from meeting notes.'},
{'role': 'user', 'content': meeting_transcript}
],
response_format=MeetingNotes
)
notes = result.choices[0].message.parsed
for item in notes.action_items:
print(f'[{item.priority.upper()}] {item.task} -> {item.assignee} by {item.due_date}')Optionalフィールドとデフォルト値
現実の文書には欠落している情報があります。履歴書に電話番号が記載されていないことや、請求書に請求書番号がないこと、商品レビューで商品名に触れていないことがあります。適切なデフォルト値を持つOptionalフィールドを使用して、欠損データを適切に処理できるPydanticモデルを設計してください。
Optional[str] = Noneとアノテーションされたフィールドは、そのフィールドが存在しない可能性があることをPydanticとLLMの両方に伝えます。モデルは抽出できないフィールドについてJSONではnullを返し、PydanticはそれをPythonのNoneにデシリアライズします。そのため、下流の処理で適切に扱うことができ、KeyError例外を避けられます。
PydanticモデルをJSON Schemaに変換する
定義したPydanticモデルは、APIに渡す際に自動的にJSON Schemaへ変換されます。このスキーマを調べることで、APIが正確に適用する内容を理解できます。モデルが期待どおりの構造を返さない場合のデバッグにも役立ちます。
from pydantic import BaseModel, Field
from typing import List, Optional
import json
class ProductExtraction(BaseModel):
name: str = Field(description='Product name as mentioned in the text')
price_usd: Optional[float] = Field(default=None, description='Price in USD')
features: List[str] = Field(default_factory=list)
in_stock: bool = Field(description='Whether the product is currently available')
# See the JSON Schema that will be sent to the API
schema = ProductExtraction.model_json_schema()
print(json.dumps(schema, indent=2))
# This shows exactly what constraints the API will enforce抽出の失敗への対処
Structured Outputsを使用していても、抽出は2通りの理由で失敗する可能性があります。モデルが応答を拒否して拒否結果を返す場合と、文書に要求された情報が実際に含まれていないため、モデルが必須フィールドにnullを返し、必須フィールドにnullを設定できないPydanticの検証エラーが発生する場合です。
最も安全な方法は、すべてのフィールドをデフォルト値付きのOptionalにし、欠損データにはnullを許容して、抽出後に独自のビジネスロジックによる検証を行うことです。こうすることで、抽出に関する処理(テキストからデータを取り出すこと)と、検証に関する処理(データが要件を満たしているか確認すること)を分離できます。
import openai
from pydantic import BaseModel, ValidationError
from typing import Optional
client = openai.OpenAI()
class ContactExtraction(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
try:
result = client.beta.chat.completions.parse(
model='gpt-4o-mini',
messages=[
{'role': 'system', 'content': 'Extract contact information.'},
{'role': 'user', 'content': 'I would like to discuss partnership opportunities.'}
],
response_format=ContactExtraction
)
msg = result.choices[0].message
if msg.refusal:
print('Refused:', msg.refusal)
else:
contact = msg.parsed
if not any([contact.name, contact.email, contact.phone]):
print('No contact information found in text')
else:
print(contact.model_dump())
except ValidationError as e:
print('Validation failed:', e)instructorライブラリでPydanticを使用する
instructorライブラリは、OpenAIクライアントにパッチを適用し、検証に失敗した場合の自動リトライを備えたPydanticベースの抽出を可能にする、人気のサードパーティパッケージです。モデルがPydanticの検証に失敗する出力を返すと、instructorは検証エラーを含むプロンプトで自動的に再試行し、モデルが自ら修正する機会を与えます。
これは、各結果を手動で確認できないバッチ抽出パイプラインで特に役立ちます。人の介入なしにシステム自身で誤りを修正できるためです。
# pip install instructor
import instructor
import openai
from pydantic import BaseModel, Field
from typing import Optional
# Patch the OpenAI client with instructor
client = instructor.from_openai(openai.OpenAI())
class ProductInfo(BaseModel):
name: str
price_usd: float = Field(gt=0, description='Price must be positive')
brand: Optional[str] = None
# instructor automatically retries if Pydantic validation fails
product = client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role': 'user', 'content': 'The Sony WH-1000XM5 headphones cost $279.99 at Best Buy.'}
],
response_model=ProductInfo, # instructor-specific parameter
max_retries=3
)
print(f'{product.name}: ${product.price_usd} by {product.brand}')判別付きUnionと動的スキーマ
Pydanticは判別付きUnionに対応しています。これは、判別フィールドの値に応じて構造が変わるスキーマです。異なる文書タイプが共通の基盤を持ちながら、追加フィールドは異なる場合に役立ちます。たとえば、経費報告書には、出発地と到着地を含む航空券の領収書、またはチェックイン日とチェックアウト日を含むホテルの領収書のいずれかが含まれることがあります。
Literalの判別フィールドを持つUnion型を使用すると、複数の文書バリエーションを処理する単一の抽出スキーマを定義できます。モデルは文書の内容に基づいて正しいサブタイプを選択し、Pydanticは判別値に基づいて正しいサブタイプに対して自動的に検証します。
from pydantic import BaseModel
from typing import Union, Literal, Optional
class FlightExpense(BaseModel):
expense_type: Literal['flight']
airline: str
departure_city: str
arrival_city: str
amount_usd: float
class HotelExpense(BaseModel):
expense_type: Literal['hotel']
hotel_name: str
check_in: str
check_out: str
amount_usd: float
class MealExpense(BaseModel):
expense_type: Literal['meal']
restaurant: Optional[str]
amount_usd: float
class ExpenseReport(BaseModel):
submitter: str
expenses: list[Union[FlightExpense, HotelExpense, MealExpense]]
total_usd: float
print('Discriminated union schema - model selects correct subtype per item')理解度チェック
このレッスンで学んだAIエンジニアリングの概念を確認しましょう。
レッスンのまとめ
このレッスンでは、PydanticのBaseModelサブクラスによって強い型付けの抽出スキーマを定義でき、OpenAIのStructured OutputsがそれをAPIレベルで適用すること、ネストしたモデル、Optionalフィールド、List型によって現実の複雑な文書構造を処理できること、そしてinstructorライブラリによって検証失敗時の自動リトライが追加され、堅牢なバッチ抽出パイプラインを構築できることを学びました。次は、非構造化テキストソースのための完全な情報抽出パイプラインを構築します。
AI チューターと学ぶ Python — 無料
ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。
- コース
- 30
- レッスン
- 120
よくある質問
「Pydanticによる構造化出力」レッスンは無料ですか?
はい。「Pydanticによる構造化出力」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「Pydanticによる構造化出力」で何を学びますか?
Pydanticモデルを出力スキーマとして定義し、新しいstructured outputs機能を介してAPIに渡して、レスポンスを型付きPythonオブジェクトへ自動的にデシリアライズします。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「Pydanticによる構造化出力」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- JSONモードとresponse_format
- Pydanticによる構造化出力
- 非構造化テキストからのデータ抽出
- 不正な出力の検証と再試行