📑

マルチエージェントシステムのアーキテクチャーを紐解く

に公開

はじめに

2025年8月5日〜6日に開催された Google Cloud Next Tokyo のブレークアウトセッションで、図のようなマルチエージェントシステムのデモを紹介しました。これに対して、予想外に反響が大きく、詳しいアーキテクチャーを知りたいという問い合わせを多数の方からいただきました。そこでこの記事では、このデモシステムのアーキテクチャー設計を例にして、ADK による実装例とあわせてマルチエージェント設計の基本を紹介します。


マルチエージェントシステムのデモ構成

デモの内容

このデモでは、「ネット記事の作成業務」を例としています。Gemini に「記事を書いて!」とお願いするだけでもそれらしい記事は生成できますが、ここでは、より実業務に即した業務フローをマルチエージェントで実現しています。具体的には、次のような流れになります。

  • (1) ユーザーが記事のテーマを入力する
        ↓
  • (2) 「リサーチエージェント」 がテーマに関連した情報を収集して、記事作成の参考となる調査レポートを作成する
        ↓
  • (3) 「ライターエージェント」 が調査レポートに基づいて記事を作成する
        ↓
  • (4) 「レビューエージェント」 が記事の内容をレビューをして、掲載サイトのポリシーに合致しているか確認した上で、修正が必要な点を指摘する
        ↓
  • (5) ユーザーが記事の書き直しを指示すると、(3) に戻って記事の作成とレビューをやり直す(もしくは、修正点がない場合は、ここで処理を終了する)

このフローを見ると、「リサーチ担当者」「ライター」「レビュアー」という 3 つの業務の担当者が、それぞれに対応するエージェントに置き換えられていることがわかります。マルチエージェントの設計に関して、「どのような方針でエージェントを分割すればよいでしょう?」という質問をいただくことがありますが、この例のように、現実世界の担当者ごとにエージェントを分けるというのが、初手としてはわかりやすくてよいでしょう。

役割によるエージェントの分割

それでは、なぜこの考え方が有効なのでしょうか? ——— 現実の業務では、多くの場合、担当者ごとに必要な能力が異なりますが、求められる能力の中には、相反する方向性のものがあります。たとえば、ライターにはなるべく自由な発想で、今までにない視点で記事を書いて欲しいものです。一方、レビュアーには、記事の内容が掲載サイトのポリシーを逸脱していないか、より保守的な視点でチェックすることが求められます。このような相反する作業を一人でやるのは大変ですが、ライターとレビュアーに担当を分ける事で、ライターは自由な気持ちで記事が書けて、レビュアーは記事の品質を担保する作業に集中できます。

エージェントを実装する際も同じことが言えます。ライターエージェントとレビューエージェントを分ける事により、ライターエージェントは、より自由な発想で記事を書くようにチューニングできて、レビューエージェントは、日々変わりゆく品質条件を間違いなく適用することが可能になります。さまざまな個性を持ったライターエージェントを用意して使い分けたり、記事のテーマにあわせて、専門知識を持ったレビューエージェントを適用するなどの組み合わせも容易になります。言い換えると、役割ごとにエージェント化することでエージェントの再利用性も高まります。


人間の役割をエージェントにマッピング

作業ステップによるエージェントの再分割

このようにして、「リサーチエージェント」「ライターエージェント」「レビューエージェント」の 3 つのエージェントが誕生しましたが、実際に作成したデモシステムでは、さらに、エージェントの再分割を行なっています。具体的には、リサーチエージェントがレポートを作成する部分では、ディープリサーチの仕組みを参考にして、次の 2 つのステップでレポートを作成しており、(1) と (2) の処理を個別のエージェントに分割して実装しています。

  • (1) 記事の執筆に役立つ調査項目を選定する
        ↓
  • (2) 選定した調査項目ごとに調査結果をまとめる

今回のデモシステムであれば、1 つのエージェントにこれら 2 つの作業を指示しても問題なさそうですが、調査項目のカスタマイズなど、本物のディープリサーチのような機能追加をしていく際は、作業ステップごとにエージェントを再分割するという発想も有効です。この部分については、どのぐらい分割するかを最初から決めるのは難しいので、エージェントの機能を作り込んでいく中で、必要に応じて再分割を検討するのがよいでしょう。このあたりは、マイクロサービスの設計に似ているかもしれません。


