💰

GPT-4 モデルを使用した Slack Bot を 月1ドル以下で運用する

2023/12/22に公開

はじめに

GPT-4を使いたくてChatGPT Plus($20)を契約してみたものの、元を取るほどには使わなかったので、安く使うためにAWS LambdaにGPT-4 Turbo(preview)を使ったSlack Botを作ってみました。

構成図

ポイントは以下のとおりです。

  • FaaS (AWS Lambda) を使い、コンピューティング費用を抑える
  • Slack Botにすることで、対話のための UI を作らなくてすむ(Slack無料プランでも利用可)
  • OpenAIのAPIを使う

今回使用したソースコードは以下に公開しています。
https://github.com/festiva1300/slack-chatbot-lambda

必要なもの

  • AWSのアカウント
  • Slackワークスペース
  • OpenAIのアカウント

利用技術について

Slack Bolt

SlackのSDKであるSlack Boltには、以下の2つのモードがあります。

  • HTTPモード
  • ソケットモード

HTTPモードは以前から存在する方法で、Slackで何らかのイベントが発生した際に、HTTPエンドポイントを呼び出して処理する方法です。インターネット経由でアクセス可能なHTTPエンドポイントを公開しておく必要があります。

ソケットモードはエンドポイントを公開する必要がないかわりに、何らかのインスタンスを常に起動しておきSlackと接続しておく必要があります。

WebSocket のコネクションを常時クラウド上のインスタンスで立ち上げると費用がかかるので、今回はHTTPモードを使用します。
具体的にはHTTPエンドポイントを作成するために、API GatewayとLambdaを使用します。

https://slack.dev/bolt-python/ja-jp/tutorial/getting-started

Lazy Listeners

ここで課題となるのが Lambda の手前に置くAPI Gateway や Slack の制約です。
API GatewayとSlack(HTTPモード)には以下の制約があります。

  • API Gatewayのタイムアウト時間は 最大30秒
  • Slack Botのタイムアウト時間は 最大3秒

GPT-4のような時間がかかる処理は、上記の時間内に終了することができません。
そこで利用するのが Slack Boltの機能である 「Lazy Listeners」 です。
簡単に言えば、Slackのタイムアウト発生前に一旦レスポンスを返し、その後に自分自身(Lambda)を非同期に呼び出して回答を生成して返すという機能です。

Lazy Listenersについては以下の記事を参考にさせていただきました。

https://dev.classmethod.jp/articles/bolt-lambda/

https://qiita.com/seratch/items/6d142a9128c6831a6718

仕様

仕様はおおむね以下の通りです。

  • @chatbot など、botアプリケーションへのメンションに対してスレッドで返信する
  • 開始したスレッドにユーザが投稿した際は、メンションがなくてもさらに返信する
  • スレッドの会話内容を保存して返信に利用する

会話内容はDynamoDBに保存しますが、トークン数の節約のため24時間以上、または20個以上前の会話内容は忘れます。

実装

中心となるコードは以下のようになります。
メンションおよびメッセージ(スレッドのリプライ)に対して返答します。
3秒以内に respond_to_slack_within_3_seconds でレスポンスを返し、その後それぞれの非同期処理を実行します。

app.py
import logging
import os
import time

import boto3
import openai
from slack_bolt import App

# (中略)

MODEL = os.environ["OPENAI_MODEL"]
SYSTEM_CONTENT = "You are an excellent assistant."
MAX_HISTORY = 20
OPENAI_API_TIMEOUT = 50

# OPENAIのAPI KEY
api_key = os.environ["OPENAI_API_KEY"]

app = App(
    # リクエストの検証に必要な値
    # Settings > Basic Information > App Credentials > Signing Secret で取得可能な値
    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
    # 上でインストールしたときに発行されたアクセストークン
    # Settings > Install App で取得可能な値
    token=os.environ["SLACK_BOT_TOKEN"],
    # AWS Lamdba では、必ずこの設定を true にしておく
    process_before_response=True,
)

def respond_to_slack_within_3_seconds(body, ack):
    # リスナーの処理を 3 秒以内に完了
    logging.debug(body)
    ack()


# メンションに反応するように設定
app.event("app_mention")(ack=respond_to_slack_within_3_seconds, lazy=[process_mention])

# メッセージに反応するように設定
app.message()(ack=respond_to_slack_within_3_seconds, lazy=[process_message])

if __name__ == "__main__":
    # python app.py のように実行すると開発用 Web サーバーで起動します
    app.start()

# これより以降は AWS Lambda 環境で実行したときのみ実行されます
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

# ロギングを AWS Lambda 向けに初期化します
SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)


# AWS Lambda 環境で実行される関数
def handler(event, context):
    # AWS Lambda 環境のリクエスト情報を app が処理できるよう変換してくれるアダプター
    slack_handler = SlackRequestHandler(app=app)
    # 応答はそのまま AWS Lambda の戻り値として返せます
    return slack_handler.handle(event, context)

遅延実行されるイベントリスナーの詳細はGithubにあるソースコードを参照してください。

https://github.com/festiva1300/slack-chatbot-lambda

デプロイ

以下の手順でアプリケーションのデプロイを行います

  1. Slackアプリの作成
  2. アプリケーションのデプロイ
  3. Slackアプリの設定変更

Slackアプリの作成

まずSlackアプリを作成します。以下に手順を示します。

  • 以下のリンクから「Create New App」を選択します。
    https://api.slack.com/apps

  • 「From an App manifest」を選択します

  • インストールするワークスペースを選択します

  • マニフェストに以下の内容を記載してアプリケーションを作成します(request_urlは後で変更します)

manifest.yaml
display_information:
  name: lambda-chatbot-app
