PydanticAI実践チュートリアル — FastAPI感覚で型安全なAIエージェントを作る方法
PydanticAI 1.88.0を実際にインストールし、TestModel、output_type、@agent.tool、マルチプロバイダー切り替えを直接テストした結果です。result_type→output_type変更のような実際の罠とFunctionModelテスト戦略も含みます。
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
agent = Agent('test', system_prompt='Pythonの専門家')
result = agent.run_sync('f-stringと.format()どっちが速いですか?', model=TestModel())
print(result.output) # → success (no tool calls)
このコードがAPIキーなしで動くのを初めて見たとき、少し驚いた。FastAPIを初めて使ったときのように — 構造があまりにも直感的で、むしろ疑いたくなった。PydanticAIはそういうライブラリだ。
正直、最初は「Instructorにラッパーかぶせただけじゃないの?」と思っていた。実際に使ってみて考えが変わった。FastAPIのように型システムを中心に設計されたフレームワークで、AIエージェントにその哲学をそのまま持ち込んだものだ。今日は直接インストールして動かした結果をまとめる。失敗したテストも含めて。
なぜPydanticAIなのか — 既存比較との違う視点
以前のPython AIエージェントライブラリ比較でPydanticAI・Instructor・Smolagentsを扱った。あのポストが「何を選ぶか」を扱うなら、今回のポストは「PydanticAIで実際にどう作るか」だ。実装方法が目的だ。
主な違いをまとめると:
| ライブラリ | 核心的な役割 | エージェントループ | 型安全性 |
|---|---|---|---|
| Instructor | LLM出力のパース | なし | 構造化出力のみ |
| PydanticAI | エージェントフレームワーク | 完全サポート | 入出力+ツール全体 |
| LangGraph | オーケストレーション | グラフベース | 弱い |
| CrewAI | マルチエージェントチーム | ロールベース | 弱い |
特に2行目が実際に使うときの違いを生む。LLMがツールを呼び出し、その結果を受け取って処理する全ループで型が維持される。ランタイムエラーが開発中のIDEエラーとして前倒しで現れる。
インストールとPre-requisites
pip install pydantic-ai
今日時点(2026年4月29日)の最新バージョンは1.88.0だ。インストールは簡単。
python3 -m venv venv
source venv/bin/activate
pip install pydantic-ai
プロバイダー別の追加パッケージは使う時点でインストールすれば良い:
pip install pydantic-ai[anthropic] # Claude使用時
pip install pydantic-ai[openai] # OpenAI GPT使用時
pip install pydantic-ai[google] # Gemini使用時
Requirements: Python 3.9以上、pydantic v2(v1は非サポート)。
最初のエージェント: TestModelで構造検証
エージェントを作るとき私が最初に使うパターンだ。実際のAPIを繋げる前にエージェントの構造が正しいか確認する。
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
agent = Agent(
'test',
system_prompt='あなたはPythonコードレビュアーです。簡潔に答えてください。',
)
result = agent.run_sync(
'f-stringと.format()どっちが速いですか?',
model=TestModel() # APIキー不要
)
print(result.output) # → "success (no tool calls)"
print(result.usage()) # → RunUsage(input_tokens=64, output_tokens=4, requests=1)
TestModelはAPIを呼び出さない。エージェントの構造、ツール設定、依存性注入が正しいか確認するためのテスト専用モデルだ。CIパイプラインでAPIコストなしにエージェントロジックを検証する時に活用する。
実際のClaudeを繋げるにはモデル文字列一つを変えるだけ:
import os
os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-...'
# 開発中: TestModel
result = agent.run_sync('コードレビューして', model=TestModel())
# 本番: 実際のClaude
result = agent.run_sync('コードレビューして', model='anthropic:claude-sonnet-4-6')
エージェントのコードはそのままで、モデルだけ交換する。
構造化出力: output_typeでPydanticモデルを返す
ここからPydanticAIの核心だ。LLMが自由テキストではなくPydanticモデルインスタンスを返すよう強制できる。
重要 — v1.88.0 Breaking Change: result_typeパラメータがoutput_typeに変更された。古いドキュメントやチュートリアルをそのまま使うと次のエラーが出る:
pydantic_ai.exceptions.UserError: Unknown keyword arguments: `result_type`
直接遭遇した。inspect.signature(Agent.__init__)で確認したらoutput_typeが正しいパラメータ名だった。
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class CodeReview(BaseModel):
severity: str = Field(description="'low', 'medium', 'high'のいずれか")
issues: list[str] = Field(description="発見された問題のリスト")
suggestions: list[str] = Field(description="改善提案のリスト")
score: int = Field(ge=0, le=100, description="コード品質スコア(0-100)")
review_agent = Agent(
'anthropic:claude-sonnet-4-6',
output_type=CodeReview, # ← v1.88.0: result_typeではない
system_prompt='Pythonコードをレビューして構造化フィードバックを提供してください。',
)
result = review_agent.run_sync('''
def get_user(id):
db = connect()
return db.query(f"SELECT * FROM users WHERE id={id}")
''')
print(type(result.output)) # → <class '__main__.CodeReview'>
print(result.output.severity) # → 'high'
print(result.output.score) # → 25
print(result.output.issues[0]) # → 'SQLインジェクション脆弱性'
返り値がdictやstrではなくPydanticモデルインスタンスだ。result.output.を入力するとIDEがseverity、issuesなどをオートコンプリートしてくれる。
自動リトライメカニズムも重要だ:
review_agent = Agent(
'anthropic:claude-sonnet-4-6',
output_type=CodeReview,
retries=3, # 出力バリデーション失敗時に最大3回リトライ
output_retries=2
)
3回全て失敗するとUnexpectedModelBehavior例外が発生する。
@agent.toolと依存性注入 — FastAPIのDepends()と同じパターン
from pydantic_ai import Agent, RunContext
class AppDeps:
def __init__(self, db_url: str, user_id: int):
self.db_url = db_url
self.user_id = user_id
agent = Agent(
'anthropic:claude-sonnet-4-6',
deps_type=AppDeps,
system_prompt='タスク管理エージェントです。',
)
# 非同期ツール
@agent.tool
async def get_pending_tasks(ctx: RunContext[AppDeps], limit: int = 5) -> list[dict]:
"""未完了タスクのリストを取得します"""
return [
{"id": f"task_{i}", "title": f"タスク {i}", "priority": "high"}
for i in range(limit)
]
# 同期ツール
@agent.tool
def calculate_priority_score(
ctx: RunContext[AppDeps],
urgency: int,
importance: int
) -> float:
"""タスクの優先度スコアを計算します"""
return urgency * 0.6 + importance * 0.4
deps = AppDeps(db_url="postgresql://localhost/taskdb", user_id=42)
result = agent.run_sync("緊急タスクで最優先のものを選んで", deps=deps)
@agent.toolは関数シグネチャとdocstringを読んでLLMに渡すJSON Schemaを自動生成する。
メッセージフローは4段階で追跡できる:
1. ModelRequest → system_prompt + user_prompt
2. ModelResponse → ToolCallPart(tool_name='get_pending_tasks', ...)
3. ModelRequest → ToolReturnPart(content=[{...}])
4. ModelResponse → 最終応答

