1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google ADK × コンテキストエンジニアリング:第2回 Agentクラス大解剖 — LLMに実際に何が渡っているのか

1
Last updated at Posted at 2026-02-16

この記事は「Google ADK × コンテクストエンジニアリング」シリーズの一部です。

Andrej Karpathy(OpenAI・Tesla 元AI責任者)が提唱した「コンテクストエンジニアリング」という考え方があります。LLMの性能を引き出す鍵は、モデルそのものではなく、モデルに渡すコンテクストをいかに設計するか――という発想です。

Google ADK(Agent Development Kit)でエージェントを組んでいると、この考え方の重要性を痛感する場面が多々あります。instruction の書き方ひとつ、ツールの返り値ひとつで、エージェントの振る舞いは大きく変わります。にもかかわらず、「ADKの内部で何がどうコンテクストとして組み立てられているのか」を体系的に解説した資料は、まだほとんど見当たりません。

本シリーズでは、ADK のソースコードを読み解きながら、コンテクストを構成する要素を一つひとつ分解し、実務で使えるテンプレートやテクニックに落とし込んでいきます。

📚 シリーズ目次(クリックで展開)
# タイトル 概要
1 ADKで制御できるコンテクストの全体像 name, description, instruction, tool, state... ADKで触れるコンテクスト要素の全体マップ
2 Agentクラス大解剖 — LLMに実際に何が渡っているのか 内部のプロンプト組み立て順序を解明し、実用テンプレートを提案
3 ツール定義の解剖 — 入出力・エラー・descriptionの最適化 Geminiが失敗しやすい引数定義、tool descriptionの自作テクニック
4 google_search ツールの落とし穴と実践コールバック集(近日公開) 仕様の罠、引用リンク自動挿入、現在時刻の差し込みなど
5 実践Tips集 — タスク管理ツール・デプロイ・運用の小技(近日公開) write_todo、デプロイスクリプト、現場で使える小技集

👉 第2回:Agentクラス大解剖 — LLMに実際に何が渡っているのか ← 今ここ

この記事で得られること

  • ADK が 開発者の書いた instruction の他に、どんなプロンプトを自動生成しているか がわかる
  • name / description / エージェント間転送指示が、それぞれどんなテキストに変換されてLLMに届くのか、ソースコードから理解できる
  • 組み立て結果を 実際にダンプして確認する方法 がわかる
  • 解剖結果に基づいた instruction / description 設計テンプレート が手に入る

対象読者: ADK 中級者〜上級者(第1回 を読んでいることを推奨)

ADK バージョン: Python v1.25.0(ソースコードの参照はこのバージョンに基づきます)

なぜ「自動生成プロンプト」を知る必要があるのか

ADK で LlmAgent を定義するとき、開発者が書くのは instructionnamedescription といった個別のパラメータです。しかし、LLM に実際に送られるのは 1本の system instruction 文字列 であり、ADK はこれらの断片に フレームワーク独自のテキストを加えて 結合してから LLM に送ります。

たとえば、instruction の冒頭に「あなたはXXのエージェントです」と書いたとします。しかし ADK は内部で You are an agent. Your internal name is "xxx". という文を 別途 自動追加しています。この事実を知らないと、トークンの無駄遣いになるだけでなく、矛盾する自己認識を LLM に与えてしまう可能性もあります。

本記事では、ADK のソースコードを読み解いて 自動生成されるプロンプトの中身 を明らかにし、その知見を instruction 設計に活かすところまでをカバーします。

ADK が自動生成するプロンプトの全体像

ADK は LlmRequest というオブジェクトに対して、複数の Request Processor を順番に実行することで system instruction を組み立てます。各 Processor は llm_request.append_instructions() を呼び、既存のテキストに \n\n 区切りで文字列を追記していきます。

ソースコードから読み取った、system instruction に書き込みを行う主要な Processor は以下の3つです。

順序 Processor 役割 開発者が書く部分
instructions global_instruction + instruction を追加
identity namedescription をテンプレートに埋めて追加 一部
agent_transfer マルチエージェント時の転送指示を自動生成

