1. はじめに

エージェントを 1 つ動かすだけなら、リポジトリ構成で悩むことはほとんどありません。プロンプトをコードに直書きし、ツールを 1 ファイルにまとめても回ります。ところが 2 つ目・3 つ目のエージェントを足した瞬間から、話は一気に難しくなります。プロンプトが各所に散り、ルーティングのロジックがハンドラに埋もれ、「どのエージェントがどのツールを使えるのか」が誰にも分からなくなる——これが多くのチームがぶつかる壁です。

この記事を書いた背景は、まさにこの「マルチエージェント化でコードが崩れる」問題に、再現性のある構成の型を提示したかったからです。幸い、Microsoft が公開している技術ワークショップ microsoft/TWL300-AI-Apps-and-agents は、複数エージェント(ショッピングアシスタント/インテリアデザイナー/在庫/ロイヤルティ/カート管理)を Microsoft Foundry 上で協調させる実装を、そのままリポジトリ構成のお手本として読める形で提供しています。

この記事の目的は、この公式サンプルを**「コード管理(リポジトリ構成)」の観点で解剖し、他プロジェクトへ移植できるベストプラクティスとして 5 原則に一般化する**ことです。特定のフレームワークに依存しない構成の考え方に落とし込むので、LangGraph・Semantic Kernel・自作オーケストレータなど、基盤が違っても応用できます。エージェント開発の全体像を先に押さえたい方は、AI エージェント開発のライフサイクル全体像も併せてどうぞ。

補足:題材リポジトリは学習用ワークショップです。一部のデプロイ用ワークフローは、受講者が .github/workflows/ にコピーして使う想定でサンプルとして src/workflows/ に置かれています。この記事では「構成の意図」を中心に読み解きます。

2. 題材:Microsoft 公式マルチエージェントサンプルの全体像

まずは全体像です。このリポジトリは大きく src/(アプリ実装)docs/(ライフサイクル順の解説)media/(スクリーンショット) に分かれます。中核の src/ は、下図のように責務ごとのレイヤとして読み解けます。

マルチエージェント AI アプリのリポジトリを、入口・UI/オーケストレーション/エージェント実行/宣言的定義/基盤・運用の 5 レイヤに責務分離した構成の全体像

src/ 直下の主要ディレクトリと役割は次のとおりです。