TestModel vs FunctionModel — テスト戦略
サンドボックスでテストしていてTestModelの重要な制限を発見した。
TestModelはstrフィールドに'a'、intフィールドに0のような最小値を返す。厳格なカスタムvalidatorがあると失敗する:
from pydantic_ai.exceptions import UnexpectedModelBehavior
class UserProfile(BaseModel):
email: str
@field_validator('email')
@classmethod
def valid_email(cls, v):
if '@' not in v: # TestModelが'a'を返すので常に失敗
raise ValueError('@が必要')
return v
agent = Agent('test', output_type=UserProfile, retries=3)
try:
result = agent.run_sync('...', model=TestModel())
except UnexpectedModelBehavior as e:
print(f"例外: {e}")
# → Exceeded maximum retries (3) for output validation
FunctionModelを使えば解決する:
from pydantic_ai.models.function import FunctionModel
from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart
from pydantic_ai.settings import ModelSettings
import json
def mock_valid_response(messages: list[ModelMessage], settings: ModelSettings) -> ModelResponse:
data = {"email": "test@example.com", "name": "テスト"}
return ModelResponse(parts=[TextPart(content=json.dumps(data))])
agent = Agent(FunctionModel(mock_valid_response), output_type=UserProfile)
result = agent.run_sync("...")
assert result.output.email == "test@example.com"
テスト戦略の整理:
class TestMyAgent:
def test_structure(self):
"""エージェント構造検証 — TestModel"""
result = my_agent.run_sync("テスト", model=TestModel())
assert result is not None
def test_tool_called(self):
"""ツール呼び出し確認 — TestModel + call_tools"""
result = my_agent.run_sync(
"DBからデータを取得して",
deps=test_deps,
model=TestModel(call_tools=['query_database'])
)
assert 'query_database' in result.output
def test_with_mock(self):
"""応答処理ロジック — FunctionModel"""
def mock_fn(messages, settings):
return ModelResponse(parts=[TextPart(content='{"email": "t@t.com"}')])
result = my_agent.run_sync("...", model=FunctionModel(mock_fn))
assert result.output.email == "t@t.com"
マルチプロバイダー切り替え
エージェントのコードを変えずにモデル文字列だけで別プロバイダーに切り替えられる:
review_agent = Agent(
system_prompt='シニアPython開発者としてコードをレビューします。',
output_type=CodeReview,
)
# 実行時にモデルを指定
result_claude = review_agent.run_sync(code, model='anthropic:claude-sonnet-4-6')
result_gpt = review_agent.run_sync(code, model='openai:gpt-4o')
result_gemini = review_agent.run_sync(code, model='google-gla:gemini-2.5-flash')
result_local = review_agent.run_sync(code, model='ollama:llama3.3')
コンテキストエンジニアリングの観点から見ると、system_promptとoutput_typeスキーマがコンテキストの核心で、その上でモデルを交換可能にするのが良い設計だ。
実際に使ってみて — 率直な評価
良かった点:
- 型安全性が実際に差を生む。
output_typeスキーマを変更するとIDEが即座に関連エラーを捕捉する @agent.toolの自動JSON Schema生成が便利だ。ツール仕様を手動で再作成する必要がない- TestModel + FunctionModelの組み合わせでAPIなしにエージェントロジックを完全に単体テストできる
残念な点:
- v1.88.0まで
result_type → output_typeのような非互換な変更が頻繁にある。ライブラリがまだ安定化段階にない。実際にinspect.signature(Agent.__init__)でパラメータを確認しなければならない状況が生じた - ストリーミング構造化出力はまだベータ版だ。LLMが部分的に応答を生成している間にPydanticモデルとしてパースするのは難しく、現在の実装が安定していない
- Pydantic v2に強く結びついている。v1のレガシーコードベースならマイグレーションコストを考慮する必要がある
プロダクションAIエージェント設計原則と合わせて読むと、エージェントフレームワーク選択においてどんな基準が重要かがより明確になる。
次のステップ
TypeScriptスタックならVercel AI SDKでClaudeストリーミングエージェントを作る方法がPythonと似たアプローチを提供する。
PydanticAIを実際のプロダクションに適用するなら、推奨する順番:
output_typeで返却スキーマを最初に定義するdeps_typeでDB接続、HTTPクライアントを依存性として管理する@agent.toolで外部API連携を追加する- TestModel → FunctionModel → 実際のモデルの順で段階的にテストする
retries=3とoutput_retries=2でリトライ戦略を設定する- バージョンを固定する(
pydantic-ai==1.88.0)。変更が頻繁なライブラリだ
PydanticAI GitHubリポジトリは急速に更新されている。公式ドキュメントよりCHANGELOGを先に読む習慣がプロダクティビティを向上させる。
他の言語で読む
- 🇰🇷 한국어
- 🇯🇵 日本語(現在のページ)
- 🇺🇸 English
- 🇨🇳 中文
この記事は役に立ちましたか?
より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください。