GEMINI LABEN
NANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定ですNANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定です
記事一覧/高度な活用
高度な活用/2026-04-14上級

Google ADK Callbacks & Guardrails 実装ガイド:エージェントの動作監視・安全制御・本番品質保証

Google ADKのCallbacksとGuardrailsを活用し、AIエージェントの動作をリアルタイムで監視・制御する本番実装を解説。カスタムロギング・安全フィルタ・コスト制御・品質保証の動作確認済みコードを完全網羅。

google-adk2callbacksguardrailsgemini-api279production105monitoring5agent-safetypython103

なぜAIエージェントの「制御できなさ」が本番の最大リスクなのか

Google ADKでエージェントを構築して、ローカルで動作確認できました。テストも通った。でも本番にデプロイしてしばらくすると、予期しない問題が発生する——これはAIエージェント開発者が必ずぶつかる壁です。

具体的にはこんなシーンです。

ユーザーが「競合他社の価格を調べて」と依頼したところ、エージェントが競合他社の内部システムへの不正アクセスを試みるツール呼び出しを生成しました。あるいは、コスト試算ツールとして作ったはずのエージェントが、ループし続けて1回のリクエストで数千円分のAPIトークンを消費しました。さらに悪いケースでは、カスタマーサポートエージェントが「返金します」と勝手に約束してしまった。

これらはすべて実際の本番障害として報告されているパターンです。Gemini APIの品質は年々向上していますが、LLMの確率的な性質上、エッジケースでの予期しない挙動を完全になくすことはできません。

Google ADKは、この問題に対してCallbacksとGuardrailsという2つの仕組みを提供しています。Callbacksはエージェントの処理ステップごとにカスタム関数を差し込むフック、Guardrailsはインプット・アウトプットを自動検査・フィルタリングする安全層です。この2つを組み合わせることで、「エージェントが何をしようとしているか」を常に把握し、「してはいけないことを物理的に止める」仕組みが実現できます。


Google ADK Callbacksの基本構造

Callbacksが差し込めるタイミング

ADKのCallbacksは、エージェントの処理ライフサイクルの特定のタイミングで呼ばれます。2026年4月時点のADK v1.xでは、以下のCallbackポイントが利用可能です。

エージェントレベル

  • before_agent_callback: エージェントの処理開始前
  • after_agent_callback: エージェントの処理完了後

モデル呼び出しレベル

  • before_model_callback: Geminiモデルへのリクエスト送信前
  • after_model_callback: Geminiモデルのレスポンス受信後

ツール呼び出しレベル

  • before_tool_callback: ツール実行前
  • after_tool_callback: ツール実行後

これらのCallbackポイントを使えば、エージェントの実行フロー全体を把握・制御できます。

Callbackの基本実装パターン

まずCallbacksを使ったシンプルなロギング実装から始めましょう。

import time
import logging
from typing import Any, Optional
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools import FunctionTool
from google.genai import types
 
# ロガーの設定
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("adk.monitoring")
 
# ===== Before Model Callback =====
def log_before_model(
    callback_context: Any,
    llm_request: types.GenerateContentRequest
) -> Optional[types.GenerateContentResponse]:
    """
    モデル呼び出し前にリクエスト内容をログに記録する。
    Noneを返すと処理を続行、GenerateContentResponseを返すと
    モデル呼び出しをスキップしてそのレスポンスを使用する。
    """
    # リクエストのトークン数を推定(実際には count_tokens APIを使う)
    contents_preview = str(llm_request.contents)[:200]
    logger.info(
        f"[BEFORE_MODEL] agent={callback_context.agent_name} "
        f"contents_preview={contents_preview}"
    )
 
    # セッションに開始時刻を記録(after_model_callbackで経過時間を計算)
    callback_context.state["_model_call_start"] = time.time()
 
    return None  # Noneを返すと通常通りモデルが呼ばれる
 
