PydanticAI ビジュアルガイド

Lesson CH03-L04

Advanced Tools

戻り値型・並列実行・条件付きツールなど発展トピック。

読了目安
11 min
Colab目安
16 min
合計
27 min
前提
ModelRetry

関連: 公式ドキュメント

一行サマリ

Tool の基本を超えて、並列実行・条件付き提供・構造化戻り値・動的 schema といった発展トピックを扱う。複数 tool を持つ Agent の挙動を読み解き、実用シナリオに必要な制御を効かせるためのテクニック集。

ヒーロー: 1 つの Agent に複数 tool を持たせると何が起きるか

LLM は すべての tool の schema を見て、必要に応じて 0 個以上を順に (場合によっては並列で) 呼ぶ 判断をします。Agent 側ができることは、tool の 見せ方を制御する こと。

図を読み込み中…
図1. 複数 tool を持つ Agent の選択挙動

パターン 1: 構造化戻り値で続く処理に橋渡し

Tool の戻り値は str や数値だけでなく Pydantic BaseModel でも OK。LLM への文脈に 構造化された情報 を渡せるので、後続の判断 / 別 tool への入力に直接使えます。

from pydantic import BaseModel
from pydantic_ai import Agent
 
class Weather(BaseModel):
    city: str
    condition: str
    temp_c: float
 
WEATHER_DB = {
    'Tokyo': Weather(city='Tokyo', condition='sunny', temp_c=22.0),
    'Sapporo': Weather(city='Sapporo', condition='snowy', temp_c=-2.0),
}
 
agent = Agent('google-gla:gemini-3-flash-preview', instructions='短く日本語で。')
 
@agent.tool_plain
def get_weather(city: str) -> Weather:
    """指定都市の天気を構造化して返します。"""
    if city not in WEATHER_DB:
        return Weather(city=city, condition='unknown', temp_c=0.0)
    return WEATHER_DB[city]
 
print(agent.run_sync('東京と札幌の天気を比較して').output)

LLM への戻り値は JSON シリアライズされて文脈に積まれる ため、複数 tool 結果の比較・差分計算が LLM 側でしやすくなります。

パターン 2: 並列 tool 呼び出し

LLM (特に Gemini 3 系) は 同じ run の中で複数 tool を並列に呼ぶ ことができます。asyncio で書いておけば PydanticAI が裏で asyncio.gather 相当を回してくれます。

import asyncio
from dataclasses import dataclass
import httpx
from pydantic_ai import Agent, RunContext
 
@dataclass
class HttpDeps:
    http: httpx.AsyncClient
 
agent_p = Agent('google-gla:gemini-3-flash-preview', deps_type=HttpDeps)
 
@agent_p.tool
async def fetch_summary(ctx: RunContext[HttpDeps], url: str) -> str:
    """URL の冒頭 200 字を返します。"""
    r = await ctx.deps.http.get(url, timeout=5.0)
    return r.text[:200]
 
# LLM は 3 件のサマリを並列に取りに行く
async def main():
    async with httpx.AsyncClient() as http:
        result = await agent_p.run(
            '次の 3 サイトを比較して: https://a.example.com, https://b.example.com, https://c.example.com',
            deps=HttpDeps(http=http),
        )
        print(result.output)

ポイント:

  • async def で書いた tool は I/O 待ちで他のタスクに譲る ため、並列化が効く
  • 同期 tool (def) でも複数呼ばれるが、その場合は逐次

パターン 3: 条件付き tool 提供 — prepare

「ユーザーの権限によって使える tool が違う」場合は prepare 関数 で run ごとに schema を組み立て直せます。

from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
from pydantic_ai.tools import ToolDefinition
 
@dataclass
class Auth:
    is_admin: bool
 
agent_a = Agent('google-gla:gemini-3-flash-preview', deps_type=Auth)
 
# 通常 tool
@agent_a.tool_plain
def list_users() -> list[str]:
    """ユーザー一覧を返します。"""
    return ['tanaka', 'sato']
 
# 管理者だけが使える tool
async def only_for_admin(ctx: RunContext[Auth], tool_def: ToolDefinition) -> ToolDefinition | None:
    return tool_def if ctx.deps.is_admin else None
 
@agent_a.tool_plain(prepare=only_for_admin)
def delete_user(name: str) -> str:
    """指定ユーザーを削除します (管理者専用)。"""
    return f'削除: {name}'
 
# 一般ユーザーで実行 — delete_user は schema に出ない
print(agent_a.run_sync('tanaka を削除して', deps=Auth(is_admin=False)).output)
 
# 管理者で実行 — delete_user が呼べる
print(agent_a.run_sync('tanaka を削除して', deps=Auth(is_admin=True)).output)

ポイント:

  • prepare 関数は run ごとに呼ばれるNone を返すとその run では tool が 存在しない扱い になる (LLM は schema にも見えない)
  • ToolDefinition を返せばそのまま使われる。動的に description を変えるのも可能

パターン 4: tool の docstring を動的に変える

prepare 関数で tool_def.description を上書きすれば、状況に応じた hint を LLM に渡せます。

async def with_user_hint(ctx: RunContext[Auth], tool_def: ToolDefinition) -> ToolDefinition:
    role = '管理者' if ctx.deps.is_admin else '一般ユーザー'
    tool_def.description = f'{tool_def.description} (現在のロール: {role})'
    return tool_def
 
@agent_a.tool_plain(prepare=with_user_hint)
def show_status() -> str:
    """現在のシステム状態を返します。"""
    return 'OK'
図を読み込み中…
図2. tool の見せ方を制御する 3 段階

デバッグ — どの tool がどう呼ばれたか見る

複数 tool を持つ Agent では「LLM が想定と違う tool を呼んだ」場面がよくあります。result.all_messages() でメッセージ履歴を見るのが基本デバッグ。

result = agent.run_sync('東京の天気は?')
for msg in result.all_messages():
    for part in msg.parts:
        if hasattr(part, 'tool_name'):
            print(f'{msg.kind}: {part.tool_name}({getattr(part, "args", "")})')

これで「呼ばれた tool / 引数 / 戻り値」が時系列に並んで見えます。

まとめ

  • 戻り値型に Pydantic BaseModel を返せば LLM の context に構造化情報を渡せる
  • async def tool は 並列実行 に乗る (Gemini 3 系で特に効く)
  • @tool(prepare=fn)run ごとに tool の表示・description を動的制御
  • 大量 tool は schema を肥大化させる → Toolset (Ch7) で整理 を検討
  • デバッグは result.all_messages() で tool 呼び出し履歴を観察

これで Ch3「Function Tools」全 4 レッスンが完了です。次の Ch4「Structured Output」 からは、Agent の最終応答そのものを Pydantic Model 等に固定する設計を扱います。

Colab で実際に動かす

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

Open in Colab

notebooks/ch03/04-advanced-tools.ipynb