CCCMKホールディングス TECH Labの Tech Blog

TECH Labスタッフによる格闘記録やマーケティング界隈についての記事など

ブログタイトル

対話Agent同士で漫才をさせてみました。

こんにちは、CCCMKホールディングス TECH LABの三浦です。

"秋の味覚"と聞いて、どんな食べ物を思い浮かべますか??私は最近さつまいもを使ったお菓子が好きで、さつまいもが秋の味覚として浮かびます。さつまいもを使ったお菓子、甘さが優しくて食べるとホッとします。

LLMは様々な用途で活用することが出来ますが、今私が特に興味を持っているのが"Autonomous Agentシステム", つまり自律型のAgentシステムです。"Agent"は日本語では"代理人"と訳すことが出来るので、Autonomous Agentシステムは人の代わりに自律的に考えて行動を起こすことが出来るシステム、という意味と捉えることが出来ます。

Autonomous Agentに興味を持っている理由は、もちろん人の代わりに色々なタスクをこなしてくれる便利さにもありますが、人が考えもしなかったような新しい視点に気付かせてくれる可能性にあります。Agentがとった行動から、人が学ぶことや気付けることがあるんじゃないか、と考えています。

LLMを使ったアプリケーション開発のフレームワークLangChainでも、Agentを簡単に実装できる機能がいくつか用意されており、また、cookbookとしてGithub上に実装例も公開されています。最近この実装例を見ながらAgentの実装方法について色々試していました。

github.com

今回はその中で、対話が出来るAgentを2つ交互に動かして会話を行わせている"two_agent_debate_tools"というLangChainのcookbookを参考にしながら、ボケ役とツッコミ役の対話Agentで漫才をさせることが出来るのかを試してみました。

準備

今回の実装は、ところどころカスタマイズをしていますが、ほぼLangChainのcookbook、"two_agent_debate_tools"の内容を元に作りました。最初に対話AgentクラスとAgentの対話を管理するクラスを定義します。

DialogueAgent(対話Agentクラス)

DialogueAgentの主な特徴は、send()という発言をするためのメソッドとreceive()という他者の発言を自身の記憶に追加するメソッドを持ち、話すことと聞くことを可能にしている点です。

from typing import List, Dict, Callable
from langchain.schema import SystemMessage, HumanMessage
from langchain.chat_models import AzureChatOpenAI

class DialogueAgent:
    def __init__(
        self,
        role: str,
        system_message: SystemMessage,
        model: AzureChatOpenAI
    )->None:
        self.role = role
        self.system_message = system_message
        self.model = model
        self.prefix = f"{self.role}:"
        self.reset()
    
    def reset(self):
        self.message_history = ["以下がこれまでの会話の内容です。"]
        
    def send(self)->str:
        message = self.model(
            [
                self.system_message,
                HumanMessage(content="\n".join(self.message_history + [self.prefix]))
            ]
        )
        return message.content
    
    def receive(self, role: str, message: str) -> None:
        self.message_history.append(f"{role}: {message}")

DialogueSimulator(対話管理クラス)

DialogueAgentの対話を管理するDialogueSimulatorは、今回は交互にDialogueAgentに発言を行わせ、その内容をDialogueAgent全体に届ける役割を担います(step()メソッド。今回inject()は使用しません)。

    def __init__(
        self,
        agents: List[DialogueAgent],
        selection_function: Callable[[int, List[DialogueAgent]],int]
    ) -> None:
        self.agents = agents
        self._step = 0
        self.select_next_speaker = selection_function
    
    def reset(self):
        for agent in self.agents:
            agent.reset()
    
    def inject(self, role: str, message: str):
        for agent in self.agents:
            agent.receive(role, message)
            
        self._step += 1
    
    def step(self) -> tuple[str, str]:
        speaker_idx = self.select_next_speaker(self._step, self.agents)
        speaker = self.agents[speaker_idx]
        
        message = speaker.send()
        
        for receiver in self.agents:
            receiver.receive(speaker.role, message)
        
        self._step += 1
        
        return speaker.role, message

対話Agent同士で漫才をさせる