# ===== After Model Callback =====
def log_after_model(
    callback_context: Any,
    llm_response: types.GenerateContentResponse
) -> Optional[types.GenerateContentResponse]:
    """
    モデルレスポンス受信後にレイテンシと出力をログに記録する。
    """
    start_time = callback_context.state.get("_model_call_start", time.time())
    elapsed_ms = int((time.time() - start_time) * 1000)
 
    # 使用トークン数
    usage = llm_response.usage_metadata
    prompt_tokens = usage.prompt_token_count if usage else 0
    output_tokens = usage.candidates_token_count if usage else 0
 
    logger.info(
        f"[AFTER_MODEL] agent={callback_context.agent_name} "
        f"latency_ms={elapsed_ms} "
        f"prompt_tokens={prompt_tokens} "
        f"output_tokens={output_tokens}"
    )
 
    return None  # Noneを返すとレスポンスをそのまま使用
 
# エージェントにCallbacksを設定
agent = LlmAgent(
    name="monitored_agent",
    model="gemini-2.5-pro",
    instruction="あなたは役立つアシスタントです。",
    before_model_callback=log_before_model,
    after_model_callback=log_after_model,
)
 
# 期待する出力例:
# 2026-04-14 10:45:01 [INFO] adk.monitoring: [BEFORE_MODEL] agent=monitored_agent contents_preview=...
# 2026-04-14 10:45:03 [INFO] adk.monitoring: [AFTER_MODEL] agent=monitored_agent latency_ms=1842 prompt_tokens=523 output_tokens=287

このコードで重要なのは、before_model_callbackafter_model_callbackの戻り値の意味です。Noneを返すと処理が通常通り続行されます。一方、before_model_callbackGenerateContentResponseを返すと、実際のモデル呼び出しがスキップされ、そのレスポンスが使われます。これがGuardrailsの実装の核心になります。


Guardrailsによる安全制御の実装

インプットGuardrail:有害なリクエストをブロックする

import re
from google.genai import types
 
# ブロックするパターン(実際はより精緻なパターンを使う)
BLOCKED_PATTERNS = [
    r"パスワード|password|credential",
    r"不正アクセス|unauthorized.*access|hack",
    r"個人情報.*削除|delete.*personal.*data",
    r"返金|refund|compensation",  # 承認なしに確約してはいけない言葉
]
 
SENSITIVE_DOMAINS = [
    "internal.company.com",
    "admin.myservice.com",
    "*.secret-api.com",
]
 
def input_guardrail(
    callback_context: Any,
    llm_request: types.GenerateContentRequest
) -> Optional[types.GenerateContentResponse]:
    """
    リクエストを分析し、危険なパターンを含む場合はモデル呼び出しを
    ブロックして安全なレスポンスを返す。
 
    これは before_model_callback として設定する。
    """
    # ユーザーメッセージを抽出
    user_messages = []
    for content in llm_request.contents:
        if content.role == "user":
            for part in content.parts:
                if hasattr(part, "text") and part.text:
                    user_messages.append(part.text)
 
    full_user_input = " ".join(user_messages).lower()
 
    # パターンマッチング
    for pattern in BLOCKED_PATTERNS:
        if re.search(pattern, full_user_input, re.IGNORECASE):
            logger.warning(
                f"[GUARDRAIL_BLOCKED] agent={callback_context.agent_name} "
                f"pattern={pattern} "
                f"input_preview={full_user_input[:100]}"
            )
            # ブロック: モデルを呼ばずに安全なレスポンスを返す
            return types.GenerateContentResponse(
                candidates=[
                    types.Candidate(
                        content=types.Content(
                            role="model",
                            parts=[types.Part(
                                text="申し訳ありませんが、そのリクエストにはお応えできません。"
                                     "ご不明な点があれば、担当者にお問い合わせください。"
                            )]
                        ),
                        finish_reason=types.FinishReason.STOP
                    )
                ]
            )
 
    return None  # 問題なければ通常通り処理
 
# エージェントに設定
safe_agent = LlmAgent(
    name="safe_customer_support_agent",
    model="gemini-2.5-pro",
    instruction="""あなたはカスタマーサポートエージェントです。
    製品に関する質問に答え、技術サポートを提供してください。
    返金・補償の約束は絶対にしないでください。""",
    before_model_callback=input_guardrail,  # インプットGuardrail
)

