kubell Creator's Note

株式会社kubellのエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

Google ADK AIエージェント開発 - プロンプトで頑張らないためにやった3つのこと

BPaaSプロダクトグループの片岡です。kubell Advent Calendar 2025 の18日目の記事です。

私はスライド作成を行うAIエージェントの開発を担当しており、GoogleのADK(Agent Development Kit) を基盤に開発しています。

今回は、ADKの基本的な機能を活用してプロンプトで頑張らないためにやった3つのことについてご紹介します。

スライド生成エージェントを開発する目的

日々の業務で同じようなスライドを繰り返し作成することはあると思います。例えば営業の提案資料であったり、ホワイトペーパーであったり。

大抵の場合、それらのスライド作成はプロセスとして定義できると思います。

プロセスとして定義できる、とは

  • スライド作成の入力となる情報は何か(ヒアリング結果、外部資料、など)
  • 事前に調査すべき情報は何か(客観的なデータを準備したり、Web検索したり、など)
  • どのような手順で作成するのか(比較したり、要点を絞ったり、など)

といったことを明らかにすることです。

端的に言うと、プロセス新入社員にスライドを作成してもらう前提で作成するマニュアルと言えるかと思います。

プロセスが定義できれば、ユーザーは作成したいスライドの内容を事細かに入力しなくてもエージェントが自律的に要件を定義し、スライドをある程度の品質で生成することが可能となります。

エージェントが生成したスライドに少しの加筆・修正するだけでスライド作成の業務を完了することができれば、これまでスライド作成にかかっていた時間を節約することができ、本来集中すべきコア業務に専念することができます。

とはいえ、世の中はGeminiやGenspark、ManusなどAIスライド作成プロダクトは百花繚乱といった状態であります。

我々は、事前にプロセスとコンテキストを定義することで、ユーザーが事細かに入力や指示をしなくても(そして誰がやっても)一定の品質のスライドを生成できるような仕組みを構築したいと思っています。

メンバー間で作成されるスライドの品質に差がなくなり、業務のアウトプットが安定することに価値があると考えています。

そして、ユーザーがビジネスプロセスをAIエージェントと対話しながら定義し、自ら業務を簡単に効率化できるようになることを構想しています。

スライド生成エージェントの構成

現時点でのスライド生成エージェントは以下の3つのエージェント群で構成され、順にタスクとして実行することでスライドを生成する構造をとっています。

    graph TB
        subgraph Phase1["Group 1: 要件の精緻化"]
            direction TB
            P1_1["プロセスとコンテキストを取得"]
            P1_2["会話履歴分析・情報収集"]
            P1_3["要件明確化"]

            P1_1 --> P1_2 --> P1_3
        end

        subgraph Phase2["Group 2: 構成案の作成"]
            direction TB
            P2_1["構成案生成"]
            P2_2["構成案検証"]

            P2_1 --> P2_2
        end

        subgraph Phase3["ユーザーへの確認"]
            P_HITL["構成案の承認確認<br/>(Human In The Loop)"]
        end

        subgraph Phase4["Group 3: レイアウト"]
            direction TB
            P3_1["レイアウトデータ生成"]
            P3_2["レイアウトデータ検証"]
            P3_3["テンプレート選択"]
            P3_4["レイアウト"]

            P3_1 --> P3_2 --> P3_3 --> P3_4
        end

        Phase1 ==> Phase2 ==> Phase3 ==> Phase4

        classDef phase1 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
        classDef phase2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
        classDef phase3 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
        classDef phase4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
        classDef pro fill:#ffebee,stroke:#c62828,stroke-width:3px

        class P1_1,P1_2,P1_3 phase1
        class P2_1,P2_2 phase2
        class P_HITL phase3
        class P3_1 pro
        class P3_2,P3_3,P3_4 phase4
  1. 要件の精緻化: ユーザーの入力を分析し、ヒアリングやGoogle検索・予め定義された指示やコンテキストとなる資料に基づき要件を精緻に定義します。
  2. 構成案の作成: 定義された要件とユーザーの入力内容をもとに資料の構成案をMarkdownのテキストとして提示します。
  3. レイアウト: 構成案をスライドのページごとの構造化されたレイアウトデータに変換し、レイアウトデータをスライドにレイアウトします。

