PydanticAI ビジュアルガイド

Lesson CH03-L02

Pydanticバリデーション

ツール引数を型 + バリデータで守り、不正入力を弾く。

読了目安
11 min
Colab目安
16 min
合計
27 min
前提
@agent.tool 基礎

関連: 公式ドキュメント

一行サマリ

Tool 引数に Annotated[..., Field(...)]Pydantic BaseModel を使うと、PydanticAI が自動で JSON Schema に制約を載せ + 受信した引数を検証。LLM が範囲外の値を組み立てたら ValidationError → 自動 retry が走り、Tool 本体には常に妥当な引数だけが届く。

ヒーロー: 型 + 制約 = LLM への "ガードレール"

def f(n: int) だけだと「整数なら何でも OK」ですが、Annotated[int, Field(ge=1, le=10)] とすると "1〜10 の整数" が schema として LLM に伝わり、かつ受信側でもチェックされます。LLM が 100 を渡してきたら受信側で弾かれ、自動で retry されます。

図を読み込み中…
図1. 型 + Field 制約が LLM・受信側の両方に効く

概念: なぜ tool に Pydantic を効かせるか

LLM は「指示通りの引数を組み立てる」のがそこそこ得意ですが、完璧ではない です。age: int と書いても文字列 "twenty" を渡してきたり、本来 0〜100 のはずの値に 999 を入れたりする可能性があります。

ここで Pydantic の検証を介在させると、

  1. schema 経由で LLM に制約を予告 できる (誤った組み立てを減らす)
  2. 受信時に再検証 されるので Tool 本体には妥当な値だけが渡る
  3. NG なら ModelRetry 相当の自動 retry で LLM に修正を促す

これにより、Tool 内に "防御的な if 文" を書かなくて済み、ロジックに集中できます。

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

パターン 1: Annotated[..., Field(...)] で範囲・正規表現を制約

from typing import Annotated
from pydantic import Field
from pydantic_ai import Agent
 
agent = Agent('google-gla:gemini-3-flash-preview', instructions='答えは日本語で。')
 
@agent.tool_plain
def book_seat(
    seat_no: Annotated[int, Field(ge=1, le=300, description='座席番号 (1〜300)')],
    name: Annotated[str, Field(min_length=1, max_length=50, description='予約者氏名')],
) -> str:
    """座席を予約します。"""
    return f'予約完了: 座席 {seat_no} / {name} さん'
 
# 「399 番の席を予約」と頼んでも、Pydantic が範囲外を弾き、LLM は範囲内に修正してくる
print(agent.run_sync('399 番に田中で予約して').output)

ポイント:

  • ge (>=) / le (<=) / gt (>) / lt (<) で 範囲制約
  • min_length / max_length長さ制約
  • pattern='正規表現'正規表現制約
  • description= は schema にも乗るので LLM に意図を伝える のに使える

パターン 2: Enum / Literal で「選択肢」に縛る

「天気は sunny / cloudy / rainy のどれか」のような 有限選択 には LiteralEnum が最適です。

from typing import Literal
from pydantic_ai import Agent
 
agent_w = Agent('google-gla:gemini-3-flash-preview', instructions='答えは日本語で。')
 
@agent_w.tool_plain
def log_weather(
    city: str,
    condition: Literal['sunny', 'cloudy', 'rainy', 'snowy'],
    temp_c: float,
) -> str:
    """天気をログに記録します。condition は 4 種類のいずれかのみ受け付けます。"""
    return f'記録: {city} / {condition} / {temp_c}°C'
 
print(agent_w.run_sync('東京は今日とても良い天気で 22 度でした。記録して。').output)
# LLM は 'good' などではなく必ず 'sunny' のいずれかを選ぶ

Literal[...] は schema 上で enum として表現され、LLM は 必ずそのリストから 1 つ選ぶ ようになります。フリーテキストよりも遥かに堅い設計です。

パターン 3: Pydantic BaseModel を引数として受ける

複数の関連フィールドをまとめて受けたいときは BaseModel を引数にする のが綺麗です。

from datetime import date
from pydantic import BaseModel, Field
from pydantic_ai import Agent
 
class BookingRequest(BaseModel):
    name: str = Field(min_length=1)
    seat_no: int = Field(ge=1, le=300)
    booking_date: date
 
agent_b = Agent('google-gla:gemini-3-flash-preview', instructions='短く日本語で答えて。')
 
@agent_b.tool_plain
def make_booking(req: BookingRequest) -> str:
    """予約を確定します。"""
    return f'予約完了: {req.name} さん / 座席 {req.seat_no} / {req.booking_date}'
 
print(agent_b.run_sync('明日 田中で 12 番座席を予約して').output)

ポイント:

  • BaseModel をまるごと引数にすると、schema が階層構造 で LLM に渡る
  • フィールド単位で Field(...) の制約を付けられる
  • ネスト構造 (BaseModel 内に別の BaseModel) もそのまま OK

パターン 4: カスタムバリデータで複合ルール

Pydantic の field_validator / model_validator を使うと、単純な制約では表せないルールも検査できます。

from datetime import date
from pydantic import BaseModel, Field, model_validator
from pydantic_ai import Agent
 
class DateRange(BaseModel):
    start: date
    end: date
 
    @model_validator(mode='after')
    def check_order(self) -> 'DateRange':
        if self.end < self.start:
            raise ValueError(f'end ({self.end}) は start ({self.start}) 以降である必要があります')
        return self
 
agent_d = Agent('google-gla:gemini-3-flash-preview', instructions='日本語で簡潔に。')
 
@agent_d.tool_plain
def query_logs(range_: DateRange) -> str:
    """指定日付範囲のログ件数を返します。"""
    days = (range_.end - range_.start).days + 1
    return f'{range_.start}{range_.end} ({days} 日間) のログ: 42 件'
 
# 「先月から今月まで」のような曖昧な指示でも、LLM は妥当な順序の日付を組み立ててくる
print(agent_d.run_sync('2026-04-01 から 2026-04-30 までのログ件数は?').output)

model_validator(mode='after')複数フィールドの整合性 を見られます。NG の場合は ValueError を raise すれば自動で retry が走ります。

図を読み込み中…
図2. Tool 引数の制約レイヤー

自動 retry の挙動

Pydantic 検証で NG のとき、PydanticAI は内部で 自動 retry を行います。デフォルトは tool ごとに retries=1 (= 最初の試行 + リトライ 1 回 = 計 2 回まで)。

増やすには:

@agent.tool_plain(retries=3)
def my_tool(x: Annotated[int, Field(ge=0)]) -> str:
    ...

ただし retry のたびに LLM 呼び出しが発生するので、過度に厳しい制約 には注意。コストが膨らみます。

まとめ

  • Annotated[..., Field(...)] で範囲・長さ・正規表現を tool 引数に直接書ける
  • 有限選択は Literal[...]Enum、構造化された引数は BaseModel
  • 複合ルールは model_validator で。ValueError を投げれば自動 retry が走る
  • description= を書くと schema に乗り、LLM の引数組み立て精度が上がる

次レッスンでは ModelRetry — 検証層を超えた "ビジネスロジック側の不一致" を LLM に修正させるパターンを扱います。

Colab で実際に動かす

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

Open in Colab

notebooks/ch03/02-pydantic-validation.ipynb