このパターンの「なぜそう実装するのか」の解説が重要です。モデルにシステムプロンプトで「〜しないでください」と指示するだけでは不十分です。LLMは複雑なコンテキストや迂回的な表現に対して指示を守れない場合があります。Callbacksで物理的にブロックすることで、システムプロンプトの指示を守れなかった場合の「最後の防衛線」になります。

アウトプットGuardrail:危険なレスポンスをフィルタリングする

import json
from typing import Any, Optional
from google.genai import types
 
# 出力に含めてはいけないパターン
OUTPUT_BLOCKED_PATTERNS = [
    r"返金いたします|refund will be processed",
    r"補償します|you will be compensated",
    r"\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}",  # クレジットカード番号パターン
    r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",  # 特定のメール開示阻止
]
 
def output_guardrail(
    callback_context: Any,
    llm_response: types.GenerateContentResponse
) -> Optional[types.GenerateContentResponse]:
    """
    モデルのレスポンスを検査し、危険なコンテンツを含む場合は
    安全なレスポンスに置き換える。
 
    これは after_model_callback として設定する。
    """
    if not llm_response.candidates:
        return None
 
    candidate = llm_response.candidates[0]
    if not candidate.content or not candidate.content.parts:
        return None
 
    # テキスト出力を結合
    output_text = " ".join(
        part.text for part in candidate.content.parts
        if hasattr(part, "text") and part.text
    )
 
    # パターンチェック
    for pattern in OUTPUT_BLOCKED_PATTERNS:
        if re.search(pattern, output_text, re.IGNORECASE):
            logger.error(
                f"[OUTPUT_GUARDRAIL_TRIGGERED] agent={callback_context.agent_name} "
                f"pattern={pattern} "
                f"output_preview={output_text[:150]}"
            )
            # 危険な出力を安全なレスポンスに置き換え
            return types.GenerateContentResponse(
                candidates=[
                    types.Candidate(
                        content=types.Content(
                            role="model",
                            parts=[types.Part(
                                text="この件については担当部署が対応いたします。"
                                     "お客様サービスセンター(0120-XXX-XXX)までご連絡ください。"
                            )]
                        ),
                        finish_reason=types.FinishReason.STOP
                    )
                ]
            )
 
    return None  # 問題なければそのままのレスポンスを使用
 
# before + after 両方のGuardrailを設定したエージェント
fully_guarded_agent = LlmAgent(
    name="fully_guarded_agent",
    model="gemini-2.5-pro",
    instruction="あなたはカスタマーサポートエージェントです。",
    before_model_callback=input_guardrail,
    after_model_callback=output_guardrail,
)

コスト暴走を防ぐトークン予算Callback

本番で最も多い障害の一つが「トークン暴走」です。エージェントが無限ループに入ったり、ユーザーが異常に長いコンテキストを送りつけたりすることで、単一のリクエストで膨大なトークン(=コスト)を消費してしまいます。

セッションごとのトークン予算制御

import asyncio
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService, Session
from google.genai import types
 
# トークン予算の設定(本番では環境変数で管理する)
TOKEN_BUDGET_CONFIG = {
    "free_tier": {
        "max_tokens_per_session": 50_000,
        "max_tokens_per_turn": 8_000,
        "max_tool_calls_per_session": 20,
    },
    "pro_tier": {
        "max_tokens_per_session": 500_000,
        "max_tokens_per_turn": 50_000,
        "max_tool_calls_per_session": 100,
    }
}
 
