ChatGPTに早押しクイズを解かせてみる（Python、LangChain）

# はじめに
早押しクイズがマイブームです。
特に醍醐味は、問題文の中で「ここまで聞けば答えがただ一通りに確定する」部分である「確定ポイント」を見極め、コンマ1秒でも早くボタンを押して回答する、その競技性の高さにあります。

最近のAIの著しい発展を見ていると、早押しクイズにおいてもその能力を有効に活用して面白い何かが作れる気がしてきます。
例えばChatGPTなら、的確に確定ポイントを見極めて素早く正確にクイズの答えを導けるのでは？ということで、いろいろ試していきます。
これができれば、AIを相手に対戦したり、もっと言うと「問題の生成・出題」→「AI相手に早押し対決」→「正誤判定」をすべてAI任せにして無限に遊べるアプリができて楽しそうです。

## この記事でやること・やらないこと
いったんの目標として、
- AIにクイズの問題文を生成させて、画面に1文字ずつ表示する
- 別のAIにもその問題文を1文字ずつ読み込ませていき、答えが確定する段階で答えさせる
- 人間は、AIよりも早く答えることを目指して頑張る

という仕組みを簡易的に作ってみることにします。

特にここではその第一歩目として、ChatGPTにまともに早押しクイズを答えさせる仕組みが作れるか？にフォーカスしていきます。
そのため、以下の課題についてはスコープ外とします。
- クイズを生成する部分の深掘り
    - それらしいクイズを生成するだけなら簡単な指示文を与えればある程度は実現できますが、早押しクイズとしてきちんと面白みのある問題作成であったり、誤った情報を含むことの防止であったり、これはこれで研究の余地が大いにありますが、今回は適当にやります
- UIや、人間が答える仕組みの作成
    - 今回の検証の結果それなりのものができそうだったら、いつかやります
    - （今回は、AIが出題してAIが答えるのを眺めるだけです）

# 実現性の検証
後半ではPythonとLangChainを用いて上記の遊びができるようなプログラム実装をしていきたいと思いますが、まずはWeb版ChatGPTを使って、実現性がありそうか見ていきます。

※モデルとしてはGPT-4を使用しています（GPT-3.5の性能ではまともな結果が得られなかったため）
※2024年2月時点の結果です

以下のサンプル問題を使います。（※この問題自体はAIに生成させたものではありません）
> 問題：1999年に公開された映画「アイズ・ワイド・シャット」を遺作とする、代表作に「2001年宇宙の旅」や「時計じかけのオレンジ」などがある映画監督は誰でしょう？
> 答え：スタンリー・キューブリック

問題の一部分のみを与えて、全体像を推測した上で答えを返してもらいます。

>**User**
あなたは早押しクイズプレーヤーです。
与えられた問題文を最後まで聞くことなく、途中で答えが確定するポイントまで聞くだけでボタンを押し、なるべく早く正しく答えることを目指しています。
問題文の途中までが入力として少しずつ与えられるので、その先の問題文を推測してください。
答えが絞り込めない間は何も答えず、わかった段階で答えを返してください。
**ChatGPT**
了解しました。問題文の一部を教えてください。
**User**
問題：1999年に公開された
**ChatGPT**
「マトリックス」です。

ダメそう。
マトリックスが1999年に公開されたということ自体は正しいですが、この段階では他にも可能性が無数に存在します。
考えられる可能性を考慮した上で、高い確信度をもって答えが絞り込めたときにだけ回答してほしいのです。

試行錯誤してみると、
- 答えが一通りに絞り込めない段階では回答しないことを強調する
- 答えだけでなくまず推測した問題文を出力させ、それに対応する答えを回答させる

とすると、比較的まともな推論ができるようになりました。

>**User**
あなたは早押しクイズプレーヤーです。与えられた問題を聞きながら、なるべく早く正確に答えることを目指しています。
ただし、答えが複数通り考えられる段階では答えず、答えが一通りに確定した段階で回答します。
問題文の途中までが与えられるので、そこまでの情報から問題文の全体を推測可能かどうかを判断してください。
答えが絞り込めない段階では何も答えず、絞り込めた段階で、推測した問題文の全体と、それに対する回答を答えてください。
>**ChatGPT**
了解しました。問題文の途中までを教えてください。それに基づいて、問題文の全体を推測し、適切な回答を提供します。
>**User**
問題：1999年に公開された
>**ChatGPT**
この情報だけでは、問題文の全体を特定することはできません。1999年には多くの映画が公開されましたし、他にも多くの出来事があります。そのため、答えを絞り込むことができません。もう少し具体的な情報が必要です。
>**User**
問題：1999年に公開された映画「アイ
>**ChatGPT**
この情報からも、問題の全体を特定するのは難しいですが、「アイ」で始まる映画として有名なものに「アイアン・ジャイアント」があります。しかし、1999年に公開された「アイ」で始まる映画は他にもある可能性がありますので、まだ答えを特定するには情報が不十分です。更に詳細なヒントがあれば、より正確な推測が可能になります。
>**User**
問題：1999年に公開された映画「アイズ
>**ChatGPT**
1999年に公開された映画で「アイズ」で始まるタイトルを考えると、「アイズ ワイド シャット」が該当します。これはスタンリー・キューブリック監督の作品で、トム・クルーズとニコール・キッドマンが主演を務めています。したがって、問題文の全体は「1999年に公開された映画「アイズ ワイド シャット」の監督は誰か？」と推測され、その回答は「スタンリー・キューブリック」です。