ディレクトリ 役割 レイヤ
src/chat_app.py / src/a2a/ ユーザー/エージェントとの接点(WebSocket チャット、A2A アプリ) 入口・UI
src/handlers/ 単一/複数エージェントのメッセージ処理パイプライン オーケストレーション
src/services/ 意図分類・ハンドオフ・フォールバックなどの業務サービス オーケストレーション
src/app/agents/ エージェントの生成(バージョニング)と実行 エージェント実行
src/app/tools/ 画像生成・在庫確認・割引計算などの能力別ツール エージェント実行
src/app/servers/ MCP サーバー/クライアント エージェント実行
src/infra/agents/*.json エージェント定義(宣言的) 宣言的定義
src/prompts/*.txt プロンプト(instructions)本体 宣言的定義
src/infra/*.bicep Azure リソースの Infrastructure as Code 基盤・運用
src/workflows/*.yml デプロイ用 GitHub Actions(サンプル) 基盤・運用
src/data/ 評価データ・レッドチーム用攻撃プロンプト 基盤・運用
src/utils/ ロギング・履歴・応答整形などの共通処理 基盤・運用

ポイントは、「エージェントを定義するもの」「エージェントを動かすもの」「エージェントを運ぶもの」がはっきり分かれていることです。以降ではこの分離を 5 つの原則として抽出します。

3. 原則1:定義とコードを分離する(宣言的エージェント定義 × 外部プロンプト)

課題

プロンプトやモデル名を Python のコードに直書きすると、**「振る舞いの変更」がすべて「コードの変更」**になります。レビューが読みにくく、非エンジニアが手を出せず、環境ごとの差し替えも難しくなります。

このサンプルのやり方

エージェント 1 つにつき 1 つの 宣言的 JSON 定義src/infra/agents/ に置いています。例えば cora.json(ショッピングアシスタント)はこうです。

{
  "description": "Cora - Zava Shopping Assistant",
  "definition": {
    "kind": "prompt",
    "model": "${GPT_DEPLOYMENT}",
    "instructions": "${INSTRUCTIONS}",
    "tools": [
      {
        "type": "function",
        "name": "mcp_product_recommendations",
        "description": "Search for product recommendations based on user query.",
        "parameters": {
          "type": "object",
          "properties": { "question": { "type": "string" } },
          "required": ["question"]
        }
      }
    ]
  }
}

注目すべきは 3 点です。

  • 1 エージェント 1 ファイルcart-manager.json / cora.json / customer-loyalty.json / handoff-service.json / interior-designer.json / inventory-agent.json と、エージェントの数だけ定義が並びます。追加・削除・差分レビューがファイル単位で完結します。
  • ${GPT_DEPLOYMENT} / ${INSTRUCTIONS} のプレースホルダ:モデルの配置名やプロンプト本体を外から注入する設計です。定義ファイルそのものは環境非依存で、開発・本番で使い回せます。
  • instructions は別ファイル:プロンプト本体は src/prompts/*.txt(例:DiscountLogicPrompt.txt)に外部化され、デプロイ時に JSON へ注入されます(→ 原則4)。

一般化した指針

  • エージェント=データとして扱う。モデル・instructions・利用ツールは、コードではなく宣言的な設定ファイル(JSON/YAML)に置く。
  • プロンプトはコードから切り出す。長いプロンプトを文字列リテラルで抱えない。差分・翻訳・A/B が容易になり、プロンプトエンジニアリングが独立した営みになります。
  • 環境依存値はプレースホルダ+注入で外出しする。定義ファイルを「テンプレート」として不変に保つのがコツです。

4. 原則2:オーケストレーションを層で分ける(ハンドオフとルーティング)

課題

複数エージェントの制御ロジックを 1 つの関数に書くと、「入力の前処理」「どのエージェントに振るか」「実行」「後処理」がひと塊になり、テストも差し替えもできなくなります。

このサンプルのやり方

制御を handlers(パイプライン)services(業務ロジック) の 2 層に分けています。

  • src/handlers/single_agent_handler.py … 1 エージェントに素通しする最小構成。
  • src/handlers/multi_agent_handler.py … 複数エージェントの本命パイプライン。「①意図分類 → ②文脈付与 → ③エージェント実行 → ④応答処理」を小さな関数に分解し、各ステップを独立してテスト可能にしています。
  • src/services/handoff_service.py … ルーティングの心臓部。
  • src/services/fallback_service.py … 失敗時の代替応答。

singlemultiハンドラ差し替えで切り替えられる構造は、「まず 1 エージェントで動かし、あとから多エージェントへ育てる」段階的な複雑化を支えます。

ルーティングは LLM の構造化出力による意図分類で行います。HandoffServiceIntentClassification(Pydantic モデル)で domain / is_domain_change / confidence / reasoning を返させ、domain → agent のマップで対象エージェントを選びます。分類に失敗したら現在のドメインを維持するか既定エージェント(cora)へフォールバックします。

ユーザーメッセージを構造化出力で意図分類し、文脈付与・エージェント実行・応答処理へ流すパイプラインと、分類失敗時のフォールバック、エージェント別ツール allow-list を示すフロー図

class IntentClassification(BaseModel):
    model_config = {"extra": "forbid", "additionalProperties": False}
    domain: str
    is_domain_change: bool
    confidence: float = Field(ge=0.0, le=1.0)
    reasoning: str

さらに handoff-service.json の定義側でも json_schema で出力形式を固定しており、「振る舞い(プロンプト)」と「出力契約(スキーマ)」の両方を宣言的に固定しています。

一般化した指針

  • オーケストレーションは “パイプライン層” と “判断ロジック層” に分ける。ハンドラは流れ、サービスは判断、と役割を割り切る。
  • ルーティングは構造化出力で明示的に。自由文をパースして分岐するのは壊れやすい。信頼度(confidence)や理由(reasoning)も返させると監視・改善に効きます。
  • 必ずフォールバック経路を持つ。分類器は間違える前提で、graceful degradation を初めから設計に入れる。
  • 単一→複数を差し替えで育てられる構造にしておく。

5. 原則3:ツール・MCP・A2A を責務境界で分割する

課題

ツールが増えると、「どのエージェントが何を呼べるか」が曖昧になり、権限過多(あるエージェントが本来使わないツールを呼ぶ)や、スキーマ定義の二重管理が起きます。

このサンプルのやり方

エージェント実行まわりを 3 つに割っています。

  • src/app/agents/agent_initializer.pyproject_client.agents.create_version(...)エージェントをバージョン付きで生成
  • src/app/agents/agent_processor.py … 実行(会話ストリーム、ツール呼び出しのディスパッチ)。
  • src/app/tools/aiSearchTools.py / discountLogic.py / imageCreationTool.py / inventoryCheck.py など能力ごとに 1 ファイル
  • src/app/servers/mcp_inventory_server.py / mcp_inventory_client.py(MCP)。

特に秀逸なのが src/app/agents/tool_definitions.py の 2 つのテーブルです。

# エージェント別のツール allow-list(最小権限)
AGENT_TOOL_ASSIGNMENTS = {
    "interior_designer": ["mcp_product_recommendations"],
    "customer_loyalty":  ["mcp_calculate_discount"],
    "inventory_agent":   ["mcp_inventory_check"],
    "cart_manager":      [],
    "cora":              ["mcp_product_recommendations"],
}
  • エージェントごとにツールを allow-list 化cart_manager は空リスト(=ツールを持たない)と、明示的に最小権限を宣言しています。
  • スキーマは MCP サーバーから動的取得_discover_tools() が MCP サーバーへ list_tools() して FunctionTool を組み立てます。つまりツールスキーマの単一の真実源は MCP サーバーで、二重管理を避けています。

そして A2A(Agent-to-Agent)は完全に独立したアプリとして src/a2a/ に切り出されています。独自の main.py(FastAPI、ポート 8001)・api/agent/static/templates/ を持ち、プロトコル境界がそのままディレクトリ境界になっています。

一般化した指針

  • ツールは能力単位で 1 ファイル、実行基盤(生成・実行)とは別ディレクトリに。
  • エージェント×ツールの割当は明示的なテーブルにする。「誰が何を呼べるか」を 1 か所で読める状態は、セキュリティレビューの前提です。
  • ツールスキーマの真実源を 1 つに(MCP など)。定義の重複は事故のもと。
  • 別プロトコル・別ランタイムは別デプロイ境界へ。A2A や外部公開 API は独立アプリに切る。

6. 原則4:IaC × GitOps でエージェントを継続的にデプロイする

課題

エージェントの本番反映を手作業でやると、「誰が・いつ・どの版を出したか」が追えず、プロンプト 1 行の修正すら怖くなります。

このサンプルのやり方

インフラは Infrastructure as Codesrc/infra/DeployAzureResources.bicepcheck_quota.py によるクォータ確認、タグ付けの Bicep)としてコード化され、エージェント定義 (src/infra/agents/*.json) と同じリポジトリでバージョン管理されています。

デプロイは GitOps です。ワークフローは on.push.paths フィルタで変更のあった対象だけを起動します。

パス限定トリガーで、変更されたエージェント定義だけを Foundry へ、src 配下の変更をコンテナとして ACR/ACA へデプロイする 2 レーンの GitOps フロー図

エージェント更新(サンプル src/workflows/0502_sample_agent_deployment.yml)はこう動きます。

on:
  push:
    paths:
      - "src/prompts/CustomerLoyaltyAgentPrompt.txt"
      - "src/infra/agents/customer-loyalty.json"
# プロンプト本文を読み、JSON テンプレートへ instructions として注入
INSTRUCTIONS=$(python3 -c "import json;print(json.dumps(open('src/prompts/CustomerLoyaltyAgentPrompt.txt').read()))")
jq --arg model "$GPT_DEPLOYMENT" --argjson instructions "$INSTRUCTIONS" \
   '.definition.model = $model | .definition.instructions = $instructions' \
   src/infra/agents/customer-loyalty.json > /tmp/agent-payload.json
# Foundry REST API で新しいエージェントバージョンを作成
az rest --method POST --url "${FOUNDRY_ENDPOINT}/agents/customer-loyalty/versions?api-version=2025-11-15-preview" \
   --headers "Content-Type=application/json" --resource "https://ai.azure.com" --body @/tmp/agent-payload.json
  • パス限定トリガーで最小デプロイ:ロイヤルティエージェントの prompt/JSON を変えたときだけ、そのエージェントが更新されます。無関係なエージェントは動きません。
  • 原則1・3 の伏線回収:外部化したプロンプトを jq でテンプレートへ合成し、${INSTRUCTIONS} を実体化してから REST API に流します。定義の分離が、そのままデプロイ単位の分割になるわけです。
  • コンテナは別レーン0501_deployment.yml):src/** の変更で Docker イメージをビルドし、ACR へ push、az containerapp update で Azure Container Apps を更新します。

docs/05_agentic_devops では、これらのワークフローを GitHub Copilot に生成させる手順(Agentic DevOps)まで扱っています。ワークフロー設計そのものを深掘りしたい場合は、GitHub Actions Reusable Workflows 実践ガイドや、シークレットレス認証のGitHub Actions から Azure への OIDC 認証設定ガイドが参考になります。

一般化した指針

  • エージェント定義・IaC・アプリコードを同じリポジトリでバージョン管理し、履歴を 1 本化する。
  • paths フィルタで差分単位のデプロイを組む。エージェントごと・サービスごとに独立して出せると、変更の blast radius が小さくなります。
  • プロンプト(テキスト)+テンプレート(JSON)+合成(jq/スクリプト) の三分割は、そのまま CI に載せられる。
  • 認証は長期シークレットより OIDC を優先。

7. 原則5:評価・レッドチーム・可観測性をリポジトリに組み込む

課題

エージェントは「動いた」だけでは不十分で、**品質(意図どおり答えるか)と安全性(攻撃に耐えるか)**を継続的に測る必要があります。これを後付けにすると、評価データが散逸します。

このサンプルのやり方

品質・安全のためのデータとドキュメントがリポジトリの一級市民として同梱されています。

  • src/data/handoff_service_evaluation_grounded.jsonl … ハンドオフ(ルーティング)の評価用データセット。想定 QA と実応答の突き合わせに使えます。
  • src/data/custom_attack_prompts.jsonレッドチーム用のカスタム攻撃プロンプト
  • docs/04_observability_ai_foundry … Microsoft Foundry での監視・トレース・アラート・評価。
  • docs/06_red_teaming_troubleshooting … レッドチーミングとトラブルシュート。
  • src/utils/performance_utils.py ほか … タイミング計測などの可観測性フック。

つまり、評価・レッドチーム・可観測性が「別プロジェクト」ではなく、アプリと同じリポジトリの中に構造として存在しています。ルーティングの評価手法はMicrosoft Foundry の評価機能で想定 QA と実回答の差分を測る方法も参考にしてください。

一般化した指針

  • 評価データセットをリポジトリに置くdata/evals/)。ルーティング・各エージェントの応答品質を回帰テストのように回せる状態にする。
  • レッドチーム用の攻撃プロンプトもバージョン管理。安全性テストを CI に組み込む土台になる。
  • 可観測性は最初から。トレース・タイミング・信頼度スコアをコードに埋め込み、運用で「なぜこのエージェントが選ばれたか」を追えるようにする。

8. 自分のリポジトリへの落とし込み — 推奨ツリーとチェックリスト

ここまでの 5 原則を、フレームワーク非依存の推奨ディレクトリツリーに一般化すると次のようになります。

repo-root/
├─ src/
│  ├─ app/
│  │  ├─ agents/           # エージェントの生成(バージョニング)と実行
│  │  ├─ tools/            # 能力ごとに 1 ファイルのツール
│  │  └─ servers/          # MCP サーバー/クライアント
│  ├─ handlers/            # single / multi のメッセージ処理パイプライン
│  ├─ services/            # 意図分類・ハンドオフ・フォールバック
│  ├─ agents/              # ★ 宣言的エージェント定義(1 エージェント 1 ファイル)
│  │  ├─ cora.json
│  │  └─ inventory-agent.json
│  ├─ prompts/             # ★ instructions を外部ファイル化
│  ├─ infra/               # IaC(Bicep / Terraform)+ クォータ確認
│  ├─ data/                # 評価データ・レッドチーム攻撃プロンプト
│  └─ utils/               # ロギング・履歴・応答整形などの共通処理
├─ a2a/                    # 別プロトコル/外部公開は独立アプリに分離
├─ .github/workflows/      # paths フィルタ付きの GitOps ワークフロー
└─ docs/                   # 01_deploy → … → cleanup のライフサイクル順

移植時のチェックリストです。

  • エージェント定義を**宣言的ファイル(1 エージェント 1 ファイル)**に切り出したか。
  • プロンプト(instructions)をコードから外部ファイルに出したか。
  • モデル名など環境依存値をプレースホルダ+注入にしたか。
  • オーケストレーションをパイプライン(handlers)と判断(services)に分離したか。
  • ルーティングを構造化出力の意図分類にし、フォールバックを用意したか。
  • 単一→複数をハンドラ差し替えで切り替えられるか。
  • ツールを能力単位に分け、エージェント別 allow-listで最小権限化したか。
  • ツールスキーマの真実源を 1 つ(MCP など)にしたか。
  • A2A・外部 API を独立デプロイ境界に分けたか。
  • IaC・エージェント定義・アプリコードを同一リポジトリでバージョン管理したか。
  • paths フィルタで差分単位のデプロイを組み、OIDC など安全な認証にしたか。
  • 評価データ・攻撃プロンプト・可観測性をリポジトリに組み込んだか。
  • ドキュメントをライフサイクル順に構造化したか。

9. まとめ

マルチエージェントのコード管理でスケールする構成は、突き詰めると 「定義」「実行」「運搬」をレイヤで分け、エージェントを宣言的なデータとして扱うことに尽きます。Microsoft 公式サンプル microsoft/TWL300-AI-Apps-and-agents から抽出した 5 原則は次のとおりです。

  1. 定義とコードを分離:宣言的なエージェント定義(1 エージェント 1 ファイル)+外部プロンプト+プレースホルダ注入。
  2. オーケストレーションを層で分割:handlers(パイプライン)と services(判断)、構造化出力によるハンドオフとフォールバック。
  3. 責務境界で分割:能力別ツール、エージェント別ツール allow-list、MCP を単一の真実源、A2A は独立アプリ。
  4. IaC × GitOps:定義・IaC・コードを同一リポジトリで管理し、paths フィルタで差分単位に自動デプロイ。
  5. 評価・レッドチーム・可観測性を内蔵:評価データと攻撃プロンプトを一級市民として同梱する。

これらは特定の SDK に縛られない構成上の原則です。まずは「プロンプトの外部化」と「エージェント定義の宣言的ファイル化」という小さな一歩から、既存リポジトリへ取り入れてみてください。

10. 参考リンク

題材リポジトリ(一次情報)

Microsoft 公式ドキュメント


この記事の執筆にあたり AI の支援を受けています。