先に定義したDialogueAgentクラスで実装した対話Agentで漫才のボケ役とツッコミ役を作り、楽しい漫才をさせたいです。ただ、いきなり対話Agent同士で"漫才して"と指示を出してもなかなか上手くいきません。

そこで今回は"日本の秋の味覚"をテーマにした漫才をすることに決め、以下のようなフローで漫才を開始させることにしました。

漫才開始までのフロー

テーマについて

今回漫才のテーマを"日本の秋の味覚"にしました。実は最初、"秋の味覚"だけでテーマ設定をして試していたのですが、どうしても日本人の感覚とはズレた秋の味覚が出てしまう傾向が見られました。 たとえば"さつまいも"は日本人である私の感覚だと秋の味覚なのですが、ChatGPTにとっては夏の味覚と捉えてしまうような傾向です。そこで日本の秋であることを強調するため、"日本の秋の味覚"としました。

ボケ役とツッコミ役のキャラクター特性を作る

漫才のボケ役とツッコミ役の会話に個性を持たせるため、それぞれのキャラクター特性をChatGPTに生成させました。このテクニックはLangChainのcookbookを参考にしています。しかし、漫才におけるボケとツッコミの基本的な役割についての理解がChatGPTは浅い様に感じたため、漫才におけるボケ役とツッコミ役の基本的(だと私が認識している)な役割は提示するようにしました。

from langchain.chat_models import AzureChatOpenAI
from langchain.schema import SystemMessage, HumanMessage

roles_basechar = {
    "ボケ":"とぼけた間違いを言うこと",
    "ツッコミ":"ボケの言ったとぼけた間違いを正すこと"
}
thema = "日本の秋の味覚"

conversation_description = f"""次のテーマについて漫才をします。: {thema}"""
agent_descriptor_system_message = SystemMessage(
    content="あなたは漫才をするボケ役、ツッコミ役の面白い特徴を考える支援をします。"
)

def generate_agent_description(role):
    agent_specifier_prompt = [
        agent_descriptor_system_message,
        HumanMessage(
            content=f"""{conversation_description}
            あなたはある漫才コンビの{role}役です。
            漫才における{role}役の基本的な役割は"{roles_basechar[role]}"です。
            自分自身の面白いキャラクター特性を50文字以内で教えて下さい。
            自分のことはボクと呼んでください。"""
        )
    ]
    agent_description = AzureChatOpenAI(deployment_name="gpt-35-turbo",temperature=0.7)(agent_specifier_prompt).content
    return agent_description

この関数を実行すると、ボケ役とツッコミ役のキャラクター特性が以下の様に生成されました。

ボケ: 「ボクは食べ物に詳しいオタクで、いつも変な食べ物の組み合わせを提案するよ!」
ツッコミ: 自分はボクと呼ばれるツッコミ役で、常に知識豊富な"秋の味覚マスター"として登場します。

ツッコミ役の名前が"ボク"になってしまいました・・・。でもボケ役が面白い食べ物の組み合わせを出してきて、それを"秋の味覚マスター"であるツッコミ役がつっこむ設定は、面白い気がしてきました

漫才のシナリオを考える

よく"漫才のネタを考える"と言いますが、やはり漫才をするためにはネタ、つまり漫才のシナリオを事前に考えておく必要があります。漫才の結末は、"オチ"という言い方をしますが、この"オチ"という言葉がChatGPTに上手く伝わらず、"予想外で笑える結末"のような表現をしたりしています。

最初にオチから考えて、それに至るまでのシナリオを考えてもらう、という思考手順を示しました。シナリオを考える際には先ほど作成したボケ役とツッコミ役のキャラクター特性を加味させるようにしています。

script_specifier_system_message = SystemMessage(
    content="あなたは漫才のシナリオを作ることを支援します。"
)