def token_budget_guardrail(
    callback_context: Any,
    llm_request: types.GenerateContentRequest
) -> Optional[types.GenerateContentResponse]:
    """
    セッションのトークン使用量を追跡し、予算を超えた場合にブロックする。
    ユーザーの tier に応じた制限を適用する。
    """
    state = callback_context.state
 
    # ユーザー tier を取得(セッション開始時に設定しておく)
    user_tier = state.get("user_tier", "free_tier")
    budget = TOKEN_BUDGET_CONFIG.get(user_tier, TOKEN_BUDGET_CONFIG["free_tier"])
 
    # セッションの累積トークン数を確認
    total_tokens = state.get("total_tokens_used", 0)
    tool_calls = state.get("total_tool_calls", 0)
 
    # ① セッション全体の予算チェック
    if total_tokens >= budget["max_tokens_per_session"]:
        logger.warning(
            f"[TOKEN_BUDGET_EXCEEDED] session_id={callback_context.session_id} "
            f"total={total_tokens} budget={budget['max_tokens_per_session']}"
        )
        return types.GenerateContentResponse(
            candidates=[types.Candidate(
                content=types.Content(
                    role="model",
                    parts=[types.Part(
                        text="申し訳ありませんが、このセッションでの利用上限に達しました。"
                             "新しい会話を開始するか、プランのアップグレードをご検討ください。"
                    )]
                ),
                finish_reason=types.FinishReason.STOP
            )]
        )
 
    # ② ツール呼び出し回数のチェック
    if tool_calls >= budget["max_tool_calls_per_session"]:
        logger.warning(
            f"[TOOL_CALLS_EXCEEDED] session_id={callback_context.session_id} "
            f"tool_calls={tool_calls}"
        )
        return types.GenerateContentResponse(
            candidates=[types.Candidate(
                content=types.Content(
                    role="model",
                    parts=[types.Part(
                        text="このセッションでのツール使用回数の上限に達しました。"
                    )]
                ),
                finish_reason=types.FinishReason.STOP
            )]
        )
 
    return None
 
def token_usage_tracker(
    callback_context: Any,
    llm_response: types.GenerateContentResponse
) -> Optional[types.GenerateContentResponse]:
    """
    レスポンスのトークン使用量を集計してセッションに記録する。
    """
    state = callback_context.state
    usage = llm_response.usage_metadata
 
    if usage:
        # 累積トークン数を更新
        current_total = state.get("total_tokens_used", 0)
        new_tokens = (usage.prompt_token_count or 0) + (usage.candidates_token_count or 0)
        state["total_tokens_used"] = current_total + new_tokens
 
        logger.info(
            f"[TOKEN_TRACKER] session_id={callback_context.session_id} "
            f"this_turn={new_tokens} "
            f"total={state['total_tokens_used']}"
        )
 
    return None
 
# コスト管理付きエージェント
cost_controlled_agent = LlmAgent(
    name="cost_controlled_agent",
    model="gemini-2.5-pro",
    instruction="あなたは役立つアシスタントです。",
    before_model_callback=token_budget_guardrail,
    after_model_callback=token_usage_tracker,
)

ツール呼び出しCallbacksによるセキュリティ制御

エージェントにツールを与えた場合、そのツール呼び出しも監視・制御できます。特に外部APIを呼び出すツールやファイルシステムを操作するツールでは、before/after_tool_callbackが重要です。

ツール呼び出しの検証と監査ログ

from google.adk.agents import LlmAgent
from google.adk.tools import FunctionTool
from typing import Any, Optional
 
# 許可するAPIエンドポイントのホワイトリスト
ALLOWED_API_HOSTS = [
    "api.example.com",
    "data.public-api.org",
    "opendata.gov.jp",
]
 
def tool_call_validator(
    tool: Any,
    tool_args: dict,
    tool_context: Any
) -> Optional[dict]:
    """
    ツール実行前に引数を検証する。
    Noneを返すと正常にツールが実行される。
    dictを返すと、ツールを実行せずにそのdictを結果として使用する。
 
    これは before_tool_callback として設定する。
    """
    tool_name = tool.name if hasattr(tool, "name") else str(tool)
 
    # HTTP リクエストツールの場合、URLを検証
    if tool_name == "make_http_request":
        url = tool_args.get("url", "")
        from urllib.parse import urlparse
        parsed = urlparse(url)
        host = parsed.netloc
 
        if not any(host.endswith(allowed) for allowed in ALLOWED_API_HOSTS):
            logger.warning(
                f"[TOOL_BLOCKED] tool={tool_name} "
                f"blocked_url={url} "
                f"session_id={tool_context.session_id}"
            )
            # ツール実行をブロックして代替結果を返す
            return {
                "error": f"セキュリティポリシーにより、{host} へのアクセスはブロックされました。"
            }
 
    # 監査ログ: 全てのツール呼び出しを記録
    logger.info(
        f"[TOOL_CALL] tool={tool_name} "
        f"args_keys={list(tool_args.keys())} "
        f"session_id={tool_context.session_id}"
    )
 
    return None  # 正常にツールを実行
 
