目次

目次

【ChatGPT】ChatGPTでChatGPTチャットボット試作

アバター画像
長島大和
アバター画像
長島大和
最終更新日2023/06/27 投稿日2023/06/27

はじめに

こんにちは。株式会社レコチョクの長島です。 2022年4月に新卒で入社し、NX開発推進部iOSアプリ開発グループに所属しています。

入社前は大学院で自然言語処理の研究をしており、どのようにすれば対話システムにキャラクタ性を付与できるのかをテーマに学会発表などを行っていました。 研究内容で自分が作った手法は、ChatGPTという対話システムのエポックメイキングが発生したことで、プロンプトを入力するだけで実現可能となりました。技術の進歩はすごいなと思うと共に、無事に研究内容が破壊されてちょっとした寂しさを覚えています。

さて、今回はそんなChatGPTを用いてChatGPTと会話できるチャットボットを構築しようと思います。

背景

数ヶ月ほど前に、Slack ソケットモードの最も簡単な始め方 (Go 編) – Qiitaの記事を見て、簡単なSlackBotを作成していました。 基本的にサンプルコードを元に実装して、Slack上で動作するBotを作成するところまではいけたのですが、その後あまり有効な活用方法を生み出すことができていませんでした。

そこから今年の4月ごろにAI活用のプロジェクトに参加することとなり、OpenAI APIを活用できる環境となりました。ここで、先述のBotとOpenAI APIのChat Completions APIを活用することで、GPT-3.5との対話ができるチャットボットが作成できるのではないかと感じられました。 そうすればSlack上で簡単にChatGPTに質問することができ便利になるし、自身の勉強になると感じられました。

しかし、Golangに慣れていないこともあり自力でコードを書く自信がありませんでした。 そこで、ChatGPTを活用してコードを読み込ませたり、どうすれば良いのかをアドバイスしてもらえれば実装できるのではないかと思いつき、「ChatGPTでChatGPTチャットボット試作」に着手することにしました。

実装

目標

背景で作成していたSlackBotには以下の2つの機能がありました。

  • Slackへ接続すること
  • メッセージを読み取ること

今回はChatGPTを用いて、以下の2つの機能を追加することを目標としました。

  • メッセージをChat Completions APIに送信する
  • その結果をSlackに返答という形で返す

こうして実装してもらったコードを用いて、実際にSlack上で話せることを確認しようと思いました。

対話(1)

対話内容(1)

背景の通り、今回はSlackに接続する部分・メッセージを読み取る部分はすでに実装しています。 そこから追加で実装してほしい場所に、「// ここにChatGPT APIを用いた返答を実装する」とコメントを書き、コード全体を記載し送信しました。

以下が、ChatGPTに送った文章になります。なお、コード部分は画像に置き換えています。最終的なコードは最後のセクションで紹介します。

Socket Modeを用いて、SlackBotに接続する以下のコードが存在します。 このコードのコメント部分である // ここにChatGPT APIを用いた返答を実装するの部分に、適切にChatGPT APIを用いて返答を返すことができるコードを記載してください。

ChatGPTの返答は以下の通りでした。

OpenAIのChatGPT APIを使って返答を作成するためには、まずOpenAIのGPT-3 APIを利用するためのクライアントライブラリが必要です。Go言語用の公式ライブラリは現在存在していないので、ここでは直接HTTPリクエストを発行してAPIを呼び出します。そのため、 net/httpとencoding/jsonパッケージを使用します。 まずはChatGPT APIにリクエストを送信する関数を定義しましょう:
この関数を使って、Slackにメッセージを返す部分を次のように書き換えます: 以上のコードは、ユーザからのメッセージをOpenAIのAPIに送信し、得られた応答をそのまままSlackに投稿するものです。このコードを含むためには、以下のパッケージの追加が必要になります: そして、OpenAIのAPIを利用するためには、OpenAIから取得したAPIキーを環境変数 OPENAI_API_KEYに設定する必要があります。 なお、このコードはシンプルな例であり、エラーハンドリングやレートリミット対策、トークンの管理などは考慮されていません。本番環境での利用を考える場合は、これらの要素を含む適切な実装が必要となります。

解説(1)

少し長いのでこの回答をChatGPTに要約してもらうと、以下のようなことが書かれていました。

  1. OpenAIのAPIを呼び出すためのGo関数callOpenAI(prompt string) (string, error)を作成
  2. 受け取ったSlackメッセージをOpenAIに送信し、応答を生成
  3. 生成した応答をSlackに投稿するコードを追加
  4. 必要なパッケージをインポートし、OpenAIのAPIキーを設定するよう指示する