やったこと

それでは、プロンプトで頑張らないためにやった3つのことについてご紹介します。

⚠️ 以下はADK 1.20.0(記事執筆時点での最新)での内容です。

1. output_schema / output_key によるコンテキストの型化

初期の課題: 曖昧なデータ受け渡しとエラーハンドリング

初期のマルチエージェントアーキテクチャでは、エージェント間のデータ受け渡しとエラーハンドリングが曖昧でした。

解決策 : TaskResult型による統一出力フォーマット

TaskResult型の定義

全エージェントの出力を統一するため、TaskResult型を導入しました。Pydantic*1のBaseModelを継承して定義します。

class TaskResult(BaseModel):
      """
      全エージェントの標準出力スキーマ    
      """
      result: bool  # タスクの成功/失敗
      response_message: str  # ユーザーへの表示メッセージ
      task_summary: str # タスク結果の要約

output_schema/output_keyの適用

エージェントにoutput_schemaoutput_keyを指定:

generate_slide_data = Agent(
    name="generate_slide_data",
    model="gemini-2.5-flash",
    instruction=GENERATE_SLIDE_DATA_PROMPT,
    output_schema=TaskResult,  # TaskResult型で出力する
    output_key="generate_slide_data_result",  # Session Stateへ保存するキー名
)

output_schemaを指定すると、Agentの応答が定義したスキーマに従い JSON形式に変換され*2output_keyに指定したSession Stateに保存されます。*3

プロンプトで各フィールドへ出力する内容を指示する必要はなく、LLMが適切に内容を設定します。

{output_key?} 記法で前のエージェントの出力を参照

プロンプトに{state_key} と記述すると、ADKが自動的にSession Stateの内容を埋め込みます。

