2026年2月: PythonでLLMを"安全に"扱う ― Pydantic AI入門(kadowaki)

門脇(@satoru_kadowaki)です。 2026年2月の「Python Monthly Topics」は、Python向けAIエージェントフレームワーク「Pydantic Ai」について紹介します。

はじめに

LLM(大規模言語モデル)をアプリケーションに組み込む開発が一般的になりつつあります。 LLMからの出力は、基本的には「ただの文字列」ですが、LLMのAPIから構造化されたデータを取得する手段も充実してきています。 主要なAIプロバイダーでは、以下のようにJSONスキーマ(JSON Schema)に沿った出力を保証する機能も提供されており、構造化された結果を得ることも比較的正確にできるようになってきました。

しかし、上記のように出力を構造化したとしても、出力結果の制約を全て満たすわけではなく、「LLMをアプリケーションに安全に組み込める」とは限りません。 実際の開発においては以下のようなケースに遭遇し、修正のためのコードを書くということもよくあります。

  • JSONスキーマの型制約は満たしているが、出力結果の制約(「n文字以内」、出力タグはn個まで」等)に違反した値が返ってくる

  • 制約違反時に、LLMへリトライさせる仕組みを実装する必要がある

  • AIプロバイダーごとに構造化出力やツール呼び出しの仕様が異なり、切り替え時にコードの書き換えが発生する

このような問題を解決するのがPydantic AIです。 Pydantic AIは、データバリデーションライブラリとして広く使われているPydanticが提供するPython向けAIエージェントフレームワークです。

本記事では、「OpenAI APIで構造化出力を扱うとどうなるか」を見た上で、Pydantic AIを体験してみます。

なお、Pydanticについては、本連載(Python Monthly Topics)でも過去に扱っていますので、ぜひ読んでみてください。

環境構築

それでは、早速進めていきましょう。 Pydantic AIの動作環境は以下となっています。

  • Python 3.10以上 3.14未満(Pydantic AIの要件)

  • Python 3.13 / pydantic-ai 1.59.0(本記事での動作確認環境)

インストール

Pydantic AIは、以下のようにpipでインストールできます。

pip install pydantic-ai

使用するモデルが決まっていて、余分なパッケージのインストールを避けたい場合は、pydantic-ai-slimパッケージを使用します。 例えば、OpenAI Modelだけを使用する場合は、以下を実行します。

pip install "pydantic-ai-slim[openai]"

APIキーの設定

本記事ではOpenAIのgpt-5.2を使用していますが、Pydantic AIは、Anthropic、Google Gemini、など多くのプロバイダーに対応しています。 モデル名の指定方法を変えるだけで切り替えられるため、記事のコード例は他のモデルでも動作します。

Pydantic AIで使用可能なモデルについては、以下を参照してください。

サンプルコードを実行するためには、OpenAIのAPIキーを環境変数に設定しておく必要があります。[1]

export OPENAI_API_KEY="sk-..."

動作確認

まずは最小限のコードでPydantic AIが動くことを確認しましょう。 以下のスクリプトをexample_1.pyとして保存して実行してみます。

example_1.py
from pydantic_ai import Agent

# 利用するLLMモデルと、エージェントの振る舞い(システム指示)を設定
agent = Agent(
    "openai:gpt-5.2",
    instructions="あなたは親切なアシスタントです。",
)

# 同期実行で1回だけ問い合わせ、レスポンスオブジェクトを受け取る
res = agent.run_sync("Pydantic AIについて一言で教えてください。")

# モデルが生成した最終テキストを表示
print(res.output)

example_1.pyの実行結果は以下のようになります。

python example_1.py
Pydantic AIは、Pydanticの型安全性を活かしてLLMアプリを「堅牢に」作るためのPythonライブラリです。

AgentはPydantic AIの中心的なクラスです。 モデル名とインストラクションを渡してインスタンスを作り、run_sync()メソッドででユーザーのプロンプトを送信します。 この時点ではLLMの出力はただの文字列(str)として返されます。