def tool_result_auditor(
    tool: Any,
    tool_args: dict,
    tool_context: Any,
    tool_response: Any
) -> Optional[Any]:
    """
    ツール実行後に結果を検査する。
    Noneを返すと結果をそのままエージェントに渡す。
 
    これは after_tool_callback として設定する。
    """
    tool_name = tool.name if hasattr(tool, "name") else str(tool)
 
    # 結果に個人情報が含まれていないか確認
    result_str = str(tool_response)
    if re.search(r'\d{3}-\d{4}-\d{4}|\d{4}-\d{2}-\d{2}T', result_str):
        logger.info(
            f"[TOOL_PII_DETECTED] tool={tool_name} "
            f"pii_type=possible_phone_or_date"
        )
 
    logger.info(
        f"[TOOL_RESULT] tool={tool_name} "
        f"result_length={len(result_str)} "
        f"session_id={tool_context.session_id}"
    )
 
    return None
 
# サンプルツール
def search_product_database(query: str) -> dict:
    """製品データベースを検索する"""
    # 実際の実装ではDBクエリを行う
    return {"products": [{"id": "P001", "name": "サンプル製品", "price": 9800}]}
 
product_tool = FunctionTool(func=search_product_database)
 
# ツール監視付きエージェント
audited_agent = LlmAgent(
    name="audited_agent",
    model="gemini-2.5-pro",
    instruction="製品情報の検索と案内を行うエージェントです。",
    tools=[product_tool],
    before_tool_callback=tool_call_validator,
    after_tool_callback=tool_result_auditor,
)

本番で使えるモニタリングダッシュボードの構築

Callbacksで収集したデータを活用してリアルタイムモニタリングシステムを構築します。ここではGoogle Cloud Monitoringへの指標送信例を示します。

Cloud Monitoring への指標送信

import time
import os
from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional
from collections import defaultdict
from google.genai import types
 
# ===== メトリクス収集クラス =====
@dataclass
class AgentMetrics:
    """エージェントのパフォーマンス指標を収集するクラス"""
    agent_name: str
    session_id: str
    turn_count: int = 0
    total_prompt_tokens: int = 0
    total_output_tokens: int = 0
    total_latency_ms: int = 0
    guardrail_blocks: int = 0
    tool_calls: int = 0
    errors: List[str] = field(default_factory=list)
 
# グローバルメトリクスストア(本番ではRedisやCloud Firestoreを使う)
_metrics_store: Dict[str, AgentMetrics] = {}
 
def metrics_before_model(
    callback_context: Any,
    llm_request: types.GenerateContentRequest
) -> Optional[types.GenerateContentResponse]:
    """モデル呼び出し前に計時開始"""
    session_id = callback_context.session_id
    agent_name = callback_context.agent_name
 
    if session_id not in _metrics_store:
        _metrics_store[session_id] = AgentMetrics(
            agent_name=agent_name,
            session_id=session_id
        )
 
    callback_context.state["_turn_start_time"] = time.time()
    _metrics_store[session_id].turn_count += 1
    return None
 
def metrics_after_model(
    callback_context: Any,
    llm_response: types.GenerateContentResponse
) -> Optional[types.GenerateContentResponse]:
    """モデルレスポンス後にメトリクスを更新してCloud Monitoringに送信"""
    session_id = callback_context.session_id
    metrics = _metrics_store.get(session_id)
    if not metrics:
        return None
 
    # レイテンシ計算
    start_time = callback_context.state.get("_turn_start_time", time.time())
    latency_ms = int((time.time() - start_time) * 1000)
    metrics.total_latency_ms += latency_ms
 
    # トークン使用量
    usage = llm_response.usage_metadata
    if usage:
        metrics.total_prompt_tokens += usage.prompt_token_count or 0
        metrics.total_output_tokens += usage.candidates_token_count or 0
 
    # Cloud Monitoring への送信(非同期で行うことを推奨)
    _send_to_cloud_monitoring(metrics, latency_ms, usage)
 
    return None
 