features:
  bot_user:
    display_name: chatbot
    always_online: true
oauth_config:
  scopes:
    bot:
      - chat:write
      - chat:write.public
      - app_mentions:read
      - channels:history
settings:
  event_subscriptions:
    request_url: https://example.execute-api.us-east-1.amazonaws.com/slack/events
    bot_events:
      - app_mention
      - message.channels
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
  • アプリケーションの一覧から作成したアプリケーションを選択し、Signing SecretBot User OAuth Token をメモします。
    • Signing Secret は左ペインのBasic Informationを選択すると表示されます
    • Bot User OAuth Token は OAuth & Permissions を選択すると表示されます

これでSlackアプリができました。

OpenAIのアクセスキーを取得

OpenAIのAPIを使うには、アクセスキーが必要です。
アカウントがない場合は、まず以下のリンクの「Sign Up」からアカウントを登録してください。
アカウントの登録には、メールアドレスと携帯電話番号が必要です。
https://platform.openai.com/

  • ログインしたら「API」を選びます

  • 左側のメニューから「API Keys」を選び、新しいキーを作成します

キーを発行したらメモしておきます。悪用されないように管理には充分注意してください。

前払いのチャージ

なお、2023年11月時点では、作成したばかりのアカウントでGPT-4 Turboのモデル(gpt-4-1106-preview)を利用するには、あらかじめ前払いでいくらかチャージしておく必要がありました。

クレジットのチャージは以下から実施できます。

  • 左側メニューの「Setting」→「Billing」を選択する

  • 「Payment methods」でクレジットカードを登録

  • 「Buy credits」でいくらかチャージ(私は$10チャージしました)

クレジットカードを登録したら、Usage Limitsで上限を低めに設定しておくと安心です。

アプリケーションのデプロイ

次にAWS Lambdaをデプロイします。Lambdaを呼び出すためのAPI Gatewayもあわせて作ります。

Serverless Frameworkのインストール

デプロイにはNode.jsのパッケージマネージャnpmとServerless Frameworkを使用します。
https://nodejs.org/en
https://www.serverless.com/

また、必要なpythonパッケージを同梱するためのプラグインをあらかじめインストールします。

$ sls plugin install -n serverless-python-requirements

必要なパッケージはrequrements.txtに記述します。

requrements.txt
slack-bolt
python-lambda
python-dotenv
openai >= 1.0
boto3

アクセスキーの設定

.env という名前のファイルを作成し、SlackのSigning SecretBot User OAuth Token、OpenAIのAPI Keysを記載します。

.env
SLACK_SIGNING_SECRET=xxxxx
SLACK_BOT_TOKEN=xxxxx
OPENAI_API_KEY=xxxxx

ソースコードを構成管理する場合、.envファイルをリポジトリに含めないように気をつけましょう。とくにGitHubなどの公開リポジトリで管理する場合は注意しましょう。

.gitignore
.env

Lambdaのデプロイ

また、serverless.ymlファイルにデプロイのための情報を記述します。
ここではGPT-4 Turboのモデル gpt-4-1106-preview を使用するように記述していますが、GPT-3.5 (gpt-3.5-turbo-1106) なども利用可能です。

serverless.yml
frameworkVersion: '3'

useDotenv: true

service: lambda-chatbot-app
provider:
  name: aws
  runtime: python3.10
  region: us-east-1
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - lambda:InvokeFunction
            - lambda:InvokeAsync
          Resource: "*"
        - Effect: 'Allow'
          Action:
            - 'dynamodb:*'
          Resource: "*"

  environment:
    SERVERLESS_STAGE: ${opt:stage, 'prod'}
    OPENAI_MODEL: 'gpt-4-1106-preview'
    SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET}
    SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN}
    OPENAI_API_KEY: ${env:OPENAI_API_KEY}
functions:
  app:
    handler: app.handler
    timeout: 180
    events:
      - httpApi:
          path: /slack/events
          method: post

resources:
  Resources:
    ExampleTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: "lambda-chatbot-app-history"
        AttributeDefinitions:
          - AttributeName: "id"
            AttributeType: "S"
          - AttributeName: "timestamp"
            AttributeType: "N"
        KeySchema:
          - AttributeName: "id"
            KeyType: "HASH"
          - AttributeName: "timestamp"
            KeyType: "RANGE"
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

package:
  patterns:
    - "!.env"
    - "!.venv/**"
    - "!.secret/**"
    - "!node_modules/**"

plugins:
  - serverless-python-requirements
custom:
  pythonRequirements:
    #zip: true
    #slim: true

準備ができたらアプリケーションをデプロイします。

$ sls deploy

非同期に実行されるときのためにlambdaのタイムアウト時間を長めに設定しているため、API Gatewayのタイムアウト時間(30秒)より長いと警告が出ますが無視してください。

作成されたendpointのURLをメモしておきます。

Slackアプリの設定変更

SlackアプリのApp ManifestのURLに設定し、アプリをインストールしなおします。

  • 以下のリンクから先ほど作成したアプリケーションを選択します。
    https://api.slack.com/apps

  • 左メニューの「App Manifest」を選択し、request_url を先ほどメモしたendpointのURLに書き換えて保存します。

動作確認

Slackのワークスペースから適当なチャンネルでchatbotに話しかけてみます。
しばらく待って返信があれば成功です。

費用

  • 週に数回会話するくらいだと、OpenAIの使用料は1か月で$1以下でした。

  • AWS Lambda/API Gatewayは無料枠の範囲に収まっています

どなたかの参考になれば幸いです。

追記(2023/12/26)

OpenAI SDK 1.x向けに修正しました。

https://github.com/festiva1300/slack-chatbot-lambda

Discussion