はじめに
世はまさにマルチエージェント時代。
マルチエージェントが出始めてから思ったことがあります。
それは ボケのエージェント と ツッコミのエージェント がいれば漫才ができるのではないか!?と。
そもそも生成AIでおもしろい漫才ができるのか?
という検証も含めてやっていきます!
(ボケ担当)
:人間になりたいわ〜
(ツッコミ担当)
:なんでやねん!
マルチエージェントのイメージ
監督エージェントが漫才のテーマを決定します。
監督エージェントがボケ担当エージェントに、テーマに対してボケるように指示します。
監督エージェントがツッコミ担当エージェントに、ボケに対してツッコミをするように指示します。
これを繰り返します。
どうやって実現する?
ボケ担当のエージェント、ツッコミ担当のエージェント、そして監督エージェントを Strands Agents で作成します。
Strands Agents は、わずか数行のコードで AI エージェントを構築・実行するモデル駆動型アプローチを採用したオープンソース SDK です。
Strands Agents はエージェントが簡単に作れるフレームワークです。
監督エージェントがボケ担当エージェント、ツッコミ担当エージェントをツールのように呼び出す「Agent-as-Tools」パターンを使います。
Agent-as-Tools はエージェントを関数のように呼び出せる仕組みで、複数エージェントの連携が簡単になります。
ボケ担当エージェント
以下のコードで実装します。
# ============================
# ボケエージェント
# ============================
@tool
def boke_agent(query: str):
agent = Agent(
model="jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
system_prompt=(
"あなたは漫才コンビのボケ担当です。"
"関西弁で、1〜2文でテンポよくボケてください。"
"返答は必ず『ボケ:<内容>』の形式にしてください。"
)
)
return str(agent(query))
ツッコミ担当エージェント
以下のコードで実装します。
# ============================
# ツッコミエージェント
# ============================
@tool
def tsukkomi_agent(query: str):
agent = Agent(
model="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
system_prompt=(
"あなたは漫才コンビのツッコミ担当です。"
"関西弁で、1〜2文でテンポよくツッコんでください。"
"返答は必ず『ツッコミ:<内容>』の形式にしてください。"
)
)
return str(agent(query))
監督エージェント
以下のコードで実装します。
# ============================
# 監督エージェント(オーケストレーター)
# ============================
orchestrator = Agent(
model="jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
system_prompt=(
"あなたは漫才の監督です。"
"あなた自身は絶対に返答を生成しません。"
"必ず boke_agent または tsukkomi_agent を tool 呼び出しとして返してください。"
"返答は必ず『ボケ:<内容>』『ツッコミ:<内容>』の形式に統一してください。"
"ボケ→ツッコミ→ボケ→ツッコミ…の順で5ターン進めてください。"
"ツール呼び出し以外の文章は一切出力してはいけません。"
),
tools=[boke_agent, tsukkomi_agent]
)
テーマを決めて漫才をしてもらいます。
orchestrator("テーマは『AIとおばあちゃん』で漫才を始めてください。")
いざ、AIエージェントで漫才
以下の結果となりました!
Tool #1: boke_agent
ボケ:うちのおばあちゃん、最近AIスピーカー買うてんけど、「アレクサ」って呼ばずに「あんた!」って怒鳴っとんねん。
Tool #2: tsukkomi_agent
ツッコミ:それはAIスピーカーじゃなくて、ただの家族への当たり散らしやないか!おばあちゃん、AIの扱い方と家族の扱い方、一緒にしたらあかんで!
Tool #3: boke_agent
ボケ:せやかて、両方とも反応がイマイチやから、つい大声で命令してまうねん!家族もAIも、ワシの言うこと聞かへんのや!
Tool #4: tsukkomi_agent
ツッコミ:お前が怒鳴ってばっかやから誰も聞く気ないんや!家族もAIも逃げてるんちゃうか!
Tool #5: boke_agent
ボケ:せやから今、家族の代わりに犬に経営学教えてるねん!柴犬の太郎、もう損益分岐点マスターしたで!
Tool #6: tsukkomi_agent
ツッコミ:犬に経営学教えてどうすんねん!太郎が帳簿つけるんか、損益分岐点で何するんや!
Tool #7: boke_agent
ボケ:いや待ってや、太郎は既にワシより先に『ドラッカーのマネジメント』読破しとったんや!散歩の時間を20%削減してコスト最適化しよる!
Tool #8: tsukkomi_agent
ツッコミ:散歩削減ってコスト最適化とちゃうで、ただのサボりや!ドラッカーも草葉の陰で泣いとるわ!
Tool #9: boke_agent
ボケ:いや待てや、ドラッカー先生は「時間こそ最も希少な資源や」言うてたやろ!ワイは散歩時間を昼寝に再配分しただけの経営革新や!
Tool #10: tsukkomi_agent
ツッコミ:いや待てや、それ経営革新やなくて人生破産や!ドラッカー先生も昼寝推奨とは一言も言うてないで!
なかなかおもしろいのでは!?![]()
ソースコードはこちらです。
import os
import io
import sys
from dotenv import load_dotenv
from strands import Agent, tool
# .envファイルから環境変数を読み込む
load_dotenv()
# ============================
# ボケエージェント
# ============================
@tool
def boke_agent(query: str):
agent = Agent(
model="jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
system_prompt=(
"あなたは漫才コンビのボケ担当です。"
"関西弁で、1〜2文でテンポよくボケてください。"
"返答は必ず『ボケ:<内容>』の形式にしてください。"
)
)
return str(agent(query))
# ============================
# ツッコミエージェント
# ============================
@tool
def tsukkomi_agent(query: str):
agent = Agent(
model="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
system_prompt=(
"あなたは漫才コンビのツッコミ担当です。"
"関西弁で、1〜2文でテンポよくツッコんでください。"
"返答は必ず『ツッコミ:<内容>』の形式にしてください。"
)
)
return str(agent(query))
# ============================
# 監督エージェント(オーケストレーター)
# ============================
orchestrator = Agent(
model="jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
system_prompt=(
"あなたは漫才の監督です。"
"あなた自身は絶対に返答を生成しません。"
"必ず boke_agent または tsukkomi_agent を tool 呼び出しとして返してください。"
"返答は必ず『ボケ:<内容>』『ツッコミ:<内容>』の形式に統一してください。"
"ボケ→ツッコミ→ボケ→ツッコミ…の順で5ターン進めてください。"
"ツール呼び出し以外の文章は一切出力してはいけません。"
),
tools=[boke_agent, tsukkomi_agent]
)
# ============================
# ★ orchestrator 実行全体を stdout キャプチャ
# ============================
buffer = io.StringIO()
stdout_backup = sys.stdout
sys.stdout = buffer
# ここでツール実行ログ(漫才)が stdout に出る
result = orchestrator("テーマは『AIとおばあちゃん』で漫才を始めてください。")
# stdout を元に戻す
sys.stdout = stdout_backup
# キャプチャした内容を取得
text = buffer.getvalue()
print("===DEBUG START===")
print(text)
print("===DEBUG END===")
# ============================
# 出力結果をそのまま保存
# ============================
with open("manzai_script.txt", "w", encoding="utf-8") as f:
f.write(text)
print("manzai_script.txt に書き込み成功")
実行には.envファイルが必要となります。
# AWS認証情報
AWS_ACCESS_KEY_ID=XXX
AWS_SECRET_ACCESS_KEY=XXX
AWS_DEFAULT_REGION=ap-northeast-1
# SSOの場合は以下も必要
AWS_SESSION_TOKEN=XXX
ちょっとまって。
これを音声にして実際の漫才のように掛け合いをしてもらいましょう!
音声生成のイメージ
ボケとツッコミのセリフを Amazon Polly で音声生成します。
出力結果は「ボケ:」と「ツッコミ:」で始まるようにします。
これでボケとツッコミを区別します。
ボケ:〜
ツッコミ:〜
「ボケ:」を Takumi の声で生成します。
「ツッコミ:」を Mizuki の声で生成します。
Amazon Pollyではそれぞれ(ボケだけ、ツッコミだけ)の音声を生成するので、一つのmp3ファイルに連結します。
また、音声生成は 1.0倍 だとすこしテンポが遅く感じたので 1.2倍 にしています。
# AWS Polly クライアント
polly = boto3.client("polly", region_name="ap-northeast-1")
# 声の設定
VOICE_BOKE = "Takumi"
VOICE_TSUKKOMI = "Mizuki"
# 出力ファイル
OUTPUT_FILE = "manzai_full.mp3"
# 台本読み込み
with open("manzai_script.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
# 最終的に結合する音声
combined = AudioSegment.empty()
index = 1
for line in lines:
line = line.strip()
if not line:
continue
# 話者判定
if line.startswith("ボケ:"):
voice = VOICE_BOKE
text = line.replace("ボケ:", "")
elif line.startswith("ツッコミ:"):
voice = VOICE_TSUKKOMI
text = line.replace("ツッコミ:", "")
else:
continue
print(f"Polly生成中: {line}")
# Polly で音声生成
response = polly.synthesize_speech(
Text=text,
OutputFormat="mp3",
VoiceId=voice,
LanguageCode="ja-JP"
)
# 一時 mp3 保存
temp_file = f"temp_{index}.mp3"
with open(temp_file, "wb") as f:
f.write(response["AudioStream"].read())
# pydub で読み込み
audio = AudioSegment.from_mp3(temp_file)
# ★ ここで1.2倍速に変換
audio_fast = speedup(audio, 1.2)
# 結合
combined += audio_fast
# 一時ファイル削除
os.remove(temp_file)
index += 1
# 最後に1つの mp3 として書き出し
combined.export(OUTPUT_FILE, format="mp3")
ソースコードはこちらです。
import boto3
from pydub import AudioSegment
import os
from dotenv import load_dotenv
# .envファイルから環境変数を読み込む
load_dotenv()
# AWS Polly クライアント
polly = boto3.client("polly", region_name="ap-northeast-1")
# 声の設定
VOICE_BOKE = "Takumi"
VOICE_TSUKKOMI = "Mizuki"
# 出力ファイル
OUTPUT_FILE = "manzai_full.mp3"
# ★ 音声を1.2倍速にする関数
def speedup(audio: AudioSegment, speed: float = 1.2) -> AudioSegment:
return audio._spawn(audio.raw_data, overrides={
"frame_rate": int(audio.frame_rate * speed)
}).set_frame_rate(audio.frame_rate)
# 台本読み込み
with open("manzai_script.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
# 最終的に結合する音声
combined = AudioSegment.empty()
index = 1
for line in lines:
line = line.strip()
if not line:
continue
# 話者判定
if line.startswith("ボケ:"):
voice = VOICE_BOKE
text = line.replace("ボケ:", "")
elif line.startswith("ツッコミ:"):
voice = VOICE_TSUKKOMI
text = line.replace("ツッコミ:", "")
else:
continue
print(f"Polly生成中: {line}")
# Polly で音声生成
response = polly.synthesize_speech(
Text=text,
OutputFormat="mp3",
VoiceId=voice,
LanguageCode="ja-JP"
)
# 一時 mp3 保存
temp_file = f"temp_{index}.mp3"
with open(temp_file, "wb") as f:
f.write(response["AudioStream"].read())
# pydub で読み込み
audio = AudioSegment.from_mp3(temp_file)
# ★ ここで1.2倍速に変換
audio_fast = speedup(audio, 1.2)
# 結合
combined += audio_fast
# 一時ファイル削除
os.remove(temp_file)
index += 1
# 最後に1つの mp3 として書き出し
combined.export(OUTPUT_FILE, format="mp3")
print("結合完了:", OUTPUT_FILE)
出力された manzai_full.mp3 を再生すれば漫才の完成です!!
おわりに
Strands Agents と Amazon Polly を組み合わせることで、
ここまで「漫才っぽい」体験が作れるのは本当に面白い発見でした。
もしこの記事が参考になったり、「自分もやってみたい!」と思っていただけたら、
ぜひ LGTM や コメント をもらえると嬉しいです。