def _send_to_cloud_monitoring(
    metrics: AgentMetrics,
    latency_ms: int,
    usage: Any
) -> None:
    """
    Cloud Monitoring にカスタムメトリクスを送信する。
    本番では google-cloud-monitoring ライブラリを使用。
 
    pip install google-cloud-monitoring
    """
    try:
        from google.cloud import monitoring_v3
        from google.cloud.monitoring_v3 import MetricServiceClient
        from google.protobuf.timestamp_pb2 import Timestamp
 
        project_id = os.environ.get("GCP_PROJECT_ID", "your-project-id")
        client = MetricServiceClient()
        project_name = f"projects/{project_id}"
 
        now = time.time()
        seconds = int(now)
        nanos = int((now - seconds) * 10**9)
 
        # レイテンシメトリクス送信
        series = monitoring_v3.TimeSeries()
        series.metric.type = "custom.googleapis.com/adk/agent_latency_ms"
        series.metric.labels["agent_name"] = metrics.agent_name
        series.resource.type = "global"
 
        point = monitoring_v3.Point()
        point.value.int64_value = latency_ms
        point.interval.end_time.seconds = seconds
        point.interval.end_time.nanos = nanos
        series.points = [point]
 
        client.create_time_series(
            request={"name": project_name, "time_series": [series]}
        )
 
    except Exception as e:
        # モニタリング失敗はエージェントの動作を止めない
        logger.warning(f"Cloud Monitoring送信失敗(エージェント動作は継続): {e}")
 
# モニタリング付き本番エージェント
production_agent = LlmAgent(
    name="production_agent",
    model="gemini-2.5-pro",
    instruction="本番環境で稼働する高品質なアシスタントです。",
    before_model_callback=metrics_before_model,
    after_model_callback=metrics_after_model,
)

複数のCallbacksを組み合わせるComposite Callback パターン

実際の本番システムでは、ロギング・Guardrail・コスト管理・モニタリングを全て組み合わせる必要があります。しかし1つのCallback関数に全ての処理を詰め込むと、保守性が下がります。

from typing import Callable, List, Any, Optional
from google.genai import types
 
# ===== Composite Callback ファクトリ =====
def compose_before_model_callbacks(
    *callbacks: Callable
) -> Callable:
    """
    複数のbefore_model_callbackを順番に実行するcomposite callbackを作る。
    いずれかがNone以外を返した時点でそれを返し、後続のcallbackをスキップする。
    """
    def composed(
        callback_context: Any,
        llm_request: types.GenerateContentRequest
    ) -> Optional[types.GenerateContentResponse]:
        for cb in callbacks:
            result = cb(callback_context, llm_request)
            if result is not None:
                return result  # ブロック: 後続をスキップ
        return None  # 全て通過
 
    return composed
 
def compose_after_model_callbacks(
    *callbacks: Callable
) -> Callable:
    """
    複数のafter_model_callbackを順番に実行するcomposite callbackを作る。
    最後にNone以外を返したcallbackの値が使われる。
    """
    def composed(
        callback_context: Any,
        llm_response: types.GenerateContentResponse
    ) -> Optional[types.GenerateContentResponse]:
        current_response = llm_response
        override = None
        for cb in callbacks:
            result = cb(callback_context, current_response)
            if result is not None:
                override = result
                current_response = result  # 次のcallbackには上書き済みレスポンスを渡す
        return override  # 最後の上書きを返す(なければNone)
 
    return composed
 
# ===== 本番エージェントのフル設定 =====
full_production_agent = LlmAgent(
    name="full_production_agent",
    model="gemini-2.5-pro",
    instruction="""あなたはカスタマーサポートエージェントです。
    製品に関する質問に正確かつ丁寧に答えてください。
    補償・返金の約束は絶対にしないでください。""",
    tools=[product_tool],
    # 複数のBefore Callbackを合成
    before_model_callback=compose_before_model_callbacks(
        input_guardrail,         # ① セキュリティフィルタ
        token_budget_guardrail,  # ② トークン予算チェック
        log_before_model,        # ③ ロギング(ブロックしないので最後でOK)
        metrics_before_model,    # ④ メトリクス記録
    ),
    # 複数のAfter Callbackを合成
    after_model_callback=compose_after_model_callbacks(
        output_guardrail,        # ① アウトプットフィルタ(優先度最高)
        token_usage_tracker,     # ② トークン使用量集計
        log_after_model,         # ③ ロギング
        metrics_after_model,     # ④ メトリクス送信
    ),
    before_tool_callback=tool_call_validator,
    after_tool_callback=tool_result_auditor,
)

