ChatGPTにハバタクカミの対策を聞いてみた（LlamaIndex、LangChain）

## 概要
OpenAIのGPT-3等の言語モデルを使う時、LlamaIndexやLangChainを使って外部データを与えることで、学習データに含まれていない最新の知識にも回答できるようになります。

## はじめに
ChatGPTはポケモンに関する質問にも答えてくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/27c84fcf-a249-17db-5001-7d2254ef00ae.png)
▲おかしい内容もありますがちゃんとポケモンの話をしており、「れいとうパンチ」など実際に有効である技が対策として挙げられています。

しかしOpenAIのGPT-3.5（ChatGPT）等のモデルは2021年頃のデータで学習されており[^gpt-models]、2022年11月に発売されたポケットモンスター スカーレット・バイオレット（以下ポケモンSV）に関する知識には回答できません。

実際に2023年3月現在ポケモンSVのランクマッチ最上位のポケモンである「ハバタクカミ」[^flutter-mane]の対策をChatGPTに聞いてみると、まったく回答ができません。

[^gpt-models]: https://platform.openai.com/docs/models/gpt-3-5

[^flutter-mane]: 『Pokémon HOME』アプリ内のシーズン3シングル 2023/02/01〜 使用率1位

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/21bd07c2-29f9-bca8-c6ea-f7ca21266e73.png)
▲害虫対策の話になってしまいます。まったくポケモンの話はできていません。

外部データを与えることで、ハバタクカミの対策を聞けるようにしてみましょう。

## 環境
- python = "^3.10"
- llama-index = "^0.4.29"
- langchain = "^0.0.115"

## 準備
外部データとして、ハバタクカミやセグレイブについて説明されたファイルを`data/sample.txt`として準備しておきます。このサンプルテキストは筆者が適当に執筆したものです。

```sample.txt
ハバタクカミはゴースト・フェアリータイプの強力なポケモンです。
すばやさととくこうが高く、ムーンフォースやシャドーボールをはじめ強力なわざを高いすばやさで上から打ってきます。
マジカルフレイム、10まんボルト、パワージェム、エナジーボールなど様々な特殊技を習得し広い範囲を持っています。
また特殊受けに対してもサイコショックがあります。
でんじはやちょうはつといった補助技も備えています。
ハバタクカミの弱点はぼうぎょが低いことです。
物理の先制技で大きなダメージを与えることができます。
ミミッキュのかげうちやハッサムのバレットパンチが有効です。
とつげきチョッキを持たせたポケモンで特殊攻撃を封じることもできます。
セグレイブはこおり・ドラゴンタイプの強力なポケモンです。
600族と呼ばれる高いステータスを持っています。
こうげきが非常に高く、つららばりやきょけんとつげきといった強力なわざを習得します。
対処が遅れるとりゅうのまいやつるぎのまいでステータスを上げられてしまい、手がつけられなくなります。
```

今回はこのサンプルテキストの中から、`"ハバタクカミの対策を教えて"`というクエリによって、ハッサムのバレットパンチ等を抜粋できれば成功とします。
このサンプルテキストには「対策」という単語は含まれておらず、単純なキーワード検索ではヒットしませんが、うまくいくでしょうか。

今回実行するプログラムは以下のようなディレクトリ構成を想定しています。お使いの構成にあわせて適宜パスなどを変更してください。

```
$ tree
.
├── data
│   └── sample.txt
├── notes
│   └── index.ipynb
└── tmp
    └── index.json
```

またOpenAI APIアカウントを作成し、API Keyを環境変数`OPENAI_API_KEY`に格納しておく必要があります。

今回のすべてのソースコードは[こちら](https://github.com/s2terminal/zerolab)のリポジトリにあります。環境構築用のDockerfile等も含まれています。

## LlamaIndex
まずはLlamaIndexを使ってみます。

https://gpt-index.readthedocs.io/en/latest/

`llama-index`をインストールしたあと、以下のコードで`data/`ディレクトリ内のテキストファイルを読み込んで、インデックスを作成します。

```python
from llama_index import (
    GPTSimpleVectorIndex,
    SimpleDirectoryReader,
)

documents = SimpleDirectoryReader('../data').load_data()
index = GPTSimpleVectorIndex(documents)
index.save_to_disk('../tmp/index.json')
```

あとは、質問します。

```python
response = index.query("ハバタクカミの対策を教えて")

print("LlamaIndexの回答:")
print("。\n".join(response.response.split("。")))
```

```:出力
LlamaIndexの回答:

ハバタクカミの対策として、物理の先制技で大きなダメージを与えることができます。
ミミッキュのかげうちやハッサムのバレットパンチが有効です。
また、とつげきチョッキを持たせたポケモンで特殊攻撃を封じることもできます。
```

ちゃんと、与えられたデータからハバタクカミの対策部分を教えてくれました。

保存された`tmp/index.json`を見てみると、GPT-3によって生成された文章の埋め込み表現（embeddings）が記録されていることが分かると思います。この情報を使って回答を探しているようです。

## LangChain
`langchain`をインストールしておきます。

https://langchain.readthedocs.io/en/latest/index.html

下記コードで`data/`以下のテキストデータを読み込んで`load_qa_chain`を実行します。

```python
from langchain.llms import OpenAI
from langchain.document_loaders import DirectoryLoader
from langchain.chains.question_answering import load_qa_chain

llm = OpenAI()
loader = DirectoryLoader('../data/')
docs = loader.load()
chain = load_qa_chain(llm)
```

回答を聞いてみます。

```python
response = chain.run(input_documents=docs, question="ハバタクカミの対策を教えて")

print("LangChainの回答:")
print("。\n".join(response.split("。")))
```

```:出力
LangChainの回答:
 ミミッキュのかげうちやハッサムのバレットパンチを使用して物理の先制技で大きなダメージを与えることができます。
また、とつげきチョッキを持たせたポケモンで特殊攻撃を封じることもできます。
```

無事に答えられました。

### Map Reduceの利用
`chain`の種類にはいくつかあって、先ほど示したデフォルトでは`stuffing`というものを使っています。これはすべてのデータを一つのプロンプトにまとめて利用するため、データの長さに制限があります。
今回のサンプルテキストは短いためうまくいきましたが、多くのデータの中から質問応答をできるようにするには`stuffing`はあまり現実的ではないでしょう。

https://langchain.readthedocs.io/en/latest/modules/indexes/combine_docs.html

ここでは`chain_type="map_reduce"`を使ってみます。これはデータを複数に分割して、それぞれに初期プロンプトを適用し、最後に回答を生成するプロンプトを適用することで1つの出力を得るもののようです。実際に使ってみます。

どうやらデフォルトで`chain_type="map_reduce"`を使うと中身で英語のクエリを自動実行されて、回答も英語で出力される事があるようです。ここでは公式ドキュメントを参考に日本語のプロンプトテンプレートを構築します。GPT-3等は通常、日本語で質問されれば日本語で回答します。

```python
question_prompt_template = """長い文書の次の部分を使って、質問に答えるために関連するテキストがあるかどうかを確認してください。

{context}

質問: {question}
関連するテキスト:"""
QUESTION_PROMPT = PromptTemplate(
    template=question_prompt_template, input_variables=["context", "question"]
)

combine_prompt_template = """次のような長文の一部分を抜き出したものと質問を与えるので、最終的な答えを日本語で作成してください。
答えがわからなければ、わからないと言ってください。答えを作ろうとしないでください。

質問: {question}
=========
{summaries}
=========
答え:"""
COMBINE_PROMPT = PromptTemplate(
    template=combine_prompt_template, input_variables=["summaries", "question"]
)

chain = load_qa_chain(llm, chain_type="map_reduce", return_map_steps=True, question_prompt=QUESTION_PROMPT, combine_prompt=COMBINE_PROMPT)
```

この`chain`を用いて回答を聞いてみます。

```python
response = chain({"input_documents": docs, "question": "ハバタクカミの対策を教えて"})

print("LangChainの回答:")
print("。\n".join(response["output_text"].split("。")))
```

```:出力
LangChainの回答:
 ハバタクカミにはミミッキュのかげうちやハッサムのバレットパンチなどの物理の先制技で大きなダメージを与えること、またとつげきチョッキを持たせたポケモンで特殊攻撃を封じることが有効です。
```

無事に回答できました。

## まとめ
外部データを与えることで、ChatGPTでは通常回答できない最新の知識に基づく回答ができました。今回はサンプルデータとして適当な短いテキストを使いましたが、一定量のテキストが用意することでそのデータを利用して回答をする有用なアプリケーションが構築できると思います。

## 参考文献
- [ハバタクカミ｜ポケモン図鑑SV｜ポケモン徹底攻略](https://yakkun.com/sv/zukan/n983)