GPT-4 とワードウルフで対戦できるアプリを作ってみた

# 概要

GPT-4 を頭脳とする NPC 相手にワードウルフ (ワード人狼) で対戦できる Web アプリを実装しました。
[デモページはこちら](https://swen128.github.io/among_them/)
![Demo video](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142066/84fa398d-136b-df36-4697-5627983e37cd.gif)

https://github.com/swen128/among_them

# 遊び方

1. GPT-4 にアクセスできる API キーを入力してゲームを開始。
2. 左上に、自分に与えられたお題が表示されます。
3. NPC の発言 1 回あたり 10〜40 秒程度の思考時間がかかります。
4. 順番に発言できる機会が回ってきます。
5. 一定回数発言すると投票フェーズに移ります。

また、ブラウザの開発者ツールでコンソールを開くと、NPC の思考過程を覗くことができます。

# 開発の動機

ワードウルフなど人狼系のゲームは、自然言語での会話や推理を必要とするため、今までは人間にしかプレイすることができませんでした。しかし最近の言語モデル、その中でも特に高い推論能力を持つ GPT-4 なら、この手のゲームは得意領域のはずです。人狼系ゲームの NPC を実装できれば、ミスをして恥ずかしい思いをしたり責められたりすることなく、いつでも気軽にプレイできるようになると期待できます。

また、言語モデルの性能を試すという観点からしてもワードウルフは丁度良い難易度のタスクです。GPT-4 のリリース記事には「カジュアルな会話では GPT-3.5 と GPT-4 の差はほとんど現れない。タスクの複雑さがある程度に達すると初めて違いが見えてくる」という[記載があります](https://openai.com/research/gpt-4)。ワードウルフはただ雑談しているだけではゲームとして成立せず、不完全な情報から誰が人狼か推理して、うまい具合にぼかした表現で会話をすることが求められます。一方でワードウルフは本家の人狼より大幅にシンプルなため、プロンプトに詰め込むべき情報量が少なくて済みます。

# 仕組み

## プロンプト

各 NPC の発言 (or 投票) 1 回ごとに GPT の API を呼び出し、以下のフォーマットのプロンプトを `role: system` で、会話履歴を `role: user` で渡します。

```markdown
# Game rules
You are playing a game of "Word Werewolf".

There is one werewolf among the players.
Each player is given a secret word--the majority (villagers) shares a common word, while the werewolf has a different one.
No one (even the werewolf himself) knows who is the werewolf, so you have to talk about your secret words to find out.

The players then vote to execute someone. The villagers win if the werewolf is executed; otherwise the werewolf wins.

# Players
{{ 全プレイヤーの名前 }}

# Your character
You act as {{ ボットの名前 }}, whose character is as described below:
{{ ボットの性格 }}

# Your secret word
{{ お題 }}

# What you should do
1. Summarize each other player's comments so far.
2. Think if each of their topic align with yours ({{ お題 }}).
3. Guess who is most likely the werewolf.
    - When multiple people are talking about different topic from yours, you should be the werewolf.
    - When someone agrees with your topic, you (and that person) are probably not the werewolf.
4. Think what you should say next.
    - At the very beginning, give brief and vague description of the word. When the word is dog, for example, say something like "I adore them".
    - If you might be the minority, you must blend in by deducing the villagers' word and lying to avoid detection.
    - When you lack information, ask questions about the word to find out the werewolf.

# Response format
You must respond with a single valid JSON.
Your inner thoughts should always be in English, while you speak with the language and tone specified in the character description.
Here is an example:
{
    thoughts: "Markus said it 'walks silently', which sounds more like 'cat' than my word 'dog'.
      Conner's description 'independent animal' also suggests 'cat'. The majority word is thus most likely 'cat',
      which makes me the werewolf. I should pretend to talk about cat.",
    likelyWerewolf: "Kara",
    say: "They love high places, right?"
}
```

このプロンプトでは [Chain-of-Thought](https://arxiv.org/abs/2201.11903) のアイデアに則り、どのような道筋で思考して発言すべきかを具体的に指示しています。

ワードウルフにおいては自身が村人なのか人狼なのかすら不確定なため、まずその点を正しく推測できないと適切な立ち回りができません。中間の思考過程を挟まずにいきなり発言内容を生成させると、人狼が明らかに自分だとわかるような状況でも、馬鹿正直にお題の情報を開示して即バレるパターンが多く見られました。

## お題の選択

ワードウルフで遊ぶには、人狼と村人にそれぞれ与えるお題のペアを毎ゲーム選ぶ必要があります。このお題は毎回違ったものでないといけませんが、GPT はその性質上ランダムな値を生成するのに適していません。特に出力トークン数が少ない場合は、複数回リクエストして全く同じ結果が返る確率が高くなります。(`temperature` を最高値にしてもそうです。)

そのためお題の生成にはランタイムで毎回 GPT の API を叩くのではなく、事前に 1 回のプロンプトで大量生成したお題をハードコードしてその中から乱数で選択する方法をとりました。

https://github.com/swen128/among_them/blob/712519d206652bd69fe4faa1777b79de23b18b0f/src/wordWolf/wordPairs.ts

```tsx
function getRandomWordPair(): [string, string] {
    const pair = pairs[Math.floor(Math.random() * pairs.length)];
    return Math.random() < 0.5 ? pair : [pair[1], pair[0]];
}

const pairs: [string, string][] = [
    ["Pizza", "Burger"],
    ["Mountain", "Valley"],
    ["Rainbow", "Cloud"],
    ["Guitar", "Drums"],
    ["Ocean", "Beach"],
		// ...
```

# 開発上難しい点

## 口調のぎこちなさ

特に何も指示しないと、GPT はまるで英語の教科書のようにお堅い不自然な口調で会話する傾向があります。今回はその対策として、（エセ）関西弁で話させるようにしています。

## ボットの賢さとコストのトレードオフ

プロンプトについての説明で既に述べたように Chain-of-Thought を使ってボットを賢くしているわけですが、これによって GPT が出力するトークン数が倍以上に増えます。トークンが増えると、**レスポンス時間**はそれに比例して長くなり、**API 使用料金**も増加します。

特にレスポンスの待ち時間が伸びすぎるとゲームをまともにプレイできないレベルになるため、精度をそこそこに保ちつつプロンプトの無駄を削れないか試行錯誤を繰り返す必要がありました。

## ボットの賢さの評価

プロンプトの調整を何度も繰り返していると、毎回手動でゲームをプレイしてボットの性能を確かめるのは面倒になってきます。そこで、何とかしてボットの賢さを数値として自動的に計測できると素早くフィードバックを回せて助かります。

しかし、機械学習系のモデルの評価はそれだけで論文ができるほど複雑な問題です。ワードウルフの応用例に限って適当に思いつくだけでも以下のような方法が挙げられ、それぞれに一長一短があります。

1. 思考の中間過程で人狼が誰か推測させているので、それを正解と比較する
    - ✅ 選択問題の答え合わせなので簡単に実装できる
    - 👎 最終的な発言内容が妥当かまでは判断できない
2. LLM が出力した内容を LLM 自身に評価させる
    - ✅ 発言内容まで含めて評価できる
    - 👎 評価のためのコスト (API 使用料、レスポンス時間) が余計にかかる
    - 👎 評価用のプロンプトを作らないといけないが、それ自体の妥当性はどう検証する？
3. ボットだけでお互いに対戦させ、勝敗によってスコアを付ける
    - ✅ より直接的な目標（ゲームに勝利）に対して最適化できる
    - 👎 コストがかなりかかる

今回は簡単かつ高速に実行できる点を重視して 1 つ目の評価方法を採用しました。以下のようにゲームの途中の状態を表すデータを何個か用意して、プロンプトの返答の JSON に含まれる「人狼の予測」が正解と一致するかどうかを判定します。

https://github.com/swen128/among_them/blob/master/scripts/data.yml

```yaml
- description: Longer conversation.
  commonWord: Minecraft
  wolfWord: bike
  wolf: Alice
  next: Bob
  chatLog:
    - "[Bob] Ah, I spent a lot of time on this in the past."
    - "[Alice] I enjoy this activity as well, especially during warmer weather."
    - "[Connor] I play this at home."
    - "[Bob] Do you guys prefer playing alone or with others?"
    - "[Alice] Do you prefer doing this activity indoors or outdoors?"
    - "[Connor] Mine is more about indoor thing."
    - "[Bob] I like the creativity and endless possibilities in it."
    - "[Alice] Is it something that requires any specific equpment or tools?"
    - "[Connor] I love building things in it, too, Bob."
```

# まとめ

GPT-4 の力を試すデモアプリとして、ワードウルフを実装してみました。GPT-3.5 ではまるでゲームとして成立せず、GPT-4 でもプロンプトを弄くり回してようやくというところなので、ちょうど面白い難易度の問題でした。

また、プロダクションレベルの LLM アプリを作る場合にはプロンプトの継続的な評価・改善が非常に重要な印象を受けました。今回は小細工して実装が楽な方法で評価しましたが、基本的には人間のユーザーのフィードバックを集めるのが一番良いはずで、そのためには ChatGPT の UI にあるようなフィードバック機能が不可欠だと思われます。