Pydantic AIで型安全なAIエージェントを作る—output_typeとツール設計

Pydantic AIで型安全なAIエージェントを作る—output_typeとツール設計 | mohablog

LLMに「JSONで返して」と頼んでも、返ってくるのはただの文字列です。json.loads が通る保証はなく、キーが欠けることもある。Pydantic AI はこの出力を Pydantic モデルで検証してから渡すエージェントフレームワークで、2026年6月23日v2.0 が安定版になりました。

目次

なぜLLMの生出力はそのままだと危ういのか

json.loads頼みのパースが崩れる場面

モデルにJSON生成を頼む実装は、こう書きがちです。

import json

raw = call_llm("この問い合わせを category と priority のJSONで分類して")
data = json.loads(raw)        # ```json ... ``` が前後に混ざると即失敗
print(data["priority"])       # キーが欠ければ KeyError

実行結果。モデルがコードフェンスで囲って返した瞬間に落ちます。

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

フェンスを剥がす前処理を足しても、今度は priority"urgent" のような想定外の値で返る。文字列を信じた分だけ、後段の分岐にガードが増えていきます。

Pydantic AIが間に挟む検証レイヤ

Pydantic AI は、出力先の型をモデルのツール呼び出し機能に渡し、返ってきたデータを Pydantic で検証します。通らなければモデルに投げ直す。公式ドキュメントが “Fully Type-safe”“Model-agnostic” を並べて掲げるとおり、出力の型を固定したまま裏側のモデルだけ差し替えられる構造です。Pydantic そのものの検証速度の違いは msgspec vs Pydantic に整理しました。

最小のエージェントを動かす

インストールとモデル指定

対応は Python 3.10 以上。導入は1行です。

pip install pydantic-ai

最小のエージェントを書きます。

from pydantic_ai import Agent

agent = Agent(
    "anthropic:claude-sonnet-4-6",
    instructions="日本語で、1文で簡潔に答える。",
)
result = agent.run_sync("Pydanticという名前の由来は?")
print(result.output)

実行結果。

Pythonの型ヒントを土台に、データ検証を担う「pedantic」から名付けられたライブラリです。

モデル指定は "provider:model" の文字列1つ。OpenAI・Anthropic・Google・xAI・Bedrock・Ollama など13以上のプロバイダを同じ書式で切り替えられます。

run_sync / run / run_stream の違い

実行メソッドは3つ。用途で選びます。

メソッド形態戻り値使う場面
run_sync同期RunResultスクリプト・バッチ
runasyncRunResultFastAPIなど非同期処理の中
run_streamasync(コンテキストマネージャ)StreamedRunResult途中経過を逐次表示する

どれも結果は result.output で取り出します。トークン消費は result 側に残るので、コスト計測もここから拾えます。

output_typeで構造化データを受け取る

output_type に型を渡すと、result.output がその型の検証済みインスタンスで返ります。生のテキストを解釈する処理を、型定義1つに寄せられます。

BaseModelを渡すと戻り値が型付きになる

from typing import Literal
from pydantic import BaseModel
from pydantic_ai import Agent

class TicketTriage(BaseModel):
    category: Literal["billing", "bug", "feature", "other"]
    priority: Literal["low", "medium", "high"]
    needs_human: bool
    summary: str

triage_agent = Agent(
    "anthropic:claude-sonnet-4-6",
    output_type=TicketTriage,
    instructions="サポート問い合わせを分類する。summaryは40字以内。",
)

ticket = "先月から二重に請求されています。至急返金してほしい。"
result = triage_agent.run_sync(ticket)
print(result.output)
print(result.output.priority)   # str ではなく Literal 型として追える

実行結果。

category='billing' priority='high' needs_human=True summary='二重請求の返金依頼'
high

result.outputTicketTriage のインスタンス。.prioritymypy もIDE補完も効きます。json.loadstry/except を後段から丸ごと外せたのが、実務で一番効いた変化でした。

検証に失敗したらModelRetryで投げ直す

型が合わないと、Pydantic AI はエラーをモデルに戻して再生成させます。自前の検証を足すときは output_validatorModelRetry を使います。

from pydantic_ai import ModelRetry

@triage_agent.output_validator
def check_summary(output: TicketTriage) -> TicketTriage:
    if len(output.summary) > 40:
        raise ModelRetry("summaryが40字を超えています。短くしてください。")
    return output

実行結果としては、ModelRetry のメッセージがそのまま次のプロンプトに渡り、モデルが短い summary を作り直します。公式の “Output validators” セクションが説明するこの仕組みで、既定の再試行回数は 1回。無限ループにはなりません。

古いサンプルの result_type と result.data は使わない

ネット上のサンプルには result_typeresult.data が残っています。これは v0.6.0(2025-08-06)output_typeresult.output に改名済み。v2系で書くなら新しい名前へ読み替えてください。