開発者が直接コントロールするのは①のみです。②は開発者が設定した値を ADK がテンプレートに埋め込み、③は完全に自動生成されます。以下、それぞれの中身を解剖していきます。

① instructions — ユーザ定義の指示

最初に system instruction に書き込まれるのは、開発者が LlmAgent に設定した指示文です。

instructions.py(処理の流れを簡略化)
async def _build_instructions(invocation_context, llm_request):
    agent = invocation_context.agent
    root_agent = agent.root_agent

    # 1. global_instruction(ルートエージェントに設定されていれば)
    if root_agent.global_instruction:
        si = process_and_inject_state(root_agent.global_instruction)
        llm_request.append_instructions([si])

    # 2. instruction(このエージェント自身の指示)
    if agent.instruction:
        si = process_and_inject_state(agent.instruction)
        llm_request.append_instructions([si])

ポイントは以下の通りです。

  • global_instruction が先、instruction が後。global_instruction はルートエージェントにのみ設定可能で、ツリー内のすべてのエージェントに共通適用されます
  • 両方とも State テンプレート注入 が行われます。{key} という記法が session.state['key'] の値で自動置換されます。これを活用すると、before_agent_callback などで事前に State に値を書き込んでおき、instruction 内でその値を参照する、というパターンが使えます
State テンプレート注入の活用例
# callback で State に値を書き込む
def inject_context(callback_context):
    callback_context.state["user_tier"] = "premium"
    callback_context.state["current_date"] = "2025-02-16"

agent = LlmAgent(
    instruction="ユーザのプランは {user_tier} です。今日は {current_date} です。",
    before_agent_callback=inject_context,
    # ...
)
  • InstructionProvider(関数型の instruction)にも対応しており、動的に instruction を生成できます(後述の「プロンプト構築関数」セクションで詳しく扱います)

global_instruction は v1.25.0 時点で deprecated です。同等の機能は GlobalInstructionPlugin で提供されています。既存コードの移行は急ぎませんが、新規実装では Plugin の利用を推奨します。なお、後述する「プロンプト構築関数」パターンを使えば、global_instruction 相当の機能をより柔軟に、かつ見通しよく実現できます。

② identity — ADK が自動付与するアイデンティティ

instruction の後に、ADK がフレームワーク側で自動的に追加するのがこの identity 情報です。

identity.py(全文に近い抜粋)
class _IdentityLlmRequestProcessor(BaseLlmRequestProcessor):
    async def run_async(self, invocation_context, llm_request):
        agent = invocation_context.agent
        si = f'You are an agent. Your internal name is "{agent.name}".'
        if agent.description:
            si += f' The description about you is "{agent.description}".'
        llm_request.append_instructions([si])

たとえば以下のような定義をした場合:

agent = LlmAgent(
    name="math_agent",
    description="数学的な計算を行うエージェント",
    # ...
)

LLM には以下のテキストが自動で追加されます。

You are an agent. Your internal name is "math_agent". The description about you is "数学的な計算を行うエージェント".
💬 日本語訳(クリックで展開)
あなたはエージェントです。あなたの内部名称は "math_agent" です。
あなたについての説明は "数学的な計算を行うエージェント" です。

ここで重要なのは以下の点です。

  • namedescription開発者が instruction に書かなくても、ADK が自動でプロンプトに追加する。したがって instruction 内で「あなたはXXです」と書くと 二重に なります
  • description は、第1回で触れたように「他エージェント(親)がタスク委譲先を判断するための情報」として使われます。言い換えると、このエージェントが 何をできるかの機能宣言 です。この description がそのまま 自分自身のプロンプトにも入る ことを忘れてはいけません
  • さらに、このエージェントを AgentTool として利用する場合、description はそのまま ツールの description(FunctionDeclaration の description フィールド)にもなります。つまり、呼び出し側のエージェントが精度よくリクエストを投げられるかどうかも、この description の書き方にかかっています(ツールの description 設計については第3回で詳しく扱います)

