PydanticAI ビジュアルガイド

Lesson CH03-L03

ModelRetry

エラー時にAgentに自己修正させて再試行させる。

読了目安
9 min
Colab目安
14 min
合計
23 min
前提
Pydanticバリデーション

関連: 公式ドキュメント

一行サマリ

Tool 内で raise ModelRetry("具体的な理由") を投げると、PydanticAI が その理由を LLM に渡して、自動で tool 引数を組み立て直させる。Pydantic 検証は型レベルの不一致を弾くだけ — 「業務ロジック上の不一致」を直させたいときに ModelRetry を使う

ヒーロー: 検証層と ModelRetry の役割分担

PydanticAI には「LLM の出力を直させる」仕組みが 2 段階あります。型・制約レベルは Pydantic が自動で業務ロジック上の不一致は手動で ModelRetry を投げる、という分担です。

図を読み込み中…
図1. Pydantic 検証 vs ModelRetry の役割分担

概念: Pydantic と ModelRetry はいつどっち?

不一致の種類仕組み
型・範囲・選択肢の違反Pydantic 検証 (自動)seat_no: Annotated[int, Field(ge=1, le=300)]0
業務的な前提違反ModelRetry (手動)"存在しないユーザーID" / "在庫切れ" / "営業時間外"
致命的エラー普通の例外DB 切断 / API 鍵切れ

「型は通ったけど、データを照らしたらダメ」というケースに ModelRetry を投げると、LLM は理由を読んで 別の引数 (例: 別のユーザー名) で再試行 してきます。

コード: 4 つの代表パターン

パターン 1: ID が見つからない場合に再尋ねさせる

実装の都合上 LLM が知らないデータ (社内ユーザーリスト等) を引かせるとき、無効 ID には ModelRetry

from pydantic_ai import Agent, ModelRetry
 
USERS = {'tanaka': 1, 'sato': 2, 'suzuki': 3}
 
agent = Agent('google-gla:gemini-3-flash-preview', instructions='短く日本語で。')
 
@agent.tool_plain
def get_user_id(name: str) -> int:
    """ユーザー名から ID を取得します。name は社内システム上の英字ユーザー名。"""
    name_lower = name.lower()
    if name_lower not in USERS:
        raise ModelRetry(
            f'ユーザー {name!r} は存在しません。'
            f'利用可能なユーザー: {", ".join(USERS.keys())}'
        )
    return USERS[name_lower]
 
# LLM は最初 'Yamada' で呼ぶかもしれない → ModelRetry の理由を読んで存在するユーザーで再試行
print(agent.run_sync('山田さんの ID を教えて。なければ田中さんの ID で。').output)

ポイント:

  • ModelRetry の メッセージ内に「正解の選択肢」を含める と LLM が一発で直せる
  • ValidationError (Pydantic) と違い 業務上の不一致を表現する明示的な手段 となる

パターン 2: 計算結果が想定外の場合に修正させる

agent_calc = Agent('google-gla:gemini-3-flash-preview', instructions='答えは整数で。')
 
@agent_calc.tool_plain
def divide(a: float, b: float) -> float:
    """a を b で割った商を返します。"""
    if b == 0:
        raise ModelRetry('0 で割ろうとしました。b に 0 以外の値を指定してください。')
    return a / b
 
print(agent_calc.run_sync('10 を 0 で割って').output)
# LLM は「不可能なので」と返すか、引数を 1 等に変えて再試行する

パターン 3: 外部 API のエラーを LLM に diff として渡す

API がエラー (例: 404) を返したとき、それをそのまま ModelRetry のメッセージに混ぜると、LLM は 自然言語の説明として読んで 引数を修正できます。

from dataclasses import dataclass
import httpx
from pydantic_ai import Agent, RunContext, ModelRetry
 
@dataclass
class WeatherDeps:
    http: httpx.AsyncClient
    api_base: str
 
agent_w = Agent(
    'google-gla:gemini-3-flash-preview',
    deps_type=WeatherDeps,
    instructions='短く日本語で。',
)
 
@agent_w.tool
async def get_weather(ctx: RunContext[WeatherDeps], city: str) -> str:
    """指定都市の天気を取得します。city は ISO 3166-1 / 主要都市の英字表記。"""
    response = await ctx.deps.http.get(f'{ctx.deps.api_base}/weather?city={city}')
    if response.status_code == 404:
        raise ModelRetry(
            f'{city!r} は API に登録されていません。'
            'Tokyo / Paris / New York のような英字の主要都市名で再試行してください。'
        )
    response.raise_for_status()
    return response.json()['summary']

パターン 4: retries で上限を変える

デフォルトは tool ごとに retries=1 (最初の 1 回 + リトライ 1 回 = 計 2 回まで)。retry ループを増やすには:

@agent.tool_plain(retries=3)
def stricter_tool(x: str) -> str:
    if not x.startswith('OK-'):
        raise ModelRetry('値は "OK-" で始める必要があります')
    return x

ただし retry が増えるごとに LLM 呼び出しが追加 → コスト・レイテンシが線形に伸びる。3 を超えたら設計を見直すサインです。

図を読み込み中…
図2. ModelRetry を投げてから LLM が修正するまで

ModelRetry vs ValueError vs 普通の例外

例外何が起きるいつ使う
ModelRetryLLM に理由を渡して tool 引数を組み立て直させる業務上の前提違反 (ID 不存在 / 在庫切れ等)
ValueError (Pydantic 検証から)同上 (Pydantic が変換)カスタム validator 内
普通の Exception (KeyError 等)呼び出し側に そのまま伝播致命的・回復不能なエラー

「LLM に直してもらえる」のと「アプリが落ちる」のを区別するのが鍵。直せそうなら ModelRetry、直せないなら普通の例外。

まとめ

  • raise ModelRetry("具体的な理由")業務ロジック上の不一致 を LLM に修正させる
  • Pydantic 検証 = 型・範囲、ModelRetry = 業務ロジック、普通の例外 = 致命的 で使い分け
  • メッセージには 正解の選択肢 / 期待値 / 次の試行のヒント を含める
  • retries=N で上限を変えられるが、増やしすぎると コスト直結

次レッスンでは、複数 tool の管理 / 条件付き提供 / 戻り値の構造化など Advanced Tools のテクニックを扱います。

Colab で実際に動かす

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

Open in Colab

notebooks/ch03/03-model-retry.ipynb