
【小ネタ】 Strands Agents の Swarm で最終エージェントの回答だけを取り出す
はじめに
こんにちは、運転練習中のコンサル部の神野(じんの)です。
早く自信満々に駐車できるようになりたいですね。
こんな話はさておき、皆さんは Strands Agents の Swarm を使っていますか?
マルチエージェントを実装するのにこのパターンを使用することもあるかと思います。
今回、Swarmを使ったところ、最終回答に複数エージェントの途中回答が全部混ざってしまい、ユーザーに見せたいのは最後のエージェントがまとめたレポートだけなのに全部見えていたらちょっと鬱陶しいですよね。
そういった場合の小ネタですが、Swarm のストリームイベントから最終エージェントの出力だけを取り出す方法をご紹介します。
前提
- Python 3.12
- strands-agents 1.47.0
Swarm の構成は、受付(front)→ 調査(researcher)→ レポート作成(synthesizer)の 3 エージェントがバトンタッチしてつながる形を想定しています。

swarm = Swarm(
[front_agent, researcher, synthesizer],
entry_point=front_agent,
max_handoffs=10,
max_iterations=15,
)
Swarm 自体の使い方や安全パラメータ(max_handoffs や execution_timeout など)については公式ドキュメントをご覧ください。
いきなり原因・解決方法
原因はシンプルで、stream_async() が流してくる multiagent_node_stream イベントは全エージェントのテキストを出力するため、連結するとエージェント全員の会話が混ざります。
この解決方法は、multiagent_node_stream イベントが持つ node_id をキーにテキストをノード別の dict に蓄積して、最後に レポート作成(synthesizer)の分だけを採用します。
有効なのは、あくまで今回のように最終エージェントの回答のみを採用したいケースのみといった点にはご留意ください。
具体的な実装を確認していきます。
今回直面した課題
まず、最初に実装したのが下記となります。なお、協調プロセスをそのままユーザーに見せたいアプリケーションであれば、この形でまったく問題ありません。
今回想定しているのはあくまで最終レポートだけを見せたい場合に困る、というケースです。
final_text = ""
async for event in swarm.stream_async(user_input):
if event.get("type") == "multiagent_node_stream":
inner = event.get("event", {})
if isinstance(inner, dict) and "data" in inner:
text = inner["data"] if isinstance(inner["data"], str) else ""
if text:
final_text += text
multiagent_node_stream のテキストを final_text にひたすら連結しています。なお、stream_async() によるストリーミングの基本はこちらの公式ドキュメントにまとまっています。
一見問題なさそうですが、これで得られる回答はこうなります。
ユーザーからの問題報告を受け取りました。以下のとおり構造化いたします。
1. 問題の要約
...
それでは、詳細な調査のために researcher にハンドオフいたします。
調査を開始します。まずは並行して情報収集を行います。
...
(ようやくここから本題のレポート)
受付エージェントの構造化も、ハンドオフの口上も、調査の実況も全部入りです。臨場感がありますね。
Swarm 内部の協調プロセスとしては正しい動きなのですが、リアルタイムで様子を見せたいなどなければ、ユーザーへの回答としては最後のレポートだけで十分なケースもありますよね。
解決方法
multiagent_node_stream イベントはトップレベルに node_id(どのエージェントの発話か)をキーとして保持しているので、これを使用してノード別の dict へテキストを蓄積します。あわせて、テキストを蓄積するたびに「最後に発話したノード」を last_text_node として控えておくと、後述のフォールバックに使えます。
node_texts: dict[str, str] = {}
last_text_node = ""
async for event in swarm.stream_async(user_input):
if event.get("type") == "multiagent_node_stream":
node_id = event.get("node_id", "")
inner = event.get("event", {})
if isinstance(inner, dict) and "data" in inner:
text = inner["data"] if isinstance(inner["data"], str) else ""
if text:
node_texts[node_id] = node_texts.get(node_id, "") + text
last_text_node = node_id
# 最終レポート担当(synthesizer)の出力だけを採用
final_text = node_texts.get("synthesizer", "").strip()
# synthesizer まで到達せずに終了した場合のフォールバック
if not final_text and last_text_node:
final_text = node_texts.get(last_text_node, "").strip()
node_texts にエージェント名をキーとしてテキストを蓄積し、最後に synthesizer の分だけを取り出しています。なお、synthesizer をキーに取り出せるのは、今回のように最終レポート担当のエージェントが決まっている構成が前提です。
フォールバックも重要です。Swarm はタイムアウトや max_handoffs 超過で最終エージェントに到達する前に終了することがあり、その場合に synthesizer のキーを使用すると回答が空になってしまいます。最後に発話したノードの出力で代替すれば、少なくとも途中までの調査結果は返せます。
動作確認
「Web アプリの一覧画面が昨日から急に遅くなった。原因を調べてまとめて」という問い合わせを改善版で実行すると、回答は synthesizer のレポートだけになりました。
なお、Agent はデフォルトで発話を標準出力に表示する(PrintingCallbackHandler)ため、検証時は各 Agent に callback_handler=None を指定しています。これは CLI で確認するときだけの話で、回答が混ざる問題自体とは関係ありません。callback_handler の挙動について詳しくはこちらをご覧ください。
nodes: {'front': 575, 'researcher': 3375, 'synthesizer': 1886}
============================================================
最終回答(synthesizer のレポートのみ)
============================================================
# 調査結果まとめ
## 問題概要
- **症状**: Web アプリケーションの一覧画面表示に 10 秒以上のレスポンス遅延が発生
- **発生時期**: 昨日から急にレスポンス遅延が開始
...
ログを見ると front も researcher もしっかり会話しているのに、回答には synthesizer の分だけが抽出されています。受付の構造化やハンドオフの口上が消えて、ユーザーに見せたいレポートだけが取り出せていますね!
ちなみに途中経過を見せたい場合は、node_texts やハンドオフの履歴を別フィールドで返却すると、UI 側でプロセスの表示に使えそうですね。
今回検証に使ったコードの全文はこちらに折りたたんでおきます。
検証に使ったコード全文
Swarm の定義です。
"""Swarm の定義(front → researcher → synthesizer の 3 エージェント構成)"""
from strands import Agent
from strands.multiagent import Swarm
MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
def create_swarm() -> Swarm:
front_agent = Agent(
name="front",
model=MODEL_ID,
callback_handler=None, # デフォルトの標準出力への自動出力を止める
system_prompt=(
"あなたは受付エージェントです。"
"ユーザーからの問い合わせを「問題の要約」「関連情報」「調査してほしいこと」の形に構造化し、"
"その内容を出力したうえで researcher にハンドオフしてください。"
"自分では調査や回答を行わないでください。"
),
)
researcher = Agent(
name="researcher",
model=MODEL_ID,
callback_handler=None,
system_prompt=(
"あなたは調査エージェントです。"
"受け取った内容について、知っている知識をもとに原因候補や関連情報を洗い出してください。"
"調査の過程や考えたことも出力してかまいません。"
"調査が終わったら synthesizer にハンドオフしてください。"
),
)
synthesizer = Agent(
name="synthesizer",
model=MODEL_ID,
callback_handler=None,
system_prompt=(
"あなたはレポート作成エージェントです。"
"これまでの調査内容を、ユーザーに見せる最終レポートとしてまとめてください。"
"「調査結果まとめ」という見出しで始め、簡潔で読みやすい日本語のレポートだけを出力してください。"
"他のエージェントへのハンドオフは不要です。"
),
)
return Swarm(
[front_agent, researcher, synthesizer],
entry_point=front_agent,
max_handoffs=10,
max_iterations=15,
)
USER_INPUT = (
"Web アプリのレスポンスが昨日から急に遅くなりました。"
"特に一覧画面の表示に 10 秒以上かかります。データベースは RDS (PostgreSQL) です。"
"考えられる原因を調べてまとめてください。"
)
全エージェントの発話をそのまま連結する例です。
"""全エージェントの発話をそのまま連結する例
協調プロセス(受付の構造化・ハンドオフの口上・調査の実況)を
すべて見せたい場合はこの形で OK。
最終レポートだけを見せたい場合は final_only.py の形にする。
"""
import asyncio
from swarm_def import USER_INPUT, create_swarm
async def main() -> None:
swarm = create_swarm()
final_text = ""
async for event in swarm.stream_async(USER_INPUT):
if event.get("type") == "multiagent_node_stream":
inner = event.get("event", {})
if isinstance(inner, dict) and "data" in inner:
text = inner["data"] if isinstance(inner["data"], str) else ""
if text:
final_text += text
print("=" * 60)
print("回答(全エージェントの発話をそのまま連結)")
print("=" * 60)
print(final_text)
if __name__ == "__main__":
asyncio.run(main())
最終エージェントの出力だけを取り出す改善版です。
"""改善版: テキストをノード別に蓄積し、最終エージェント(synthesizer)の分だけを採用する
multiagent_node_stream イベントはトップレベルに node_id を持つため、
それをキーにノード別の dict へ蓄積する。
synthesizer に到達せず終了した場合は、最後に発話したノードの出力で代替する。
"""
import asyncio
import logging
from swarm_def import USER_INPUT, create_swarm
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
FINAL_NODE = "synthesizer"
async def main() -> None:
swarm = create_swarm()
node_texts: dict[str, str] = {}
last_text_node = ""
async for event in swarm.stream_async(USER_INPUT):
if event.get("type") == "multiagent_node_stream":
node_id = event.get("node_id", "")
inner = event.get("event", {})
if isinstance(inner, dict) and "data" in inner:
text = inner["data"] if isinstance(inner["data"], str) else ""
if text:
node_texts[node_id] = node_texts.get(node_id, "") + text
last_text_node = node_id
# 最終レポート担当(synthesizer)の出力だけを採用
final_text = node_texts.get(FINAL_NODE, "").strip()
# synthesizer まで到達せずに終了した場合のフォールバック
if not final_text and last_text_node:
final_text = node_texts.get(last_text_node, "").strip()
# デバッグ用: どのエージェントが何文字話したか
logger.info(f"nodes: { {k: len(v) for k, v in node_texts.items()} }")
print("=" * 60)
print("最終回答(synthesizer のレポートのみ)")
print("=" * 60)
print(final_text)
if __name__ == "__main__":
asyncio.run(main())
実行は以下のとおりです(Bedrock の Haiku 4.5 を使用)。
uv init
uv add "strands-agents==1.47.0"
AWS_REGION=us-west-2 uv run stream_all.py
AWS_REGION=us-west-2 uv run final_only.py
おわりに
Swarm の協調プロセスを全部表示するか、最終回答だけを見せるかはアプリケーション次第ですが、イベントをノード別に仕分けておけばどちらでも対応できたので今回やってみました。
本記事が少しでも参考になりましたら幸いです。
最後までご覧いただきありがとうございました!