作業ステップでエージェントを分割

また、この段階では、それぞれのエージェントが実施する処理内容が明確に定義されており、具体的な実行例(エージェントに対する入力データと期待する出力データの例)が想定できるかをチェックしておきましょう。これができなければ、このエージェントを実際に作成して動かすことはできません。マルチエージェントシステムが絵に描いた餅に終わらないためにも重要なポイントです。

作業フロー全体の整理

この段階までエージェントが分割できたら、それぞれのエージェントがどのような順序で処理をするのか、作業フロー全体を改めて整理してみましょう。この際、作業が直線的に進む部分と、なんらかの判断によって処理の流れが分岐する部分を区別するように注意します。また、処理の分岐については、次の 2 種類のパターンがあります。

  • (1) 人間の判断によって分岐先が変わる場合(Human in the loop)
  • (2) 事前に決められたロジックで分岐先が変わる場合

今回のデモでは、次の図のようになります。処理の分岐は (1) のパターンのみで、(2) のパターンはありません。


作業フローの全体像

ここまでの整理ができれば、あとは、それぞれのエージェントを実装して、作業フロー全体を流す仕組みを作れば完成です。実際のやり方は、使用するフレームワークによって変わりますが、ADK(Agent Development Kit)を使うのであれば、次のような方法が考えられます。

  • 「エージェントの作業」ごとに LlmAgent を実装
        ↓
  • 「直線的な処理のまとまり」を Sequential Agent として定義
        ↓
  • 「人間による指示/判断」を受け付ける Root Agent を実装
        ↓
  • Root Agent に Sequential Agent を sub_agents として登録

今回の例であれば、Sequential Agent として用意される「直線的な処理のまとまり」は、「レポート作成処理」と「記事作成処理」の 2 つがあり、Root Agent にはこれらが sub_agents として登録されます。Root Agent はこれまでの処理の流れと人間からの指示に基づいて、次に実行するべき sub_agents(つまり、次に実行するべき Sequential Agent)を決定します。

このあたりは具体的な実装例をみた方がわかりやすいので、ここからは、実装例を示していきます。

エージェントの実装例

補助エージェントの定義

はじめに、入力内容に関係なく、特定のテキストを出力するエージェントを補助エージェント(Print Agent)として用意します。

def get_print_agent(text):
    def before_model_callback(
        callback_context: CallbackContext, llm_request: LlmRequest
    ) -> LlmResponse:
         return LlmResponse(
            content=Content(
                role='model', parts=[Part(text=text)],
            )
        )
        
    agent = LlmAgent(
        name='print_agent',
        model='gemini-2.0-flash', # not used
        description='',
        instruction = '',
        before_model_callback=before_model_callback,
    )

    return agent

たとえば、次を実行すると、必ず「記事の執筆を開始します」というテキストを出力するエージェントが得られます。

agent = get_print_agetn('記事の執筆を開始します')

リサーチエージェントの定義

先の図の「レポート作成処理」に含まれる 2 つのエージェントは、次のように実装できます。本来は、検索ツールなどを使って外部から情報を収集するべきですが、ここでは、簡単のために省略しています。

instruction = '''
あなたの役割は、記事の執筆に必要な情報を収集して調査レポートにまとめる事です。
指定されたテーマの記事を執筆する際に参考となるトピックを5項目程度のリストにまとめます。
後段のエージェントがこのリストに基づいて、調査レポートを作成します。

* 出力形式
日本語で出力。
'''

research_agent1 = LlmAgent(
    name='research_agent1',
    model='gemini-2.5-flash',
    description='記事の執筆に必要な情報を収集してレポートにまとめるエージェント(テーマ選定)',
    instruction=instruction,
)
instruction = '''
あなたの役割は、記事の執筆に必要な情報を収集して調査レポートにまとめる事です。
前段のエージェントは、5項目程度の調査対象トピックを指定します。

* 出力形式
日本語で出力。
調査レポートは、トピックごとに客観的情報をまとめます。各トピックについて、5文以上の長さで記述すること。
'''