このような流れで、実際のコードと必要な他の処理に関して説明してくれました。

結果(1)

回答を元に実装したところ、無事にプログラムが実行できました。しかし、Slackにメッセージを送るとエラーによりクラッシュしてしまいました。

中身を見てみると、以下のエラーが発生していました。

panic: runtime error: index out of range [0] with length 0

配列の周りでエラーが発生しているようなので、これをどう修正するべきか・どこで起きているのかを調査する必要がありました。

対話(2)

対話内容(2)

対話(1)で発生したエラーを元に、どのように解決するべきかを考えましたが、そもそもエラーの要因に関してもChatGPTが推測してくれるかもしれません。

そう考え、以下の内容をChatGPTに送信しました。

先程のコードを実装し、以下の形になりました。 (コード全文) しかし、 go run main.goを行い、Slack上で話しかけたところ、以下のエラーが発生しました。 panic: runtime error: index out of range [0] with length 0 どうすれば良いでしょうか。

これに対してChatGPTは以下の形で返答しました。

エラーメッセージによれば、 panic: runtime error: index out of range [0] with length 0とあります。これは、空の配列やスライスから要素を取得しようとした際に生じるエラーで、おそらく以下の行が原因であると考えられます:
return respBody.Choices[0].Text, nil この行では、OpenAI APIからのレスポンスを表す respBody.Choicesスライスの最初の要素にアクセスしようとしています。しかし、エラーメッセージから推測するに、respBody.Choicesが空(つまり、長さが0)である場合があるようです。 OpenAI APIからの応答が期待通りでない、またはエラーが発生している可能性があります。まず、以下のようにレスポンスを確認してみてください: この修正により、OpenAI APIが200以外のステータスコードを返した場合、または選択肢が0の場合にエラーが返されます。この情報は、OpenAI APIからの応答に何か問題があるかどうかを特定するのに役立ちます。 また、OpenAI APIへのリクエストが正しいかどうかを確認してみてください。特に、 OPENAI_API_KEY環境変数が正しく設定されているか、またPromptフィールドが適切な値を持っているかを確認してみてください。

解説(2)

この回答もChatGPTに要約してもらいました。

  1. OpenAI APIのレスポンスが200(正常)ステータスコードか確認するコードを追加
  2. レスポンスの要素数が0かどうかを確認するコードを追加
  3. 環境変数が正常に設定されているかを確認するように指示(APIキー・Promptフィールドの値)

最初に実装したコードは、APIが必ず正常な値を返すことが前提になっていたようです。そのため、エラーハンドリングを追加して先程のエラーが出ない実装にしているようでした。

結果(2)

上記のコードにそのまま置き換えたところ、ステータスコードが 404で返却されていることがわかりました。 ステータスコード 404はNot Foundとして広く知られており、ページやリソースが見つからなかったことを表しています。

対話(3)

対話内容(3)

こちらもエラーの内容に関して、どのようにすれば良いのか提案してくれるかもしれなかったので、以下のように送信しました。

上記のコードを実装したところ、クラッシュすることはなくなりました。しかし以下エラーが出ています。 2023/05/15 19:32:10 Error calling OpenAI API: OpenAI API returned non-200 status code: 404

すると、ChatGPTはベータ版機能であるブラウジング機能を用いて、以下の返答を行いました。

あなたが見ている404エラーコードは、通常、サーバーが要求されたリソースを見つけられなかったことを示しています。2021年9月の最終更新時点のOpenAI APIドキュメンテーションによれば、チャットの完了を作成するためのエンドポイントは (URL) です。 しかし、最新のOpenAI Pythonライブラリのドキュメンテーションによれば、チャットの完了はopenai.ChatCompletion.create(model=”gpt-3.5-turbo”, messages=[{“role”: “user”, “content”: “Hello world”}])を使用して作成されます。 これは、OpenAI APIのエンドポイントの構造が私の最後の更新後に変更された可能性があることを示しています。エンドポイントが変更されている場合、古いエンドポイントを使用すると404エラーが発生する可能性があります。 これを確認するには、公式のOpenAI APIドキュメンテーションを再度確認するか、正しいエンドポイントを使用していること、およびリクエストボディが適切にフォーマットされていることを確認するためにAPIリクエストをチェックすることができます。 残念ながら、私はブラウジングの制限のために直接最新のOpenAI APIエンドポイントを取得することはできません。正しいエンドポイントについては、最新のOpenAI APIドキュメンテーションをご確認ください。

