PydanticAI ビジュアルガイド

Lesson CH02-L01

DI入門

deps_typeとRunContextで依存値を注入する基本パターン。

読了目安
12 min
Colab目安
18 min
合計
30 min
前提
静的型チェック

関連: 公式ドキュメント

一行サマリ

deps_type=YourDeps で Agent に 依存値の型 を宣言し、agent.run(..., deps=YourDeps(...)) で実行時に値を渡す — Agent 内のあらゆる関数は RunContext[YourDeps] 経由で ctx.deps として参照できる。これが PydanticAI の DI。

ヒーロー: Agent の "外" にあるべきもの

Agent 自身がやるべきは「LLM に投げる・結果を返す」だけです。HTTP クライアント・DB 接続・APIキー・設定値などは Agent の外で生成し、deps として注入 するのが正解です。

図を読み込み中…
図1. DI で外部リソースを Agent に「差し込む」

概念: なぜ Agent に DI が必要か

「依存性注入 (DI)」と呼ぶと大げさですが、目的はシンプルに次の 3 つです。

  1. テスト容易性agent.run(..., deps=FakeDeps(...)) で本物の HTTP / DB の代わりにモックを差し込める (Ch2-L02 で扱う)
  2. 明示性 — Agent が "何に依存しているか" が引数として表に出る。グローバル変数で隠れない
  3. 再利用性 — 同じ Agent インスタンスをユーザー別 / 環境別の deps と組み合わせて使い回せる

PydanticAI の DI はフレームワーク特有の "コンテナ" を持たず、deps_type という型 + RunContext 経由のアクセス」だけ のミニマル設計です。覚えるべきことは少ないです。

図を読み込み中…
図2. DI ありとなしのコード比較

コード: 4 つのパターン

パターン 1: もっとも単純な Deps (in-memory store)

from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
 
@dataclass
class CityDb:
    cities: dict[str, str]   # 国名 -> 首都名 の辞書
 
agent = Agent(
    'google-gla:gemini-3-flash-preview',
    deps_type=CityDb,
    instructions='ユーザーの国名から首都を答えてください。',
)
 
@agent.instructions
def known_cities(ctx: RunContext[CityDb]) -> str:
    countries = ', '.join(ctx.deps.cities.keys())
    return f'参照可能な国は: {countries}'
 
deps = CityDb(cities={'日本': '東京', 'フランス': 'パリ', 'ブラジル': 'ブラジリア'})
print(agent.run_sync('フランスの首都は?', deps=deps).output)

ポイント:

  • Deps は @dataclass で定義 するのが最もシンプル (mutable / コンストラクタ自動生成 / 型で見通しよし)
  • deps_type=CityDb を Agent に宣言 → RunContext[CityDb] で受けられる
  • 同じ Agent を agent.run(..., deps=CityDb({'日本': '東京'})) のように 呼び出しごとに別 deps で動かせる

パターン 2: 複数のリソースをまとめる

実プロジェクトでは依存が 1 つで済むことは稀です。1 つの dataclass に全部まとめる のが定石です。

import os
from dataclasses import dataclass, field
import httpx
from pydantic_ai import Agent, RunContext
 
@dataclass
class AppConfig:
    api_base_url: str
    user_lang: str
 
@dataclass
class AppDeps:
    config: AppConfig
    http: httpx.AsyncClient
    cache: dict[str, str] = field(default_factory=dict)
 
agent = Agent(
    'google-gla:gemini-3-flash-preview',
    deps_type=AppDeps,
    instructions='ユーザーの問いに答えてください。',
)
 
@agent.instructions
def lang(ctx: RunContext[AppDeps]) -> str:
    return f'回答は {ctx.deps.config.user_lang} で。'
 
# 起動時に 1 度だけ重い初期化
async def make_deps() -> AppDeps:
    return AppDeps(
        config=AppConfig(api_base_url=os.environ['API_BASE'], user_lang='ja'),
        http=httpx.AsyncClient(timeout=10.0),
    )
 
# 利用側
deps = await make_deps()
print((await agent.run('Tokyo の現在気温は?', deps=deps)).output)

