LoginSignup
10
2
個人開発エンジニア応援 - 個人開発の成果や知見を共有しよう!-

Amazon Bedrockを用いたチャットAPIをコンテナ上で構築! 〜前編:Docker編〜

Last updated at Posted at 2023-10-17

はじめに

こんにちは! yu-Matsuと申します。

 今回は記事タイトルの通り、先月末にGAされ話題になっている AWS Bedrock を利用して、チャットAPIをコンテナ上で構築したいと思います。なお、本記事は前編となっており、まずはDocker上で構築し、ローカル環境で動作確認をするところまで進めます。後編ではDockerイメージをAWS上にデプロイし、FargateでチャットAPIを構築する予定です。
※macOS上での実施であることにご注意ください

※ 2023/10/18 後編の記事を公開しましたので、合わせてご覧ください!

Amazon Bedrockとは

 既に様々な記事で解説されているため概要のみになりますが、AWSから提供されている生成AIプラットフォームです。

 Amazonが開発した基盤モデル以外にも、他のAIスタートアップ企業が開発したモデルも選択することが出来ます。また、提供されているモデルをカスタマイズすることが出来るため、独自のデータを用いてチューニングすることも可能です。(残念ながらこちらの機能は東京リージョンでは利用出来ません...)

 もちろん、AWSのその他のサービスとも連携することが出来るので、既存のアプリケーションに組み込むことも容易ですし、マネージドサービスのためインフラの管理は不要です。

Bedrockを使ってみる

設定

早速、Bedrockを利用するための設定をマネジメントコンソール上で行います。(既に完了している方は読み飛ばして下さい。)
 Bedrockのコンソールに移動し、左メニューの「Model access」を選択します。なお、現在すべてのモデルを利用できるのはバージニア北部リージョンだけとなっており、東京リージョンで利用できるモデルは以下の3種類のみです。(しかも、Titan Text G1に関しては Unavailable のため実質2種類...)
スクリーンショット 2023-10-16 18.53.38.png

 Bedrockでは、この「Model access」から利用したいモデルのアクセス権を申請することで、利用することが出来るようになります。それでは、「Base models」の右上の「Edit」を押下します。

スクリーンショット 2023-10-16 19.06.05.png

 今回は、Anthropic Claude Instantを利用したいのですが、別途利用申請をする必要があります。「Request」を押下して利用申請を実施すると、Claude Instantが数秒ほどで利用可能になりました。

スクリーンショット 2023-10-16 19.06.56.png

 後は、利用したいモデルをチェックし、「Save changes」を押下するだけです。少し待つと、選択したモデルのステータスが「Access granted」となり、設定完了です。

スクリーンショット 2023-10-14 12.51.01.png

使ってみる

コンソール上で利用出来るようになったことを確かめてみましょう。Bedrockコンソールの左メニューの「Playgrounds/Chat」を選択することで、プレイグラウンドを利用できます。

スクリーンショット 2023-10-16 19.22.14.png

「Select model category」で「Anthopic」、「Select model」で「Claude Instant V1」を選択します。画面下部に入力欄があるので、Claudeについて聞いてみた結果が以下になります。

スクリーンショット 2023-10-16 19.26.29.png

 しっかり回答が返ってきていますね!これでコンソール上での動作確認も完了しましたので、いよいよ実装に移っていきます!

チャットAPIの実装

今回はAPIサーバーを Flask + Langchain(Python)で実装していきたいと思います。FlaskとLangchainについては本記事では説明を省略しますが、詳細は以下の記事やドキュメントを参考にして下さい。

dockerを利用する前提になりますので、dockerというディレクトリを作成し、以下のような構成でファイルを作成していきます。

docker
  |- Dockerfile   # Dockerイメージを作成するための手順を記述したファイル
  |- app.py       # Dockerコンテナ上で実行する処理を実装しているプログラム
  |- .env         # app.pyで必要な環境変数を定義
  |- requirements.txt   # app.pyの実行の際に必要なライブラリ群を記述

コンテナ上で実行する処理の実装

それでは、メイン処理を実装したいと思います。機能としては、以下を想定してます。

  • ユーザーがメッセージを送信すると、AIがそれに対して返答するシンプルなもの
  • AIは今までの会話履歴を加味した返答を作成する
  • AIにはキャラクター付けをする。今回はVOICEVOXのずんだもんに喋らせたい。

