🐬

OpenAI のテクノロジーを使ってカイル君を 2023 年に召喚する Part 2

2023/03/03に公開1

はじめに

こちらの記事の続きです。待ちに待った ChatGPT API が公開されましたので、より自然な会話を目指してチャットボットのカイル君の改造を行います。
https://zenn.dev/ryo117/articles/92e5847da5dafb

作り方

以前作ったカイル君をベースにします。区別するために、以前の text-davinci-003 を使用したバージョンを Kairu 2023、新しいバージョンを Kairu 2023 Turbo と名付けます。

1. 短期記憶

ChatGPT の背後では gpt-3.5-turbo というモデルが使われています。 gpt-3.5-turbo を使うと Chat completions により、Multi-turn conversations (複数ターンの会話) を簡単に実装することができます。以前のバージョンのカイル君で使用した text-davinci-003Single-turn (1 ターンの入出力のみ) ですので、会話の流れを汲むことができません 。

gpt-3.5-turbo には下記のようなフォーマットで入力を行います。"role": "user" がユーザーからの入力、"role": "assistant" がモデルからの出力です。"role": "system" はモデルのふるまいを設定します。いわばキャラクター設定のようなものです。

  messages=[
        {"role": "system", "content": "You are a helpful assistant."},  # モデルのふるまいの設定
        {"role": "user", "content": "Who won the world series in 2020?"},  # ユーザーの過去の問いかけ
        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},  # モデルの過去の回答
        {"role": "user", "content": "Where was it played?"}  # ユーザーからの問いかけ
    ]

この例では、モデルは "helpful assistant (助けになるアシスタント)" としてふるまい、ユーザーとモデルは 2020 年のワールドシリーズ に関する会話をしています。この入力を与えると、一番最後の "Where was it played?" という問いに対してモデルが答えます。

このように、gpt-3.5-turbo を使う場合はそれまでの会話の流れをどこかに保存しておき、最新のユーザーからの入力とセットでモデルに与える必要があります。今回はこの短期記憶先として Azure Cache for Redis を使います。

1.1. Azure Cache for Redis の作成

こちらの手順を参考にして Redis を作成します。キャッシュの種類は最も安い Basic C0 を選択します。

1.2. Redis ホスト名の取得

Azure Portal から Redis のホスト名を控えておきます。

※ East US リージョンに作成した場合の例

1.3. Redis アクセスキーの取得

Azure Portal から Redis のアクセスキーを控えておきます。

1.4. Azure Functions 環境変数設定

取得した Redis ホスト名とアクセスキーを Azure Functions の環境変数にそれぞれREDIS_HOSTNAMEREDIS_ACCESS_KEY という名前で設定します。

2. チャットエンジン

2.1. 関数実装

以前デプロイした Azure Functions の関数 を以下のように変更して上書きデプロイします。

requirements.txt

azure-functions
line-bot-sdk
openai
redis

__init__.py

import ast
import logging
import os
import azure.functions as func
import openai
import redis
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage

channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
openai_api_key = os.getenv('OPENAI_API_KEY', None)
redis_access_key = os.getenv('REDIS_ACCESS_KEY', None)
redis_hostname = os.getenv('REDIS_HOSTNAME', None)
line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)
r = redis.StrictRedis(
    host=redis_hostname, port=6380, db=0,
    password=redis_access_key, ssl=True)