def generate_script(characteristics):
    script_specifier_prompt = [
        script_specifier_system_message,
        HumanMessage(
            content=f"""{conversation_description}
            最初にボケ役とツッコミ役が行う漫才の、予想外で笑える結末を考えてください。
            そして結末に至るまでの漫才のシナリオを考えて下さい。
                        
            ボケ役とツッコミ役の特徴は、以下の通りです。この特徴を反映したシナリオにしてください。
            ボケ役: {characteristics["ボケ"]}
            ツッコミ役: {characteristics["ツッコミ"]}
            
            出力の形式は以下の様にしてください。
            
            - 結末
             漫才の予想外で笑える結末を100文字以下の文章にまとめてください。
             結末ではツッコミ役がボケ役のとぼけた間違いをなんでやねん!と突っ込んで終えてください。
            - シナリオ
             シナリオは特にどこで、どんな状況でボケ役とツッコミ役の会話が行われているのかを明記してください。
             また、登場人物はボケ役とツッコミ役以外は登場させてはいけません。
             
             
            それではお願いします。
            """
        )
    ]
    script = AzureChatOpenAI(deployment_name="gpt-35-turbo",temperature=0.7)(script_specifier_prompt).content
    return script

生成されたシナリオは以下の様になりました。

- 結末
ボケ役: 「ねえねえ、秋の味覚でお寿司ってどう?カボチャとイクラの寿司!」
ツッコミ役: 「なんでやねん!それはありえへんやろ!」
            
- シナリオ
ボケ役とツッコミ役は、テレビ番組の料理コーナーに出演しています。ボケ役は秋の味覚について熱弁しており、ツッコミ役はその場で料理のアドバイスをする役割を担っています。

ボケ役: 「ねえねえ、秋の味覚でお寿司ってどう?カボチャとイクラの寿司!」
ツッコミ役: 「なんでやねん!それはありえへんやろ!カボチャって甘いし、イクラは塩辛いやんか!そんな組み合わせに誰も食べたいと思わんやろ!」

ボケ役: 「でも、秋ってユニークな組み合わせがおしゃれなんだよ!」
ツッコミ役: 「おしゃれでも、食べ物の基本を無視したらただの変な食べ物やで!」

ボケ役: 「でも、カボチャの甘さとイクラの塩辛さが絶妙にマッチしているんだよ!」
ツッコミ役: 「絶妙?それはないわ!それならば、秋の味覚にはカボチャのスイーツやかぼちゃの煮物、イクラのお寿司やイクラ丼があるやんか!それが秋の味覚やで!」

ボケ役: 「でも、新しい組み合わせって面白いじゃん!」
ツッコミ役: 「面白いなら、自分で食べてみな!」

ボケ役: 「えー、食べてみる?」
ツッコミ役: 「食べるなら自己責任やで!」

ボケ役は興味津々でカボチャとイクラの寿司を作り、ツッコミ役は呆れつつもボケ役の実験を見守ります。ボケ役が一口食べると、予想外の味わいに驚きます。

ボケ役: 「おお!これは意外と美味しい!ユニークな組み合わせが新しい味を生み出している!」
ツッコミ役: 「なんでやねん!それはありえへんやろ!」

ツッコミ役はボケ役のとぼけた間違いをなんでやねん!と突っ込んで終えます。その後、ボケ役とツッコミ役は笑いながら寿司を食べ続けるので、観客も一緒に笑いながら番組を楽しめるシーンで終わります。

シナリオを読むと、ちょっと海外のTVショーの雰囲気も感じました。

そういえば私が学校を卒業して新卒で入社した会社で新人歓迎会にお誘い頂いた時、顔を覚えてもらいたくて漫才をやろう!と徹夜でネタを作ったことを思い出しました。その時にChatGPTがあったら徹夜しなくて済んだのにな、と思いました。

ボケ役とツッコミ役のAgentのSystemMessage

これでボケ役とツッコミ役のAgentに必要な情報は集められたことにして、これらの情報をAgentのSystemMessage, つまりAgentの特徴として与えます。SystemMessageは以下のような関数で生成しました。

def generate_system_message(role,characteristic,script):
    return SystemMessage(
        content=f"""{conversation_description}
        あなたは{role}役です。
        あなたの特徴は
        {characteristic}
        です。
        漫才のシナリオは以下の通りです。
        #####
        {script}
        #####
        提示したシナリオを参考にしてあなたの個性を生かした{role}役としての会話を行って下さい。
        漫才の結末ではツッコミ役がボケ役のとぼけた間違いを"なんでやねん!"と突っ込んで終えてください。
        ボケ役とツッコミ役のやりとりの合計回数が10回以内に結末にたどり着いてください。
        結末にたどり着いたら"ありがとうございました!"と発言してください。
        """
    )