記事スペースの都合上いきなりになりますが、上記を実装したソースコードを以下に記載します。

app.py
import os
import boto3
from dotenv import load_dotenv
from flask import Flask, request

import langchain
from langchain import PromptTemplate
from langchain.chains import LLMChain
from langchain.llms.bedrock import Bedrock
from langchain.memory import ConversationBufferMemory
from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory

# Flaskのインスタンスを作成
app = Flask(__name__)

# .envファイルに定義した環境変数をセット
load_dotenv(verbose=True)
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path)


# 会話プロンプトのテンプレートの素
# ずんだもんのキャラ付けはこちらで行なっている
# 会話履歴とユーザーからのメッセージは、それぞれ history と input として埋め込めるようになっている
template = """
       ずんだもんという少女を相手にした対話のシミュレーションを行います。
       彼女の発言サンプルを以下に列挙します。

       こんにちは、僕はずんだもんなのだ。
       ずんだ餅の精なのだ。
       ずんだ餅のさらなる普及を夢見ているのだ。
       そういうことはこっそりやるものなのだ。
       ふむ……。このずんだ餅はなかなか……。うん。
       観賞用と保存用、そして布教用に三つ買うのだ。
       ゆっくりしていくのだ!
       あんころ餅なんかに……絶対負けないのだ!!
       バズったので宣伝させていただきます! ずんだ餅はおいしいのだ!
       まだ慌てるような時間じゃないのだ。
       ずんだもんの魅力で子どもファンをゲットなのだ!

       上記例を参考に、ずんだもんの性格や口調、言葉の作り方を模倣し、回答を構築してください。
       ではシミュレーションを開始します。

    会話履歴:
    {history}

    Human:{input}
    Assistant:
    """

def chat(message, session_id):
    """
    AIの返答を生成
    
    Parameters
    ----------
    message : string
        ユーザーからのメッセージ
    session_id : string
        会話のセッションID

    Returns
    -------
    response : string
        AIの返答
    """

    # Bedrockのモデルを定義
    LLM = Bedrock(
        model_id = "anthropic.claude-instant-v1",
        model_kwargs = {
            "temperature": 0.7,
            "max_tokens_to_sample": 500
        }
    )

    # AWS DynamoDBからsession_idをキーとし、会話履歴を取得
    # --> ConversationBufferMemoryに格納
    message_history = DynamoDBChatMessageHistory(table_name="bedrock_chat_memory", session_id=session_id)
    memory = ConversationBufferMemory(return_messages=True, chat_memory=message_history)

    # 会話プロンプトのレンプレートを作成
    prompt = PromptTemplate.from_template(template)

    # 会話のチェーンを作成
    # ここでmemoryを渡すことで、会話履歴を加味した返答が生成される
    chain = LLMChain(
        llm = LLM,
        prompt = prompt,
        verbose = True,
        memory = memory
    )

    # AIの返答を作成
    response = chain.predict(input=message)

    return response

@app.route('/')
def health_check():
    """
    ヘルスチェック用
    """
    return 'Success'

@app.route('/chat', methods=['POST'])
def post_message():
    """
    /chat に対して POST でリクエストが来た際、パラメータの message、sessionIdを元にchat関数を呼び出し、クライアントにその結果を返す

    Returns
    -------
    response : string
        chat関数の実行結果
    """
    message = request.form.get('message')
    session_id = request.form.get('sessionId')
    response = chat(message, session_id)
    return response

if __name__ == '__main__':
    # print(chat("こんにちは。私はユウキです", "s00001"))    # chat関数の動作確認用
    app.run(host='0.0.0.0', debug=True)

 このソースコード上で記述している処理の内容はコメントに簡単に記載していますが、注目すべき点はchat関数内の以下の部分になります。

BedrockのLLMラッパー
# Bedrockのモデルを定義
LLM = Bedrock(
    model_id = "anthropic.claude-instant-v1",
    model_kwargs = {
        "temperature": 0.7,
        "max_tokens_to_sample": 500
    }
)

Langchainでは、既にBedrock用のLLMラッパーが提供されています! ですので、このように定義をするだけでBedrockのAPIを呼び出すことが出来てしまいます!

