LoginSignup
25
13

More than 1 year has passed since last update.

ChatGPT風に会話が成り立つLINEボット on AWS Lambda

Last updated at Posted at 2023-02-14

はじめに

ChatGPTのもとになっているOpenAIを使ったチャットボットを作ってる人はたくさんいるものの、ステートフルに会話を成り立たせてる例がなかったので作ってみました。
image.png
会話をデータベースに保存しないと実現できないので、割と面倒ですが、LambdaとDynamoDBを使えばそこそこシンプルに実現可能です。

初期設定

下記のような流れ。

  1. OpenAIからキーをゲット
  2. LINEのBotを作ってキーをゲット
  3. Lambdaに関数を作る

OpenAI

Secret keyを取得する

下記URLから生成できる
https://platform.openai.com/account/api-keys

LINE

あたらしいProviderをつくる

image.png

Messaging API Channelをつくる

image.png
必須項目をうめる
image.png

  • Auto replyはDisabled
  • Greeting messagesはEnabledにしておかないと、追加したボットを探すのが面倒らしい。

Channel access tokenを取得する

image.png

AWS Lambda

新しいLambda Functionを作る

  • 名前を適当につけて、Pythonを選ぶ。
  • Enable function URLは必須。これでLINEのAPIがWebhookしてくるエンドポイントが生える。権限はNONEで。
    image.png

Requestsモジュールを使うためのLayerを追加

image.png

  • 適当なのを選んどけばrequestsくらいなら大体入ってるはず。
  • 本当はちゃんとrequirements.txtから生成してzipしたほうがいい。

環境変数の設定

Lambdaの環境変数に下記を設定しておく

  • LINE_CHANNEL_ACCESS_TOKEN
  • LINE_CHANNEL_SECRET
  • LINE_REPLY_ENDPOINT
  • OPENAI_API_KEY
  • OPENAI_COMPLETIONS_ENDPOINT
    image.png

LINE側のWebhook URLを設定する

AWSで取得できるFunction URLを
image.png
LINEのWebhook URLにセットする
image.png
今回はLINEからメッセージを受け取った時のエラーハンドリングをちゃんと書かないので、多分Webhook redeliveryはしないほうがいい。

まずはオウム返し

とりあえずはOpenAI関係なく、LINEの内容をオウム返しだけさせる。

Lambdaのコード

下記をLambdaのCodeに張り付ければ動くはず。
https://github.com/michitomo/openai-line-bot/blob/main/pingpong.py
メインの関数はこんな感じ。

  1. メッセージを取り出す
  2. 返信用Tokenを取り出す
  3. LINEの返信用エンドポイントにPOSTする
def lambda_handler(event, context):
    
    message = json.loads(event['body'])['events'][0]['message']['text']
    reply_token = json.loads(event['body'])['events'][0]['replyToken']

    # message = openai_completions(message)
    
    line_reply(reply_token, message)
    return {
        'statusCode': 200
    }

実行結果

入れたものがそのまま返ってくるはず
image.png

デバッグ

うまく動かなかったらView CloudWatch logsからログが確認できる。
image.png

OpenAPIに問い合わせて返事を返す

オウム返しの代わりに、OpenAIにクエリして、その返答をLINEに戻すようにする。

Lambdaのタイムアウトを伸ばす

OpenAPIのレスポンスは結構遅いので、とりあえず1分にしておく。
image.png

Lambdaのコード

  1. OpenAIのRequest BodyのpromptにLINEからのメッセージを与え、OpenAIへのリクエストを生成する
  2. レスポンスをLINEに返す

OpenAIのAPIは結構よく壊れてるので、エラーハンドリングを入れておく。

def openai_completions(prompt):
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer {}'.format(OPENAI_API_KEY)
    }
    data = {
        "model": "text-davinci-003",
        "prompt": prompt,
        "temperature": 0,
        "max_tokens": 100,
        "top_p": 1,
        "frequency_penalty": 0,
        "presence_penalty": 0,
    }
    try:
        print('OpenAI request: {}'.format(prompt))
        openai_response = requests.post(OPENAI_COMPLETIONS_ENDPOINT, headers=headers, data=json.dumps(data))
        print('OpenAI response: {}'.format(openai_response.json()))
        return openai_response.json()['choices'][0]['text']
    except:
        return 'OpenAIが壊れてました😢'

実行結果

何も指示を与えていない場合、基本的な挙動としては、続きを書いてくれる感じになることが多い。
image.png

返答をChatGPT風にする(ステートレス)

ここまではよく見るやつ。
ChatGPT風の返答にするためには、OpenAPIに以下のような初期値を与えている。

  • AIに「あなたはアシスタントである」と伝える
  • AIにhelpful, creative, clever, and evry friendlyな返事をするように指示する
  • Temperatureを高めに設定し、返答に使うコンテキストを広げる

詳細は下記。
https://platform.openai.com/examples/default-chat

GPTに与える指示テンプレートを追加する

https://github.com/michitomo/openai-line-bot/blob/main/stateless_openai_chat.py
OpenAIに送るリクエストをちょっと変えるだけ

    data = {
        "model": "text-davinci-003",
        "prompt": "The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly.\n\n"
        + "Human: Hello, who are you?\n"
        + "AI: I am an AI created by OpenAI. How can I help you today?\n"
        + "Human: " + prompt
        + "\nAI:",
        "temperature": 0.9,
        "max_tokens": 100,
        "top_p": 1,
        "frequency_penalty": 0,
        "presence_penalty": 0.6,
    }

実行結果

image.png
日本語の場合、Tokenが100だと全然足りないので、もっと増やす必要がある。ただしその分、費用がかさむ(ので、下記でいくらかかったかも表示するようにする)

ステートフルにする

だいぶ面倒くさいうえに、会話が長くなればなるほど、OpenAIの利用コストが高くなる。
会話の保存先はDynamoDBを利用。

DynamoDBにテーブルを作る

DynamoDBはKVSなので、LINE側のIDをキーにして、OpenID用の会話履歴をJSONでValueに丸ごと保存します。
リアルタイムにValueで検索する要件がある場合は、DynamoDBはあまり向いていないと思います。

KEY VALUE
line_id 会話履歴のJSON

一人のユーザに複数の会話履歴を保存したい場合は、別のソートキーを作る必要があります。会話履歴を念のために保存しておきたいだけなら、会話のリセット時に{line_id}{line_id}-{timestamp}のようにすれば良いです。

image.png

Lambda関数にDynamoDBへのアクセス権を与える

Lambda関数 -> 設定 -> アクセス権限
から、ロール名をクリックします。すると、ポリシーの画面が出てくるので、ここに
AmazonDynamoDBFullAccessAWSLambdaBasicExecutionRoleを追加します。

image.png
image.png

LambdaからDynamoDBに会話を保存するコードをかく

https://github.com/michitomo/openai-line-bot/blob/main/lambda_function.py
(要望があれば説明追記します)

実行結果

リセットを押さない限り、前の会話の内容を引き継いでくれる。会話が長くなるとどんどんAIの利用料が高くなるので、適度なタイミングでリセットを押したほうがいい。
image.png
DynamoDB側からも会話記録を参照可能。
image.png

雑記

  • OpenAIのAPIは不安定すぎる。
  • Lambdaの関数はzipにまとめてあげるより、Layersでやっておけば、コードをブラウザから編集できて便利。
  • 普通にアクセスさせるだけなら、AWS API Gatewayは不要。そのままFunction URLにアクセスさせられる。
25
13
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
13