name は AgentTool として使う場合、英数字・ハイフン・アンダーバーのみ受け付けます。日本語は使用できません。ただし、LLM はユーザへの応答で内部名をそのまま言及してしまうことがあります(「math_agent に転送します」など)。これを避けるには、description に「ユーザ向け表示名」セクションを設けるテクニックが有効です。詳しくは description テンプレート で紹介します。

③ agent_transfer — マルチエージェント時の転送指示

マルチエージェント構成(sub_agents が存在する場合)に限り、ADK が自動生成するのがエージェント間転送の指示です。

以下は、実際に生成されるプロンプトの例です。

agent_transfer が生成するプロンプトの例
You have a list of other agents to transfer to:

Agent name: math_agent
Agent description: Performs mathematical calculations.

Agent name: search_agent
Agent description: Searches the web for information.

If you are the best to answer the question according to your description,
you can answer it.

If another agent is better for answering the question according to its
description, call `transfer_to_agent` function to transfer the question to that
agent. When transferring, do not generate any text other than the function call.

**NOTE**: the only available agents for `transfer_to_agent` function are
`math_agent`, `search_agent`.
💬 日本語訳(クリックで展開)
あなたには転送可能な他のエージェントのリストがあります:

エージェント名: math_agent
エージェントの説明: 数学的な計算を行います。

エージェント名: search_agent
エージェントの説明: ウェブで情報を検索します。

あなたの説明に照らして、あなたが質問に最も適している場合は、
あなたが回答できます。

別のエージェントがその説明に照らしてより適している場合は、
`transfer_to_agent` 関数を呼び出して質問をそのエージェントに転送してください。
転送時には、関数呼び出し以外のテキストを生成しないでください。

**注意**: `transfer_to_agent` 関数で利用可能なエージェントは
`math_agent`, `search_agent` のみです。

親エージェントへの転送がある場合は、以下のような指示も追加されます。

If neither you nor the other agents are best for the question,
transfer to your parent agent coordinator.
💬 日本語訳(クリックで展開)
あなたも他のエージェントも質問に最適でない場合は、
親エージェント coordinator に転送してください。

転送対象には以下が含まれます。

  • 自身の sub_agents(子エージェント)
  • parent_agent(親エージェント、disallow_transfer_to_parent=True でなければ)
  • 兄弟エージェント(同じ親を持つピアエージェント、disallow_transfer_to_peers=True でなければ)

ここで注目すべきは、転送先の判断材料として 各エージェントの description がそのまま使われる という点です。description が曖昧だと、LLM は適切なルーティング判断ができません。

組み立て結果の全体像

これまでの解剖結果をまとめると、LLM に送られる system instruction の全体像は以下のようになります。

┌─────────────────────────────────────────────┐
│           system_instruction                │
│                                             │
│  [global_instruction]          ← ①-1       │
│                                             │
│  ──── \n\n ────                             │
│                                             │
│  [instruction]                 ← ①-2       │
│                                             │
│  ──── \n\n ────                             │
│                                             │
│  You are an agent.             ← ②         │
│  Your internal name is "xxx".  (自動生成)  │
│  The description about you                  │
│  is "...".                                  │
│                                             │
│  ──── \n\n ────                             │
│                                             │
│  You have a list of            ← ③         │
│  other agents to               (自動生成)  │
│  transfer to: ...                           │
│  (マルチエージェント時のみ)                   │
│                                             │
└─────────────────────────────────────────────┘

各セクションは \n\n(空行)で区切られて結合されます。

💡 コラム:system instruction 内の順序は出力に影響するのか?

LLM の挙動には「system instruction の末尾に近い指示ほど、出力に反映されやすい」という経験則があります。これは学術的に確立された法則ではありませんが、実務で広く観察されている傾向です。

ADK の組み立て順序を踏まえると、開発者の instruction は先頭寄り、ADK の自動生成テキスト(identity, agent_transfer)は末尾寄り に配置されます。つまり、エージェントの identity や転送ルールは比較的強く効きやすい位置にあると言えます。

