世は大 AI 時代ということで、調べ事や開発に様々な AI を利用するようになりました。 AI 失業だの SaaS is dead だのと騒がしいですが、そういうのは今日は置いておきます。
AI を使うのも良いですけど、せっかくソフトウェアエンジニアをやっているのですから、自分で作ってみるのもいいですよね。 結論から先に書いておくと、AIエージェントも今どきは簡単に自作できるようになっています。
この記事では Google 製の Agent Development Kit (ADK) を使いますが、何を使うにせよ、そもそも AI エージェントがどう動いているか理解しておかないと効率が悪いです。 それだって AI に聞けば出てくる、、わけですが、まあ人間が要点をまとめた記事にもまだ五円くらいは価値があるかなってことでまとめてみました。 ... お察しの通り、AI に指示して書かせたわけですけど、まあ ymmt 監修ということで。
AI エージェント開発の基本概念と、Go で Google Agent Development Kit (ADK) を使ってエージェントを構築する方法を解説します。 LLM ベースのエージェント開発が初めての Go 開発者を対象としています。
目次
- AI エージェントとは
- エージェントとフロントエンドの通信
- スレッド、ブランチ、セッション、ラン
- フロントエンドの要件
- ADK と genai SDK
- Gemini モデル: Google AI Studio と Vertex AI
- LLM エージェントの構造
- インストラクション
- グラウンディング
- ツールの使い方
- サブエージェント
- 全体像
1. AI エージェントとは
AI エージェントとは、大規模言語モデル (LLM) を推論エンジンとして利用するプログラムです。 テキスト生成だけを行う単純なチャットボットとは異なり、エージェントはアクションを実行できます。ツールを呼び出し、情報を検索し、外部システムと連携し、複数ステップのワークフローを調整できます。
主な構成要素は以下の通りです:
- 自然言語を理解し、何をすべきか判断する LLM
- LLM が呼び出せるツール (Web 検索、API 呼び出し、コード実行など)
- 会話と作業データを追跡するセッション状態
- ツールの結果を LLM にフィードバックし、最終応答を生成するまで繰り返すオーケストレーションループ
graph TD
User((ユーザー)) -->|メッセージ| Orch
Orch -->|"テキスト応答"| User
Orch["オーケストレーター"] -->|"プロンプト"| LLM["LLM"]
LLM -->|"テキスト / ツール呼び出し"| Orch
Orch -->|"実行"| Tools["ツール<br/>(検索, API, コード, ...)"]
Tools -->|"結果"| Orch
Orch -.->|"読み書き"| State["セッション状態"]
2. エージェントとフロントエンドの通信
リクエスト-レスポンスモデルが適さない理由
従来の REST API は、1 つのリクエストに対して 1 つのレスポンスを返します。 これは AI エージェントにはうまく機能しません:
- レスポンスに数秒かかる (LLM の推論は遅い)
- エージェントは出力をトークン単位で生成する — ユーザーはテキストが徐々に表示されることを期待する
- エージェントはフロントエンドに表示すべき中間ステップ (ツール呼び出し、サブエージェントへの転送) を実行することがある
イベントストリーミング
代わりに、エージェントはイベントストリーミングでフロントエンドと通信します。 バックエンドは永続的な接続 (通常は Server-Sent Events / SSE) を開き、型付きイベントのシーケンスをプッシュします:
| イベント型 | 説明 |
|---|---|
| Run started | 新しいエージェントターンが開始 |
| Text delta | 生成されたテキストのチャンク (トークン単位) |
| Tool call | エージェントがツールを呼び出し中 (名前 + 引数) |
| Tool result | ツールが結果を返した |
| State update | セッション状態が変更された |
| Run finished | エージェントターンが完了 |
| Run error | エラーが発生 |
フロントエンドはこれらのイベントを消費し、インクリメンタルにレンダリングします。テキストは到着次第ストリーミング表示し、ツール呼び出しの進捗を表示するなどです。
sequenceDiagram
participant User as ユーザー
participant Frontend as フロントエンド
participant Backend as バックエンド
participant LLM
User->>Frontend: メッセージを入力
Frontend->>Backend: POST /run (ユーザーメッセージ)
Backend->>Backend: "ラン" を作成
Backend-->>Frontend: Event: Run started
Backend->>LLM: 会話履歴 + インストラクションを送信
LLM-->>Backend: テキストトークン (ストリーミング)
Backend-->>Frontend: Event: Text delta (繰り返し)
LLM-->>Backend: ツール呼び出しリクエスト
Backend-->>Frontend: Event: Tool call
Backend->>Backend: ツールを実行
Backend-->>Frontend: Event: Tool result
Backend->>LLM: ツール結果を送信
LLM-->>Backend: 最終テキストトークン
Backend-->>Frontend: Event: Text delta (繰り返し)
Backend-->>Frontend: Event: Run finished
Frontend->>User: 完成した応答を表示
イベントストリーミングのプロトコル
エージェントとフロントエンド間のイベントストリーミングを標準化するプロトコルがいくつか存在します。 そのひとつが AG-UI で、標準的なイベント型セットと SSE トランスポートを定義しています。
3. スレッド、ブランチ、セッション、ラン
会話の構造を理解することは、エージェント開発に不可欠です。
スレッド
スレッドはユーザーから見た会話 — メッセージのやり取りの連続です。 これはフロントエンド側の概念です。
ブランチ
ユーザーが以前のメッセージを編集して再送信すると、会話がフォークします。 元のパスと新しいパスは同じスレッドの異なるブランチです。 スレッドは複数のブランチを持ち、ツリー構造を形成できます。
graph TD
A["ユーザー: こんにちは"] --> B["エージェント: こんにちは!"]
B --> C["ユーザー: Go について教えて"]
C --> D["エージェント: Go は言語で..."]
B --> E["ユーザー: Rust について教えて"]
E --> F["エージェント: Rust は言語で..."]
style C fill:#e8f5e9
style D fill:#e8f5e9
style E fill:#fff3e0
style F fill:#fff3e0
緑のパスとオレンジのパスは、同じスレッドの 2 つのブランチです。
セッション
セッションは ADK のサーバーサイドでの会話の表現です。 以下を含みます:
- イベント: すべてのインタラクションの時系列シーケンス — ユーザーメッセージ、エージェント応答、ツール呼び出し、ツール結果
- 状態 (State): スコープ別に整理されたキーバリューデータ:
- セッション状態 (プレフィックスなし): この会話固有のデータ
- ユーザー状態 (
user:プレフィックス): 同じユーザーのセッション間で永続化 - アプリ状態 (
app:プレフィックス): すべてのユーザーとセッションで共有 - 一時状態 (
temp:プレフィックス): 現在のインボケーションでのみ有効
インボケーションとラン
インボケーション (invocation) は ADK における 1 回のユーザーターン — ユーザーがメッセージを送信してからエージェントが応答を完了するまでのすべてを指します。
1 回のインボケーションには、複数の LLM 呼び出し、ツール実行、サブエージェント転送が含まれる場合があります。
各インボケーションは invocation_id で識別されます。
ラン (run) は同じ概念のフロントエンド側の用語です。 フロントエンドがユーザーメッセージを送信し、イベントストリームを受信するまでが 1 つのランです。
1 つのセッションには時間の経過とともに多くのインボケーション (ラン) が含まれます:
セッション ├─ インボケーション 1: ユーザーが質問 → エージェントが応答 ├─ インボケーション 2: ユーザーがフォローアップ → エージェントがツールを呼び出し → エージェントが応答 ├─ インボケーション 3: ユーザーが別の質問 → エージェントがサブエージェントに転送 → サブエージェントが応答 └─ ...
Rewind: ADK によるブランチのサポート
ADK は会話のブランチをサポートする rewind メカニズムを提供しています。 ユーザーが以前のメッセージを編集した場合:
- フロントエンドが取り消すべきインボケーションを特定する
- バックエンドがそのインボケーションの ID で ADK の
rewindを呼び出す - ADK がセッションをそのインボケーション以前の状態に論理的に巻き戻す (それ以降のインボケーションも含む)
- 巻き戻されたイベントは監査のために履歴に保持されるが、モデルのコンテキストからは除外される
- 新しいメッセージは巻き戻された状態からの新規インボケーションとして処理される
これにより、履歴を失わずに「編集してリトライ」する UI が実現できます。
詳細は ADK ドキュメント: セッションの巻き戻し を参照してください。
4. フロントエンドの要件
エージェントのバックエンドを構築するだけでは半分です。 フロントエンドはイベントストリームを処理し、会話状態を管理し、レスポンスをインクリメンタルにレンダリングする必要があります。
イベントストリームの消費
フロントエンドはバックエンドへの接続 (通常は SSE) を開き、イベントが到着するたびに処理します。 最低限、以下を処理する必要があります:
- ラン ライフサイクル — ランの開始と終了を検知してローディング状態を表示
- テキストストリーミング — テキストデルタを現在のメッセージに追加し、「入力中」の体験を提供
- ツール呼び出し — エージェントがどのツールを呼び出しているか、その結果を表示 (オプション、透明性のため)
- エラー — ランが失敗した場合にエラーメッセージを表示
簡略化されたイベントループは以下のようになります:
for each event in stream:
switch event.type:
case RUN_STARTED: ローディングインジケーターを表示
case TEXT_DELTA: 現在のメッセージにテキストを追加
case TOOL_CALL: ツール呼び出し UI を表示 (オプション)
case TOOL_RESULT: ツール結果を表示 (オプション)
case RUN_FINISHED: ローディングを非表示、メッセージを確定
case RUN_ERROR: エラーをユーザーに表示
スレッドとブランチの管理
フロントエンドが責任を持つ事項:
- スレッド ID: 会話開始時にユニークな ID (例: UUID) を生成する。すべてのランリクエストに含めて、バックエンドが正しいセッションにランを関連付けられるようにする。
- ラン ID: ユーザーがメッセージを送信するたびに新しい ID を生成する。
- メッセージ履歴: 現在のスレッドのメッセージリストを維持する。新しいメッセージを送信する際に会話履歴を含め、バックエンドが完全なコンテキストを持てるようにする (プロトコルによっては、バックエンドがセッション経由で履歴をサーバーサイドで管理する場合もある)。
- ブランチ: ユーザーが以前のメッセージを編集した場合、フロントエンドは:
- 編集ポイントで会話をフォークする
- バックエンドにセッションの巻き戻しを通知する (セクション 3 — Rewind を参照)
- 巻き戻された状態から新しいランとして新しいメッセージを送信する
- 両方のブランチを維持し、ユーザーがブランチ間を移動できるようにする
チャット UI ライブラリ
ストリーミング、ブランチ、リッチコンテンツを備えたチャット UI をゼロから構築するのは複雑です。 主要なフロントエンドフレームワーク向けにこれらの機能を提供するライブラリが存在します:
| ライブラリ | フレームワーク | 主な機能 |
|---|---|---|
| assistant-ui | React | テキストストリーミング、ブランチ、ツール呼び出し表示、拡張可能なメッセージコンポーネント |
| flutter_chat_ui | Flutter | メッセージリスト、ストリーミングメッセージ、カスタムメッセージタイプ |
これらのライブラリはスクロール管理、メッセージレイアウト、ストリーミングテキストアニメーションなどの低レベルなレンダリングを処理し、イベントストリームアダプターやカスタム UI コンポーネントに集中できるようにします。
5. ADK と genai SDK
ADK は Google の genai SDK の上に構築されています。このレイヤー構造を理解すると、どのパッケージを使うべきか判断しやすくなります。
genai SDK (google.golang.org/genai)
Gemini モデルを呼び出すための低レベル Go SDK です。 以下を処理します:
- チャット補完 (メッセージの送信、レスポンスの受信)
- ツール/ファンクション宣言
- コンテンツ生成の設定 (temperature、レスポンスフォーマット、安全設定)
- ストリーミングレスポンス
Gemini の HTTP クライアントと考えてください — API プロトコルを話しますが、会話状態、ツール実行、マルチエージェント連携は管理しません。
ADK (google.golang.org/adk)
genai の上に構築された高レベルのエージェントフレームワークです。 以下を追加します:
- エージェントのライフサイクル — 名前、説明、インストラクションを持つエージェントを定義
- セッション管理 — 会話履歴と状態を追跡
- ツールオーケストレーション — 自動ツール呼び出しループ (LLM がツールを呼び出し → 実行 → 結果をフィードバック → 繰り返し)
- マルチエージェント連携 — サブエージェント、エージェント転送、シーケンシャル/パラレルワークフロー
- コールバック — 各段階でのフック (エージェント呼び出し前後、モデル呼び出し前後、ツール呼び出し前後)
- Runner — すべてを結び付けるイベントループ
アナロジー: genai は Go の net/http パッケージ (生のプロトコルアクセス)、ADK は Echo や Gin のような Web フレームワーク (構造化された抽象化) に相当します。
両者の接続
ADK でモデルを作成する際、基盤となる genai クライアントを設定する genai.ClientConfig を渡します:
m, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{ Backend: genai.BackendVertexAI, Project: "my-gcp-project", Location: "global", })
gemini.NewModel (google.golang.org/adk/model/gemini パッケージ) は genai クライアントを ADK の model.LLM インターフェースにラップします。これ以降は ADK の抽象化を通じて操作します。
6. Gemini モデル: Google AI Studio と Vertex AI
Gemini モデルは 2 つの異なるバックエンドからアクセスできます。
同じモデル名 (例: gemini-2.5-flash, gemini-2.5-pro) を使用します — 違いは接続方法です。
Google AI Studio (Gemini API)
- エンドポイント:
generativelanguage.googleapis.com(ai.google.dev経由) - 認証: API キー (
GOOGLE_API_KEY環境変数) - 最適な用途: プロトタイピングと開発
- 特徴: 新機能が最初にリリースされる、実験的
m, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{ Backend: genai.BackendGoogleAI, APIKey: os.Getenv("GOOGLE_API_KEY"), })
Vertex AI
- エンドポイント:
aiplatform.googleapis.com(Google Cloud 経由) - 認証: Vertex AI API が有効な GCP プロジェクトの Application Default Credentials (ADC)
- 最適な用途: 本番ワークロード
- 特徴: エンタープライズグレード (IAM、クォータ、ロギング、コンプライアンス)、ただし新機能は遅れがち
m, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{ Backend: genai.BackendVertexAI, Project: "my-gcp-project", Location: "global", })
機能比較
| 機能 | Gemini API | Vertex AI |
|---|---|---|
| 認証 | API キー | GCP ADC |
| GoogleSearch ツール | 対応 | 対応 |
| URLContext ツール | 対応 | 対応 |
| Function calling | 対応 | 対応 |
| ツール結合 (ビルトイン + カスタムを 1 エージェントで) | 対応 | 非対応 |
エージェント開発における最も重要な違いはツール結合です。
Gemini API は IncludeServerSideToolInvocations を使用して、ビルトインツール (GoogleSearch, URLContext) とカスタムファンクションツールを 1 つのエージェントで混在させることができます。
Vertex AI はこれをサポートしていません — エラーになります。
Vertex AI での回避策は、各ビルトインツールを agenttool.New() でサブエージェントにラップすることです (セクション 10a と セクション 11 を参照)。
詳細は ADK ドキュメント: Google Gemini モデル を参照してください。
7. LLM エージェントの構造
各コンポーネントの詳細に入る前に、ADK におけるエージェントの全体像を示します:
graph TD
Runner["Runner<br/>(イベントループ & オーケストレーター)"]
Runner --> Agent
subgraph Agent["エージェント"]
Model["LLM モデル<br/>(Gemini via genai)"]
Instructions["インストラクション<br/>(システムプロンプト)"]
Tools["ツール<br/>(LLM が呼び出せる関数)"]
Toolsets["ツールセット<br/>(動的ツール集合, 例: MCP)"]
SubAgents["サブエージェント<br/>(専門化された子エージェント)"]
Callbacks["コールバック<br/>(前後フック)"]
Config["GenerateContentConfig<br/>(レスポンス形式, temperature, ...)"]
end
Runner --> SessionService["セッションサービス<br/>(会話状態)"]
Runner --> ArtifactService["アーティファクトサービス<br/>(バイナリストレージ)"]
| コンポーネント | 役割 |
|---|---|
| LLM (モデル) | 頭脳 — 会話履歴 + インストラクションを受け取り、応答またはツール呼び出しを生成する。genai SDK で設定 (セクション 5-6)。 |
| インストラクション | エージェントの振る舞い、ペルソナ、制約を形作る永続的なガイダンス (システムプロンプト)。毎回のリクエストで LLM に送信される。 |
| ツール | LLM が呼び出せる機能 — ビルトイン Gemini ツール、カスタムファンクションツール、MCP ツール (セクション 10)。 |
| ツールセット | ツールの動的コレクション。外部サーバーに接続する MCP ツールセットなど。 |
| サブエージェント | ルートエージェントが委譲できる専門化された子エージェント (セクション 11)。 |
| GenerateContentConfig | 追加のモデル設定 — レスポンス形式制約 (JSON スキーマ)、temperature、ツール設定、安全設定。 |
| コールバック | エージェント呼び出し、モデル呼び出し、ツール呼び出しの前後に実行されるフック。バリデーション、状態操作、ショートサーキットに使用。 |
| Runner | すべてを結び付けるオーケストレーター — イベントループを管理し、セッションサービスに状態変更をコミットし、イベントを呼び出し元にストリーミングする。 |
| セッションサービス | セッションのライフサイクルを管理 — 作成、取得、イベントの追加。 |
| アーティファクトサービス | エージェント実行中に生成されたバイナリアーティファクト (画像、ファイル) を保存。 |
インストラクションについてはセクション 8 で詳しく説明します。
エージェントの構築
実際のエージェント構築は以下のようになります:
// ルートエージェントの作成 agent, err := llmagent.New(llmagent.Config{ Name: "my_assistant", Description: "A helpful AI assistant", Model: model, // Gemini モデル Instruction: instruction, // システムプロンプト Tools: tools, // カスタム + ビルトインツール Toolsets: toolsets, // MCP ツールセット SubAgents: subAgents, // 専門サブエージェント AfterToolCallbacks: afterToolCallbacks, // ツール後フック })
Runner はセッションサービスとアーティファクトサービスとともに別途設定します:
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(agent),
SessionService: session.InMemoryService(),
ArtifactService: artifactService,
}
以降のセクションでは、インストラクション (セクション 8)、グラウンディング (セクション 9)、ツール (セクション 10)、サブエージェント (セクション 11) について詳しく説明します。
8. インストラクション
Instruction フィールドはエージェントのシステムプロンプトです。LLM に対して、自分が何者で、何をすべきか、どう振る舞うべきかを伝えます。
インストラクションはエージェント設定において最も重要な部分と言えます。同じモデルでもインストラクションが異なれば、まったく異なるエージェントとして振る舞います。
含めるべき内容
良いインストラクションには通常以下が含まれます:
- 役割とペルソナ: エージェントが何者か。「あなたは親切な AI アシスタントです。」
- ツール使用ガイダンス: 各ツールをいつ、どのように使うか。明示的に — LLM はどの状況でどのツールを使うべきか知る必要がある。
- サブエージェント委譲ルール: いつサブエージェントに転送するか。「ユーザーが視覚的なデータ表示を求めた場合は
ui_generatorに委譲してください。」 - 出力形式: エージェントがレスポンスをどう構造化すべきか。「Markdown で応答してください。生の JSON は出力しないでください。」
- 制約: エージェントがやってはいけないこと。「情報を捏造しないでください。事実を確認するために
web_searchを使用してください。」
インストラクションが具体的であるほど、エージェントの振る舞いは安定します。 曖昧なインストラクションは一貫性のない動作につながります。
静的インストラクション
最もシンプルな形式はプレーンな文字列です:
agent, err := llmagent.New(llmagent.Config{
Name: "capital_agent",
Model: model,
Instruction: `You are an agent that provides the capital city of a country.
When a user asks for the capital:
1. Identify the country name from the user's query.
2. Use the 'get_capital_city' tool to look it up.
3. Respond clearly with the capital city name.`,
Tools: []tool.Tool{capitalTool},
})
インストラクションへの状態値の埋め込み
ADK は中括弧構文を使ったテンプレート置換をインストラクション内でサポートしています。 任意のセッション状態キーを参照でき、ADK は実行時にインストラクションを LLM に送信する前に、現在の値で置換します。
agent, err := llmagent.New(llmagent.Config{
Instruction: `You are a helpful assistant.
Always respond in {user:preferred_language}.
The user's name is {user:name}.`,
})
session.state["user:preferred_language"] が "Japanese" で session.state["user:name"] が "Taro" の場合、LLM は以下を受け取ります:
You are a helpful assistant. Always respond in Japanese. The user's name is Taro.
サポートされる状態プレフィックスは以下の通りです:
| 構文 | スコープ | 例 |
|---|---|---|
{key} |
セッション状態 | {current_topic} |
{user:key} |
ユーザー状態 (セッション間で永続化) | {user:preferred_language} |
{app:key} |
アプリ状態 (すべてのユーザーで共有) | {app:system_notice} |
{temp:key} |
一時状態 (現在のインボケーションのみ) | {temp:intermediate_result} |
オプションキー: キーが存在しない場合のエラーを回避するには ? を付加します: {user:preferred_language?}。
この仕組みはエージェント間のデータ受け渡しに有用です。 例えば、シーケンシャルワークフローで最初のエージェントが出力を一時状態に保存し、次のエージェントがテンプレート置換でそれを読み取ることができます:
const specStateKey = session.KeyPrefixTemp + "spec" // specStateKey = "temp:spec" // 最初のエージェント: 出力を一時状態に保存 firstAgent, err := llmagent.New(llmagent.Config{ Name: "spec_generator", OutputKey: specStateKey, // temp:spec に出力を保存 // ... }) // 次のエージェント: テンプレート置換で一時状態から読み取り secondAgent, err := llmagent.New(llmagent.Config{ Name: "renderer", Instruction: `You are a renderer. Input specification (JSON): {` + specStateKey + `} Convert this specification into the final output.`, // 実行時に {temp:spec} が実際の JSON 仕様で置換される })
インストラクションのプログラム的構築
複雑なエージェントでは、設定に基づいてインストラクションを部品から組み立てたい場合があります:
func BuildInstruction(baseInstruction string, cfg Config) string { var b strings.Builder b.WriteString(baseInstruction) // ツール使用ガイダンスを常に追加 b.WriteString("\n\nAlways call current_time when the user's query involves " + "relative time expressions such as \"today\", \"now\", or \"recently\".") // 条件付きで追加機能のガイダンスを追加 if cfg.SearchEnabled { b.WriteString("\n\nUse web_search for current events, news, or weather.") } return b.String() }
このパターンにより、各機能のガイダンスがその実装の近くに配置され、ビルダーが実際に有効なものに基づいてそれらを組み立てるため、インストラクションの保守性が向上します。
効果的なインストラクションを書くためのヒント
- ツール使用について具体的に: 「必要に応じてツールを使ってください」ではなく、「ユーザーが 'today'、'now'、または時間に関する表現を使った場合は必ず
current_timeを呼び出してください」と書く。 - 優先順位を確立する: 複数のツールがリクエストを処理できる場合、どちらを優先するか伝える。
- 構造化されたフォーマットを使用: Markdown の見出し、番号付きリスト、箇条書きは、長いインストラクションをモデルがパースしやすくする。
- Few-shot の例を含める: 複雑な出力形式の場合、期待する内容をモデルに示す。
- サブエージェントのインストラクションは焦点を絞る: 各サブエージェントはそのドメインに特化した狭いインストラクションを持つべき。ルートエージェントのインストラクションがオーケストレーションを担当する。
9. グラウンディング
グラウンディングとは、モデルを現実世界の最新情報に接続し、ハルシネーション (事実と異なる情報の生成) を防ぐことです。 LLM のトレーニングデータにはカットオフ日があり、今日の日付、最新ニュース、特定の Web ページの内容を知りません。
モデルを現実に接続するツールを与えることで「グラウンディング」します。
モデルは自律的にいつ使うか判断します。例えば、ユーザーが「東京の今日の天気は?」と聞いた場合、モデルは google_search (天気のため) と current_time (「今日」が何日か知るため) を呼び出します。
| グラウンディングのニーズ | ツール | 役割 |
|---|---|---|
| 現在の日時 | current_time (カスタムツール) |
モデルは今何時か知らない。このツールが現在の日時を提供する。 |
| 最新情報 | google_search (ビルトイン) |
モデルが Web を検索し、推測の代わりに実際のソースを引用できる。 |
| 特定の Web コンテンツ | url_context (ビルトイン) |
モデルがユーザーが参照した特定の URL を取得して読める。 |
グラウンディングは概念であり、ツール (セクション 10) がメカニズムです。 次のセクションでツールの仕組みを詳しく説明します。
10. ツールの使い方
ツールは LLM が呼び出すことを決定できる関数です。 エージェントにツールを登録すると、LLM はツールの名前、説明、入力スキーマ (JSON Schema) を確認します。 会話の内容に基づいて、LLM は自律的にいつ、どのツールを呼び出すか決定します。
ツール呼び出しループ
ADK はツール呼び出しループを自動的に管理します:
graph TD
A["LLM がツール呼び出し<br/>リクエストを生成"] --> B["ADK がツール関数<br/>を実行"]
B --> C["ツールが<br/>結果を返す"]
C --> D["ADK が結果を<br/>LLM に送信"]
D --> E{"LLM が次の<br/>アクションを決定"}
E -->|"別のツールを呼び出す"| A
E -->|"最終応答を生成"| F["ユーザーへの<br/>テキスト応答"]
1 回のランで複数のツール呼び出しが発生する場合があります。LLM は最終的なテキスト応答を生成するのに十分な情報が得られるまでツールを呼び出し続けます。
10a. ビルトイン Gemini ツール
Gemini モデル自体が提供する機能で、カスタムコードではありません。
genai.Tool 構造体を使用して宣言します。
| ツール | 説明 |
|---|---|
GoogleSearch |
引用付き Web 検索。モデルが Web を検索し、実際の結果に基づいて応答する。 |
URLContext |
指定された URL の Web ページコンテンツを取得して読み取る。 |
CodeExecution |
モデルがサンドボックス環境で Python コードを書いて実行する。 |
Vertex AI の制限: Vertex AI では、これらのビルトインツールはカスタムファンクションツールと同じエージェントで共存できません。
回避策は、各ビルトインツールを agenttool.New() を使用して専用のサブエージェントにラップすることです。
これによりビルトインツールを隔離して実行し、その結果をファンクションレスポンスとしてメインエージェントに返します。
以下は Google Search をエージェントツールとしてラップする例です:
// GoogleSearch を Vertex AI 用にサブエージェントでラップ func NewGoogleSearchTool(m model.LLM) (tool.Tool, error) { searchAgent, err := llmagent.New(llmagent.Config{ Name: "web_search", Description: "Searches the web for current information.", Model: m, Instruction: `You are a web search specialist. Use Google Search to find the information the user is asking about. Provide a clear answer based on the search results.`, Tools: []tool.Tool{geminitool.GoogleSearch{}}, DisallowTransferToParent: true, DisallowTransferToPeers: true, }) if err != nil { return nil, err } // エージェントをツールとしてラップ — メインエージェントが関数のように呼び出せる return agenttool.New(searchAgent, nil), nil }
10b. カスタムツール (ファンクションツール)
カスタムツールは自分で書く Go 関数で、functiontool.New を使って ADK に登録します。
JSON スキーマアノテーション付きの型付き入出力構造体を定義します:
// current_time ツールの例 type currentTimeInput struct { Timezone string `json:"timezone,omitempty" jsonschema_description:"IANA time zone name (e.g. Asia/Tokyo). Defaults to UTC."` } type currentTimeOutput struct { CurrentTime string `json:"currentTime"` Timezone string `json:"timezone"` } func NewCurrentTimeTool() (tool.Tool, error) { return functiontool.New[currentTimeInput, currentTimeOutput](functiontool.Config{ Name: "current_time", Description: "Returns the current date and time in RFC 3339 format.", }, func(_ tool.Context, input currentTimeInput) (currentTimeOutput, error) { loc := time.UTC if input.Timezone != "" { l, err := time.LoadLocation(input.Timezone) if err != nil { return currentTimeOutput{}, fmt.Errorf("invalid timezone %q: %w", input.Timezone, err) } loc = l } now := time.Now().In(loc) return currentTimeOutput{ CurrentTime: now.Format(time.RFC3339), Timezone: loc.String(), }, nil }) }
ポイント:
tool.Contextパラメータでセッション状態、アーティファクト、アクション制御にアクセスできる- ADK が構造体タグから JSON Schema を自動生成し、LLM に渡す
- LLM は
Name、Description、生成されたスキーマを確認し、いつツールを呼び出すか判断する
10c. MCP ツールセット
MCP (Model Context Protocol) は、LLM を外部ツールプロバイダーに接続するための標準プロトコルです。
ADK は mcptoolset パッケージを提供し、MCP サーバーに接続してそのツールをエージェントに公開します。
// MCP サーバーへの接続 endpoint := "http://localhost:8080/mcp/my-server" transport := &mcp.StreamableClientTransport{Endpoint: endpoint} ts, err := mcptoolset.New(mcptoolset.Config{ Transport: transport, }) if err != nil { log.Fatalf("Failed to create MCP toolset: %v", err) } // エージェントにツールセットとして登録 agent, err := llmagent.New(llmagent.Config{ // ... Toolsets: []tool.Toolset{ts}, })
MCP サーバーのツールはエージェントに自動的に利用可能になります。 LLM は他のツールとまったく同じようにそれらを呼び出せます。
11. サブエージェント
ADK はルートエージェントが専門化されたサブエージェントにタスクを委譲するマルチエージェントアーキテクチャをサポートしています。 各サブエージェントは独自のインストラクション、ツール、オプションで独自のモデルを持ちます。
なぜサブエージェントを使うか
- 関心の分離: 各エージェントが特化したインストラクションで 1 つのタスクに集中する
- ツールの隔離: サブエージェントはメインエージェントのツールと競合するツールを使用できる (例: Vertex AI でのビルトイン Gemini ツール)
- 異なるモデル: サブエージェントはメインエージェントよりも高性能な (またはコストの低い) モデルを使用できる
2 つのパターン
サブエージェント (LLM 転送)
ルートエージェントがサブエージェントに制御を転送します。
LLM が transfer_to_agent("name") ファンクションコールを生成し、ADK が会話をターゲットエージェントにルーティングします。
サブエージェントはユーザーに直接応答します。
// 転送パターンのサブエージェント例 subAgent, err := llmagent.New(llmagent.Config{ Name: "ui_generator", Description: "Generates rich UI surfaces for data visualization.", Model: m, Instruction: instruction, GenerateContentConfig: &genai.GenerateContentConfig{ ResponseMIMEType: "application/json", ResponseJsonSchema: uiSchema, }, // 応答後はメインエージェントに戻る (このサブエージェントに留まらない) DisallowTransferToParent: true, DisallowTransferToPeers: true, })
主なフラグ:
- DisallowTransferToParent: true — サブエージェントの応答後、次のターンはメインエージェントに戻る
- DisallowTransferToPeers: true — サブエージェントは兄弟エージェントに転送できない
サブエージェントは親に登録します:
agent, err := llmagent.New(llmagent.Config{
// ...
SubAgents: []agent.Agent{subAgent},
})
Agent-as-Tool
エージェントを agenttool.New() でラップし、親が関数のように呼び出せるようにします。
親が入力を送信し、ラップされたエージェントが実行され、親が結果を受け取ります。
// シーケンシャルワークフローをツールとしてラップする例 func NewReportTool(m model.LLM) (tool.Tool, error) { researchAgent, err := buildResearchAgent(m) // ステップ 1: 調査 // エラー処理は省略 writerAgent, err := buildWriterAgent(m) // ステップ 2: 執筆 workflow, err := sequentialagent.New(sequentialagent.Config{ AgentConfig: agent.Config{ Name: "report_workflow", Description: "Researches a topic and writes a report.", SubAgents: []agent.Agent{researchAgent, writerAgent}, }, }) // ワークフローをツールとしてラップ — メインエージェントが関数のように呼び出せる return agenttool.New(workflow, nil), nil }
この例はワークフローエージェントも示しています — sequentialagent はサブエージェントを順番に実行します (調査、次に執筆)。
使い分け
| パターン | 使用場面 | 例 |
|---|---|---|
| サブエージェント (転送) | サブエージェントがユーザーに直接応答すべき場合 | UI 生成エージェント |
| Agent-as-Tool | 親が結果を受け取ってさらに処理する必要がある場合 | Web 検索結果 → 親が次に何をするか決定 |
| Agent-as-Tool + ワークフロー | 複数ステップのパイプラインを 1 つのツール呼び出しとして実行すべき場合 | レポート: 調査 → 執筆 |
12. 全体像
すべてのパーツがどのように接続されるかを示します:
graph TD
User((ユーザー))
User --> Frontend["フロントエンド"]
Frontend -- "イベントストリーム (SSE)" --> Backend["バックエンド"]
Backend -- "ADK REST API" --> Runner
subgraph ADK["ADK エージェントサービス"]
Runner["Runner"]
Runner --> RootAgent["ルートエージェント"]
Runner --> SessionSvc["セッションサービス"]
RootAgent --> Model["Gemini モデル<br/>(genai SDK 経由)"]
RootAgent --> Tools["ツール"]
RootAgent --> SubAgents["サブエージェント"]
Tools --> CustomTools["カスタムツール"]
Tools --> AgentTools["Agent-as-Tool<br/>(web_search, url_context, ...)"]
Tools --> MCPTools["MCP ツールセット"]
end
Model -- "API 呼び出し" --> Gemini["Gemini<br/>(Vertex AI or Google AI Studio)"]
MCPTools -- "MCP プロトコル" --> MCPServers["MCP サーバー"]
典型的なユーザーインタラクションの流れ:
- ユーザーがフロントエンド経由でメッセージを送信
- フロントエンドがバックエンドに POST し、SSE 接続を開く
- バックエンドが ADK セッションの存在を確認し、メッセージを ADK Runner に送信
- Runner が完全な会話履歴とともにルートエージェントを起動
- ルートエージェントの LLM が何をすべきか決定:
- テキストで直接応答する
- ツールを呼び出す (カスタム、ビルトイン、または MCP)
- サブエージェントに転送する
- ツールが呼び出された場合、ツール呼び出しループは LLM が最終応答を生成するまで継続
- すべてのイベントが SSE 接続を通じてフロントエンドにストリーミングされる