解説(3)

要約すると、以下のようなことが指示されていました。

  1. 404エラーはページやリソースが見つからなかったことを示す
  2. OpenAI APIのエンドポイントが変更されていると404エラーが発生する可能性がある
  3. 最新のOpenAI APIドキュメンテーションで正しいエンドポイントを確認できるため、それをチェックする必要がある

公式のドキュメントを参照することが問題解決になると分かりました。

結果(3)

OpenAI APIの公式ドキュメントの内容を眺めたところ、APIのエンドポイントに間違いがあることが分かりました。エンドポイントはURLのことであり、これはプログラム中に指定できる場所があったため、ここを自分で書き換えました。

ここを修正して実行すると、今度はステータスコード 400が返却されるようになりました。400はBad requestを意味するため、リクエストボディの間違いを表しています。

対話(4)

対話内容(4)

対話(3)の結果を踏まえ、ChatGPTにはリクエストボディの間違いを修正してもらうことにしました。 本当はURLを渡してブラウジングしてもらいたかったのですが、それは制限があるらしく不可能だったため、サイトの内容をそのままコピーする形としました。入力したプロンプトは以下のようになりました。

エンドポイントを書き換えたところ、別のエラーになりました。 2023/05/15 19:40:42 Error calling OpenAI API: OpenAI API returned non-200 status code: 400 URLの問題は解決しましたが、リクエストボディが異なるようですね。 そこで、OpenGPTの最新のAPI Documentを見て、Request Bodyに関する章を見てきました。(URL) あなたのために必要そうな部分をコピペします。 (APIドキュメントのRequest Bodyの項目を全てコピペする) この情報を元に、コードを書き直してください。

これに対して、ChatGPTは以下の形で返答しました。

最新のOpenAI Chat APIに基づいて、あなたのコードを更新する必要があります。 まず、リクエストとレスポンスの構造体を修正します。 以下のように、 OpenAIRequest と OpenAIResponse の定義を更新します。 次に、 callOpenAI 関数を更新して、新しいリクエストボディを使用します。 これにより、リクエストとレスポンスの構造がOpenAIの新しいChat APIに一致するようになります。

解説(4)

APIドキュメントの内容を元にし、以下のような修正を加えていました。

  1. リクエストとレスポンスの構造体を修正
  2. OpenAIRequest と OpenAIResponse の定義を更新
  3. callOpenAI 関数を更新し、新しいリクエストボディを使用

結果(4)

この形に合わせて修正を行い、実際にコードを実行したところ、 Slack上でChatGPTを用いたチャットボットが動作しました。

スクリーンショット 2023-05-15 19.47.30.png

Botの名前は背景で作成したSlackBotのものを流用しているため、ここでは無視してください。

コード

最終的なコードはこのようになりました。このコードを実行しようとする際は環境変数( SLACK_BOT_TOKEN, SLACK_APP_TOKEN,OPENAI_API_KEY)を設定する必要があるため、手元で実行しようとする際は注意してください。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/slack-go/slack/socketmode"

    "github.com/slack-go/slack"
    "github.com/slack-go/slack/slackevents"
)

const openaiApiUrl = "https://api.openai.com/v1/chat/completions"

type Prompt struct {
    Content string `json:"content"`
}

type OpenAIRequest struct {
    Model    string      `json:"model"`
    Messages []ChatInput `json:"messages"`
}

type ChatInput struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type OpenAIResponse struct {
    Id      string `json:"id"`
    Object  string `json:"object"`
    Created int    `json:"created"`
    Model   string `json:"model"`
    Choices []struct {
        FinishReason string `json:"finish_reason"`
        Message      struct {
            Role    string `json:"role"`
            Content string `json:"content"`
        } `json:"message"`
        Index int `json:"index"`
    } `json:"choices"`
}

func callOpenAI(prompt string) (string, error) {
    // Prepare the request
    reqBody := &OpenAIRequest{
        Model: "gpt-3.5-turbo", // Or whatever model you're using
        Messages: []ChatInput{
            {
                Role:    "user",
                Content: prompt,
            },
        },
    }
    jsonReqBody, err := json.Marshal(reqBody)
    if err != nil {
        return "", err
    }

    req, err := http.NewRequest("POST", openaiApiUrl, bytes.NewBuffer(jsonReqBody))
    if err != nil {
        return "", err
    }

    // Add necessary headers
    req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    // Make the request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    // Parse the response
    var respBody OpenAIResponse
    err = json.NewDecoder(resp.Body).Decode(&respBody)
    if err != nil {
        return "", err
    }

    // Return the generated text
    return respBody.Choices[0].Message.Content, nil
}