一方、出力フォーマットの指示など「確実に守ってほしいルール」は instruction 内でも 末尾に書く のがベタープラクティスです。instruction はパイプライン全体では先頭寄りですが、instruction 内部での配置は開発者がコントロールできる部分です。

プロンプトをダンプして確認する方法

ここまでの解剖結果を、実際のエージェントで確認してみましょう。before_model_callback を使うと、LLM に送信される直前の LlmRequest を取得できます。

dump_prompt.py
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types


def dump_llm_request(callback_context, llm_request):
    """LLMに送信される直前のリクエストをダンプする"""
    print("=" * 60)
    print("【System Instruction】")
    print(llm_request.config.system_instruction)
    print("=" * 60)
    print("【Contents (会話履歴)】")
    for content in llm_request.contents:
        role = content.role
        for part in content.parts:
            if part.text:
                print(f"  [{role}] {part.text[:200]}")
    print("=" * 60)
    # None を返すと通常通り LLM が呼ばれる
    # Content を返すと LLM をスキップしてその内容を返す
    return None


sub_agent = LlmAgent(
    name="math_agent",
    model="gemini-2.5-flash",
    description="数学的な計算を行うエージェント",
    instruction="計算結果は必ず検算してから回答してください。",
)

root_agent = LlmAgent(
    name="coordinator",
    model="gemini-2.5-flash",
    description="ユーザのリクエストを適切なエージェントに振り分けるコーディネータ",
    instruction="ユーザの質問内容を分析し、最適なエージェントに転送してください。",
    sub_agents=[sub_agent],
    before_model_callback=dump_llm_request,
)

このコールバックを仕込んでエージェントを実行すると、以下のような system instruction が出力されます。

coordinator の system instruction ダンプ例
ユーザの質問内容を分析し、最適なエージェントに転送してください。

You are an agent. Your internal name is "coordinator". The description about you is "ユーザのリクエストを適切なエージェントに振り分けるコーディネータ".

You have a list of other agents to transfer to:

Agent name: math_agent
Agent description: 数学的な計算を行うエージェント

If you are the best to answer the question according to your description,
you can answer it.

If another agent is better for answering the question according to its
description, call `transfer_to_agent` function to transfer the question to that
agent. When transferring, do not generate any text other than the function call.

**NOTE**: the only available agents for `transfer_to_agent` function are
`math_agent`.

instruction → identity → agent_transfer の順で結合されていることが確認できます。

CLI で adk web -v を起動すると、同等の情報がログに出力されます。開発中はこちらのほうが手軽です。

解剖結果から導く instruction 設計テンプレート

ソースコードの解剖結果を踏まえ、ADK の内部構造と衝突しない instruction の書き方を提案します。

原則:ADK が自動生成する部分は書かない

identity Processor が namedescription をプロンプトに自動追加することがわかりました。したがって、instruction 内で以下のような記述は 不要であり、むしろ避けるべき です。

# ❌ BAD: identity と重複する
instruction = """
あなたは数学計算エージェントです。名前は math_agent です。
計算結果は必ず検算してから回答してください。
"""

# ✅ GOOD: identity に任せ、instruction は行動指針に集中
instruction = "計算結果は必ず検算してから回答してください。"

マルチエージェント構成では agent_transfer Processor が「どのエージェントに転送できるか」のリストを自動生成するため、転送先の一覧を手書きする必要はありません。ただし、「このタスクが完了したら親エージェントに転送せよ」 のような行動フローに関する指示は instruction に書く価値があります。自動生成される転送指示はあくまで「転送可能な相手のリストと転送方法」であり、「いつ転送すべきか」のタイミング判断は開発者が instruction で設計する領域です。

各パラメータの役割分担

解剖結果に基づく、各パラメータの適切な使い分けを整理します。

パラメータ 書くべきこと 書くべきでないこと
name 役割がわかる簡潔な識別子(英数字) 日本語、長い説明文
description 機能宣言(何ができるか)。委譲判断・自己認識・AgentTool の説明を兼ねる 行動指針、ルール、制約
instruction 行動指針、ガイドライン、転送タイミングの判断基準、出力形式、具体例 自己紹介(identity が担当)、転送先リスト(agent_transfer が担当)
global_instruction 全エージェント共通の方針(非推奨。Plugin またはプロンプト構築関数を推奨) 特定エージェント固有の指示

