Google ADK(Agent Development Kit)で複数のエージェントを組み合わせていると、内部処理の途中経過がそのままユーザーに見えてしまうことがあります。

検索エージェントが「○○を検索しました」「△△が見つかりました」と逐一報告してきたり、要約エージェントの生の出力がチャット画面に流れ込んできたり……。ユーザーに見せるべきは最終的な結果だけのはずなのに、内部の処理ログがだだ漏れになってしまう。

この記事では、その原因と AgentTool を使った解決策、さらに「プロダクション環境なら不要では?」という疑問への回答と、実装時のコツを紹介します。

1. 構築していたエージェントの構成

今回は PoC として ADK Web を使いながら開発・検証を進めていました。

構成としては、複数の検索エージェントが並列でデータを取得し、その結果を要約エージェントがまとめて返却するという流れです。

エージェントの構造

  1. analysis_agent(呼び出し元)
  2. └── sequential_search_agents(並列検索、要約を順次呼び出し)
  3.     ├── parallel_search_agents(並列で書籍検索を実行)
  4.     │   └── search_agent1 / search_agent2 / search_agent3
  5.     └── summary_agent(要約の実行)

実装コード(Python)

Python

  1. from google.adk.agents import Agent
  2. from google.adk.agents.parallel_agent import ParallelAgent
  3. from google.adk.agents.sequential_agent import SequentialAgent
  4. # 並列検索エージェント(3つ)
  5. search_agent1 = Agent(name=”search_agent1″, …)
  6. search_agent2 = Agent(name=”search_agent2″, …)
  7. search_agent3 = Agent(name=”search_agent3″, …)
  8. # 並列実行の設定
  9. parallel_search_agents = ParallelAgent(
  10.     name=”parallel_search_agents”,
  11.     sub_agents=[search_agent1, search_agent2, search_agent3],
  12. )
  13. # 要約エージェント
  14. summary_agent = Agent(name=”summary_agent”, …)
  15. # 「並列検索 → 要約」の順で実行するシーケンシャルエージェント
  16. sequential_search_agents = SequentialAgent(
  17.     name=”sequential_search_agents”,
  18.     sub_agents=[parallel_search_agents, summary_agent],
  19. )
  20. # メインの呼び出し元エージェント
  21. analysis_agent = Agent(
  22.     name=”analysis_agent”,
  23.     sub_agents=[sequential_search_agents],# ← sub_agentsに登録
  24. )

2. 発生した問題:中間出力がユーザーに筒抜けになる

この構成で実行すると、各検索エージェントの出力や、要約エージェントの処理ログがそのままチャット画面に流れてきてしまいます。

[search_agent1]

  「○○に関する情報を検索しました。結果は以下の通りです:…(長い検索結果)…」

[search_agent2]

  「△△について検索しました。…(長い検索結果)…」

[summary_agent]

  「要約結果を生成しました。」

[analysis_agent]

  「では分析を始めます…」

ユーザーが知りたいのは最後の analysis_agent による分析結果のみ。その過程にあるプロセスはあくまで「内部処理」であり、ユーザーに表示する必要はありません。

3. 解決策:AgentTool でラップして「ツール」化する

結論から言うと、AgentTool でエージェントをラップし、sub_agents ではなく tools として登録するのが正解でした。

修正後のコード

Python

  1. from google.adk.agents import Agent
  2. from google.adk.tools.agent_tool import AgentTool
  3. # 1. 中間出力を隠したいエージェントをAgentToolでラップする
  4. sequential_search_tool = AgentTool(agent=sequential_search_agents)
  5. # 2. sub_agentsではなく「tools」として登録する
  6. analysis_agent = Agent(
  7.     name=”analysis_agent”,
  8.     tools=[sequential_search_tool], # ← ここがポイント!
  9.   …
  10. )

たったこれだけで、中間出力が一切表示されなくなりました。

4. なぜ AgentTool で解決するのか?