OpenAIのAPIだけで試してみる

Pydantic AIを理解するために、まずはOpenAIのAPIを直接使って構造化出力を取得してみましょう。 題材として、本連載「Python Monthly Topics」の記事一覧ページ(https://gihyo.jp/list/group/Python-Monthly-Topics)のHTMLから、各記事のメタデータを構造化して取得してみます。 記事一覧ページは、実際のHTMLの内容を抜粋したものを入力データとして、以下をpytopics.htmlとして保存し使用します。

pytopics.html
<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でパースとバリデーションを行うコードを以下のようにします。 Pydantic自体のバースやバリデーションに関する細かい解説は割愛しますが、記事情報のリストを表すPydanticモデルを定義しています。

example_2.py
from pathlib import Path

from pydantic import BaseModel, Field


# 解析対象のHTMLをファイルから読み込む
html_path = Path(__file__).with_name("pytopics.html")
with open(html_path, encoding="utf-8") as f:
    SAMPLE_HTML = f.read()


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_formatにJSONスキーマを指定し、Pydanticでパースとバリデーションを行うコードは以下のようになります。

example_2.py
from openai import OpenAI

from article_common import SAMPLE_HTML, ArticleList

# OpenAI APIクライアントを初期化
client = OpenAI()


# ---- OpenAI API Structured OutputsとAPI呼び出し ----
# Structured Outputsでは、すべてのオブジェクト型にadditionalProperties: false が必要
# 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から記事情報を構造化抽出する
response = client.responses.create(
    model="gpt-5.2",
    instructions="与えられたHTMLから記事の一覧情報を抽出してください。",
    input=SAMPLE_HTML,
    text={
        "format": {
            "type": "json_schema",
            "name": "ArticleList",
            "schema": schema,
        }
    },
)

# 返却されたJSON文字列をPydanticで検証しながらパース
article_list = ArticleList.model_validate_json(response.output_text)

# 抽出した記事情報を出力
for article in article_list.articles:
    print(f"{article}")

OpenAIのStructured Outputsは、スキーマ内のすべてのオブジェクト型にadditionalProperties: falseが設定されていることを要求しますが、Pydanticのmodel_json_schema()はこれを出力しません。そのため、スキーマを後処理するヘルパー関数が必要になります。 これはOpenAI固有の制約で、他のプロバイダーではまた別の調整が必要になる場合があります。

example_2.pyを実行すると、結果は以下となります。

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_dateが本当にYYYY-MM-DD形式になっているか

  • url/article/で始まる妥当なパスであるか

Pydanticのfield_validatorで検証することはできますが、バリデーション失敗時にLLMへ再生成を促すリトライの仕組みは自分で書く必要があります。 上記以外にも、ツール呼び出しの管理やモデルの切り替えについても同様で、AIプロバイダーが提供するAPIだけでは、開発者が多くのコードを書くことになります。

Pydantic AIでの実装

Pydantic AIでは、どのようにしてLLMを安全に使うことができるのか、まずはOpenAIのAPIで実装したコードと同じ処理をPydantic AIで書き換えてみましょう。

Pydantic AIで書き換える

サンプルHTMLの読み込みや、ArticleInfoArticleListモデルの定義はexample_2.pyをそのまま使用します。 OpenAIのAPIで実装していた部分を、Pydantic AIによる実装に書き換えます。 以下が書き換え後のコードです。

example_3.py
from pydantic_ai import Agent

from article_common import SAMPLE_HTML, ArticleList


# ---- Pydantic AIのAgentを使った構造化出力 ----
# Agentのoutput_typeにArticleListを指定
agent = Agent(
    "openai:gpt-5.2",
    output_type=ArticleList,  # スキーマを直接指定
    instructions="与えられたHTMLから記事の一覧情報を抽出してください。",
)

# SAMPLE_HTMLを入力としてAgentを実行
res = agent.run_sync(SAMPLE_HTML)

# res.outputはArticleList型のオブジェクトとして返される
for article in res.output.articles:
    print(f"{article}")

主な変更点としては、前述の動作確認で使用したAgentクラスの引数にoutput_type=ArticleListを指定するだけです。 これだけで、Pydantic AIが以下のような部分を吸収してくれています。

  • モデル定義からスキーマの生成

  • LLM呼び出し

  • レスポンスのパース、バリデーション

example_2.pyで使用したmake_strict_schema()のようなヘルパー関数や、JSONスキーマの変換も不要なため、シンプルでわかりやすいコードになっています。 また、res.outputArticleList型のオブジェクトとして返されるため、res.output.articles[0].titleのように名前付きの属性としてアクセスできます。

ここでは実行結果を割愛しますが、example_3.pyを実行するとexample_2.pyと同様の結果が得られます。

より安全に、バリデーションを強化する

Pydantic AIを使用することは、コードがシンプルになるだけではありません。 前述のとおり、Structured Outputsで型レベルの制約は保証されていても、「日付が妥当か」などの制約をLLMが守ってくれるとは限りません。 Pydantic AIでは、Pydanticモデルに以下のようなバリデーションルールを書くことで、LLMからの出力を安全に扱うことができます。

example_4.py
from datetime import date
from pathlib import Path

from pydantic import BaseModel, Field, field_validator
from pydantic_ai import Agent

# 解析対象のHTMLをファイルから読み込む
html_path = Path(__file__).with_name("pytopics.html")
with open(html_path, encoding="utf-8") as f:
    SAMPLE_HTML = f.read()


# 記事のメタデータを表すPydanticモデルを定義
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(相対パス)")

    # URLの形式を厳密に検証するためのカスタムバリデータ
    @field_validator("url")
    @classmethod
    def validate_url_format(cls, v: str) -> str:
        if not v.startswith("/article/"):
            raise ValueError(
                f"URLは'/article/'で始まる相対パスである必要があります(実際の値: {v})"
            )
        return v


# 記事情報のリストを表すPydanticモデルを定義
class ArticleList(BaseModel):
    """記事一覧"""

    # 記事のリストが空であってはならないことを示すルールを追加
    articles: list[ArticleInfo] = Field(min_length=1)


# 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(f"{article}")

具体的にどの部分のバリデーションが強化されているか、内容を以下にまとめます。

  • published_dateをstrではなくdate型を指定

    • LLMが「2025年12月」のような曖昧な形式で返した場合、Pydanticの日付検証でバリデーションエラーが発生する

  • field_validatorで記事URLが/article/で始まるパスであることを検証

    • LLMがフルURL(https://gihyo.jp/article/...)や不正なパスを返した場合、エラーメッセージが返される

  • titleやauthorは、min_length=1で空値を防ぐ

    • articlesリストにもmin_length=1を指定し、記事が1件も抽出されないケースを防いでいる

  • バリデーション失敗の場合に自動リトライ

    • Agentクラスにretries=3を指定するだけで、バリデーションエラー → LLMへのフィードバック → 再生成のループが最大3回まで自動実行される。エラーメッセージはPydanticが生成する具体的な内容であるため、LLMへのリトライも明確になる

このように、Pydantic AIではPydanticモデルの定義がLLM出力の検証ゲートになり、プロンプトの工夫に頼るだけでない、コードレベルの品質を保証することができます。

[コラム] なぜPydantic AI? - 他のアプローチとの比較

Pydantic AIのコードのシンプルさを体感したところで、LLMアプリケーション構築における他の選択肢と比較して、Pydantic AIの立ち位置を整理してみます。

AIプロバイダー提供のAPIやSDK

前述のようなOpenAIのAPIを使った方法です。 最も軽量で依存が少ない一方で、スキーマ変換(additionalPropertiesの付与など)、バリデーション失敗時のリトライ、ツール呼び出しループの管理といったコードを自前で書く必要があります。 AIプロバイダーを切り替える場合は、コードの大幅な書き換えも発生するため、小規模なスクリプトには十分ですが、アプリケーションによってはそれなりにコードを書く必要があります。

LangChain

LangChain は、LLMアプリケーション開発のフレームワークとして最も早くから普及し、あらゆるコンポーネントを提供してくれています。 しかし最近では、以下のようなLangChainの抽象化の複雑さに対する懸念も見られるようになりました。

  • 非推奨になったAPIが残り続けているため、同じことを実現する方法が複数あり、デバッグ時のスタックトレースを追いにくい

  • シンプルなユースケースに対して、LangChainの抽象レイヤーが複雑でオーバーヘッドが大きい

特に本記事のような「構造化された出力を型安全に取得したい」といったケースでは、LangChainのオーバーヘッドが大きいと感じるかもしれません。

Pydantic AIの立ち位置

Pydantic AIは、LangChainのような「全部入り」のフレームワークではなく、Pydanticの設計思想をそのまま受け継いだ、スキーマファーストのエージェント構築基盤です。 先ほどのコードで試したとおり、以下のような特徴があります。

  • 型安全が前提

    • output_typeにPydanticモデルを指定するだけで、スキーマ生成・LLM呼び出し・バリデーション・リトライが一貫して行われるため、パーサーやスキーマ変換関数を自前で書く必要がない

  • プロバイダー非依存

    • モデル名を"openai:gpt-5.2"から"anthropic:claude-sonnet-4-5"のように変えるだけで切り替えられる

    • additionalPropertiesのようなプロバイダー固有の制約もPydantic AIが吸収してくれる

  • デバッグしやすい

    • 標準のPythonオブジェクトが返るため、型チェッカーも利用可能

    • 外部の関数やAPIを呼び出す処理の定義はデコレーター付きの関数やdataclassなど、Python標準の書き方で完結できる

「LLMの出力をPydanticモデルで安全に結果を受け止める」ことで、LLMエージェントを使用したアプリケーション開発にPydanticを取り入れるというシンプルな目的を実現します。

Pydantic AIのさらに便利な機能

ここまでは「HTMLを読んでメタデータを返す」というタスクをLLMに与えていました。 しかし、実際のアプリケーション開発では、データベースへの問い合わせや外部APIの呼び出しなど、LLMだけでは完結しない処理が多くあります。 Pydantic AIでは、こうした外部処理をエージェントに組み込む仕組みとして「ツール(Tools)」が用意されています。 ツールは、Python関数にデコレーターを付けるだけで登録でき、LLMが必要に応じて呼び出します。

ツール(Tools)を定義する

ここでは例として、各記事にカテゴリやタグを付与してみます。 カテゴリの判定には記事本文を使用します。 記事本文の処理をツールとして切り出すことで、「データ処理部分は確実な処理として組み込み、判定のみLLMに任せる」という役割分担を実現します。 なお、記事の取得処理部分もHTTPXなどのHTTPクライアントを使用して取得するのが一般的ですが、今回はその処理部分は割愛して以下のように記事が取得済みである想定とします。

article_body.py
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の出力モデルにcategorytagsを追加します。

snippet_1.py
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は前節と同じため省略

最後にエージェントとツールを以下のように定義します。

snippet_2.py
# 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_article_bodyツールを使用するように指示することで、LLMは各記事に対してを呼び出し、本文を参照してカテゴリとタグが判定されます。

ツールは、@agent.tool_plainデコレーターを使用して関数をツールとして定義しています。 ツールの特徴として、Pydantic AIが関数のdocstringをツールの説明文として自動的にLLMに渡すというのがあります。 引数の説明もdocstring内のArgsセクションから抽出されるため、Pythonの標準的なドキュメンテーション慣習に従うだけでLLMへのツール説明が整うことになります。

ここまでのコードを組み合わせた完全版をexample_5.pyとして保存します。 実行結果は以下のようになり、カテゴリとタグが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)

最後に依存性注入(Dependency Injection)について解説します。

前述のツール定義では、記事本文データ(ARTICLE_BODIES)をグローバル変数として定義していました。 しかし実際のアプリケーションでは、「本番ではデータベースから取得し、テストではモックデータを使いたい」ということがあります。 このような場面で役立つのが依存性注入(Dependency Injection)です。 依存性注入とは、関数やクラスが必要とするリソース(依存)を外部から渡す方法のことです。 Pydantic AIでは、deps_typeパラメータで依存リソースの型を宣言し、RunContextを通じてツールやインストラクションの中からそのリソースにアクセスできます。

ここでは、前節のグローバル変数 ARTICLE_BODIES を依存性注入に置き換える例を示します。 依存リソースはdataclassやTypedDictなど、Pythonの標準的な方法で定義できます。 以下のように、後の処理でツールからctx.deps.article_bodiesとしてアクセスできるように、記事本文の辞書をarticle_bodiesフィールドとしてクラスを定義します。

snippet_3.py
from dataclasses import dataclass


@dataclass
class ArticleDeps:
    # エージェントが必要とする依存リソースをまとめたdataclass
    article_bodies: dict[str, str]

続いてAgentdeps_type引数にArticleDepsを渡します。 これにより、Pydantic AIがツールやinstructionsの中でリソースへアクセスできるようになります。

snippet_4.py
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.tool_plainの代わりに@agent.toolデコレーターを使い、第1引数にRunContext[ArticleDeps]を指定して受け取ります。 これにより、ctx.deps 属性を使用してリソースにアクセスできるため、グローバル変数を参照する必要がなくなります。

snippet_5.py
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_sync()メソッドのdeps引数に依存リソースを渡します。 エージェントはこの依存リソースを実行中に保持し、ツールが呼び出されるたび依存関係としてLLMに提供します。

snippet_6.py
# 本番用データ(実際には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型チェッカーです...",
        # ...
    }
)