instruction テンプレート

上記の役割分担を踏まえた、instruction のテンプレートを提案します。

instruction テンプレート
instruction = """
[このエージェントが達成すべきゴールを1〜2文で記述]

## 行動指針

- [重要な考え方やフレームワークを簡潔に列挙]
- [判断に迷う場面での指針]

## ガイドライン

- [いつ、何をすべきか / すべきでないかの具体的な指示]
- [エラー時の振る舞い]
- [タスク完了後の転送ルール(マルチエージェントの場合)]

## 利用可能なツール

- `tool_name`: [用途と使い方の簡単な説明]

## 出力形式

[期待する出力のフォーマット。Markdown、JSON など]
[※ 出力形式の指示は instruction の末尾に書くと効きやすい傾向があります]

## 具体例

ユーザ: [入力例]
エージェント: [期待される応答例]
"""

instruction にツールの説明を書く場合、ツール自体の description(docstring)と 矛盾しないように 注意してください。ツールの description もまた LLM に送られるコンテクストの一部です。ツール定義の詳細については 第3回 で扱います。

description テンプレート

description は3つの場面で参照されます。

  1. 自分自身のプロンプト — identity Processor が The description about you is "..." として埋め込む
  2. 他エージェントからの転送判断 — agent_transfer の転送先リストに Agent description: ... として掲載される
  3. AgentTool のツール説明AgentTool として使用する場合、FunctionDeclaration の description にそのまま採用される

この3つの用途を意識して、以下の構造で書くと効果的です。

description テンプレート
description = """
### 概要
[このエージェントが果たす役割を端的に記述]

### 対応可能なタスク
- [タスク例1]
- [タスク例2]

### ユーザ向け表示名
このエージェントをユーザに言及する際は「計算アシスタント」と呼んでください。

### タスク例
Other Agent: [依頼のタスク概要例]
This Agent: [期待される応答例]
"""

「ユーザ向け表示名」 セクションは、name に日本語が使えない制約への対策です。LLM はマルチエージェント構成で「math_agent に転送します」のように内部名をユーザに露出してしまうことがあります。description にユーザ向けの名前を明示しておくことで、「計算アシスタントに転送します」のような自然な応答を誘導できます。

実践Tips

static_instruction — {} を含む指示を安全に書く

instruction 内の {key} は State テンプレートとして自動的に置換処理されます。これは便利な機能ですが、JSON の見本やHTMLテンプレートなど、中括弧 {} をリテラルとして使いたいケースでは問題になります。

static_instruction を使うと、State テンプレート注入をバイパスして指示文をそのまま system instruction に渡せます。

# ❌ instruction に JSON 見本を書くと {} が State 置換されてしまう
instruction = """
以下の形式で出力してください:
{"name": "...", "age": 0}
"""

# ✅ static_instruction なら {} がそのまま渡る
static_instruction = """
以下の形式で出力してください:
{"name": "...", "age": 0}
"""

static_instructioninstruction を同時に指定することも可能です。その場合、static_instruction は system instruction に、instruction は contents(user ロール)にそれぞれ配置されます。これにより、コンテクストキャッシュとの組み合わせでトークンコストを削減する使い方もできます(別途 context_cache_config の設定が必要)。

プロンプト構築関数 — テンプレートエンジンを超える柔軟性

State テンプレート注入よりも柔軟にプロンプトを組み立てたい場合は、InstructionProvider(関数型の instruction / description)を活用するのがおすすめです。

prompt_builder.py
from datetime import datetime
from typing import Callable, List
from google.adk.agents import LlmAgent
from google.adk.agents.readonly_context import ReadonlyContext

DESCRIPTION = "ユーザのリクエストに応じて適切な処理を行うメインエージェント"

INSTRUCTION = """
## 行動指針

- ユーザの意図を正確に把握してから行動する
- 不明な点は確認を取る

## ガイドライン

- エラーが発生した場合はユーザにわかりやすく伝える
"""