メカニズムの解説

AgentTool でラップされたエージェントは、親エージェントから見ると単なる 「ツール(関数)」 として扱われます。ADK の仕様上、ツールの内部的な実行プロセスは親エージェントにカプセル化されるため、最終的な「戻り値」だけが呼び出し元に返され、結果として出力が抑制されます。

「プロダクション環境ならフロントで消せばいい」は本当か?

フロントエンド側で出力を非表示にすることは可能ですが、AgentTool を使うべき決定的な理由が他にあります。

観点 sub_agents に登録 AgentTool でラップ
ユーザーへの表示 表示される 表示されない
LLMが受け取る履歴 中間出力がすべて残る ツール結果として圧縮される
イベントログの増大 防げない 防げる
フロントエンド依存 実装に依存する 依存しない(バックエンドで完結)

特に重要なのが 「コンテキスト(会話履歴)の節約」 です。

sub_agents のままだと、不要な中間出力がトークンとして蓄積され続け、レスポンスの遅延やコスト増加、精度の低下を招きます。

[Tips] AgentTool 化する際の「説明」と「状態管理」のコツ

ツール化することで出力が綺麗になる一方で、データの扱い方に少しだけ工夫が必要になります。

1. ツールの説明は「自動継承」と「親への明記」のセットで伝える

AgentTool を定義する際、引数に description を別途渡す必要はありません。ラップされたエージェント自身の description がそのままツールの説明として使われます。

加えて、親エージェントの instruction 内にツールの使い方を明示することが重要です。「何のためのツールか」「呼ぶと state のどのキーに何が保存されるか」を書いておくことで、エージェントが意図通りにツールを活用してくれます。

Python

  1. # ラップ元のエージェントに役割を記述(これがツール説明になる)
  2. sequential_search_agents = SequentialAgent(
  3.     name=”sequential_search_agents”,
  4.     description=”書籍を並列検索し、要約をsession.stateに保存する逐次エージェントです。“,
  5.     sub_agents=[parallel_search_agents, summary_agent],
  6. )
  7. # AgentToolでラップ(追加設定なしでOK)
  8. sequential_search_tool = AgentTool(agent=sequential_search_agents)

親エージェントの instruction 内に記載する例:

  1. # ANALYSIS_AGENT_INSTRUCTION.py
  2. ###  【利用可能なツール】
  3. *    sequential_search_agents: 書籍の並列検索と要約を実行します。
  4.      *    結果はsession.state[“result”]に自動保存されます。
  5.      *    中間ログは非表示で実行されるため、完了通知を待ってからstateを参照してください。

2. session.state の更新はツール内部のサブエージェントで行う

「ツールの結果を受け取った親エージェントが state を更新する」と思いがちですが、実際にはツール内部のサブエージェントが直接 save_to_state を呼んで更新を行うのがスムーズです。親エージェントはツール完了後に「state に保存済み」という前提で次の処理に進みます。

エージェントの構造

  1. analysis_agent(呼び出し元)
  2. └──> sequential_search_agents を呼び出す
  3.      ├──> parallel_search_agents (内部で並列検索)
  4.      └──> summary_agent (要約を実行)
  5.           └──> save_to_state session.state[“result”] に保存 【★ここで更新
  6. analysis_agent(呼び出し元)は、session.state[“result”]を参照して次の処理へ

親の instruction 側にも「ツール完了後にどのキーを参照すればよいか」を明記しておくと、エージェントが迷わず次のステップに進めます。

5. まとめ

  • sub_agents を使うべき時: エージェント間の対話そのものをユーザーに見せたい場合。
  • AgentTool を使うべき時: 内部処理を隠蔽し、コンテキストを節約して精度を維持したい場合。

「サブエージェントの出力がうるさいな……」と感じたら、ぜひ AgentTool へのラップを試してみてください。

Google ADK を活用したエージェント開発の参考になれば幸いです。

最後まで読んでいただきありがとうございました。