func main() {
    webApi := slack.New(
        os.Getenv("SLACK_BOT_TOKEN"),
        slack.OptionAppLevelToken(os.Getenv("SLACK_APP_TOKEN")),
        slack.OptionDebug(true),
        slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)),
    )
    socketMode := socketmode.New(
        webApi,
        socketmode.OptionDebug(true),
        socketmode.OptionLog(log.New(os.Stdout, "sm: ", log.Lshortfile|log.LstdFlags)),
    )
    authTest, authTestErr := webApi.AuthTest()
    if authTestErr != nil {
        fmt.Fprintf(os.Stderr, "SLACK_BOT_TOKEN is invalid: %v\n", authTestErr)
        os.Exit(1)
    }
    selfUserId := authTest.UserID

    go func() {
        for envelope := range socketMode.Events {
            switch envelope.Type {
            case socketmode.EventTypeEventsAPI:
                // イベント API のハンドリング
                socketMode.Ack(*envelope.Request)

                eventPayload, _ := envelope.Data.(slackevents.EventsAPIEvent)
                switch eventPayload.Type {
                case slackevents.CallbackEvent:
                    switch event := eventPayload.InnerEvent.Data.(type) {
                    case *slackevents.MessageEvent:
                        if event.User != selfUserId {
                            // Call the OpenAI API
                            prompt := event.Text
                            response, err := callOpenAI(prompt)
                            if err != nil {
                                log.Printf("Error calling OpenAI API: %v", err)
                                continue
                            }

                            // Send the response to Slack
                            _, _, err = webApi.PostMessage(event.Channel, slack.MsgOptionText(response, false))
                            if err != nil {
                                log.Printf("Error sending message to Slack: %v", err)
                            }
                        }
                    default:
                        socketMode.Debugf("Skipped: %v", event)
                    }
                default:
                    socketMode.Debugf("unsupported Events API eventPayload received")
                }
            }
        }
    }()
    socketMode.Run()
}

このコードがしていることを、改めてChatGPTに説明してもらったところ、以下のような形で返してくれました。

  1. リアルタイムのメッセージを Slackから受け取る
  2. 自分自身からのメッセージを無視する
  3. 受け取ったメッセージをOpenAI APIに送信し、会話を生成する
  4. 生成された会話をSlackに投稿する

こうして、何も知らない状態からChatGPTを使ってChatGPTと会話できるチャットボットを構築することができました。

まとめ

自分のしたことは以下の通りです。

  1. コメントで「ここを実装して」と伝える
  2. 実装する
  3. エラー文をどうすれば良いか聞く
  4. (同上)
  5. (同上)
  6. 動作するチャットボットが完成する

一連の実装は2時間程度で完了しました。

おわりに

ChatGPTを用いてChatGPTと会話できるチャットボットを構築しようと思い、実装を頼み、エラー文を次々に入力する。 この繰り返しのみで、ほとんど知らないGolangを用いたチャットボットが簡単に構築できました。

個人的に良かった体験としては以下のような形になります。

  • APIリクエストを送る処理をほとんど提示されたコードで動かせた
  • エラー文から要因を探すまでの作業時間を大きく短縮できた
  • ブラウジング機能により、エンドポイントやリクエストボディの変更を検知し、その対応策をきちんと教えてくれた
  • コードが何をしているのかの要約を出せるので、自身の学習にも役立った

上記のような体験をするために、自分自身が感じたChatGPT活用のポイントは以下の通りです。

  • 最初にコメントに実装してほしい部分を書いたソースコードを提示すると、そこの部分を書き変える形で実装してくれるので安定する
  • エラー文やドキュメントを適切に伝えることで、より的を得た回答をしてくれる
  • 「最新の」「2023年の」などの単語を用いて、ブラウジング機能を用いて回答してもらうとより正確度が上がりそうに感じられた
  • 実装してもらったコードの要約・解説を頼むことで、効率的に学習ができそうに思えた

ここで得られた知見を元に、自動議事録作成や予定管理、実際の業務に活用できそうなツール類の実装に向けてさらに活用していこうと思っています。

アバター画像

長島大和

目次