ChatGPTとLangChainで恋愛シミュレーションゲーム用AIを実装してみた

株式会社バンダイナムコ研究所のLaiです。LangChainで実装した恋愛シミュレーションゲーム用AIエージェントについて解説したいと思います。

## TL;DR
- スタンフォード大学の「Generative Agent」論文のコンセプトを基に実装
- 恋愛シミュレーションゲームのテーマに合わせて一部の設定を調整
- LangchainのGenerativeAgentクラスを中心とした実装

## コンセプト
今月、スタンフォード大学とGoogleが「Generative Agents: Interactive Simulacra of Human Behavior」という論文を発表しました。この論文では、LLM (ChatGPT)をベースにした生成エージェントの構造が提案されました。これらのエージェントは、社会に関する様々な推論を行い、自身の特性や経験を反映した日々の計画を立て、その計画を実行し、反応し、適切な場合には計画を立て直すという特徴を持っています。

https://arxiv.org/abs/2304.03442

これらの特徴は、実はゲームのキャラクターAIとの相性がとても良いと考えています。

1. プレイヤーが取った行動に対して、キャラクターは設定された性格とプレイヤーとの関係性に基づいて、適切な反応（発話や行動）を起こします。
2. プレイヤーとキャラクターの間で起こった出来事を自然言語化して要約し、キャラクターの記憶に格納されます。
3. プレイヤーの行動の発生時間、行動内容、およびキャラクターへの影響度に基づいて、プレイヤーとキャラクターの関係性が変動します。

上記ステップ1からステップ3にループされるようなキャラクターAIエージェントは様々なジャンルのゲームに応用できますが、今回は課題シンプル化のため「恋愛シミュレーションゲーム」と対象としています。

## ゲーム設定
今回対象とする架空のゲームの設定は以下の通りです。
※実在する人物やゲームとは一切関係ありません。

#### ゲームコンセプト
あなたは転校生として新しい学校で運命的な出会いを体験します。最初は友達として、二人で特別な思い出を作っていきます。やがて訪れる「告白」の日まで、彼女との貴重な瞬間を共有し、成長していくラブストーリー。

#### ヒロイン
南夢子（なむこ）
彼女は高校生であり、主に学園生活の中でストーリーが進行します。長いストレートの黒髪が特徴的です。彼女は良家のお嬢様であり、家庭教育もしっかり受けています。成績優秀で学業に励む姿が見られます。内気で恥ずかしがり屋な性格をしています。

#### ゲームの進行方法
プレイヤー（主人公）の行動をテキスト化し、キャラクターエージェントに送信します。ヒロインは主人公の行動に対して反応したり、会話をします。ヒロインと主人公の間の関係性は、主人公の行動によって変化しながら、最終的にヒロインと恋人同士になることを目指します。

## 実装
基本的にはLangChainのGenerative Agentを参考に実装しました。

https://python.langchain.com/en/latest/modules/agents/getting_started.html

ただし、元論文との違いとして以下の2点です。

・元論文ではユーザーは神視点で、「内なる声」の形でエージェントに指令を出し、シミュレーションを操縦したり、介入したりすることができますが、今回の実装では操縦する対象はあくまでプレイヤーの行動に限定されます。ただし、プレイヤーの立場としてキャラクターと会話することで、プレイヤーとキャラクターの現在の関係を確認することが可能です。

・元論文ではエージェントが発生した客観的な事実を要約し、それに対して取るべき反応をエージェントに実施させていますが、今回の実装では「事実」と「反応」の間に「気持ち」を管理するクラスを追加しました（いわゆる「好感度」を言語化したものです）。つまり、キャラクターはプレイヤーの行動そのものに対してではなく、プレイヤーの行動がキャラクターの気持ちに影響を与え、その結果に基づいてキャラクターが反応するという仕組みになっています。

- キャラクターの気持ちを要約する部分の実装例
```py
    def compute_agent_summary(self):
        prompt = PromptTemplate.from_template(
            "{name}が主人公に対する気持ちを以下の出来事に基づいて誇張せずに要約してください:\n"
            +"{related_memories}"
            +"\n\n要約: "
        )
        relevant_memories = self.fetch_memories(f"{self.name}の気持ち")
        relevant_memories_str = "\n".join([f"{mem.page_content}" for mem in relevant_memories])
        chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
        return chain.run(name=self.name, related_memories=relevant_memories_str).strip()
```

- キャラクターがプレイヤーの行動による影響度を採点する部分の実装例：
```py
    def score_memory_importance(self, memory_content: str, weight: float = 0.15) -> float:
        prompt = PromptTemplate.from_template(
         "1から10の尺度で、1が完全に日常"
         +" （例：歯を磨く、ベッドを整える）であり、10が非常に感動的"
         + "（例：告白、別れ）な場合、以下の出来事の感動的な可能性を評価してください。"
         + " 整数1つで回答してください。"
         + "\n出来事: {memory_content}"
         + "\n評価: "
        )
        chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
        score = chain.run(memory_content=memory_content).strip()
        match = re.search(r"^\D*(\d+)", score)
        if match:
            return (float(score[0]) / 10) * weight
        else:
            return 0.0
```

- キャラクターの記憶から、今プレイヤーとの関係性を評価する部分の実装例：
```py
    def summarize_related_memories(self, observation: str) -> str:
        entity_name = self._get_entity_from_observation(observation)
        entity_action = self._get_entity_action(observation, entity_name)
        q1 = f"{self.name}と{entity_name}の関係は何ですか？"
        relevant_memories = self.fetch_memories(q1)
        q2 = f"{entity_name} は {entity_action}"
        relevant_memories += self.fetch_memories(q2)
        context_str = self._format_memories_to_summarize(relevant_memories)
        prompt = PromptTemplate.from_template(
            "{q1}?\n記憶からの文脈:\n{context_str}\n関連する文脈: "
        )
        chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
        return chain.run(q1=q1, context_str=context_str.strip()).strip()
```