ポイント:

  • AppConfig のような 不変設定httpx.AsyncClient のような 長寿命オブジェクト を 1 つの dataclass に集約
  • field(default_factory=dict)可変なフィールド にデフォルトを与える
  • 重い初期化は make_deps() のような factory 関数 に分離して、テストでは make_test_deps() を用意

パターン 3: Tool / Validator から deps を参照

@agent.instructions だけでなく、Tool (Ch3)output_validator からも同じ RunContext[Deps] で deps にアクセスできます。

from pydantic_ai import Agent, RunContext, ModelRetry
 
@dataclass
class Deps:
    blocked_words: set[str]
 
agent = Agent(
    'google-gla:gemini-3-flash-preview',
    deps_type=Deps,
    instructions='短い日本語で答えてください。',
)
 
@agent.output_validator
def no_blocked(ctx: RunContext[Deps], output: str) -> str:
    for word in ctx.deps.blocked_words:
        if word in output:
            raise ModelRetry(f'「{word}」を含めずに言い換えてください。')
    return output
 
deps = Deps(blocked_words={'秘密', 'パスワード'})
print(agent.run_sync('社内システムのログイン手順を 1 行で', deps=deps).output)

deps は 1 つの run の中で instructionsoutput_validatortool も同じインスタンスを共有 します。「ユーザーごとに違う設定」のような run スコープのデータを置くのに最適です。

パターン 4: ライフサイクル — 重いリソースの開閉

httpx.AsyncClientasyncpg.Pool のようなリソースは 作成・破棄が高コスト です。async withアプリ起動時に作って終了時に閉じる のが定石です。

import asyncio
 
async def main():
    async with httpx.AsyncClient() as http:
        deps = AppDeps(config=AppConfig('https://api.example.com', 'ja'), http=http)
        # 何度でも run を回せる
        for q in ['Tokyo', 'Paris', 'Brasilia']:
            r = await agent.run(f'{q} の現在気温は?', deps=deps)
            print(r.output)
    # async with を抜けたら http が自動で close される
 
asyncio.run(main())
図を読み込み中…
図3. Deps のライフサイクル — 起動時に作って、N 回 run に渡す

設計のコツ

コツなぜ
Deps は 1 つの dataclass にまとめるAgent の依存関係が「型 1 個読めば全部わかる」ようになる
不変値は frozen dataclass / Pydantic BaseModelrun 中に書き換えられない保証で事故防止
可変キャッシュは dict フィールドrun スコープのキャッシュをそのまま置ける
deps_type = run の "リクエストスコープ"リクエスト/タスクが終わったら捨てる前提で設計
factory 関数を別に作るプロダクション用と TestModel 用の 2 種類の make_deps() を作りやすい (Ch2-L02)

いつ Tool / Instructions / Deps のどれを使うか

PydanticAI には情報を Agent に流す手段が複数あります。混乱しがちなので役割を整理します。

手段評価タイミング用途
Static instructions起動時 (固定文字列)役割・口調・形式
Dynamic instructions (@agent.instructions)run のたび軽い文脈 (時刻・ユーザー名・環境)
Deps (deps_type + RunContext.deps)run のたびリクエストスコープのオブジェクト全部 (HTTP / DB / 設定)
Tool (@agent.tool, Ch3)LLM が判断したときLLM 自身が "取りに行く" 必要がある重い情報

Deps は「外から渡す軽〜中量のオブジェクト全般」と覚えればだいたい正しいです。

まとめ

  • deps_type=YourDeps + agent.run(..., deps=YourDeps(...)) で外部リソースを安全に注入
  • 受け取り口は RunContext[YourDeps]instructions / output_validator / tool の全部から同じ deps を参照できる
  • Deps は 1 つの dataclass にまとめる。重い初期化は factory 関数 + async with
  • リクエストスコープの "コンテナ" として設計し、本物 / テストの差し替えで Ch2-L02 の TestModel と組み合わせる

次レッスンでは、ここで作った DI 構造を TestModel と組み合わせてユニットテスト する方法を扱います。LLM を呼ばずに Agent ロジックを完全テストするのが目標です。

Colab で実際に動かす

本レッスンの内容を Google Colab 上で実行できるノートブックを用意しています。下のボタンから自分のColab環境に開けます (要 Google アカウント / GOOGLE_API_KEY)。

Open in Colab

notebooks/ch02/01-di-basics.ipynb