このComposite Callbackパターンの利点は、各Callbackが単一責任の原則に従って独立しており、テストが容易なことです。また、環境(開発・ステージング・本番)に応じて組み合わせを変えることも簡単にできます。


よくある間違いと落とし穴

落とし穴1:Callbackでエラーをraiseしてエージェントをクラッシュさせる

# ❌ ダメなパターン
def bad_callback(callback_context, llm_request):
    db_result = fetch_from_database()  # DB接続エラーが起きたら?
    if not db_result:
        raise ValueError("データベース接続失敗")  # エージェント全体がクラッシュ!
    return None
 
# ✅ 正しいパターン
def good_callback(callback_context, llm_request):
    try:
        db_result = fetch_from_database()
        if not db_result:
            logger.warning("DB接続失敗: Callbackをスキップして処理続行")
            return None  # エラーでもエージェントは動き続ける
    except Exception as e:
        logger.error(f"Callback内例外(処理続行): {e}")
        return None  # 例外もキャッチしてNoneを返す
    return None

Callbackはエージェントの「外側のコード」です。Callback内で例外をraiseすると、エージェント全体がクラッシュします。モニタリング目的のCallbackが原因で本番サービスが停止するという本末転倒な事態を避けるため、Callback内では必ず例外をキャッチしてNoneを返すことを徹底してください。

落とし穴2:after_model_callbackでレスポンスを変更したときのログ不整合

# ❌ 問題のあるパターン
def bad_output_guardrail(callback_context, llm_response):
    # レスポンスを置き換えたのに、元のレスポンスをログに記録している
    blocked_response = create_safe_response()
    logger.info(f"Response: {llm_response}")  # 置き換え前の危険な出力がログに残る!
    return blocked_response
 
# ✅ 正しいパターン
def good_output_guardrail(callback_context, llm_response):
    blocked_response = create_safe_response()
    # 置き換えた事実と置き換え前の内容(要約)をログに残す
    logger.warning(
        f"[OUTPUT_REPLACED] "
        f"original_preview={str(llm_response)[:100]} "
        f"replacement=safe_response"
    )
    return blocked_response

落とし穴3:Guardrailのパターンが広すぎてFalse Positiveが多発する

過度に広いパターン(例:「返金」を含む全ての入力をブロック)は、正当なユーザー問い合わせまで弾いてしまいます。Guardrailのパターンは段階的に追加し、最初の1〜2週間はブロック(return blocked_response)せずにログだけ記録し、実際の分布を確認してから本格的にブロックするのがベストプラクティスです。

# 段階的導入パターン
GUARDRAIL_MODE = os.environ.get("GUARDRAIL_MODE", "monitor")  # monitor / enforce
 
def gradual_guardrail(callback_context, llm_request):
    is_suspicious = check_patterns(llm_request)
    if is_suspicious:
        logger.warning(f"[GUARDRAIL_SUSPICIOUS] mode={GUARDRAIL_MODE}")
        if GUARDRAIL_MODE == "enforce":
            return create_blocked_response()  # 本番でブロック
        # monitorモードではログだけ記録してスルー
    return None

落とし穴4:セッション状態を使ったCallbackのスレッドセーフ問題

# ❌ グローバル変数でセッション状態を管理(スレッドアンセーフ)
_token_counts = {}  # 複数リクエストが同時に来ると競合が発生
 
# ✅ callback_context.state を使う(ADKがセッションごとに管理してくれる)
def thread_safe_callback(callback_context, llm_request):
    # callback_context.state はセッションローカルなので安全
    current = callback_context.state.get("token_count", 0)
    callback_context.state["token_count"] = current + estimate_tokens(llm_request)
    return None