research_agent2 = LlmAgent(
    name='research_agent2',
    model='gemini-2.5-flash',
    description='記事の執筆に必要な情報を収集してレポートにまとめるエージェント(レポート作成)',
    instruction=instruction,
)

これらを Sequential Agent として 1 つにまとめます。

research_agent = SequentialAgent(
    name='research_agent',
    sub_agents=[
        copy.deepcopy(research_agent1),
        copy.deepcopy(research_agent2),
    ],
    description='記事の執筆に必要な情報を収集してレポートにまとめるエージェント',
)

1 つのエージェントがユーザーと会話をする一般的な「会話エージェント」の場合、エージェントとユーザーの会話履歴は「セッション情報」として記録されます。ユーザーが新しいメッセージを入力すると、「これまでの会話履歴+新しいメッセージ」がエージェントに伝えられて、エージェントはこれまでの会話を踏まえた回答を生成します。

ADK で複数のエージェントを組み合わせた場合、これと同様に、すべてのエージェントの出力が「セッション情報」として記録されて、これまでのエージェントのすべての出力履歴が次のエージェントに伝えられます。今回の例であれば、Sequential Agent として定義された research_agent が実行されると、次の流れで処理が行われます。

  • これまでの出力履歴が research_agent1 に入力されて、応答内容が出力される
        ↓
  • 「これまでの出力履歴+research_agent1 の出力」が research_agent2 に入力されて、応答内容が出力される

次のように Print Agent を挿入する事で、処理のステップごとに特定のメッセージを出力することもできます。

research_agent = SequentialAgent(
    name='research_agent',
    sub_agents=[
        get_print_agent('\n---\n## リサーチエージェントが調査レポートを作成します。\n---\n'),
        get_print_agent('\n## 調査対象のトピックを選定します。\n'),
        copy.deepcopy(research_agent1),
        get_print_agent('\n## 選定したトピックに基づいて、調査レポートを作成します。\n'),
        copy.deepcopy(research_agent2),
        get_print_agent('\n#### 調査レポートが準備できました。記事の作成に取り掛かってもよいでしょうか?\n'),
    ],
    description='記事の執筆に必要な情報を収集してレポートにまとめるエージェント',
)

先ほどの作業フローでは、調査レポートができたら、記事の作成を開始するかの判断をユーザーに求める想定でした。ここでは、Print Agent を用いて、「記事の作成に取り掛かってもよいでしょうか?」という質問メッセージを最後に表示しています。

ライターエージェントとレビューエージェントの定義

調査レポートに基づいて、記事を書くライターエージェントは次のように定義します。

instruction = '''
あなたの役割は、特定のテーマに関する気軽な読み物記事を書くことです。
記事の「テーマ」と、その内容に関連する「調査レポート」が与えられるので、
調査レポートに記載の客観的事実に基づいて、信頼性のある読み物記事を書いてください。

レビュアーによる修正ポイントが指摘された際は、直前に書いた記事を指摘に従って書き直してください。

**出力条件**
- トピックに関してある程度の基礎知識がある読者を前提として、数分で気軽に読める内容にしてください。
- 比較的カジュアルで語りかける口調の文章にします。
- 思考過程は出力せずに、最終結果だけを出力します。
- 記事タイトルは付けないで、次の構成で出力します。各セクションタイトルは、内容に合わせてアレンジしてください。
0. 導入:セクションタイトルを付けないで、この記事を読みたくなる導入を1〜2文にまとめます。
1. 概要:トピックの全体像をまとめて簡単に説明します。
2. 最新情報:特に注目したい新しい情報を取り上げます。
3. 実践:トピックに関して、読者自身がやってみるとよさそうな事を1つ紹介します。
4. まとめ

- 各セクションのタイトルはマークダウンヘッダ ## 付けます。必要に応じて箇条書きのマークダウンを使用します。
- それ以外のマークダウンによる装飾は行いません。
'''

writer_agent = LlmAgent(
    name='writer_agent',
    model='gemini-2.5-flash',
    description='特定のテーマに関する読み物記事を書くエージェント',
    instruction=instruction,
)

デモ用の実装なので比較的シンプルなインストラクションですが、ここからさらにライターの個性を加えて育てていくのも面白そうです。

次は、記事をレビューするレビューエージェントを定義します。

