PydanticAI ビジュアルガイド

Lesson CH08-L02

Hand-off

制御を完全に別Agentに引き渡すハンドオフパターン。

読了目安
10 min
Colab目安
15 min
合計
25 min
前提
Delegation

関連: 公式ドキュメント

一行サマリ

Hand-off は アプリケーションコード側で「次にどの Agent を呼ぶか」を決定 し、前 Agent の出力を次 Agent の入力として 完全に制御を移譲 するパターン。Delegation と違って前 Agent には戻らないため、役割が完全に分離した工程パイプライン や、入力に応じたルーティング に向く。

ヒーロー: 制御を「次の Agent」に渡し切る

Delegation (Ch8-L01) は親が子を呼んで戻ってくる コール・リターン の関係。Hand-off は前の Agent が終わったら 役目を終え、後続 Agent が独自の deps / model / instructions で続きを担当します。前後 Agent はお互いを 知らなくてよい ため、結合度が下がります。

図を読み込み中…
図1. Delegation vs Hand-off

概念: 2 つの典型形

構造
直列パイプラインA → B → C を順番に流す翻訳 → 校正 → 要約 / 分類 → 抽出 → 整形
ルーティング最初の Agent が「どの専門 Agent に渡すか」を決定カスタマーサポート → (請求 / 技術 / 解約) ヘ振り分け

直列は Python の単純な順次呼び出し、ルーティングは 最初の Agent の output_type に Enum / Literal を入れて分岐するパターンが定番です。

コード: 3 つのパターン

パターン 1: 直列パイプライン (要約 → 翻訳)

import asyncio
from pydantic_ai import Agent
from pydantic_ai.models.google import GoogleModel
 
model = GoogleModel('gemini-3-flash-preview')
 
summarizer = Agent(
    model,
    instructions='与えられた英語の文章を 3 文以内の英語で要約してください。',
)
 
translator = Agent(
    model,
    instructions='与えられた英文を自然な日本語に翻訳してください。',
)
 
async def pipeline(en_text: str) -> str:
    # Hand-off 1: 要約
    summary_en = (await summarizer.run(en_text)).output
    # Hand-off 2: 翻訳 — summarizer の出力を translator の入力に
    summary_ja = (await translator.run(summary_en)).output
    return summary_ja
 
ja = asyncio.run(pipeline(
    'Pydantic AI is a Python agent framework that focuses on type safety and developer experience...'
))
print(ja)

ポイント:

  • アプリケーションコード (Python の async 関数) が指揮者
  • 各 Agent は前後を知らない (summarizertranslator の存在を知らない)
  • usage は 各 result.usage() を別々に取得 して必要なら合算する

パターン 2: ルーティング (分類 → 専門 Agent)

from typing import Literal
from pydantic import BaseModel, Field
from pydantic_ai import Agent
from pydantic_ai.models.google import GoogleModel
 
model = GoogleModel('gemini-3-flash-preview')
 
class RouteDecision(BaseModel):
    category: Literal['billing', 'technical', 'cancel', 'other']
    reason_ja: str = Field(description='判断理由 (30 字以内)')
 
router = Agent(
    model,
    output_type=RouteDecision,
    instructions='ユーザー問い合わせを billing / technical / cancel / other に分類してください。',
)
 
billing_agent = Agent(model, instructions='請求関連の問い合わせに丁寧に答える担当です。')
tech_agent = Agent(model, instructions='技術トラブルの一次対応をする担当です。')
cancel_agent = Agent(model, instructions='解約手続きを案内する担当です。')
fallback_agent = Agent(model, instructions='その他の問い合わせを丁寧に受け止める担当です。')
 
async def handle(user_msg: str) -> str:
    decision = (await router.run(user_msg)).output
    target = {
        'billing': billing_agent,
        'technical': tech_agent,
        'cancel': cancel_agent,
    }.get(decision.category, fallback_agent)
    final = (await target.run(user_msg)).output
    return f'[{decision.category}] {final}'

router「振り分けだけ」を仕事にする極小 Agent。本実装では具体応答を生成しないので トークン消費が小さい のがメリット。

パターン 3: Hand-off + 構造化引き継ぎ

工程間で 構造化データを丸ごと引き継ぎ たいとき。前 Agent の output_type=BaseModel を、次 Agent への入力テキストに整形して渡します。

from pydantic import BaseModel, Field
from pydantic_ai import Agent
from pydantic_ai.models.google import GoogleModel
 
model = GoogleModel('gemini-3-flash-preview')
 
class ExtractedInvoice(BaseModel):
    vendor: str = Field(description='請求元')
    total_yen: int = Field(description='合計金額 (円)')
    items: list[str] = Field(description='品目リスト')
 
extractor = Agent(
    model,
    output_type=ExtractedInvoice,
    instructions='請求書テキストから vendor / total_yen / items を抽出してください。',
)
 
approver = Agent(
    model,
    instructions='構造化された請求書情報を読み、承認可否と理由を 2 文で答えてください。',
)
 
async def review(invoice_text: str) -> str:
    inv = (await extractor.run(invoice_text)).output  # ExtractedInvoice
    prompt = (
        f'次の請求書を承認するかどうか判定してください:\\n'
        f'- 請求元: {inv.vendor}\\n'
        f'- 金額: {inv.total_yen}\\n'
        f'- 品目: {", ".join(inv.items)}'
    )
    return (await approver.run(prompt)).output

各 Agent は 専門の input/output 型 に集中でき、テストも容易になります。

図を読み込み中…
図2. ルーティング Hand-off の構造

観察: usage は集約されない

Hand-off では各 agent.run(...) の usage は 独立 です。Delegation のように usage=ctx.usage で集約されないため、必要なら自前で合算します。

r1 = await router.run(msg)
r2 = await target.run(msg)
total_in = r1.usage().input_tokens + r2.usage().input_tokens
total_out = r1.usage().output_tokens + r2.usage().output_tokens

Logfire (Ch9) を使うと trace ID で繋がった可視化 ができ、自前合算なしで全体像が見えます。

まとめ

  • アプリケーションコード が次の Agent を選ぶ Programmatic ハンドオフ
  • 直列パイプライン + ルーティング が 2 大典型形
  • 前後 Agent はお互いを知らない → 結合度が低い / テスト容易
  • usage は集約されないので 自前合算 or Logfire
  • 直列 4 段以上は Pydantic Graph (次レッスン) を検討

次レッスンでは Pydantic Graph — 状態と遷移を持つグラフでマルチステップ処理を組むパターンを扱います。

Colab で実際に動かす

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

Open in Colab

notebooks/ch08/02-handoff.ipynb