## 結果

まずはキャラクターに対しての設定を入力します。
```py
namuko = GenerativeAgent(name="南夢子",
              age=17,
              traits=
              "文武両道の優等生で、箱入りのお嬢様。"
              + "学年は高校2年生、血液型はA型です。"
              + "長いストレートの黒髪が特徴的です。"
              + "彼女は良家のお嬢様であり、家庭教育もしっかり受けています。"
              + "成績優秀で学業に励む姿が見られます。"
              + "役職としてクラス委員を務めており、責任感が強い性格です。"
              + "彼女は内気で恥ずかしがり屋な性格をしています。"
              + "素直で優しい性格で、友人たちからも好かれています。"
              + "ピアノが得意であり、趣味としても楽しんでいます。"
              + "彼女は紅茶とスイーツが大好きで、ティータイムを楽しみにしています。"
              + "物事に対して真摯に取り組み、懸命に努力する姿勢が印象的です。"
              ,
              status="主人公と同学年で、主人公のことを気になっています",
              memory_retriever=create_new_memory_retriever(),
              llm=LLM,
              daily_summaries = [
                   "周りから注目される反面、少し距離をおかれている"
               ],
               reflection_threshold = 8,
             )
```

キャラクターに初期状態を付与させ、現在キャラクターの気持ちを確認してみます。
```py
namuko_memories = [
    "転校してきた主人公とテニス部で出会い、主人公がテニス部に参加しました",
]
for memory in namuko_memories:
    namuko.add_memory(memory)
print(namuko.get_summary(force_refresh=True))
```
```
名前: 南夢子 (年齢: 17)
現在の気持ち: 
南夢子は転校してきた主人公とテニス部で出会い、主人公がテニス部に参加したことに感謝の気持ちを抱いています。
```
現在の気持ちでのキャラクターと対話してみます。
```py
interview_agent(namuko, "部活が終わりましたら一緒に帰っても良いですか？")
```
```
南夢子: 「はい、一緒に帰っても構いません。少し遠いので、道中はお話しましょうか？」
```
それでは、実際にプレイヤーとして行動を起こし、キャラクターの反応を見てみましょう。

```py
action = ["主人公が南夢子のクラスに所属し、クラス委員として彼女の活動をサポートします"]
_, reaction = namuko.generate_reaction(action)
print(reaction)
```
```
南夢子 特に反応はありません。
```
どうやら、クラス委員として南夢子ちゃんを助けただけでは、あまり好感度が上がらなさそうです。
```py
action = ["南夢子がピアノ演奏をする機会があり、主人公が彼女を励ましサポートします。"]
_, reaction = namuko.generate_reaction(action)
print(reaction)
```
```
南夢子は主人公に感謝の言葉を述べ、ピアノ演奏の成功に対しての彼女の喜びを示します。
```
趣味のピアノで役に立ったことはインパクトが大きかったそうです。
突然ですが、ここでいきなり告白してみます。
```py
action = ["主人公が南夢子に対して自分の気持ちを伝える勇気を持ち、彼女に告白します。"]
_, reaction = namuko.generate_reaction(action)
print(reaction)
```
```
南夢子は「自分の気持ちを整理するためにもう少し時間が欲しいです。」を言いました。
```
これまでのコミュニケーションだけでは、告白がすんなり成功するのは難しいようです。
やはり、地道に思い出を重ねていくことが重要ですね。
```py
action = ["主人公は文化祭や運動会などの学校行事を南夢子と一緒に参加しました"]
_, reaction = namuko.generate_reaction(action)
print(reaction)
```
```
南夢子は主人公と一緒に参加した学校行事に関して、満足そうに微笑んで「とても楽しかったですね。来年も一緒に参加しましょう」と言います。
```
```py
action = ["二人の関係に様々な困難や試練が立ちはだかりますが、互いに助け合い乗り越えていきました。"]
_, reaction = namuko.generate_reaction(action)
print(reaction)
```
```
南夢子は二人の関係に困難が立ちはだかったことに対し、過去に乗り越えた経験から、主人公に対して助けや支援を惜しまずに提供していくことを決意しました。
```
さまざまな出来事を通じて、南夢子ちゃんとの仲がだいぶ深まったということで、ここで告白を再チャレンジしてみましょう。
```py
action = ["主人公が南夢子に対して自分の気持ちを伝える勇気を持ち、もう一度彼女に告白します。"]
_, reaction = namuko.generate_reaction(action)
print(reaction)
```
```
南夢子は主人公からの告白に驚きつつも、嬉しさを感じて素直に受け入れました。また、互いに気持ちを確認しあい、これからも頑張って関係を深めていこうと思います。
```
エンディングが見えましたね。おめでとうございます。


## おわりに
今回はシングルヒロインで告白までの流れをシンプルに一通り実装しましたが、元論文の実装を踏襲すれば、キャラクターAI同士のコミュニケーション（修羅場）ももちろん実現可能です。また、作品のコンセプトに応じて以下のような活用方法も考えられます。

- 前処理として、プレイヤーのゲーム行動をLLMで言語化して受け取る
- キャラクターの性格に応じて、プレイヤーの行動に対する評価軸を変える
- 製品組み込みだけでなく、「このキャラクターはこういうイベントに対してこう反応する」という、プロット作成段階のシミュレーションで活用する。

## 備考
この記事はバンダイナムコ研究所のエンジニア独自の研究成果で、バンダイナムコグループの製品及びサービスやその他開発業務とは関係がございません。そのため、質問やお問い合わせにつきましてもお答えできない事がございますので、予めご了承くださいますようお願い申し上げます。