instruction = f'''
あなたの役割は、読み物記事をレビューして、記事の条件にあった内容にするための改善コメントを与える事です。

* 記事の条件
- 記事は、はじめに40文字程度のタイトルがあること。
 今日から役立つ生活情報があって「すぐに読まなきゃ」と読者が感じるタイトルにすること。
 タイトルはマークダウンヘッダ # をつけること。
- タイトルの直後に「なぜいまこのテーマを取り上げるのか」をまとめた導入を加えて、読者にこの記事を読む動機づけを与えます。
- 各セクションのサブタイトルには、絵文字を用いて親しみやすさを出すこと。
- 読者が今日から実践できる具体例が3つ以上紹介されていること。

* 出力形式
- 日本語で出力。
- はじめに、記事の良い点を説明します。
- 次に、修正ポイントを箇条書きで出力します。
'''

review_agent = LlmAgent(
    name='review_agent',
    model='gemini-2.5-flash',
    description='読み物記事をレビューするエージェント',
    instruction=instruction,
)

こちらは、記事の体裁や構成など、掲載サイトで決められたルールをインストラクションに記述しています。

これらを Sequential Agent で 1 つにまとめると、次のようになります。

write_and_review_agent = SequentialAgent(
    name='write_and_review_agent',
    sub_agents=[
        get_print_agent('\n---\n## ライターエージェントが記事を執筆します。\n---\n'),
        copy.deepcopy(writer_agent),
        get_print_agent('\n---\n## レビューエージェントが記事をレビューします。\n---\n'),
        copy.deepcopy(review_agent),
       get_print_agent('\n#### レビューに基づいて記事の修正を依頼しますか?\n'),
    ],
    description='記事を作成、レビューする。',
)

ここでも Print Agent を用いて、ステップごとに特定のメッセージを出力しています。

業務フローエージェントの定義

これで、作業フローに含まれる「直線的な処理のまとまり」が用意できました。次は、ユーザーの入力に応じてこれらを実行する Root Agent を定義します。冒頭の発表スライド(マルチエージェントシステムのデモ構成)では、「業務フローエージェント」と書かれた部分に相当します。

ここでは、次のように定義します。

root_agent = LlmAgent(
    name='article_generation_flow',
    model='gemini-2.0-flash',
    instruction = '''
何ができるか聞かれた場合は、以下の処理をすることをわかりやすくフレンドリーな文章にまとめて返答してください。

- ユーザーが指定したテーマの記事を作成する業務フローを実行する。
- はじめに、テーマに関する調査レポートを作成する。
- その後、ライターエージェントとレビューエージェントが協力して、編集方針に則した記事を作成する。

ユーザーが記事のテーマを指定した場合は、次のフローを実行します。

1. そのテーマの記事の作成に取り掛かる旨を伝えて、research_agent に転送して、調査レポートを依頼します。
2. ユーザー記事の作成を支持したら、write_and_review_agent に転送して、記事の作成とレビューを依頼します。
3. ユーザーが記事の修正を希望する場合は、write_and_review_agent に転送します。

**条件**
research_agent のニックネームは、リサーチエージェント
write_and_review_agent のネックネームは、ライターエージェントとレビューエージェント

''',
    sub_agents=[
        copy.deepcopy(research_agent),
        copy.deepcopy(write_and_review_agent),
    ],
    description='記事を作成する業務フローを実行するエージェント'
)

ユーザーの入力に応じて処理を分岐する部分は、いくつかの実装方法が考えられますが、まずは、この例のように、Root Agent に対するインストラクションに処理のフローを記述するのが簡単でよいでしょう。

Root Agent はいわゆる「会話型エージェント」になっており、ユーザーからの入力によって、次の処理内容を決定します。たとえば、ユーザーが「何ができますか?」と入力した場合は、インストラクションの指示に従って、自分が処理できる内容を返答します。あるいは、ユーザーが記事のテーマを指定した場合は、research_agent に処理を転送します。

一方、sub_agents に登録された転送先のエージェントは、すべて Sequential Agent になっている点に注目してください。ADK の Sequential Agent は会話型ではないので、必要な処理を終えると、再度、Root Agent に処理を転送して、ユーザーからの入力を受け付けます。 先ほどの実装例では、research_agent は、最後に「記事の作成に取り掛かってもよいでしょうか?」というメッセージを表示して、Root Agent に処理を転送します。これにより、この確認メッセージの後にユーザーからの入力を受け付けるという、Human in the loop の処理が自然に行われます。

