PydanticAI ビジュアルガイド

Lesson CH07-L03

Deferred Tools

実行を後回しにする遅延ツールでHuman-in-the-Loopを表現する。

読了目安
11 min
Colab目安
18 min
合計
29 min
前提
MCPクライアント

関連: 公式ドキュメント

一行サマリ

@agent.tool_plain(requires_approval=True)承認が要るツール を、または raise CallDeferred(...)結果を後で渡す外部ジョブ型ツール を表現できる。実行を保留した結果は DeferredToolRequests として返り、人間 / 外部システムが応答してから agent.run_sync(message_history=..., deferred_tool_results=...) で続きを再開する 2 段実行モデル

ヒーロー: 「実行を一時停止する」ツール

通常、Agent は LLM が呼んだツールを 同じプロセスでその場で実行 します。Deferred Tools はその実行を 意図的に保留 し、外部 (人間や別システム) からの応答を待ってから続きを再開する仕組み。Human-in-the-Loop の標準パターン です。

図を読み込み中…
図1. 通常実行と Deferred の流れの違い

概念: 2 つのパターン

パターントリガー用途
承認型 (Approval)requires_approval=True または raise ApprovalRequired()ファイル削除 / 大規模変更 / 課金など、人間判断が要る操作
ジョブ型 (CallDeferred)raise CallDeferred(metadata=...)数分〜数時間かかる外部ジョブ / 別マイクロサービスへの委譲

どちらも output_type=[str, DeferredToolRequests] のように 正常出力と保留要求を Union で受け取れるようにする点が共通。

コード: 3 つのパターン

パターン 1: requires_approval=True で削除前に承認を取る

from pydantic_ai import (
    Agent,
    DeferredToolRequests,
    DeferredToolResults,
    ToolDenied,
)
from pydantic_ai.models.google import GoogleModel
 
agent = Agent(
    GoogleModel('gemini-3-flash-preview'),
    output_type=[str, DeferredToolRequests],
    instructions='ファイル操作の依頼を受けたら delete_file を呼んでください。',
)
 
@agent.tool_plain(requires_approval=True)
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する (要承認)。"""
    # 承認後に呼ばれるとここに到達する
    return f'{path} を削除しました'
 
# 1 回目の run: 承認が必要なら DeferredToolRequests が返る
result = agent.run_sync('/tmp/old.log を削除してください')
 
if isinstance(result.output, DeferredToolRequests):
    # 人間にお伺いする処理 (UI / Slack / メール 等)
    requests = result.output
    results = DeferredToolResults()
    for call in requests.approvals:
        # ここでは UI 越しに承認を取った想定で True
        results.approvals[call.tool_call_id] = True
        # 拒否する場合は: ToolDenied('権限が不足しています')
 
    # 2 回目の run: message_history + deferred_tool_results で再開
    final = agent.run_sync(
        message_history=result.all_messages(),
        deferred_tool_results=results,
    )
    print(final.output)
else:
    print(result.output)

ポイント:

  • output_type=[str, DeferredToolRequests]str も DeferredToolRequests も受け取れる Union 型
  • @agent.tool_plain(requires_approval=True) で承認必須にマーク
  • requests.approvals を回して tool_call_id 単位で承認 / 拒否
  • 拒否は ToolDenied('理由') で返すと LLM 側にも伝わる

パターン 2: 条件付き承認 (raise ApprovalRequired())

「通常は OK、特定パスだけ承認必須」を実装したいとき。

from pydantic_ai import (
    Agent, ApprovalRequired, DeferredToolRequests, DeferredToolResults,
    RunContext,
)
from pydantic_ai.models.google import GoogleModel
 
agent = Agent(
    GoogleModel('gemini-3-flash-preview'),
    output_type=[str, DeferredToolRequests],
)
 
@agent.tool
def update_file(ctx: RunContext, path: str, content: str) -> str:
    """ファイルを書き換える。重要パスは承認必須。"""
    if path in {'.env', 'production.yml'} and not ctx.tool_call_approved:
        raise ApprovalRequired()
    return f'{path} を更新しました'

ctx.tool_call_approved再開後の 2 回目以降の run で承認済みなら True。1 回目は False なので ApprovalRequired() で保留に回ります。

パターン 3: CallDeferred で長時間ジョブを外部委譲

from pydantic_ai import (
    Agent, CallDeferred, DeferredToolRequests, DeferredToolResults,
    RunContext,
)
from pydantic_ai.models.google import GoogleModel
 
agent = Agent(
    GoogleModel('gemini-3-flash-preview'),
    output_type=[str, DeferredToolRequests],
)
 
@agent.tool
async def render_video(ctx: RunContext, prompt: str) -> str:
    """動画レンダリング (数分〜)。外部キューに enqueue して結果を後で返す。"""
    job_id = enqueue_render_job(prompt)  # 仮想関数: 別システムにジョブ投入
    raise CallDeferred(metadata={'job_id': job_id, 'prompt': prompt})
 
result = await agent.run('海辺の夕日の動画を作って')
 
if isinstance(result.output, DeferredToolRequests):
    requests = result.output
    results = DeferredToolResults()
    for call in requests.calls:
        meta = requests.metadata[call.tool_call_id]
        # 外部キューから結果を待つ (ここでは擬似的に取得)
        url = wait_for_render_job(meta['job_id'])
        results.calls[call.tool_call_id] = url
 
    final = await agent.run(
        message_history=result.all_messages(),
        deferred_tool_results=results,
    )
    print(final.output)

metadata には 後で結果を取りに行くための ID 等 を詰めて、外部側で照合できるようにします。

図を読み込み中…
図2. 2 段実行モデルのデータの流れ

観察: HandleDeferredToolCalls でハンドラ集約

「保留が出たら 1 つの関数で全部捌きたい」場合は HandleDeferredToolCalls capability を使うと、Agent 内部で 2 段実行を自動繰り返してくれます。

from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities import HandleDeferredToolCalls
 
async def handle_deferred(ctx, requests):
    return requests.build_results(approve_all=True, calls={
        c.tool_call_id: 'mock_result' for c in requests.calls
    })
 
agent = Agent(
    'google-gla:gemini-3-flash-preview',
    capabilities=[HandleDeferredToolCalls(handler=handle_deferred)],
)

UI を出す余地が無いバックエンド処理で、自動承認 + 自動ジョブ完了 を組むときに便利。

まとめ

  • requires_approval=True / raise ApprovalRequired()人間承認 を要求
  • raise CallDeferred(metadata=...)外部ジョブ に処理を委譲
  • 1 回目の run は DeferredToolRequests を返し、2 回目で DeferredToolResults を渡して再開
  • HandleDeferredToolCalls capability でハンドラ集約 (UI 不要時)
  • 破壊的・課金・送信系 にだけ使う原則

🎉 Ch7 完結 (Toolsets, MCP)

  • ✅ L01 Toolset / L02 MCP クライアント / L03 Deferred Tools
  • ✅ Agent の ツール統合の入り口を 3 段階 で総覧 (内製 → 外部 → Human-in-the-Loop)

次の Ch8 Multi-Agent からは、複数 Agent を協調させる Delegation / Hand-off / Pydantic Graph に進みます。

Colab で実際に動かす

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

Open in Colab

notebooks/ch07/03-deferred-tools.ipynb