23
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ChatGPT APIを利用したChatBotを作ってみた【Ruby】

Posted at

2023年3月1日(日本時間3月2日)遂にChatGPT APIが公開されました。
それ以前にも「ChatGPTを使った〜〜」みたいなサービスが続々と出ていましたが、基本的には本家のChatGPT以外は「OpenAIのAPIを使ったAIサービス」でしかありませんでした。
ここからは各サービスが、ChatGPTを利用しているサービスとして胸を張っていけますね。

というわけで今回は、以前に作ったChatBotを改良して流行りのChatGPT APIを使ったChatBotに進化させていきます。ChatBot自体の作り方に関しては前回の記事を参考にして作ってください!

実装していく

いきなり寄り道

開発に取り掛かる前に、本家ChatGPTにこの記事を書くことについてどう思うか聞いてみました。
スクリーンショット 2023-03-30 17.27.51.png
とのことだったので頑張って作っていきます。実装手順もついでに聞いてみたのですが、引っかかることが多かったので自力で実装していきます。

OpenAIに登録してAPI Keyを取得する

アカウントを持っていない人はOpenAIのアカウントを登録してください。その後、Account API Keys - OpenAI APIでAPIのKeyを取得できます。
決済情報などは登録しなくても無償で$5のクレジットが付与されており、その分は利用可能です。
スクリーンショット 2023-03-30 13.51.50.png

※無償分の仕様は変更される恐れがあるのでご了承ください

コード実装

公式ドキュメントを参考にコードを実装します。
ヘッダー情報にAPI KEYを入れ、対象となるmodel(使用するAPIバージョン)とメッセージを送ることで利用できます。
ChatGPTへ問い合わせする部分のソースは以下の通りです。

# ChatGPTへの問い合わせ
def post_chat_gpt_chat(message)
    api_key = ssm_client.get_parameter({ name: '/openai/api_key', with_decryption: true }).parameter.value
    headers = {
        'Content-Type': 'application/json',
        'Authorization': "Bearer #{api_key}"
    }
    data = {
        "model": "gpt-3.5-turbo",
        "messages": [{"role": "user", "content": message}]
    }
    response = http_post_request('https://api.openai.com/v1/chat/completions', headers, data)
    JSON.parse(response.body)['choices'][0]['message']['content']
end

GPT-4が既に公開されていますが、僕のアカウントではまだ利用できないので、今回はgpt-3.5-turboを利用しています。

実験

とりあえずメッセージを送ってみたところ返事がありませんでした。何故だと思ってログを見に行ったところTask timed out after 3.01 secondsと出ており、Lambdaのタイムアウト時間が短いのが原因でした。デフォルトもう少し長い認識だったのですが、かなり短かったのでお気をつけください。
タイムアウト時間を伸ばして再実行してみるとうまくいきました。
Screenshot_20230330-175507.png
これで完成かと思うかもしれませんが、現在の作りだと連続した会話ができません。
Screenshot_20230331-075745.png
ピジン言語でプログラミングするしかないですね。

会話の履歴を持つようにする

連続した会話に対応するために、過去の会話の履歴を保持するように作り替えます。
DynamoDBを利用したり、S3にファイルとして持たせたりしてもいいのですが、今回は簡易的に実装するためにLambdaのインメモリを利用しました。そのため、長時間会話が保持されることはないので、本格的な実装をする場合にはAWS内部で実装するならDynamoDBあたりを使うといいかなと思います。
履歴を保持する作りに変えてメッセージを送ると、連続した会話ができるようになりました!
Screenshot_20230331-075754.png

Rubyで実装する場合はインスタンス変数に入れるだけでOKです。履歴が保持される期間はLambdaのインスタンスが再利用されるかどうかに依存します。ここはブラックボックスな部分なので再利用されるように祈ってください。
今回実装したコード全文は以下の通りです。

require 'json'
require 'aws-sdk-ssm'

def lambda_handler(event:, context:)
    lambda_event_body = JSON.parse(event['body'])

    logger.info lambda_event_body
    
    line_event = lambda_event_body['events'][0]
    # メッセージ以外は受け取らない
    return unless line_event['type'] == 'message' && line_event['message']['type'] == 'text'
    
    line_user_id = line_event['source']['userId']
        
    # ChatGPTに問い合わせ
    return_message = post_chat_gpt_chat(line_event['message']['text'])
        
    # ラインへ返信
    push_line_message(line_user_id, return_message)
end

# ChatGPTへの問い合わせ
def post_chat_gpt_chat(message)
    api_key = ssm_client.get_parameter({ name: '/openai/api_key', with_decryption: true }).parameter.value
    headers = {
        'Content-Type': 'application/json',
        'Authorization': "Bearer #{api_key}"
    }
    # メッセージ履歴
    @cache_messages ||= []
    @cache_messages.push({ "role": "user", "content": message })
    logger.info @cache_messages
    data = {
        "model": "gpt-3.5-turbo",
        "messages": @cache_messages
    }
    response = http_post_request('https://api.openai.com/v1/chat/completions', headers, data)
    gpt_message = JSON.parse(response.body)['choices'][0]['message']['content']
    @cache_messages.push({ "role": "assistant", "content": gpt_message })
    
    gpt_message
end

# LINEへのメッセージ送信
def push_line_message(line_user_id, message)
    access_token = ssm_client.get_parameter({ name: '/line/access_token', with_decryption: true }).parameter.value
    headers = {
      'Content-Type': 'application/json',
      'Authorization': "Bearer #{access_token}"
    }
    data = {
        'to': line_user_id,
        'messages': [{ 'type': 'text', 'text': message }]
    }
    http_post_request('https://api.line.me/v2/bot/message/push', headers, data)
end

def http_post_request(api_url, headers, data)
    uri = URI.parse(api_url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'
    req = Net::HTTP::Post.new(uri.path)
    req.body = JSON.generate(data)
    req.initialize_http_header(headers)
    http.request(req)
end

def logger
    @logger ||= Logger.new(STDOUT, formatter: proc { |severity, timestamp, _, msg|
        JSON.dump({ time: timestamp, level: severity, message: msg })
    })
end

def ssm_client
    @ssm_client ||= Aws::SSM::Client.new
end

役割を指定してみる

実装自体は終わったのですが、ChatGPT APIに送っている値のroleについて簡単に説明しておきます。roleuser,assistant,systemの3種類存在しています。userは入力側のメッセージ、assistantはChatGPT側のメッセージを表しています。そして、今回の実装では使っていないsystemは役割を与えることができます。
「役割を与えるとは?」となったので、具体的に見ていきましょう。
あなたは嘘をつくAIですという役割を与えてみます。{"role": "system", "content": "あなたは嘘をつくAIです。"}を一番最初のメッセージに指定することで実装できます。
1.png
怒られました。

代わりにあなたは賢いAIですという役割を与えてみると以下のようになりました。
2.png
役割の指定なしの場合は2です。としか回答されなかったので少し内容が変わっています。役割の指定はもっと活用する術がありそうです。是非探求してみてください。

最後に

実装自体はいたってシンプルで、特にパラメーターを作り込まずに使えるレベルだなと感じました。
AIの回答はあくまでも学習によって生成されたものなので嘘を平気でついてきます。用法用量を守って正しくお使いください。

P.S.

記事がバズるかどうか聞いてみたところ
Screenshot_20230331-085313.png
とのことでした。みなさん是非いいねしてください。

23
11
0

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
23
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?