門脇
はじめに
LLM
- [OpenAI]
OpenAI Developers: Structured model outputs - [Anthropic]
Claude API Docs: Structured outputs - [Google]
Gemini API: Structured outputs
しかし、上記のように出力を構造化したとしても、出力結果の制約を全て満たすわけではなく、
- JSONスキーマの型制約は満たしているが、出力結果の制約
(n文字以内、出力タグはn個まで等) に違反した値が返ってくる - 制約違反時に、LLMへリトライさせる仕組みを実装する必要がある
- AIプロバイダーごとに構造化出力やツール呼び出しの仕様が異なり、切り替え時にコードの書き換えが発生する
このような問題を解決するのがPydantic AIです。Pydantic AIは、データバリデーションライブラリとして広く使われているPydanticの開発チームが手がけるPython向けAIエージェントフレームワークです。
本記事では、
なお、Pydanticについては、本連載
- 2025年10月:データ検証ライブラリPydanticの紹介
環境構築
それでは、早速進めていきましょう。Pydantic AIの動作環境は以下となっています。
- Python 3.
10以上 - Python 3.
14 / pydantic-ai 1. 62. 0 (本記事での動作確認環境)
インストール
Pydantic AIは、以下のようにpipでインストールできます。
$ pip install pydantic-ai
使用するモデルが決まっていて、余分なパッケージのインストールを避けたい場合は、pydantic-ai-slimパッケージを使用します。たとえば、OpenAI Modelだけを使用する場合は、以下を実行します。
$ pip install "pydantic-ai-slim[openai]"
APIキーの設定
本記事ではOpenAIのgpt-5.を使用していますが、Pydantic AIは、Anthropic、Google Gemini、など多くのAIプロバイダーのAPIに対応しています。モデル名の指定方法を変えるだけで切り替えられるため、記事のコード例は他のモデルでも動作します。
Pydantic AIで使用可能なモデルについては、以下を参照してください。
サンプルコードを実行するためには、OpenAIのAPIキーを環境変数に設定しておく必要があります[1]。
export OPENAI_API_KEY="sk-..."
動作確認
まずは最小限のコードでPydantic AIが動くことを確認しましょう。以下のスクリプトをexample_
from pydantic_ai import Agent
# 利用するLLMモデルと、エージェントの振る舞い(システム指示)を設定
agent = Agent(
"openai:gpt-5.2",
instructions="あなたは親切なアシスタントです。",
)
# 同期実行で1回だけ問い合わせ、レスポンスオブジェクトを受け取る
res = agent.run_sync("Pydantic AIについて一言で教えてください。")
# モデルが生成した最終テキストを表示
print(res.output)
example_
$ python example_1.py Pydantic AIは、Pydanticの型安全性を活かしてLLMアプリを「堅牢に」作るためのPythonライブラリです。
AgentはPydantic AIの中心的なクラスです。モデル名とインストラクションを渡してインスタンスを作り、run_メソッドでユーザーのプロンプトを送信します。この時点ではLLMの出力はただの文字列
OpenAIのAPIで構造化出力を試す
Pydantic AIを理解するために、まずはOpenAIのAPIを直接使って構造化出力を試してみましょう。題材として、本連載https://)
<ul>
<li><a href="/article/2026/01/monthly-python-2601">
Pythonで始めるマルチエージェントAI ― CrewAI入門
<span class="author">杉田雅子</span>
<time>2026-01-27</time>
</a></li>
<li><a href="/article/2025/12/monthly-python-2512">
Rustで書かれた高速Python型チェッカー「Pyrefly」の紹介
<span class="author">筒井隆次</span>
<time>2025-12-04</time>
</a></li>
<li><a href="/article/2025/11/monthly-python-2511">
t-string:テンプレート文字列リテラルの紹介
<span class="author">鈴木たかのり</span>
<time>2025-11-18</time>
</a></li>
<li><a href="/article/2025/10/monthly-python-2510">
データ検証ライブラリPydanticの紹介
<span class="author">寺田学</span>
<time>2025-10-28</time>
</a></li>
<li><a href="/article/2025/09/monthly-python-2509">
Python 3.14の新機能:asyncioタスク可視化機能を使ってみよう
<span class="author">福田隼也</span>
<time>2025-09-30</time>
</a></li>
<li><a href="/article/2025/08/monthly-python-2508">
PrefectではじめるPythonワークフロー・フレームワーク
<span class="author">門脇諭</span>
<time>2025-08-28</time>
</a></li>
</ul>
OpenAI APIでの実装
各記事のメタデータをリストとして抽出してみます。最初に、処理に使用するHTMLの読み込みと、Pydanticでパースとバリデーションを行うコードを以下のようにします。この部分は、OpenAIのAPIの例とPydantic AIの例の両方で使用する共通処理です。Pydantic自体のパースやバリデーションに関する細かい解説は割愛しますが、記事情報のリストArticleInfoのリストArticleList)
from pathlib import Path
from pydantic import BaseModel, Field
# 解析対象のHTMLをファイルから読み込む
html_path = Path(__file__).with_name("pytopics.html")
SAMPLE_HTML = html_path.read_text(encoding="utf-8")
class ArticleInfo(BaseModel):
"""記事のメタデータ"""
title: str = Field(description="記事のタイトル")
author: str = Field(description="著者名")
published_date: str = Field(description="公開日(YYYY-MM-DD形式)")
url: str = Field(description="記事のURL(相対パス)")
class ArticleList(BaseModel):
"""記事一覧"""
articles: list[ArticleInfo]
続いて、OpenAIのAPIを使用する例です。response_
from openai import OpenAI
# 共通処理で宣言したSAMPLE_HTMLとクラスをインポート
from article_common import SAMPLE_HTML, ArticleList
# OpenAI APIクライアントを初期化
client = OpenAI()
# ---- OpenAI API Structured OutputsとAPI呼び出し ----
# PydanticモデルからJSONスキーマを生成し、OpenAI Structured Outputsの要件に合わせて加工する
# Structured Outputsでは、すべてのオブジェクト型にadditionalProperties: false が必要となっている
# 参考: https://developers.openai.com/api/docs/guides/structured-outputs
# しかし、Pydanticの model_json_schema() はこの設定を自動で追加しないため、手動で追加する
def make_strict_schema(schema: dict) -> dict:
"""JSONスキーマにadditionalProperties: falseを再帰的に追加する"""
if schema.get("type") == "object":
schema["additionalProperties"] = False
for value in schema.values():
if isinstance(value, dict):
make_strict_schema(value)
return schema
schema = make_strict_schema(ArticleList.model_json_schema())
# Responses APIでHTMLから記事情報を構造化抽出する
# openai.types.responses.response.Responseオブジェクトが返される
res = client.responses.create(
model="gpt-5.2",
instructions="与えられたHTMLから記事の一覧情報を抽出してください。",
input=SAMPLE_HTML,
text={ # JSONスキーマに従って構造化したデータを返すように指示
"format": {
"type": "json_schema",
"name": "ArticleList",
"schema": schema,
}
},
)
# 返却されたJSON文字列をPydanticで検証しながらパース
article_list = ArticleList.model_validate_json(res.output_text)
# 抽出した記事情報を出力
for article in article_list.articles:
print(article)
OpenAIのStructured Outputsは、スキーマ内のすべてのオブジェクト型にadditionalProperties: falseが設定されていることを要求しますが、Pydanticのmodel_はこれを出力しません。そのため、スキーマを後処理するヘルパー関数が必要になります。これはOpenAI固有の制約で、他のプロバイダーではまた別の調整が必要になる場合があります。additionalPropertiesについては前述のStructured model outputsに必須項目の解説がありますので、参照してみてください。
example_
$ python example_2.py title='Pythonで始めるマルチエージェントAI ― CrewAI入門' author='杉田雅子' published_date='2026-01-27' url='/article/2026/01/monthly-python-2601' (省略) title='PrefectではじめるPythonワークフロー・フレームワーク' author='門脇諭' published_date='2025-08-28' url='/article/2025/08/monthly-python-2508'
上記の結果から、構造化出力データをJSON化することが可能であることがわかります。しかし、前述の説明にもあるように、Pydanticモデルからスキーマを生成してOpenAIに渡すだけでもひと手間かかっています。
また、仮に以下のような制約を設けたい場合、JSONスキーマだけでは対応しきれません。
published_が本当にYYYY-MM-DD形式になっているかdate urlが/article/で始まる妥当なパスであるか
これらは、Pydanticのfield_で検証することはできますが、バリデーション失敗時にLLMへ再生成を促すリトライの仕組みは自分で書く必要があります。上記以外にも、ツール呼び出しの管理やモデルの切り替えについても同様で、AIプロバイダーが提供するAPIだけでは、開発者が多くのコードを書くことになります。
Pydantic AIで構造化出力を実装する
Pydantic AIでは、どのようにしてLLMから適切に構造化したデータを受け取ることができるのか、まずはOpenAIのAPIで実装したコードと同じ処理をPydantic AIで書き換えてみましょう。
Pydantic AIで書き換える
サンプルHTMLの読み込みや、ArticleInfo、ArticleListモデルの定義はarticle_
from pydantic_ai import Agent
# 共通処理で宣言したSAMPLE_HTMLとクラスをインポート
from article_common import SAMPLE_HTML, ArticleList
# Agentのoutput_typeにArticleListを指定
agent = Agent(
"openai:gpt-5.2",
output_type=ArticleList, # スキーマとしてPydanticのモデルを指定
instructions="与えられたHTMLから記事の一覧情報を抽出してください。",
)
# SAMPLE_HTMLを入力としてAgentを実行
res = agent.run_sync(SAMPLE_HTML)
# res.outputはArticleList型のオブジェクトとして返される
for article in res.output.articles:
print(article)
主な変更点としては、前述の動作確認で使用したAgentクラスの引数にoutput_を指定するだけです。これだけで、Pydantic AIが以下のような部分を吸収してくれています。
- Pydanticのモデル定義からスキーマの生成
- LLM呼び出し
- レスポンスのパース、バリデーション
example_make_のようなヘルパー関数や、JSONスキーマの変換も不要なため、シンプルでわかりやすいコードになっています。また、res.はArticleList型のオブジェクトとして返されるため、res.のように名前付きの属性としてアクセスできます。
ここでは実行結果を割愛しますが、example_
より安全に、バリデーションを強化する
Pydantic AIを使用することは、コードがシンプルになるだけではありません。前述のとおり、Structured Outputsで型レベルの制約は保証されていても、
これまでのコードと同様に、バリデーションルールをarticle_
from datetime import date
from pathlib import Path
from pydantic import BaseModel, Field, field_validator
# 解析対象のHTMLをファイルから読み込む
html_path = Path(__file__).with_name("pytopics.html")
SAMPLE_HTML = html_path.read_text(encoding="utf-8")
class ArticleInfo(BaseModel):
"""記事のメタデータ"""
title: str = Field(
min_length=1, description="記事のタイトル"
) # min_length=1で空文字を禁止
author: str = Field(min_length=1, description="著者名")
published_date: date = Field(description="公開日") # date型で日付形式を強制
url: str = Field(description="記事のURL(相対パス)")
@field_validator("url")
@classmethod
def validate_url_format(cls, v: str) -> str:
"""URLが`/article/`で始まる相対パスかを検証し、不正なら例外を送出する"""
if not v.startswith("/article/"):
raise ValueError(
f"URLは'/article/'で始まる相対パスである必要があります(実際の値: {v})"
)
return v
class ArticleList(BaseModel):
"""記事一覧"""
articles: list[ArticleInfo] = Field(min_length=1)
具体的にどの部分のバリデーションが強化されているか、内容を以下にまとめます。
- titleやauthorは、
min_で空値を防ぐlength=1 - articlesリストにも
min_を指定し、記事が1件も抽出されないケースを防いでいるlength=1
- articlesリストにも
- published_
dateをstrではなくdate型を指定 - LLMが
「2025年12月」 のような曖昧な形式で返した場合、Pydanticの日付検証でバリデーションエラーが発生する
- LLMが
field_で記事URLが/article/で始まるパスであることを検証validator - LLMがフルURL
(https:// gihyo. jp/ article/ ...) や不正なパスを返した場合、エラーメッセージが返される
- LLMがフルURL
Agentクラスについては前述のコードとほぼ同じですが、バリデーション失敗時に自動リトライが行われるようにretries引数を指定します。
from pydantic_ai import Agent
# 共通処理で宣言したSAMPLE_HTMLと厳密な記事一覧モデルをインポート
from article_common_strict import SAMPLE_HTML, ArticleList
# retriesオプションを追加
agent = Agent(
"openai:gpt-5.2",
output_type=ArticleList,
instructions="与えられたHTMLから記事の一覧情報を抽出してください。",
retries=3, # バリデーション失敗時に最大3回リトライ
)
res = agent.run_sync(SAMPLE_HTML)
# res.outputはArticleList型のオブジェクトとして返される
for article in res.output.articles:
print(article)
Agentクラスにretries=3を指定するだけで、バリデーションエラー → LLMへのフィードバック → 再生成のループが最大3回まで自動実行されます。また、エラー発生時のエラーメッセージは、Pydanticが生成する具体的な内容であるため、LLMへのリトライも明確になり、より正確なリトライへ繋げることができます。
このように、Pydantic AIではPydanticモデルの定義がLLM出力の検証ゲートになり、プロンプトの工夫に頼るだけでない、コードレベルの品質を保証することができます。
Pydantic AIのさらに便利な機能
ここまでは
Pydantic AIでは、こうした外部処理をエージェントに組み込む仕組みとして
ツール(Tools)を定義する
ここでは例として、各記事にカテゴリやタグを付与してみます。カテゴリの判定には記事本文を使用します。記事本文の処理をツールとして切り出すことで、
なお、記事の取得処理部分もHTTPXなどのHTTPクライアントを使用して取得するのが一般的ですが、今回はその処理部分は割愛して以下のように記事が取得済みである想定とします。
ARTICLE_BODIES: dict[str, str] = {
"/article/2026/01/monthly-python-2601": "CrewAIは複数のAIエージェントを協調させるフレームワークです...",
"/article/2025/12/monthly-python-2512": "PyreflyはMeta社が開発したRust製のPython型チェッカーです...",
"/article/2025/11/monthly-python-2511": "t-stringはPython 3.14で導入されたテンプレート文字列リテラルです...",
"/article/2025/10/monthly-python-2510": "Pydanticはデータバリデーションと設定管理のためのライブラリです...",
"/article/2025/09/monthly-python-2509": "Python 3.14ではasyncioにタスク可視化機能が追加されました...",
"/article/2025/08/monthly-python-2508": "PrefectはPythonベースのワークフロー管理フレームワークです...",
}
続いて、Pydanticの出力モデルにcategoryとtagsを追加します。
from datetime import date
from typing import Literal
from pydantic import BaseModel, Field
class ArticleInfo(BaseModel):
"""記事のメタデータ(カテゴリ・タグ付き)"""
title: str = Field(min_length=1, description="記事のタイトル")
author: str = Field(min_length=1, description="著者名")
published_date: date = Field(description="公開日")
url: str = Field(description="記事のURL(相対パス)")
# カテゴリは以下の選択肢のいずれかとする
category: Literal[
"Web開発",
"型・ツール",
"AI・機械学習",
"パッケージ管理",
"Python新機能",
"その他",
] = Field(description="記事のカテゴリ")
# タグは1~5個のリストとする
tags: list[str] = Field(
min_length=1,
max_length=5,
description="技術タグのリスト",
)
# field_validatorは前節と同じため省略
最後にエージェントとツールを以下のように定義します。
# Agentのinstructionsで、ツールを使って記事本文を参照するように指示
agent = Agent(
"openai:gpt-5.2",
output_type=ArticleList,
instructions=(
"与えられたHTMLから記事の一覧情報を抽出してください。"
"各記事のカテゴリとタグを判定するために、"
"fetch_article_bodyツールで記事本文を参照してください。"
),
retries=3,
)
# fetch_article_bodyツールを定義
@agent.tool_plain
def fetch_article_body(url: str) -> str:
"""記事のURLを受け取り、記事本文の冒頭部分を返す。
Args:
url: 記事の相対パス(例: /article/2026/01/monthly-python-2601)
"""
body = ARTICLE_BODIES.get(url)
if body:
return body
return f"{url}: 本文が見つかりません"
Agentでは、instructions引数にカテゴリとタグ判別を追加しています。その際、fetch_
ツールは、@agent.デコレーターを使用して関数をツールとして定義しています。ツールの特徴として、Pydantic AIが関数のdocstringをツールの説明文として自動的にLLMに渡すという機能があります。引数の説明もdocstring内のArgsセクションから抽出されるため、Pythonの標準的なドキュメンテーション慣習に従うだけでLLMへのツール説明が整うことになります。
ここまでのコードを合わせた完全版をexample_
実行結果は以下のようになり、カテゴリとタグがLLMによって判定されていることが確認できます。
$ python example_5.py title='Pythonで始めるマルチエージェントAI ―CrewAI入門' author='杉田雅子' published_date=datetime.date(2026, 1, 27) url='/article/2026/01/monthly-python-2601' category='AI・機械学習' tags=['Python', 'マルチエージェント', 'AI', 'CrewAI'] title='Rustで書かれた高速Python型チェッカー「Pyrefly」の紹介' author='筒井隆次' published_date=datetime.date(2025, 12, 4) url='/article/2025/12/monthly-python-2512' category='型・ツール' tags=['Python', '型チェック', 'Pyrefly', 'Rust'] title='t-string:テンプレート文字列リテラルの紹介' author='鈴木たかのり' published_date=datetime.date(2025, 11, 18) url='/article/2025/11/monthly-python-2511' category='Python新機能' tags=['Python', 'Python 3.14', 't-string', '文字列'] title='データ検証ライブラリPydanticの紹介' author='寺田学' published_date=datetime.date(2025, 10, 28) url='/article/2025/10/monthly-python-2510' category='Web開発' tags=['Python', 'Pydantic', 'データ検証', 'スキーマ'] title='Python 3.14の新機能:asyncioタスク可視化機能を使ってみよう' author='福田隼也' published_date=datetime.date(2025, 9, 30) url='/article/2025/09/monthly-python-2509' category='Python新機能' tags=['Python', 'Python 3.14', 'asyncio', '可視化'] title='PrefectではじめるPythonワークフロー・フレームワーク' author='門脇諭' published_date=datetime.date(2025, 8, 28) url='/article/2025/08/monthly-python-2508' category='その他' tags=['Python', 'Prefect', 'ワークフロー', 'ETL']
依存性注入(Dependency Injection)
最後に依存性注入
前述のツール定義では、記事本文データARTICLE_)
ここでは、前節のグローバル変数 ARTICLE_ を依存性注入に置き換える例を示します。依存リソースはdataclassやTypedDictなど、Pythonの標準的な方法で定義できます。以下のように、後の処理でツールからアクセスできるように、記事本文の辞書をarticle_フィールドとしてクラスを定義します。
from dataclasses import dataclass
@dataclass
class ArticleDeps:
# エージェントが必要とする依存リソースをまとめたdataclass
article_bodies: dict[str, str]
続いてAgentのdeps_引数にArticleDepsを渡します。これにより、Pydantic AIがツールやinstructionsの中でリソースへアクセスできるようになります。
from pydantic_ai import Agent
agent = Agent(
"openai:gpt-5.2",
output_type=ArticleList,
deps_type=ArticleDeps, # ← 依存リソースの型を宣言
instructions=(
"与えられたHTMLから記事の一覧情報を抽出してください。"
"各記事のカテゴリとタグを判定するために、"
"fetch_article_bodyツールで記事本文を参照してください。"
),
retries=3,
)
ツールの宣言では、RunContextからリソースを受け取るようにします。以下のように、前述の@agent.の代わりに@agent.デコレーターを使い、第1引数にRunContext[ArticleDeps]を指定して受け取ります。これにより、前述のdeps_パラメータで依存リソースの型を宣言し、RunContextを通じてツールやインストラクションの中からそのリソースにアクセスすることができます。
fetch_関数内では、ctx. 属性を使用してctx.のようにリソースにアクセスできるため、グローバル変数を参照する必要がなくなります。
from pydantic_ai import RunContext
@agent.tool
def fetch_article_body(ctx: RunContext[ArticleDeps], url: str) -> str:
"""記事のURLを受け取り、記事本文の冒頭部分を返す。
Args:
ctx: 実行コンテキスト(依存リソースへのアクセス手段)
url: 記事の相対パス(例: /article/2026/01/monthly-python-2601)
"""
body = ctx.deps.article_bodies.get(url)
if body:
return body
return f"{url}: 本文が見つかりません"
最後にrun_メソッドのdeps引数に依存リソースを渡します。エージェントはこの依存リソースを実行中に保持し、ツールが呼び出されるたび依存関係としてLLMに提供します。
# 本番用データ(実際にはDBやAPIから取得)
prod_deps = ArticleDeps(
article_bodies={
"/article/2026/01/monthly-python-2601": "CrewAIは複数のAIエージェントを協調させるフレームワークです...",
"/article/2025/12/monthly-python-2512": "PyreflyはMeta社が開発したRust製のPython型チェッカーです...",
# ...
}
)
# run_syncの第1引数は、モデルに解析させる対象データ(SAMPLE_HTML)
# deps引数は、ツール内で参照する外部依存データ(prod_deps)
res = agent.run_sync(SAMPLE_HTML, deps=prod_deps)
for article in res.output.articles:
print(article)
ここまでのコードを合わせた完全版をexample_
依存性注入の最大のメリットは、テスト時に依存リソースを容易に差し替えられる点です。deps引数に渡すオブジェクトを切り替えるだけでテストと本番環境での実行が行えます。
依存性注入とグローバル変数の違いを簡単にまとめると以下のようになります。
| 比較項目 | グローバル変数 | 依存性注入 |
|---|---|---|
| テスト | モック化が難しい | deps を差し替えるだけ |
| 本番・ |
コードの書き換えが必要 | インスタンス生成時に分岐するだけ |
| 可読性 | 依存関係が暗黙的 | 何に依存しているか型で明示される |
| 並行実行 | 状態競合のリスクあり | 実行ごとにdepsを渡すため安全 |
Pydantic AIの依存性注入は、deps_・RunContext・deps の3つのキーワードを押さえるだけです。グローバル変数への依存をなくし、テスタブルで保守しやすいエージェントコードを書くために、ぜひ活用してみてください。
まとめ
本記事では、Pythonで型安全なLLMアプリを作成する
OpenAI APIを直接使う場合と比較すると、output_ にPydanticモデルを指定するだけでスキーマ生成・field_、型制約)、ツールによる外部処理の組み込み、依存性注入によるテスタビリティの確保と、実際のアプリケーション開発で求められる要素が一通りカバーされています。
また、本記事では紹介しきれませんでしたが、Pydantic AIはMCP
「LLMの出力をPydanticモデルで安全に受け止める」