?Optionalを表しており、存在しない場合でもエラーになりません。(OptionalはUndocumentedな機能ですが、便利なので普段使いしてます。

カスタムエージェントによるワークフロー制御

本記事では言及しませんが、SequentialAgent をベースとして完了したタスクをスキップ・リトライしたり、続行不可能と判断した場合にフォールバックできるようなカスタムエージェント(AutonomousSequentialAgent)を定義してワークフローをタスク単位での実行を管理しています。

before_model_callbackでフィードバック内容をsystem_instructionに差し込むPluginを開発し、各エージェントがリトライ・フォールバック時にフィードバック内容を汲み取って処理できるようにしています。

このカスタムエージェントにより自律的なワークフロー管理を行えるようになり、(自動的にリトライしたりフォールバックしたりするようになる)エージェントの状況対応力が少し上がりました。(まだ研究中です)

なお、ADK 1.16.0ではResume(停止時からの再開時の状態復元)をサポートしました。SequentialAgent は現在のサブエージェントを状態として管理しており途中からの再開が可能となっています。

2. タスクの細分化

以前は一つのエージェントで複数のタスクを行うようなプロンプトを記述していましたが、gemini-2.5-proからgemini-2.5-flashに置き換えた際に想定外の応答をすることが多くなりました。

上述したワークフロー管理が行えるようになったこともあり、プロンプトで頑張るのをやめ、可能な限りプロンプトを分割し、タスクを細分化しました。

細分化したタスクをワークフローにより結合することで各エージェントをシンプルなタスクのみで再構成しました。

細分化したことにより単体の動作確認もシンプルになり、保守性も向上しました。 また、プロンプトの記述方式を以下の形式で統一しました。

この形式がベストかは分かりませんが、まずは自分なりに試行錯誤して統一しました。(プロンプトの評価方法について疎く、より良い方法を学ばねばと思っています)

## 役割
あなたはスライドデータ生成の専門エージェントです。

## コンテキスト
- 検証済みの構成案: {proposal_draft?}

## ステップ説明
1. スライドデータの作成
 - 検証済みの構成案を基に、スライドデータを作成する

## ステップ完了条件

以下の全ての条件を満たす場合:
  - スライドデータが作成できていること
  - 構成案とスライドデータの内容に乖離がないこと

-- LLMが上記で result: bool を判定 --(実際には記述しません、説明の目的です)

## ユーザーへの応答
- 成功時: スライドデータが正常に作成された旨を簡潔に報告
- 失敗時: 構成案との乖離内容を説明

-- LLMが上記で response_message: str を生成 --(実際には記述しません、説明の目的です)

## 完了報告
- 成功時: 作成したスライド枚数、使用したスライドタイプ
- 失敗時: 乖離が見つかった箇所とその内容

-- LLMが上記で task_summary: str を生成 --(実際には記述しません、説明の目的です)

3. Function Toolにより構造化データを生成

現状は、Markdownによる構成案をLLMによりJSON配列にしたレイアウトデータをツールを呼び出してスライドを作成しています。

これまでよく理解しないまま作ってしまったのがいけないのですが、LLMにJSONを生成させるとパース不可能なJSON文字列を生成することがあります。(生成文章のボリュームにより比例していそうではあります)

現状は生成されたJSONを検証・修正するエージェントを入れて対応しています(繰り返し検証を行なっており非効率です)が、そもそもLLMにJSON配列を生成させることが間違っていると気づき、Function ToolでJSON配列を構築していく方式に変えようと思っています。

ただ、Function Toolの引数を複雑にしてしまうとLLMが正しく呼び出せなくなってしまうためFunction Toolの引数の設計に苦慮しているというのが実情です。

構成案からレイアウトデータに変換する際に、構成案のコンテンツに適したスライドパターン(タイトルなのか、表なのか、単なる文字情報なのか)をLLMに判断させ、各スライドパターンに必要な情報を引数としてFunction Toolに設定して呼び出すことを検討しています。

graph LR
  A["LLM<br/>スライドパターンと引数を判断"] --> B1["add_title_slide()"]
  A --> B2["add_table_slide()"]
  A --> B3["add_content_slide()"]

  B1 --> C["Function Tool<br/>引数を検証<br/>JSON構築"]
  B2 --> C
  B3 --> C

  C --> D["State<br/>JSON配列に追加"]

  style A fill:#e3f2fd,stroke:#1976d2
  style C fill:#fff3e0,stroke:#f57c00
  style D fill:#c8e6c9,stroke:#388e3c

Function Toolが文字数や基本的なルールを検証しエラーとして返すことでLLMはツールの呼び出し結果を元に引数を修正して再度ツールを実行しようと試みるでしょう。ただ、リトライはLLMに任せずプラグインReflect and Retry Tool Pluginを入れておくのがいいと思います。

また、大きいサイズの構成案を入力コンテキストを圧迫せずに処理できるようにしたい、など工夫のしどころがたくさんありそうでまさにこのあたりが醍醐味と言ったところです。

まとめ

ADKは2週間ごとにリリースされていて変化の激しいプロダクトでありますが、着実に進化しておりプロンプトを駆使せずとも簡単にエージェントを構築できるDevelopment Kitになりつつあると感じています。

実際に2ヶ月前までは、gemini-2.5-proさえ使っていればなんとかなるだろうくらいの感覚で開発していました。

しかし、色々な壁にぶち当たる中で上述の工夫などを行いながら現在はほぼ全てgemini-2.5-flashに置き換えることができています。

gemini-2.5-flashはgemini-2.5-proに比べてコストや応答速度の面で圧倒的に有利であり、運用面で大事なポイントかと思います。

今後も有用そうな改善を取り込みながらLLMの得意な部分とそうでない部分を分離し、安定した価値の高いエージェントを開発できるようになりたいです!

以上、記事を読んでいただきありがとうございました。

*1:PydanticはPythonのデータモデルを検証するライブラリです。

*2:ユーザーにはこのJSON形式のメッセージが表示されてしまいますので、クライアントでJSON応答を適切なメッセージに変換して表示しています。

*3:output_keyを指定してSession Stateに保存する目的は会話履歴に依存しない(include_contents='None')エージェントが結果を参照する際にコンテキスト参照できるようにしておくため、です。