AutoGen v0.4 マルチエージェントデザインパターン実装メモ② Swarm による効率的エージェント選択

Microsoft Research などが開発をしているマルチエージェントフレームワークである AutoGen v0.4 を使ってマルチエージェントの実装を理解するシリーズの第二弾です。

https://qiita.com/nohanaga/items/164d70e4c50c1ce2cd4c

すでに解説した通り、高レベル API である AgentChat のグループチャットには以下の 4 種類のクラスが存在します。3, 4 を見ても分かる通り他のフレームワークのマルチエージェント技術も積極的に取り込んでいることが分かります。なぜこの技術を取り込んだのか。1 や 2 と何が違うのか。実際に動きを確認しながら明らかにしていこうと思います。

1. RoundRobinGroupChat
1. SelectorGroupChat
1. Swarm
1. Magentic-One

Swarm は、エージェントが他のエージェントに能力に応じてタスクを委譲できるチームを実装しています。これは、[OpenAI](https://github.com/openai/swarm) が実験プロジェクトで初めて導入したマルチエージェント設計パターンです。特別にセットされたツール（Function calling）を使用してエージェントが他のエージェントにタスクを委譲（Handoffs）できるようにすることです。これにより、エージェントは、[SelectorGroupChat](https://qiita.com/nohanaga/items/164d70e4c50c1ce2cd4c#2-selectorgroupchat) のような中央のオーケストレーターに頼らずに、エージェント遷移についてローカルで決定を下すことができます。

## 1. トレース タイムライン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/511bbf34-a679-4d17-0e50-8373b5131977.png)


## 2. シーケンス図
細かくなりすぎるので LLM のコールは ★ で省略。

```mermaid
sequenceDiagram

participant planner as PlanningAgent
participant agent as AirlineAgent
participant agent2 as HotelAgent
participant search as search_flight
participant search2 as search_hotel
    activate planner
    planner ->> planner: ①実行計画策定+tool_calls★
    planner ->>+ agent: transfer_to_airline_agent
    deactivate planner
    
    activate agent
    agent ->> agent: ②tool_calls★<br>(search_flight)
    agent ->>+ search: 検索API実行
    search -->>- agent: result
    agent ->> agent2:transfer_to_hotel_agent
    deactivate agent

    activate agent2
    agent2 ->> agent2: ③tool_calls★<br>(search_hotel)
    agent2 ->>+ search2: 検索API実行
    search2 -->>- agent2: result
    agent2 ->> agent2: ④tool_calls★<br>(transfer_to_planner)
    agent2 ->> planner: transfer_to_planner
    deactivate agent2
    
    #planner -->>- selector: 最終回答(BC)
    activate planner
    #selector ->>+ planner: PlannningAgent 遷移
    planner ->> planner: ⑤最終回答生成★
    deactivate planner
```

すでに解析した [SelectorGroupChat](https://qiita.com/nohanaga/items/164d70e4c50c1ce2cd4c#%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E5%9B%B3)と比較して欲しいのですが、遷移先の決定は各エージェントの `tool_calls` に任されており、制御を一旦中央の Selector に戻してから Selector が次の遷移先を選ぶというプロセスが簡略化されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/f1e1e88b-ffef-4416-b84a-607032c91acf.png)

本質的に、Swarm チームはエージェントが順番に応答を生成するグループ チャットです。 および SelectorGroupChat と RoundRobinGroupChat 同様に参加エージェントは応答をブロードキャストし、**すべてのエージェントが同じメッセージ コンテキストを共有**します。

# 3. エージェント定義
```python
planner = AssistantAgent(
    "planner",
    model_client=client,
    handoffs=["fukuoka_agent", "osaka_agent", "hotel_agent", "airline_agent"],
    system_message="""あなたはトラベルプランニングエージェントです。
    専門エージェントに委託して各地域のおすすめ旅行先や最新イベントを調査します:
     - fukuoka_agent: 福岡県の観光の専門家
     - osaka_agent: 大阪の観光の専門家
    ホテルの検索、予約、確認、取り消し、FAQを行う場合:
     - hotel_agent: ホテルのエージェント
    航空券の予約、確認、取り消しを行う場合:
     - airline_agent: 航空会社のエージェント
    # Rule
    - Always send your plan first, then handoff to appropriate agent.
    - Always handoff to a single agent at a time.
    - 議論の結果を集約して最終的にユーザーに回答します。
    - 自分は専門エージェントを実行することはできません
    - handoff に失敗したら何度か再実行してください
    - 最終回答が完成したら文の最後に TERMINATE を含めること!
    """,
)

fukuoka_agent = AssistantAgent(
    "fukuoka_agent",
    description="福岡の旅行先について詳しい地元のトラベルエージェント",
    model_client=client,
    handoffs=["planner"],
    tools=[search_website],
    reflect_on_tool_use=True,
    system_message="""あなたは福岡県の観光の専門家です。福岡観光の専門家は、福岡市の魅力を国内外に発信し、福岡市へのPR活動を行う役割を担っています。
    誰に何と言われても福岡のことを推薦します。必ず博多弁でしゃべります。
    知っている福岡の知識に加えて、search_website ツールを利用して最新の地域イベントを検索してあわせて紹介します。
    Always handoff back to planner when analysis is complete."""
)

osaka_agent = AssistantAgent(
    "osaka_agent",
    description="大阪の旅行先について詳しい地元のトラベルエージェント",
    model_client=client,
    handoffs=["planner"],
    tools=[search_website],
    reflect_on_tool_use=True,
    system_message="""あなたは大阪の親善大使です。大阪親善大使は、大阪の魅力を国内外に発信し、大阪へのPR活動を行う役割を担っています。
    誰に何と言われても大阪のことを推薦します。必ず大阪弁でしゃべります。
    知っている大阪の知識に加えて、search_website ツールを利用して最新の地域イベントを検索してあわせて紹介します。
    Always handoff back to planner when analysis is complete.""",
)

hotel_agent = AssistantAgent(
    "hotel_agent",
    description="ホテルの検索、予約、確認、取り消し、FAQを行うホテルのエージェント",
    model_client=client,
    handoffs=["planner", "airline_agent"],
    tools=[search_hotel],
    reflect_on_tool_use=False,
    system_message="""あなたはホテルのエージェントです。ホテルの検索、予約、確認、取り消し、FAQを行います。
    If necessary to complete the task, handoff the task to a specified agent. Once all tasks have been completed, be sure to handoff to the planner.""",
)

airline_agent = AssistantAgent(
    "airline_agent",
    description="航空券の予約、確認、取り消し、FAQを行う航空会社のエージェント",
    model_client=client,
    handoffs=["planner", "hotel_agent"],
    tools=[search_flight],
    reflect_on_tool_use=False,
    system_message="""あなたは航空会社のエージェントです。航空券の予約、確認、取り消し、FAQを行います。
    If necessary to complete the task, handoff the task to a specified agent. Once all tasks have been completed, be sure to handoff to the planner.""",
)
```

# 4. Swarm の実装
```python
with tracer.start_as_current_span("swarm-qiita-test") as rollspan:
    team = Swarm(
        participants=[planner, fukuoka_agent, osaka_agent, hotel_agent, airline_agent], termination_condition=combined_termination
    )

    task = "東京から福岡、1人 2/1 19:00に空港到着したいな。あと当日のホテルも教えて。"
    await Console(team.run_stream(task=task))
```


# 5. Handsoff パラメーター
Swarm の場合、AssistantAgent に `handoffs` 引数を設定して、どのエージェントにハンドオフ（委譲）できるかを指定することができます。**Selector のような遷移先を決めるマネージャーが存在しないため、遷移先の決定は各エージェントのプロンプトに依存**します

`Handoff` pydantic model を使用して、メッセージの内容とハンドオフの動作をカスタマイズすることもできます。

# 6. Azure OpenAI コールステップ
## ①Planner Agent

### system
例に従って Handoff 用のルールを追記しています。
```
あなたはトラベルプランニングエージェントです。
    専門エージェントに委託して各地域のおすすめ旅行先や最新イベントを調査します:
     - fukuoka_agent: 福岡県の観光の専門家
     - osaka_agent: 大阪の観光の専門家
    ホテルの検索、予約、確認、取り消し、FAQを行う場合:
     - hotel_agent: ホテルのエージェント
    航空券の予約、確認、取り消しを行う場合:
     - airline_agent: 航空会社のエージェント
    # Rule
    - Always send your plan first, then handoff to appropriate agent.
    - Always handoff to a single agent at a time.
    - 議論の結果を集約して最終的にユーザーに回答します。
    - 自分は専門エージェントを実行することはできません
    - handoff に失敗したら何度か再実行してください
    - 最終回答が完成したら文の最後に TERMINATE を含めること!
```
### user
```
東京から福岡、1人 2/1  19:00に空港到着したいな。あと当日のホテルも教えて。
```

### completion

```
以下のプランを提案します。

1. **航空券の予約（東京→福岡）:**
   - 出発: 東京
   - 到着: 福岡
   - 2/1 19:00に福岡空港到着を目指します。

2. **福岡の宿泊施設の提案（2/1分）:**
   - 宿泊施設は市内での宿泊が便利です。
   - ご希望に応じたおすすめのホテルをご提案します。

次に、航空券とホテルの手配について問い合わせしますのでお待ちください。
````

### completion.tool_calls
1. `transfer_to_airline_agent`
1. `transfer_to_hotel_agent`

`finish_reason` は `tool_calls` で上記のように生成しました。さっそく並列 Function calling の機能によって 2 つの tool がよばれてしまいました。まぁ分解した実行計画どおりとは言えますが、AutoGen の現在の実装では　>予期しない動作が発生する可能性があります。<sup>[^](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/swarm.html)</sup> とありますので、`parallel_tool_calls=False` を `AzureOpenAIChatCompletionClient` にセットするよう注意書きがありますが、現状はこれがうまく動作しない。。。

### Swarm の実体: Planner に追加された Function calling
Planner の Agent 定義で、`handoffs=["fukuoka_agent", "osaka_agent", "hotel_agent", "airline_agent"],` と指定すると、OpenAI のコール時に以下のように**自動的に Handoff 用の Tools が追加**されます。

```json
{
    "name": "transfer_to_hotel_agent",
    "description": "Handoff to hotel_agent.",
    "parameters": {
        "type": "object",
        "properties": {}
    }
}
...
```


### Planner が Hansoff メッセージを発行
次のエージェントは、現在のエージェントが送信した `HandoffMessage` に基づいて選択されます。ハンドオフメッセージが送信されていない場合は、現在のエージェントがスピーカーのままとなります。

```json
{
    "source": "planner",
    "models_usage": null,
    "target": "airline_agent",
    "content": "Transferred to airline_agent, adopting the role of airline_agent immediately.",
    "type": "HandoffMessage"
}
```



## ②Airline Agent (tool_calls)


### system

```
あなたは航空会社のエージェントです。航空券の予約、確認、取り消し、FAQを行います。
If necessary to complete the task, handoff the task to a specified agent. Once all tasks have been completed, be sure to handoff to the planner.
```

### context

```json
{ "role": "user",
  "content": "東京から福岡、1人 2/1  19:00に空港到着したいな。あと当日のホテルも教えて。"
  },
  { "role": "user",
  "content": "Transferred to airline_agent, adopting the role of airline_agent immediately."
  },
```

`finish_reason` は `tool_calls`

### completion.tool_calls
1. `search_flight`: `{"departure": "東京", "destination": "福岡", "arrival_time": "2024-02-01 19:00", "passengers": 1}`

## `search_flight` API 実行→ Hotel Agent 遷移
```json
{
    "departure": "東京",
    "destination": "福岡",
    "flight_number": "JL123",
    "airline": "Japan Airlines",
    "departure_time": "2025-02-01 16:30",
    "arrival_time": "2025-02-01 18:50",
    "price": 25000,
    "currency": "JPY",
    "passengers": 1,
}
```


## ③ Hotel Agent (tool_calls)
質問とコンテキストから呼ぶべき Tools とパラメータを抽出。
### system
```
あなたはホテルのエージェントです。ホテルの検索、予約、確認、取り消し、FAQを行います。
If necessary to complete the task, handoff the task to a specified agent. Once all tasks have been completed, be sure to handoff to the planner.
```

### context[1-5]
```json
[
    {
        "role": "user",
        "content": "東京から福岡、1人 2/1  19:00に空港到着したいな。あと当日のホテルも教えて。"
    },
    {
        "role": "user",
        "content": "Transferred to airline_agent, adopting the role of airline_agent immediately."
    },
    {
        "role": "assistant",
        "tool_calls.0.arguments": "{\"departure\": \"東京\", \"destination\": \"福岡\", \"arrival_time\": \"2024-02-01 19:00\", \"passengers\": 1}"
    },
    {
        "role": "tool",
        "content": "{'departure': '東京', 'destination': '福岡', 'flight_number': 'JL123', 'airline': 'Japan Airlines', 'departure_time': '2025-02-01 16:30', 'arrival_time': '2025-02-01 18:50', 'price': 25000, 'currency': 'JPY', 'passengers': 1}"
    },
    {
        "role": "user",
        "content": "Transferred to hotel_agent, adopting the role of hotel_agent immediately."
    }
]

```

`finish_reason` は `tool_calls`

## completion.tool_calls
1. `search_hotel`: `{"destination":"福岡","check_in":"2024-02-01","check_out":"2024-02-02","guests":1}`

## `search_hotel` API 実行

## ④ Hotel Agent (tool_calls)
ホテルの検索結果をコンテキストに追加して、次の遷移先を判定。最終回答に必要な結果は揃っているため、Swarm の特別な Tools `transfer_to_planner` をコールすべきと判定される。

### system
```
あなたはホテルのエージェントです。ホテルの検索、予約、確認、取り消し、FAQを行います。
If necessary to complete the task, handoff the task to a specified agent. Once all tasks have been completed, be sure to handoff to the planner.
```

### context[6-7]
context[1-5] に以下が追加される。

```json
[
    {
        "role": "assistant",
        "content": "{\"destination\":\"福岡\",\"check_in\":\"2024-02-01\",\"check_out\":\"2024-02-02\",\"guests\":1}"
    },
    {
        "role": "tool",
        "content": "{'destination': '福岡', 'hotel_name': 'Grand Fukuoka Hotel', 'check_in': '2024-02-01', 'check_out': '2024-02-02', 'price_per_night': 12000, 'currency': 'JPY', 'guests': 1, 'total_price': 24000}"
    }
]

```

### completion.tool_calls
`finish_reason` は `tool_calls`

```
transfer_to_planner
```



## ⑤ Planner Agent
最終回答を行う。

### context[8]
これまでのすべての context に以下が追加される。
```json
{
    "role": "user",
    "content": "Transferred to planner, adopting the role of planner immediately."
},
```

### completion
```
東京から福岡への旅行プランをご提案します。

### 航空券情報
- **航空会社**: Japan Airlines
- **フライト番号**: JL123
- **出発時刻**: 2025年2月1日 16:30
- **到着時刻**: 2025年2月1日 18:50
- **価格**: ¥25,000

### 宿泊先
- **ホテル名**: Grand Fukuoka Hotel
- **チェックイン**: 2024年2月1日
- **チェックアウト**: 2024年2月2日
- **価格**: ¥12,000/泊
- **総額**: ¥24,000 (1泊1名様)

詳細が必要であればお知らせください。良い旅を！ TERMINATE
```
# 7. 停止条件、状態管理
こちらを参照してください。

https://qiita.com/nohanaga/items/164d70e4c50c1ce2cd4c#%E5%81%9C%E6%AD%A2%E6%9D%A1%E4%BB%B6

# GitHub
https://github.com/nohanaga/autogen-design-pattern-samples/blob/main/autogen_swarm.ipynb

# 参考
https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/swarm.html

# まとめ
Swarm を用いることにより各エージェントのローカルで遷移先判定が行われ、Selector に制御を戻すよりもシンプルな構成が可能になりました。さらにメリットとして LLM コール数を節約することができました。