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 の標準パターン です。
概念: 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 等 を詰めて、外部側で照合できるようにします。
観察: 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を渡して再開 HandleDeferredToolCallscapability でハンドラ集約 (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)。
notebooks/ch07/03-deferred-tools.ipynb