ツールで外部データを呼ぶ

tool_plainで依存の無い処理を渡す

エージェントに関数を持たせると、モデルが必要と判断したときに呼び出します。依存を持たない関数は @agent.tool_plain で登録します。

URGENT_WORDS = ("至急", "返金", "解約", "障害")

@triage_agent.tool_plain
def urgent_hits(text: str) -> list[str]:
    """本文に含まれる緊急語を返す。優先度の判断材料にする。"""
    return [w for w in URGENT_WORDS if w in text]

実行結果。先ほどの問い合わせなら、モデルがこのツールを呼んで次を受け取ります。

['至急', '返金']

docstringがそのままツールスキーマになる

関数のdocstringはツールの説明に、引数は入力スキーマに変換されます。公式の “Tool Schema” によれば、docstringから引数の説明も抽出され、google・numpy・sphinx形式を自動判別。require_parameter_descriptions=True を付ければ、説明の無い引数をエラーにできます。関数名とdocstringが雑だと、モデルがツールを呼ぶ精度も落ちる。ここは普通の関数設計と同じ注意が要ります。

依存性注入でテストしやすくする

ツールからDBや外部APIを触りたい場面。Pydantic AI は RunContext 経由の依存性注入を用意しています。

depsをdataclassにまとめる

from dataclasses import dataclass
from pydantic_ai import RunContext

@dataclass
class SupportDeps:
    plan_of: dict[str, str]   # email から プラン名

triage_agent = Agent(
    "anthropic:claude-sonnet-4-6",
    output_type=TicketTriage,
    deps_type=SupportDeps,
    instructions="サポート問い合わせを分類する。enterpriseなら優先度を上げる。",
)

@triage_agent.tool
def customer_plan(ctx: RunContext[SupportDeps], email: str) -> str:
    """問い合わせ者の契約プランを返す。"""
    return ctx.deps.plan_of.get(email, "free")

deps = SupportDeps(plan_of={"vip@example.com": "enterprise"})
result = triage_agent.run_sync("vip@example.com から: 管理画面が開けません", deps=deps)
print(result.output.priority)

実行結果。enterprise契約が効いて優先度が上がります。

high

@agent.tool は第1引数に RunContext[SupportDeps] を取り、ctx.deps から注入値を読みます。依存の無い tool_plain との違いはここだけ。

テストではdepsを差し替える

本番のDBを繋がなくても、SupportDeps にダミーの辞書を渡せばツールの分岐を検証できます。実際のLLM呼び出しを避けたいときのために、Pydantic AI はテスト用のモデルも同梱しています。ツールの入出力と依存の差し替えを、ネットワーク無しで固められる構成です。

instructions と system_prompt はどちらを使うか

message_historyを渡したときの挙動差

2つの差は、会話を続けて message_history を渡したときに出ます。履歴内の instructions は今回のリクエストに含まれません。一方 system_prompt は過去のリクエストで使ったものが保持されます。公式は大半のケースで instructions を勧めています。

観点instructionssystem_prompt
message_history越しの保持含まれない(毎回入れ替わる)保持される
向く場面単発の分類・抽出複数エージェントで同じ前提を引き継ぐ

v2のcapabilities primitive

v2.0 で設計思想が変わりました。変更履歴の言葉を借りると “a single, composable unit that bundles an agent’s tools, hooks, instructions, and model settings”。ツール・フック・instructions・モデル設定を1つの単位にまとめる capabilities が中心概念です。ここまでのように Agent(...) の引数で書く形はそのまま動くので、まずは引数ベースで始めて構いません。最新は v2.9.0

まとめ

Pydantic AI は、LLMの出力をPydanticモデルで検証してから返すフレームワークです。

  • output_type にPydanticモデルを渡すと、result.output が検証済みインスタンスで返る
  • 検証に失敗したら ModelRetry でモデルに投げ直す。既定の再試行は1回
  • @agent.toolRunContext 経由でdepsを受け取り、@agent.tool_plain は受け取らない
  • deps_type とdataclassでDBやクライアントを注入し、テストで差し替える
  • 会話履歴をまたぐなら system_prompt、単発なら instructions
  • 旧サンプルの result_typeresult.data は改名済み

よくある質問

Q. モデルを変えるとコードも書き直し?
"provider:model" の文字列を差し替えるだけで、13以上のプロバイダに切り替わります。

Q. 非同期処理の中で使える?
run がasync、run_sync はその同期ラッパ、run_stream がストリーミング。FastAPIなら run をそのまま await できます。

Q. LangChainとの違いは?
Pydantic AI は出力とツール引数をPydanticの検証で縛るのが軸。型が合わなければ自動で投げ直す前提で組めます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次