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 を投げる、という分担です。
概念: 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 を超えたら設計を見直すサインです。
ModelRetry vs ValueError vs 普通の例外
| 例外 | 何が起きる | いつ使う |
|---|---|---|
ModelRetry | LLM に理由を渡して 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)。
notebooks/ch03/03-model-retry.ipynb