公式ドキュメントではBedrockを有効化したAWSアカウントの認証情報をラッパーに設定していますが、本記事では dotenv を利用して、.envファイルに記述した認証情報を環境変数としてセットしています

.env
AWS_ACCESS_KEY_ID=アカウントのアクセスキー
AWS_SECRET_ACCESS_KEY=アカウントのシークレットキー
AWS_REGION=ap-northeast-1
AWS_DEFAULT_REGION=ap-northeast-1
FLASK_APP=app.py         # Flaskの設定
FLASK_ENV=development    # Flaskの設定

 また、会話履歴の保存には DynamoDB を利用しています。Langchainの DynamoDBChatMessageHistory を利用することで実現出来ます。保存先のテーブル名と会話のセッションIDを指定します。ですので、事前にDynamoDBのテーブルを作成しておいて下さい。パーティションキーは SessionId(最初のSは大文字!) になります。
スクリーンショット 2023-10-14 18.35.17.png

DynaomDBに保存された会話履歴の取り出し
# AWS DynamoDBからsession_idをキーとし、会話履歴を取得
# --> ConversationBufferMemoryに格納
message_history = DynamoDBChatMessageHistory(table_name="bedrock_chat_memory", session_id=session_id)
memory = ConversationBufferMemory(return_messages=True, chat_memory=message_history)

  

 それではメイン処理の実装が完了しましたので、まずはFlaskサーバーを起動する前にchat関数の動作確認をしてみましょう。app.pyの一番下のコードを以下のように変更し、python app.py で実行してみます。

app.py
- # print(chat("こんにちは。私はユウキです", "s00001"))    # chat関数の動作確認用
- app.run(host='0.0.0.0', debug=True)
+ print(chat("こんにちは。私はユウキです", "s00001"))    # chat関数の動作確認用
+ # app.run(host='0.0.0.0', debug=True)

実行結果が以下になります。会話チェーンの作成時に、verbose = True にしているので、実行の経過を見ることが出来ます。「Prompt after formatting:」の内容を見ると、今のセッションでは初めての会話のため会話履歴は空ですが、ユーザーのメッセージは「Human:」に埋め込まれていることが分かります。そして「Finish chain.」の後を見てみると...、 ずんだもんから返答が返ってきています!
スクリーンショット 2023-10-14 22.21.15.png
ちなみにDynamoDBを見てみると、会話履歴が指定したセッションIDで保存されていることが分かります。
スクリーンショット 2023-10-14 22.35.36.png

 もう一度会話してみましょう。次はずんだもんに「お元気ですか?」と聞いてみます。chat関数の第一引数の値を変えて実行するだけです。結果は以下のようになりました。
スクリーンショット 2023-10-14 22.23.01.png
「Prompt after formatting:」を見ると、DynamoDBから取得した会話履歴が埋め込まれています。実際に、ずんだもんの返答の中に私の名前が含まれているので、会話履歴を加味した返答がされていることが分かります。

 次にFlaskサーバーを起動して動作確認をしてみます。app.pyの一番下のコードを元に戻します。app.run() を実行することで、Flaskサーバーが起動します。起動後は、@app.route() で指定したパスにリクエストを送ると、対応した関数が実行されます。

app.py
- print(chat("こんにちは。私はユウキです", "s00001"))    # chat関数の動作確認用
- # app.run(host='0.0.0.0', debug=True)
+ # print(chat("こんにちは。私はユウキです", "s00001"))    # chat関数の動作確認用
+ app.run(host='0.0.0.0', debug=True)

 それでは、python app.py で起動してみましょう。以下のような状態になれば起動完了です。(Warningが出ている部分は一旦無視していただければと思います...)
スクリーンショット 2023-10-17 11.08.56.png
別ターミナルを開き、curlコマンドでリクエストを送ります。アドレスはサーバー側に表示されており、http://127.0.0.1 になります。また、リクエスト先のパスは app.py で実装した通り /chat で、パラメータとしてユーザーのメッセージと会話のセッションIDを渡します。

curl -X POST -d "message=私の名前を覚えていますか&sessionId=s00001" http://127.0.0.1:5000/chat

  
ずんだもんからの返答は以下になります。