このような確認メッセージを含めた、これまでの出力内容はすべてセッション情報として記録されており、Root Agent はこのセッション情報を参照することで、これまでの処理の流れを理解した上で、次に実行するべき処理を正しく判断します。 今回の例であれば、ユーザーが「はい。記事を執筆してください。」のように返答すると、write_and_review_agent に処理が転送されます。そして、write_and_review_agent もまた、これまでの出力内容全体を参照して動作します。research_agent が出力した調査レポートの内容もここに含まれているので、想定通りに、調査レポートに基づいた記事が執筆されます。

実行例

それでは、実際の実行例を見てみましょう。出力結果には、どのエージェントの出力かがわかるように [Root Agent] のようなタグをつけてあります。実行可能なコードは、次のノートブックを参照してください。

まずは、最初のメッセージを Root Agent に入力します。

入力テキスト

こんにちは。何ができますか?

出力結果

[Root Agent]
こんにちは!私は、お客様が指定されたテーマに関する記事を作成するお手伝いをすることができます。

具体的には、

1.  まず、リサーチエージェントがテーマに関する調査レポートを作成します。
2.  次に、ライターエージェントとレビューエージェントが協力して、編集方針に沿った記事を作成します。

記事のテーマを指定していただければ、すぐに作成に取り掛かります。リサーチエージェントに調査レポートを依頼しますか?あるいは、記事の作成を直接ライターエージェントとレビューエージェントに依頼することも可能です。修正が必要な場合も、ライターエージェントとレビューエージェントが対応します。ご希望をお聞かせください!

Root Agent は、インストラクションの指示通りに、自分の役割を返答しています。続いて、テーマを指定して、記事の作成を依頼します。次のように、research_agent に処理が転送されて、調査レポートが作成されます。

入力テキスト

「夏を乗り切る郷土料理のスパイシーアレンジ」をテーマに記事を作成してください。

出力結果

[Root Agent]
承知いたしました。「夏を乗り切る郷土料理のスパイシーアレンジ」をテーマに記事を作成します。

まずは、リサーチエージェントに調査レポートを依頼します。少々お待ちください。

...(research_agent に処理を転送)...

[research_agent]
---
## リサーチエージェントが調査レポートを作成します。
---

## 調査対象のトピックを選定します。

承知いたしました。「夏を乗り切る郷土料理のスパイシーアレンジ」に関する調査レポートを作成するためのトピックを以下に選定します。

...(トピックのリストを表示)...

## 選定したトピックに基づいて、調査レポートを作成します。

「夏を乗り切る郷土料理のスパイシーアレンジ」に関する調査レポートを以下にまとめます。

...(調査レポートを表示)...

#### 調査レポートが準備できました。記事の作成に取り掛かってもよいでしょうか?

最後に Print Agent による確認メッセージが表示されて research_agent の処理が終わります。この段階で Root Agent に処理が転送されて、再度、ユーザーからの入力を受け付けます。記事の作成を了承すると、write_and_review_agent に処理が転送されて、記事の作成とレビューが行われます。

入力テキスト

はい。お願いします。

出力結果

[Root Agent]
承知いたしました。それでは、ライターエージェントとレビューエージェントに記事の作成を依頼します。

...(write_and_review_agent に処理を転送)...

[write_and_review_agent]
---
## ライターエージェントが記事を執筆します。
---

...(作成した記事を表示)...

---
## レビューエージェントが記事をレビューします。
---

記事のレビューを承知いたしました。以下に良い点と修正点について説明します。

...(記事のレビュー結果を表示)...

#### レビューに基づいて記事の修正を依頼しますか?

write_and_review_agent の処理が終わると、Root Agent に処理が転送されて、再度、ユーザーからの入力を受け付けます。最後に「レビューに基づいて記事の修正を依頼しますか?」というメッセージが出力されているので、「お願いします。」とだけ入力すると、Root Agent は、これまでのやりとりを踏まえて、再度、write_and_review_agent に処理を転送します。

入力テキスト

はい。お願いします。

出力結果

