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 されます。
概念: なぜ tool に Pydantic を効かせるか
LLM は「指示通りの引数を組み立てる」のがそこそこ得意ですが、完璧ではない です。age: int と書いても文字列 "twenty" を渡してきたり、本来 0〜100 のはずの値に 999 を入れたりする可能性があります。
ここで Pydantic の検証を介在させると、
- schema 経由で LLM に制約を予告 できる (誤った組み立てを減らす)
- 受信時に再検証 されるので Tool 本体には妥当な値だけが渡る
- 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 のどれか」のような 有限選択 には Literal か Enum が最適です。
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 が走ります。
自動 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)。
notebooks/ch03/02-pydantic-validation.ipynb