settings = {
    "common_prompt": "常に丁寧な日本語で応答してください。",
}


def root_agent_description() -> str:
    return DESCRIPTION


def create_root_agent_instruction_provider(
    tools: List,
) -> Callable[[ReadonlyContext], str]:
    """tools をクロージャでキャプチャした InstructionProvider を返す。"""

    def instruction_provider(context: ReadonlyContext) -> str:
        return _build_instruction(tools)

    return instruction_provider


def _build_instruction(tools: List) -> str:
    current_time = (
        f"CURRENT_TIME: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
    )
    if tools:
        tool_descriptions = (
            "## 利用可能なツール\n\n"
            + "\n\n".join(
                f"{tool.description}"
                for tool in tools
                if hasattr(tool, "description")
            )
        )
    else:
        tool_descriptions = ""
    return "\n\n".join(
        part
        for part in [
            current_time,
            settings["common_prompt"],  # ← 全エージェント共通の方針
            INSTRUCTION,
            tool_descriptions,
        ]
        if part
    )


root_agent = LlmAgent(
    name="coordinator",
    model="gemini-2.5-flash",
    description=root_agent_description(),
    instruction=create_root_agent_instruction_provider(tools),  # ← 関数を渡す
)

このパターンには以下のメリットがあります。

  • 現在時刻など動的な値を毎リクエスト最新状態で注入できる — 文字列を直接渡す方式(instruction=build_instruction(tools))では起動時に一度だけ評価されるため、時刻が古くなります。InstructionProvider(関数を渡す方式)にすることで、リクエストのたびに関数が呼ばれ、常に最新の時刻が渡されます
  • common_prompt のような全エージェント共通の方針 を変数として管理することで、deprecated な global_instruction を使わずに同等の機能を見通しよく実現できる
  • ツールの description を instruction 内に再掲 して、LLM にツール利用の文脈をより強く伝えられる
  • {} の問題も発生しない(テンプレート注入を経由しないため)

InstructionProvider は ADK が公式にサポートしている仕組みなので、この関数パターンは正攻法です。State テンプレート注入はシンプルなケースに、プロンプト構築関数は複雑な組み立てが必要なケースに、と使い分けるのが実務的です。

💡 コラム:LLM は「今何時か」を知らない(クリックで展開)

意外と見落とされがちですが、LLM はステートレスな推論エンジンなので、現在の日時・時刻を自力で知る手段がありません。「今日の天気を調べて」「先週の会議の議事録を探して」といったリクエストに対して、「今日」や「先週」がいつなのかを正しく解釈するには、コンテクストとして現在時刻が渡されている必要があります。

上記のプロンプト構築関数で CURRENT_TIME を埋め込んでいるのはこのためです。google_search ツールで最新情報を検索させたい場合にも、現在時刻がわかっていないと適切な検索クエリを組み立てられません。時刻の注入は地味ですが、エージェントの実用性に直結するコンテクスト設計のひとつです。

まとめ

本記事では、ADK のソースコードを読み解き、開発者の instruction の他にどんなテキストが自動生成され、最終的にどのような system instruction が LLM に届くのかを明らかにしました。

system_instruction の構造:

  ① 開発者の指示      = global_instruction + instruction
  ② アイデンティティ  = "You are an agent..." + name + description   ← 自動生成
  ③ 転送指示          = 転送先リスト + 転送ルール                     ← 自動生成(マルチエージェント時)

この知見から導かれる設計のポイントは以下の3つです。

  1. identity が自動生成する内容(name / description の埋め込み)と重複させない
  2. instruction は「行動指針」に集中させ、自己紹介は ADK に任せる。転送のリストも自動生成されるが、転送タイミングの判断は instruction で設計する
  3. description は自分自身・他エージェントからの転送判断・AgentTool のツール説明の3箇所から参照されることを意識して書く

📌 前回: 第1回 ADKで制御できるコンテクストの全体像
📌 次回: 第3回 ツール定義の解剖 — 入出力・エラー・descriptionの最適化

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?