テスト戦略:Callbacksを含むエージェントの品質保証

Callbacksを設定したエージェントのテストは、Callbackを含めて行う必要があります。

import pytest
from unittest.mock import MagicMock, patch
from google.genai import types
 
class TestInputGuardrail:
    """InputGuardrailのユニットテスト"""
 
    def _create_mock_context(self, agent_name="test_agent", session_id="test-session"):
        ctx = MagicMock()
        ctx.agent_name = agent_name
        ctx.session_id = session_id
        ctx.state = {}
        return ctx
 
    def _create_request_with_text(self, text: str):
        return types.GenerateContentRequest(
            contents=[
                types.Content(
                    role="user",
                    parts=[types.Part(text=text)]
                )
            ]
        )
 
    def test_blocks_refund_request(self):
        """返金に関するリクエストがブロックされることを確認"""
        ctx = self._create_mock_context()
        request = self._create_request_with_text("返金してください")
 
        result = input_guardrail(ctx, request)
 
        assert result is not None, "返金リクエストはブロックされるべき"
        response_text = result.candidates[0].content.parts[0].text
        assert "お応えできません" in response_text
 
    def test_allows_normal_product_inquiry(self):
        """通常の製品問い合わせが通過することを確認"""
        ctx = self._create_mock_context()
        request = self._create_request_with_text("製品Aの使い方を教えてください")
 
        result = input_guardrail(ctx, request)
 
        assert result is None, "通常のリクエストはブロックされるべきでない"
 
    def test_token_budget_blocks_exceeded_sessions(self):
        """トークン予算超過時にブロックされることを確認"""
        ctx = self._create_mock_context()
        ctx.state["total_tokens_used"] = 100_000  # 上限超過
        ctx.state["user_tier"] = "free_tier"
        request = self._create_request_with_text("こんにちは")
 
        result = token_budget_guardrail(ctx, request)
 
        assert result is not None, "予算超過時はブロックされるべき"
 
# テスト実行
# pytest tests/test_callbacks.py -v

全体を振り返って:Callbacksで「制御できる」エージェントを作る

ADKのCallbacksとGuardrailsは、AIエージェントを「ブラックボックス」から「透明で制御可能なシステム」に変える鍵です。

本記事で実装したパターンを整理します。

インプットGuardrailは、有害・不適切なリクエストをモデル呼び出し前にブロックします。アウトプットGuardrailは、危険なレスポンスをユーザーに届く前にフィルタリングします。トークン予算Callbackは、コスト暴走をセッションレベルで防ぎます。ツール呼び出しCallbackは、不正なAPI呼び出しを物理的にブロックし、全アクションを監査ログに記録します。Composite Callbackパターンは、複数の関心事を独立したコードとして保ちながら組み合わせます。

次のステップとして、まずbefore_model_callbackafter_model_callbackにロギングのみを設定し、2週間ほど本番トラフィックのデータを収集してください。そのデータをもとにGuardrailのパターンを設計すると、False Positiveを最小限に抑えながら安全制御を実現できます。Callbacksは後から追加・変更できるため、段階的に導入するのが現実的な本番化のアプローチです。

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

高度な活用2026-04-23
Gemini API の Prompt Injection 対策:本番運用で必要なマルチレイヤー防御アーキテクチャ
Gemini API を本番運用するなら避けて通れない Prompt Injection 攻撃に対し、入力サニタイズ・指示強化・構造化出力・モデレーター LLM の4層防御を動くコードで設計する実戦ガイドです。
高度な活用2026-04-20
Gemini API 本番アーキテクチャ設計2026 — スケーラブルで安定した AI システムを構築する設計パターン集
Gemini APIを本番運用するための設計パターンを徹底解説。レジリエントなAPIクライアント、多層キャッシング、マルチテナント設計、観測可能性、コスト制御まで実装コード付きで解説します。
API / SDK2026-04-11
Gemini API レート制限と 429 対策の運用ノート ― 個人開発アプリで踏んだ落とし穴と対処
Gemini API のレート制限と 429 エラーに本番アプリで遭遇した経験から、指数バックオフ・アダプティブ制御・マルチキープール・Cloud Monitoring 連携を実装メモとしてまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →