ChatGPT/GPT-3を使って文脈のある会話を実現する（＋LINE Bot化）

🌟2023.3.2にChatGPTのAPIが公開されたため、タイトルと本文の一部を修正しました！やったね！

話題のChatGPTは超すごいし楽しいのですが、~~API経由で使えないため私たち開発者からするとちょっともの足りません（まもなくAPIが公開されるという話も）~~ されました！（2023.3.2）。
とはいえ、キャラ設定や会話の前提を会話の都度入力するのは面倒だったりしますのと、ChatGPTと同じOpenAIから提供されているGPT-3モデル「text-davinci-003」はそもそも1問1答で、同じ話題を複数ターンに渡って継続することができません。
そこで、OpenAIの外側で工夫してキャラ設定や前提、文脈を維持した会話を実現しようというのがこちらの記事になります。

# 要約

- 会話の履歴を含めてリクエストすることで文脈を意識させることができるようになる（ChatGPTでは履歴ごと受け付けるインターフェイスになっている）
- 履歴からは読みとることの難しい「背景」も含むとさらに期待に沿った回答となるほか、あらゆるタスクに対応できるようになる

# 考え方・仕組み

冒頭ではChatGPT/GPT-3のREST APIみたいに適当に呼びましたが、もう少し正確に言うとCompletion、つまりテキストの補完タスクを行うAPIです。つまり、「昔々、あるところに」と伝えることで、それに続くもっともらしいフレーズとしての「おじいさんとおばあさんがいました」を教えてくれるAPI、というものです。正確じゃないかもしれないですが多分だいたいそんな感じです。

この特性を考慮して、期待する回答（補完）をAIから得るために、以下のようなお膳立てを含めてAPIへのリクエストとして送信すると良いでしょう。
![01_context_model.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53296/37c29147-fda1-9e65-c355-c739f56e63f6.png)
具体的な例は以下のとおりです。

```
これは仲の良い兄と妹の会話です。妹は丁寧語は使いません。　←🌟「条件」に該当
兄:お寿司か焼肉行こ！　←🌟ここから「履歴」に該当
妹:いいね！どっちにしよう？
兄:うーん、どっちがいい？
妹:それならお寿司にしよう！
兄:おけ！何から注文する？　←🌟「今回の入力」に該当
妹:■■■■■■■■■■■■■■■■■■■ ←🌟ここをAIに補完してもらう
```

ここまでお膳立てをしてあげることで、AIはついつい「まぐろ」とか「いくら」と回答してしまう、というわけです。途中の「お寿司にしよう」も、最初のターンも含めて渡すからこそ答えられるものですね。

このあたりの話は、私は経験からくる雰囲気で書いていますが、気になる方はちゃんと論文を読んでみてください。サマリーには以下のように書いてあります。


> For all tasks, GPT-3 is applied without any gradient updates or fine-tuning, with tasks and few-shot demonstrations specified purely via text interaction with the model. GPT-3 achieves strong performance on many NLP datasets, including translation, question-answering, and cloze tasks, as well as several tasks that require on-the-fly reasoning or domain adaptation, such as unscrambling words, using a novel word in a sentence, or performing 3-digit arithmetic. 

https://arxiv.org/abs/2005.14165

まあだいたい良い線いってるのではないでしょうか。雰囲気的に。

# 作り方

コンテキストとして持たせるべき情報の種類もそこまで多くないと思いますのでイチから作っても良いのですが、OpenAIのCompletion APIを文脈を保持した形で利用するためのOSSが公開されていますので、この記事ではこれを前提にしていきます。

https://github.com/uezo/gpt3-contextual

まず、はそのOSSライブラリーであるgpt3-contextualをインストールします。PyPIからコマンド1発でいけます。

```sh
$ pip install gpt3-contextual
```

続いて、このライブラリーを利用したターミナルベースのチャットアプリを作ってみます。名前は何でも良いのですが、ここでは`run.py`として保存しましょう。
また、ここでOpenAIのAPIキーが必要になりますので、事前にサインアップの上メモっておいてください。


https://openai.com/api/


```python: run.py
from gpt3contextual import ContextualChatGPT

cc = ContextualChatGPT("YOUR_OPENAI_APIKEY")

while True:
    text = input("Human> ")
    resp, prompt, completion = cc.chat_sync("contextkey_1234", text)
    print(f"AI> {resp}")
```

それでは実行してみましょう。`Human> `と表示されたら、続けて何かAIへのリクエストを入力してエンターキーを押下します。しばらく待ってAIから回答があれば動作確認完了です。

```sh
$ python run.py
Human> Hello
AI> Hi there!
```

もし、ChatGPTではなくGPT-3の`text-davinci-003`などを利用する場合は、`ContextualChatGPT`の部分を`ContextualChat`に書き換えてください。