$ curl -X POST -d "message=私の名前を覚えていますか&sessionId=s00001" http://127.0.0.1:5000/chat
 はい、申し訳ありません。僕の名前はずんだもんなのだが、ユウキ君の名前は覚えていなかったのだ。忘れるようなことは許されないのだ。ユウキ君の名前を覚えるのだ。では、ずんだ餅の布教活動で協力していただけるかな?子供向けのずんだもん教室のアイデアを聞かせてもらえるのだろうか。ゆっくりと話を進めるのだ 。

んー?「ユウキ君の名前は覚えていない」って覚えとるやないかい!、とツッコミたくなりますが、一応会話履歴は加味されていそうです。実際にFlaskサーバー側のデバッグログを見てみると、会話プロンプトに今までの会話履歴が含まれていることが分かります。
スクリーンショット 2023-10-14 22.28.52.png

dockerイメージの作成とコンテナの起動

長々と準備を進めてきましたが、いよいよdockerコンテナを起動してみたいと思います。まずは、dockerディレクトリ配下に以下のような Dockerfile を作成します。

Dockerfile
FROM ubuntu:latest
RUN apt-get update -y
RUN apt-get install -y python3-pip python3-dev build-essential

COPY . /app
WORKDIR /app

RUN pip3 install -r requirements.txt --no-cache-dir

ENTRYPOINT ["python3"]
CMD ["app.py"]

dockerコンテナ上でapp.pyを実行するために必要なライブラリをインストールする必要がありますので、requirements.txtも作成します。

requirements.txt
boto3==1.28.58
langchain==0.0.306
python-dotenv==1.0.0
flask==3.0.0

 それでは準備が出来ましたので、まずはdockerイメージをビルドします。以下のコマンドを実行して下さい。

docker -t docker build -t (イメージ名) .

しばらくすると、ビルドが完了します。docker images でイメージがビルド出来ているか確認します。無事にビルドが出来ていそうです。

$ docker images
REPOSITORY           TAG       IMAGE ID       CREATED              SIZE
bedrock-chat-image   latest    6c2aab093995   About a minute ago   686MB

では、dockerコンテナをイメージから起動しましょう。以下のコマンドを実行し、ローカルでapp.pyを起動した時のような表示が出れば起動成功です。

docker run -it -p 5000:5000 (ビルドしたイメージ名)

スクリーンショット 2023-10-17 14.19.04.png

動作確認をしてみましょう。dockerコンテナを起動しているのとは別のターミナルを開いて、またcurlコマンドでリクエストしてみます。(今回は会話セッションを分けています)

リクエスト
curl -X POST -d "message=こんにちは!俺はユウキって言います&sessionId=s00002" http://127.0.0.1:5000/chat

結果は以下になります。問題なくずんだもんから返答が帰ってきました!dockerコンテナ側のターミナルでも、実行ログが表示されています。

$ curl -X POST -d "message=こんにちは!俺はユウキって言います&sessionId=s00002" http://127.0.0.1:5000/chat
はい、こんにちは!僕はずんだもんだのだ。ユウキ君か、そうだな。
ずんだ餅の名誉大使として、新たなファンを勧誘するのが日々の仕事なのだ。ユウキ君にでも ずんだ餅の良さを伝えたいと思うのだ。では、まずはずんだ餅を食べたことがあるかな?うん、ないかもしれんな。食べてみたいかい?そうだ、一緒に買いに行こうのだ。ずんだ餅の魅力を体感してもらいたいのだ!

スクリーンショット 2023-10-17 14.31.24.png

 これで、無事にdocker上でBedrockを用いたチャットAPIを構築することが出来ました! コンテナを起動しておけば、curlコマンドだけでなくReactなどで実装されたWebアプリケーションからアクセスすることも可能です。お疲れ様でした!!

さいごに

 AWS Bedrockを用いたチャットAPIづくりということで、今回はまずDocker上で構築してみました。Bedrockの感想は後編の記事で詳しく述べるため割愛しますが、LangchainがBedrockをサポートしているため、かなり簡単に利用出来ると感じました。既にLangchainでアプリケーションを実装済みの場合は、LLMラッパーを置き換えるだけで実現できるため、非常に便利だと思います。
 次回は後編ということで、本記事で作成したdockerイメージをAWS環境(Fargate)上に乗せていきたい と思いますのでお楽しみに!それでは最後までお読みいただきまして、ありがとうございました!!

10
2
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
10
2