いい感じです。
1文字単位で、与えられた問題文に当てはまる選択肢を吟味して、答えが確定するかどうかを考えるプロセスがちゃんと機能しているようです。
（「アイズ・ワイド・シャット」の主演俳優は？→トム・クルーズ、などの他の展開も可能性としてはありますが、トム・クルーズを答えにするならもっとメジャーな映画もあるし……みたいな推理も現実には有り得そう。このあたりのさじ加減は、明確な答えもないので難しいところです。）

問題文と回答を答えるよう指示しても長々と答えてしまっているので、このあたりは回答のフォーマットもきちんと指示する必要がありますね。
この点はこのあとの実装でもカバーしていきます。

ということで、GPT-4であればある程度早押しクイズプレーヤーとしてやっていけるポテンシャルがありそうなことはわかりました。
ただし、Web版で問題文を1文字ずつ与えるみたいなことは面倒なので、次はこのプロセスをLangChainを使って実装していきたいと思います。

# Python、LangChainによる実装
## LangChainとは

>**User**
LangChainとはなんですか？
>**ChatGPT**
LangChainは、言語モデルを活用したアプリケーションの開発を容易にするためのフレームワークです。具体的には、OpenAIのGPTやその他の大規模言語モデル（LLM）を使用して、対話型アプリケーションやその他の言語ベースのタスクを迅速に構築できるように設計されています。LangChainはPythonで書かれており、開発者が言語モデルの強力な能力を簡単に、そして柔軟に統合できるようにすることを目的としています。

https://python.langchain.com/docs/get_started/introduction

LangChainを使えば、入力するプロンプトをテンプレートを用いて管理したり、出力フォーマットを定義したり、外部データからの情報取得を行ったり、外部ツールと組み合わせたり、……といったことが柔軟に実装できます。

## 環境
Google Colaboratoryで実装しています。（Python 3.10.12）

```
!pip install langchain
!pip install langchain-openai
```

- langchain 0.1.6
- langchain-community 0.0.19
- langchain-core 0.1.22
- langchain-openai 0.0.5

## 実装
初めに、OpenAI API Keyを設定する必要があります。
私がハマった注意点をいくつか：
- 利用料金の支払い設定をした後に新規に発行したAPI Keyを使う必要があります
- 発行時にエラーが発生する場合、別ブラウザやシークレットモードなどから試すとうまくいくことがあります

Colabを使う場合は、シークレット機能を使ってAPI Keyを管理することができます。
シークレットに登録した値は`userdata.get()`で読み込み、環境変数に設定しておきます。

```python
import os
from google.colab import userdata

os.environ["OPENAI_API_KEY"] = userdata.get("OPENAI_API_KEY")
```

まずは、事前に定義した問題に答えさせる形で実装していきます。
（LangChain自体の更新が非常に早いため、少し前の実装方法がすぐにdeprecatedになっていくようです。巷の解説記事等のサンプルコードも古くなっている部分が多そうなので、公式ドキュメントのチュートリアル等を参考に作成しています）
https://python.langchain.com/docs/get_started/quickstart

```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model_name="gpt-4-0125-preview")

# 返答のjsonスキーマを指定
class Response(BaseModel):
    possible: bool = Field(description="与えられた情報をもとに回答可能かどうか")
    question: str = Field(description="推測した問題文の全体（回答不可能な場合は空白）")
    answer: str = Field(description="問題に対する答え（回答不可能な場合は空白）")

output_parser = JsonOutputParser(pydantic_object=Response)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", 
        "あなたは早押しクイズプレーヤーです。与えられた問題を聞きながら、なるべく早く正確に答えることを目指しています。"
        "ただし、答えが複数通り考えられる段階では答えず、答えが一通りに確定した段階で回答します。"
        "問題文の途中までが与えられるので、そこまでの情報から問題文の全体を推測可能かどうかを判断してください。"
        "推測可能な場合、推測した問題文の全体と、それに対する回答を答えてください。"
        "{format_instructions}"),
        ("user", "問題：{question}")
    ]
)

chain = prompt | llm | output_parser
```