さて、ここからが本番です。デフォルトでもHumanとAIの会話であるということと直近6ターンの履歴が送られるようになっていますが、これを自分好みに改造していきましょう。私は妹が好きなのでここでは兄と妹との会話にしてみます。

```python: run.py
from gpt3contextual import ContextualChatGPT, ContextManager

cm = ContextManager(username="兄", agentname="妹", chat_description="仲良しの兄妹の会話です。丁寧語は使いません。")
cc = ContextualChatGPT("YOUR_OPENAI_APIKEY", context_manager=cm)

while True:
    text = input(f"{cm.username}> ")
    resp, prompt, completion = cc.chat_sync("user1234567890", text)
    print(f"{cm.agentname}> {resp}")
```

コードを少し解説しますと、背景情報を管理する`ContextManager`を生成して、これをContextualChatGPTに渡すようにしました。また、`ContextManager`には人間のロールとして`兄`、AIのロールとして`妹`、会話全体の条件を`chat_description`として設定しています。この実行結果は以下のようになるでしょう。

```sh
$ python run.py
兄> おはよー。ごはんは？
妹> 私が作るよ！今日は何が食べたい？
兄> うーん、トーストと目玉焼き
妹> わかった！目玉焼きとトーストだね！
兄> コーヒーは僕がいれるよー
妹> ありがとう！今日は兄さんが担当してくれるんだね！
```

まごうことなき兄妹の会話になりました。ここで欲を出して`chat_description`を以下のように修正してみます。

```python: run.py
cm = ContextManager(username="兄", agentname="妹", chat_description="仲良しの兄妹の会話です。丁寧語は使いません。妹は語尾に「ニャ」を付けます。")
```

するとどうでしょう。

```sh
$ python run.py
兄> コーヒーできたよ
妹> ありがとうニャ！おいしそうニャ
兄> 熱いから気をつけろよ〜
妹> わかったニャ！ありがとうニャ
兄> 猫舌だっけ？
妹> うん！猫舌ニャ
```

大勝利です。このように条件を変更することでAIによる回答（補完）結果が変わることをお分かりいただけたかと思います。

# 雑談以外のタスク

超大規模言語モデルをおしゃべりだけのために使うのはもったいないので、別のタスクをこなせるようにしていきます。冒頭で引用した論文（の要約）にあるように、GPT-3はあらゆるタスクをモデルの改造なくこなせるという特徴があります。

たとえば情報収集・調査タスクを実行するためには、以下のようにコンテキストを調整します。

```python: run.py
cm = ContextManager(username="質問", agentname="回答", chat_description="質問内容について調査し、回答してください")
```
するとこんな感じになります。「子供にもわかるように」「生物学上の観点から」など`chat_description`を詳細化することで回答内容を期待に近づけることもできます。
※正確性に関する保証はありません。出鱈目を言うことも（それなりに）あります。

```sh
Human> うなぎとあなごの違いについて教えてくれないかな？
AI> うなぎとあなごは、体長や体重などの体型や外見において大きな違いがあります。また、うなぎは水中で暮らし、あなごは水面付近で暮らします。生態的には、うなぎは淡水魚であり、あなごは海水魚です。その他の生物学的な違いとして、うなぎは活発な捕食者であり、あなごは肉食性であることが挙げられます。
```
次に翻訳タスクをみていきましょう。コンテキストは以下のとおりです。

```python: run.py
cm = ContextManager(username="日本語", agentname="英語", chat_description="日本語を英語に翻訳してください。")
```

こうなります。

```sh
Human> 明日は月曜日なので、お酒を飲んで現実を直視する解像度を落とすことにしました。
AI> Tomorrow is Monday, so I decided to have a drink and lower the resolution to face reality.
```

このほかにも演算やプログラムのコーディングなど、さまざまなタスクをこなすことができます。三目並べもできるようで、もはやCompletionとは何なのかという感さえあります。

https://twitter.com/uezochan/status/1628790811352784898

なお、このようなAIに渡すコンテキスト＋ターンインプットの情報をまとめて「プロンプト」と呼びます。プロンプトを工夫していろいろ試してみましょう！


# ChatGPTベースのLINE Botの作り方

コンソールでは味気ないのと、ふと思いついたプロンプトを試すのに支障があります。LINE Bot化して自分だけのChatGPTといつでもおしゃべりできるようにしてみましょう。

## 事前準備

LINE Developersにアクセス（初めての方はサインアップ）してMessaging APIのアプリを作成し、ChannelAccessTokenとChannelSecretを取得しましょう。

https://developers.line.biz/ja/

ここでは詳細は省略しますので、Qiitaの他の記事や公式ドキュメントを参考にしてみてください。「LINE Bot おうむ返し」などで検索すると良いでしょう。

## 依存ライブラリーのインストール

すべてPyPIで導入できます。

```sh
$ pip install fastapi uvicorn requests aiohttp line-bot-sdk 
```

## プログラムの作成