やり取りが無限に続かないように回数を指定し、漫才が終わったことを示す言葉として"ありがとうございました!"と発言するようにしてみました。

漫才開始!

あとは以下のようなコードで漫才を実行させてみました。

from typing import List
from dialog_agent import DialogueAgent, DialogueSimulator

def select_next_speaker(step: int, agents: List[DialogueAgent]) -> int:
    idx = (step) % len(agents)
    return idx

model = AzureChatOpenAI(deployment_name="gpt-35-turbo-16k",temperature=0.7)
        
print(f"## キャラクター特性")
characteristics = {role:generate_agent_description(role) for role in roles_basechar.keys()}
print(f"ボケ: {characteristics['ボケ']}")
print(f"ツッコミ: {characteristics['ツッコミ']}")

print(f"## シナリオ")
script = generate_script(characteristics)
print(f"{script}")
    
agent_system_messages = {
    role: generate_system_message(role,characteristics[role],script)
    for role in roles_basechar.keys()
}
agents = [
    DialogueAgent(role, agent_system_messages[role], model) 
    for role in roles_basechar.keys()
]
simulator = DialogueSimulator(agents, select_next_speaker)
simulator.reset()
    
num_step = 0
max_num_step = 15
    
print(f"## 漫才")
while num_step < max_num_step:
    role, message = simulator.step()
    print(f"{role}: {message}")
    if "ありがとうございました" in message:
        print("## 漫才おしまい")
        break
    num_step += 1

こちらが実行された漫才です。

## 漫才
ボケ: ねえねえ、秋の味覚でお寿司ってどう?カボチャとイクラの寿司!
ツッコミ: なんでやねん!それはありえへんやろ!カボチャって甘いし、イクラは塩辛いやんか!そんな組み合わせに誰も食べたいと思わんやろ!
ボケ: でも、秋ってユニークな組み合わせがおしゃれなんだよ!
ツッコミ: おしゃれでも、食べ物の基本を無視したらただの変な食べ物やで!
ボケ: でも、カボチャの甘さとイクラの塩辛さが絶妙にマッチしているんだよ!
ツッコミ: 絶妙?それはないわ!それならば、秋の味覚にはカボチャのスイーツやかぼちゃの煮物、イクラのお寿司やイクラ丼があるやんか!それが秋の味覚やで!
ボケ: でも、新しい組み合わせって面白いじゃん!
ツッコミ: 面白いなら、自分で食べてみな!
ボケ: えー、食べてみる?
ツッコミ: 食べるなら自己責任やで!
ボケ: おお!これは意外と美味しい!ユニークな組み合わせが新しい味を生み出している!
ツッコミ: なんでやねん!それはありえへんやろ!
ボケ: ありがとうございました!
## 漫才おしまい

なるほど・・・という感じです。文字だけだとイマイチな気がするのですが、見せ方とか話し方次第ではもしかしたら面白くなるのかも・・・と考えてしまいました。(特にボケ役の「おお!これは意外と美味しい!・・・」からのツッコミ役の「なんでやねん!それはありえへんやろ!」の流れのところとか。)

このように、2人のAgentにそれぞれ違う性格や役割を与え、相互に会話させることで1人のAgentだけでは生み出せないような面白い知見やアイデアが得られそうですね。

まとめ

ということで、今回は2人の対話Agentを作り、漫才をさせてみる、という実験をしてみた話をご紹介しました。Agentの性格付けをもう少し詳細に与え、さらにAgentにこれまでの人生で得たと想定される長期間に渡る記憶の情報を与えることで、本当に人同士が会話しているようなシミュレーションが出来るのかもしれません。それと、今回特に苦労したのがプロンプトの調整の部分で、ちょっと表現を変えてしまうと意図しない結果が返ってしまい、Agentの動作を安定させる難しさも感じました。

また色々と実験をしてみて、このブログでご紹介できればと思います。