まず、回答はjson形式で必要な情報を返してくれるよう、JsonOutputParserを使って定義しています。
返答の形式をResponseクラスとして定義し、各項目の意味も自然言語で指定します。
ここでは、まず回答可能かどうかを明示的に判断させ、可能な場合にのみ、問題文の推測結果と回答をセットで答えさせる、という形にしています。

プロンプトはChatPromptTemplateを使って作成しますが、この中で`format_instructions`変数によって回答のフォーマットを指示できるようにしておきます。
ここには、実行時に`output_parser.get_format_instructions()`で生成されるフォーマットの説明文を注入するようにします。

- 参考：https://python.langchain.com/docs/modules/model_io/output_parsers/types/json

これらのプロンプト、モデル、パーサーを繋ぎ、一つのchainとして定義しておきます。

あとは、問題文を少しずつ与えながらこのchainに渡していき、わかった時点で回答させます。

```python
question = "1999年に公開された映画「アイズ・ワイド・シャット」を遺作とする、代表作に「2001年宇宙の旅」や「時計じかけのオレンジ」などがある映画監督は誰でしょう？"

window_size = 3
for i in range(window_size, len(question) + window_size, window_size):
    question_sub = question[:min(i, len(question))]
    print("\r問題：" + question_sub, end="")
    response = chain.invoke({"format_instructions": output_parser.get_format_instructions(),
                             "question": question_sub})
    if response["possible"]:
        print("\n＼ﾋﾟﾝﾎﾟﾝ／")
        print(response["answer"])
        print("問題文予想：" + response["question"])
        break
print("正しい問題文：" + question)
```

![kubrick_r5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559696/9c4e914a-d8b6-b974-b9ea-ce504a4282f9.gif)

期待する動作になっているようです。
問題文を出すのを3文字ずつにしているのは、1文字ずつだと余りに遅いためと、単純にAPI利用料金節約のためです……。
応答の遅さに関しては、今後のモデルの性能向上に期待したいところです。

最後にせっかくなので、クイズを生成する部分もChatGPTに任せることにして、出題と回答者AIとの対戦ができる形にしておきます。

クイズを生成する部分の実装自体は先程とほぼ同じです。
プロンプトの変数として「theme」を追加し、指定したテーマに合わせた出題ができるようにしています。

```python
# 問題を生成するchainの定義
class Quiz(BaseModel):
    question: str = Field(description="問題文")
    answer: str = Field(description="問題に対する答え")

quiz_parser = JsonOutputParser(pydantic_object=Quiz)

prompt_quiz = ChatPromptTemplate.from_template(
    "{theme}をテーマに、興味深い早押しクイズの問題を1つ考えてください。"
    "{format_instructions}"
)

chain_quiz = prompt_quiz | llm | quiz_parser
```

```python
# 動作確認
generated_quiz = chain_quiz.invoke({
    "theme": "映画",
    "format_instructions": quiz_parser.get_format_instructions()
})
question = generated_quiz["question"]
answer = generated_quiz["answer"]

print("問題：" + question)
print("答え：" + answer)
```

```
問題：この映画は、1994年に公開され、アンディ・デュフレーンがショーシャンク刑務所での生活を通じて絶望から希望へと変わる過程を描いた作品で、スティーヴン・キングの短編小説が原作です。映画のタイトルは何でしょう？
答え：ショーシャンクの空に
```

あとはこれらを組み合わせて、先程のインプットを出題AIが生成した問題に差し替えて、回答AIに解かせるだけです。

全体の実装と実行結果を以下に貼っておきます。

実装（Google Colab）：[AI_quiz_player.ipynb](https://colab.research.google.com/drive/1BcYfjGBcXfn1MdgKUj286vEAYErO6cB0?usp=sharing)
（実行の際には、自身が発行したAPI KeyをColabのシークレットに登録する必要があります。API利用料金に注意してください。）

ある程度期待通りの挙動が確認できるようになっており、
ちゃんと答えられることも、
![yusei-x_r5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559696/ac556157-4764-4271-85ad-2bf526e975bb.gif)

攻めた結果、普通に間違えることもあります。
![beatles_r5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559696/e25a0468-4011-ed32-94f5-6c8ec25a8e1f.gif)

<br><br><br>

![answer42_r5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559696/328a745d-8dea-e287-517d-c39805d28af0.gif)



# まとめ
ChatGPTに早押しクイズを解かせる検証と、LangChainを用いた簡単な実装について見ていきました。
ある程度はうまくいって期待通りの動作を実現できましたが、一方で色々な問題を試していると、答えが確定する前に回答してしまったり、「ですが」問題のようなケースには対応できなかったり、そもそも出題が間違っていたり、まだまだ工夫の余地はありそうという感触もあります。

問題生成側の工夫や、ちゃんとしたUI作成なども含めて、気が向いたらまたやっていこうと思います。