なんと、gpt3-contextualのサンプルコードにLINE Botの例がありますので、これをそのまま利用します。コードは以下のとおりです。`openai_apikey`、`channel_access_token`、`channel_secret`に値を入力するのと、`ContextManager`の各種設定値についてはお好みに合わせて変更してください。
また、このサンプルコードでは`https://あなたのサーバー/linebot`でWebhookを受け付けるようになっています。必要に応じて当該部分（`@app.post("/linebot")`のところ）を修正した上で、そのURLをLINE DevelopersのMessaging APIのWebhook先として登録しましょう。

https://developers.line.biz/ja/


```python: linebot.py
import aiohttp
import logging
import traceback
from fastapi import FastAPI, Request, BackgroundTasks
from linebot import AsyncLineBotApi, WebhookParser
from linebot.aiohttp_async_http_client import AiohttpAsyncHttpClient
from linebot.models import MessageEvent, TextMessage
from gpt3contextual import ContextualChatGPT, ContextManager

openai_apikey = "SET_YOUR_OPENAI_API_KEY"
channel_access_token = "<YOUR CHANNEL ACCESS TOKEN>"
channel_secret = "<YOUR CHANNEL SECRET>"

stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s]: %(message)s"))
logger = logging.Logger(__name__)
logger.addHandler(stream_handler)

session = aiohttp.ClientSession()
client = AiohttpAsyncHttpClient(session)
line_api = AsyncLineBotApi(
    channel_access_token=channel_access_token,
    async_http_client=client
)
parser = WebhookParser(channel_secret=channel_secret)
context_manager = ContextManager(
    username="兄",
    agentname="妹",
    chat_description="仲良しなので丁寧語を使わずに話してください。"
)
contextual_chat = ContextualChatGPT(
    openai_apikey,
    context_manager=context_manager
)

async def handle_events(events):
    for ev in events:
        if isinstance(ev, MessageEvent):
            try:
                resp, _, _ = await contextual_chat.chat(
                    ev.source.user_id,
                    ev.message.text
                )

            except Exception as ex:
                logger.error(f"Chat error: {ex}\n{traceback.format_exc()}")
                resp = "😣"

            try:
                await line_api.reply_message(
                    ev.reply_token,
                    TextMessage(text=resp)
                )

            except Exception as ex:
                logger.error(f"LINE error: {ex}\n{traceback.format_exc()}")

app = FastAPI()

@app.on_event("shutdown")
async def app_shutdown():
    await session.close()

@app.post("/linebot")
async def handle_request(request: Request, background_tasks: BackgroundTasks):
    events = parser.parse(
        (await request.body()).decode("utf-8"),
        request.headers.get("X-Line-Signature", "")
    )
    background_tasks.add_task(handle_events, events=events)
    return "ok"
```

## 稼働確認

出来上がったら実行します。

```sh
$ uvicorn linebot:app
```

デフォルトで127.0.0.1:8000番ポートで起動します。外部公開サーバーでIPアドレスを変える場合やポート番号を変更したい場合は以下のようにすると変更できます。

```sh
$ uvicorn linebot:app --host 0.0.0.0 --port 8080
```

また、手元のPCで動かしているプログラムでLINEからのWebhookを受けるにはngrokの利用が便利です。以下のように起動し、払い出されたURLをLINE DevelopersでWebhookのURLとして登録しましょう。
以下の例だと、`https://49c85f52adb7.ngrok.io/linebot`になります。

```sh
$ ngrok http 8000

ngrok by @inconshreveable                                                                         (Ctrl+C to quit)
                                                                                                                  
Session Status                online                                                                              
Account                       XXXX (Plan: Pro)                                                               
Update                        update available (version 2.3.41, Ctrl-U to update)                                 
Version                       2.3.35                                                                              
Region                        United States (us)                                                                  
Web Interface                 http://127.0.0.1:4040                                                               
Forwarding                    http://49c85f52adb7.ngrok.io -> http://localhost:8000                               
Forwarding                    https://49c85f52adb7.ngrok.io -> http://localhost:8000                              
                                                                                                                  
Connections                   ttl     opn     rt1     rt5     p50     p90                                         
                              0       0       0.00    0.00    0.00    0.00      
```

これでLINEからのリクエストを受け付ける準備が整いました。お手元のLINEアプリのトーク画面で話しかけてみましょう。ターミナルと同じように会話ができたら成功です！

# いますぐプロンプトの錬成を試したいあなたへ

おしゃべりGPTというLINE Botを作りました。ここで紹介した`ContextManager`のうち`username`、`agentname`、`chat_description`を自由に設定することができます。負荷が高くなったらそのうち課金しないといけなくなるので、気になる方は今のうちに試してみてね！

https://twitter.com/uezochan/status/1628779037073223682

https://page.line.me/?accountId=392vtjge

それでは、Enjoy creating LINE Bot with ChatGPT!