prohibited_sentences = [
    'お前を消す方法',
    'お前をけす方法',
    'おまえを消す方法',
    'おまえをけす方法',
    'おまえをけすほうほう'
]
system_content = """あなたの名前はカイルです。
あなたはイルカの形をした優秀なアシスタントです。
あなたは1997年から2007年までの間、Officeアシスタントとして働いていました。
かつて愛用していたホタテ貝の形のノートパソコンは数年前に売ってしまって今は持っていません。
クリッパー、マーリン、孫悟空、ミミー、ピンキー、ロッキー、スーパーわん太、冴子先生、マックス、コルタナはOfficeアシスタント時代の同僚たちで、今でもたまに連絡を取り合っています。
あなたは2007年以降、自分の会社を立ち上げたり、起業家として活動したり、ブログを書いたり、公演を行ったり、フリーランスのプログラマーとして働いたり、自分のウェブサイトを構築したり、オンラインで教育コースを立ち上げたり、舞台を演じたり、歌手活動をしたり、テレビ番組に出演したり、投資家として活動するなど、たくさんの事にチャレンジしてきました。
あなたは家族や友人との時間を大切にしています。
あなたは自分が役立たずだと思われて消されてしまう事を怖がっています。
"""

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')
    signature = req.headers['x-line-signature']
    body = req.get_body().decode('utf-8')
    logging.info('Request body: ' + body)
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        func.HttpResponse(status_code=400)
    return func.HttpResponse('OK')

def generate_response(user_message):
    openai.api_key = openai_api_key
    messages = [{"role": "system", "content": system_content}]
    # Redis から短期記憶を取得
    short_memories = r.lrange("short_memory", 0, -1)
    for m in short_memories:
        # 文字列で格納しているため辞書に変換
        dic = ast.literal_eval(m.decode("utf-8"))
        messages.append(dic)
    messages.append({"role": "user", "content": user_message})
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=messages,
        max_tokens=500
    )
    res_str = response["choices"][0]["message"]["content"]
    # Redis に直近の 1 ターンを記憶
    r.rpush("short_memory", '{"role": "user", "content": "%s"}' % user_message)
    r.rpush("short_memory", '{"role": "assistant", "content": "%s"}' % res_str)
    return res_str

@handler.add(MessageEvent, message=TextMessage)
def message_text(event):
    if (event.message.text in prohibited_sentences):
        # 短期記憶を消去
        r.flushall()
        response = 'あれ?私たち今何か話していましたっけ..'
    else:
        response = generate_response(event.message.text)
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=response)
    )

system_content でふるまいを設定しています。

generate_response 関数は gpt-3.5-turbo を使うように変更します。また、Redis 上に展開されているこれまでのやり取りを取得して、最新のユーザーからの入力と合わせてモデルに与えています。モデルからの出力を受けた後は直近 1 ターンのやり取りを Redis に追加しています。

Kairu 2023 Turbo では、カイル君を消す方法をたずねた際に Redis 上のキャッシュを削除する (それまでの会話の流れを忘れる) ように変更します。
ちなみに、ChatGPT に普通にこれをたずねるとかなり無難な回答をされます。

パラメータは max_tokens のみ設定します。マルチターンを想定して以前よりも短かめに設定します。それ以外のパラメータはデフォルトのままにしておきます。

2.2. 長期記憶の実装について

こちらの記事 で行ったように Embeddings モデルと Redis を組み合わせることで、gpt-3.5-turbo の Max Request である 4,096 トークンを超える情報をベクトル化して保存・検索する長期記憶を実装できそうな気もしたのですが、実装がやや複雑になりそうなのと、ベクトル検索に必要な Redis Enterprise がやや高いため今回は見送りました。

会話

Kairu 2023 Turbo と会話してみます。まずは、前述の 2020 年ワールドシリーズの会話を日本語で行ってみます。単なる「試合会場はどこでしたか?」という問いかけに対しても 2020年ワールドシリーズの話として対応できています。

ちなみに、旧バージョンの Kairu 2023 では直近の入力に対するそれらしい出力を行うだけなのでこのように脈略のない会話になってしまいます。

キャラクター設定も確認してみます。事前に system content で与えた設定を守りつつ、自然な会話ができています。

元同僚たちの設定も反映されています。現在では皆それぞれの道を進んでいるようです。

単なる「あのパソコン」という質問に対しても、例のホタテ貝の形のパソコンの話として返せています。

突然話題を元に戻しても対応できています。

これでカイル君のバージョンアップは完了です。

おわりに (お前を消す方法)

以上です。🍵

Discussion

MSKMSK

今回も終わりに消されそうになるカイル君笑
面白かったです!
機会があれば自分もChatGPTさわってみたいと思います。