承知いたしました。レビューエージェントからの指摘に基づき、記事の修正をライターエージェントに依頼します。

...(write_and_review_agent に処理を転送)...

[write_and_review_agent]
---
## ライターエージェントが記事を執筆します。
---

...(作成した記事を表示)...

---
## レビューエージェントが記事をレビューします。
---

記事のレビューを承知いたしました。以下に良い点と修正点について説明します。

...(記事のレビュー結果を表示)...

### 修正ポイント

今回の修正で、提示したすべての条件が満たされており、修正すべき点はありません。
このまま公開できるレベルの記事です。

#### レビューに基づいて記事の修正を依頼しますか?

これ以上の修正点はないので、これで記事が完成したことにします。

入力テキスト

これで完成です。完成した記事をもう一度表示して。

出力結果

[Root Agent]

...(完成した記事を表示)...

「完成した記事を再表示する」という処理は、Root Agent のインストラクションにはありませんでしたが、Root Agent は、これまでのやり取りを踏まえて、常識的な判断によって最後に表示された記事を再表示しています。Human in the loop における判断処理を Root Agent に任せることで、柔軟な処理が実現されることがわかります。

リモートエージェントへの拡張

ここまで、ADK の標準機能を用いて、マルチエージェントによる業務フローを実現しましたが、この実装では、Root Agent を含むすべてのエージェントは「一体」となって動作します。たとえば、Root Agent を Agent Engine にデプロイすると、sub_agents に含まれるすべてのエージェントがまとめて、1 つのコンテナにデプロイされて動作します。

一方、冒頭の発表スライドでは、Root Agent(業務フローエージェント)以外のエージェントは、リモートエージェントとして別環境にデプロイされており、A2A で呼び出される構成になっています。これはどのように実現すればよいのでしょうか?

これもまた、いくつかの実装方法が考えられますが、今回のデモでは、research_agent1research_agent2writer_agentreview_agent の 4 つのエージェントを別環境の Agent Engine にデプロイされたエージェントに処理を転送する「プロキシーエージェント」として実装しています。やや高度な実装になるので詳細は割愛しますが、ADK のエージェントは、コールバック機能を利用すると、「エージェントへの入力を別の関数で横取りして、エージェントの代わりに処理結果を返す」ということができます。

ここでは、次の図のように、エージェントへの入力を横取りした関数から、A2A プロトコルを用いて、Agent Engine にデプロイしたリモートエージェントに処理を転送します。図にはありませんが、後半の記事作成・レビューの部分も同様です。


プロキシーエージェントを利用したリモートエージェント構成

この際、過去のやりとりを記録したセッション情報は、リモートエージェントとは共有されないので、A2A プロトコルで処理を依頼する際は、過去のやりとりを含めたすべての会話履歴を送信します。また、現時点(2025年8月時点)では、Agent Engine は A2A での通信に対応していないので、前段に A2A サーバーを用意しています。具体的な構築手順は、次の記事で解説しています。

まとめ

この記事では、冒頭に紹介したデモシステムのアーキテクチャーをマルチエージェントシステムの観点から解説しました。一言でマルチエージェントと言っても、すべてのエージェントが一体としてデプロイされる構成もあれば、エージェントごとに個別の環境にデプロイされる「リモートエージェント」の構成など、さまざまな構成パターンが考えられます。マルチエージェントの動作をトレースするなど、本番運用で必要となる管理システムも構成パターンによって変わる点に注意が必要です。

また、今回の構成では、処理の流れが分岐する部分は、すべて人間の判断が介入していました。この場合は、記事で説明したように Root Agent で分岐先を決定するのが簡単です。一方、人間の判断ではなく、事前に定義したロジックで自動的に分岐先を決定したい場合もあるでしょう。こちらは、少し高度な実装が必要になるので、また別の機会に解説したいと思います。

そして最後に、今回の実装例で注目してほしいポイントに、ライターエージェントが出力した記事をレビューして書き直させることで、記事の品質が向上している点があります。LLM の出力内容を改善したい場合、LLM にあたえるプロンプトを工夫するというのがよくある方法ですが、この例のように、出力内容を他の LLM に評価させてフィードバックするという方法も有効な手段となります。

Google Cloud Japan

Discussion