res = agent.run_sync(SAMPLE_HTML, deps=prod_deps)
for article in res.output.articles:
    print(article)

依存性注入の最大のメリットは、テスト時に依存リソースを容易に差し替えられる点です。 deps引数に渡すオブジェクトを切り替えるだけでテストと本番環境での実行が行えます。

グローバル変数との違いを簡単にまとめると以下のようになります。

比較項目

グローバル変数

依存性注入

テスト

モック化が難しい

deps を差し替えるだけ

本番・開発環境の切り替え

コードの書き換えが必要

インスタンス生成時に分岐するだけ

可読性

依存関係が暗黙的

何に依存しているか型で明示される

並行実行

状態競合のリスクあり

実行ごとにdepsを渡すため安全

Pydantic AIの依存性注入は、deps_typeRunContextdeps の3つのキーワードを押さえるだけです。 グローバル変数への依存をなくし、テスタブルで保守しやすいエージェントコードを書くために、ぜひ活用してみてください。

まとめ

本記事では、LLMアプリ開発でPythonを安全に活用する「Pydantic AI」を紹介しました。

OpenAI APIを直接使う場合と比較すると、output_type にPydanticモデルを指定するだけでスキーマ生成・バリデーション・リトライが一貫して行われ、プロバイダーの差異もPydantic AIが吸収してくれるのは開発者にとって大きなメリットです。 バリデーションの強化(field_validator、型制約)、ツールによる外部処理の組み込み、依存性注入によるテスタビリティの確保と、実際のアプリケーション開発で求められる要素が一通りカバーされています。

また、本記事では紹介しきれませんでしたが、Pydantic AIはMCP(Model Context Protocol)にも対応しています。 本記事のような用途ではツールを直接定義する方がシンプルですが、「社内で共通ツールを管理したい」「Claude Desktopと同じツールをエージェントでも使いたい」といった場面では、MCPとの連携が有効な選択肢になります。 興味のある方は、Pydantic AIのMCPについても試してみてください。

「LLMの出力をPydanticモデルで安全に受け止める」というシンプルな思想のもと、Pythonらしい書き方でエージェントを構築できるのがPydantic AIの魅力です。 LLMをアプリに組み込む際の選択肢の一つとして、ぜひ試してみてください。