電通総研 テックブログ

電通総研が運営する技術ブログ

ChatGPTを利用するためのサーバーレスアプリケーションをAWS上に構築する

こんにちは!金融ソリューション事業部の孫です。
現在、我々はChatGPT全般の活用を積極的に検討しており、その多様な可能性の中にはメタバースにおけるChatGPTの利用も含まれています。
この観点から、AWS上にサーバーレスアーキテクチャを採用したChatGPT活用アプリケーションの設計と構築を実施いたしました。

はじめに

ChatGPTベースのアプリケーションを構築する際には、2つの一般的な選択肢があります。
一つは既存のアプリケーションにOpenAI APIを直接統合すること、もう一つはOpenAI APIをラップしてインフラサービス化にすることです。
今回は、以下の点を考慮して、後者の選択をしました:

  1. セキュリティリスク:アプリケーション側でOpenAI API Keyを管理する必要があること
  2. コスト管理:OpenAIとのやり取りは有料で、ユーザーの質問の頻度と単語数を制御する必要があること
  3. 再利用可能性:開発したソリューションは他のプロジェクトに適用可能で、コストを節約できること

今回は、インフラストラクチャを設計する前に、以下の要件を明確にしました:

  • 複数のユーザーが同時に一つのOpenAIアカウントを使用できること
  • 従量課金制を実装し、コストを削減すること
  • サーバーの維持コストを削減すること
  • APIトラフィック制御を実装すること
  • OpenAIへのリクエストの同時実行数を制限すること

これらの要件に基づいて、今回は以下のインフラストラクチャ設計を完成させました。

インフラストラクチャの全体像

本記事では、下記の構成図に基づき、構築作業を進めていきます。

AWSアーキテクチャ図

  • WebSocket API Gateway:クライアントのリクエストをバックエンドサービスにルーティングするために使用します。
  • meta_connect_handler:ユーザーの認証機能を提供します。
    • 本記事では、インフラ構築に焦点を当てるため、認証に関する実装は対象外としております
  • meta_handler_chat:ユーザーのリクエストをAmazon SNSサービスに転送する機能を提供します。
    • ユーザーのリクエストはSNSを介して処理されるため、OpenAIサーバーへの即時リクエストが不要となり、非同期処理になります
  • meta_chat:ユーザーからのリクエストをOpenAIサーバーに送信し、その結果を受け取る機能を提供します。
    • 本記事においては、LangChain(version: 0.0.317)フレームワークを活用して、OpenAI APIを介してOpenAIとのやり取りを行います
    • LangChainフレームワークを使用することで、OpenAI APIを直接利用することなく、LLM(Large Language Models)アプリケーションの開発を高速化できます
    • ただし、LangChainのバージョン更新は頻繁に行われるため、本記事で使用しているAPIが将来的に非推奨(deprecated)になる可能性にご注意ください。そのため、特定のバージョンを固定することをお勧めします。
  • meta_disconnect_handler:ユーザーがログアウトした後、その会話履歴を削除する機能を提供します。
  • Dynamodb:ユーザーの会話履歴を保存するテーブルを作成します。
  • SNS:ユーザーのリクエストを非同期に処理するために使用します。

上記のAWSアーキテクチャ図に記載した番号①〜⑨に沿って、処理の流れを説明します。

①: ユーザーはWebSocket URL接続を介してChatGPTサービスの利用を開始する
②: 接続は最初にWebSocketのデフォルトの/$Connectルートにルーティングされ、meta_connect_handlerで接続の認証を実施する
③: ユーザーは/sendpromptルートを通じてチャット内容をmeta_handler_chatに送信する
④: meta_handler_chatが処理した後、ユーザーリクエストはSNSサービスのトピックに送信する
⑤:SNSはユーザーリクエストをサブスクリプションmeta_chat Functionに送信する
⑥: meta_chatLangchainを利用し、OpenAIとの対話機能をそろえ、会話の文脈はDynamoDBに保存される
⑦: OpenAIサーバーは質問回答を返し、WebSocket接続を介してユーザーに返却する
⑧: ユーザーがLogoutしたら、/$disconnectルートがトリガーされる
⑨: meta_disconnect_handlerはDynamoDBのユーザー会話の文脈を削除し、プロセスが終了する

実装手順

構築手順

以下の手順で構築を行います。

  1. Lambda関数の作成
  2. DynamoDBの作成
  3. WebSocket API Gatewayの作成
  4. SNSトピックの作成

事前準備

  1. NodeJS開発環境のセットアップ
    • ローカルにNode.jsをダウンロードしてインストールします(18.xバージョンを推奨)
    • 本記事で提供されたコードは、Lambdaにコピーすることでそのままクラウド環境でビルド・運用が可能です
    • ただし、後述するLangChainレイヤーの作成プロセスにおいてローカル環境でLangChainパッケージをダウンロードする必要があり、その際にはNodeJS環境が使用されることにご注意ください
  2. AWSアカウントの作成
    • AWSアカウントを持っている場合は、対応不要です
    • AWSアカウントをお持ちでない場合は、カウント作成の流れを参考に、アカウントを作成してください
    • ※本検証を行う場合、使用するAWSリソースの利用料金が発生する点にご注意ください
  3. OpenAIアカウントの作成
    • OpenAI APIのアカウントを作成しAPI KEYを入手します
    • API KEYの発行手順はこちらを参照してください

手順1. Lambda関数の作成

  • meta_connect_handler
    AWS Lambdaコンソールでmeta_connect_handlerという名前のLambda関数を作成し、ランタイムとしてNode.js 18.xを選択します。
    ※上述した通り、本記事ではインフラ構築に焦点を当てるため、認証に関する実装は対象外です。デフォルトで生成されたコードをそのまま使用してください。

  • meta_handler_chat
    上記と同様にAWS Lambdaコンソールでmeta_handler_chatという名前のLambda関数を作成します。
    この関数はユーザーリクエストを受け取り、SNSサービスのトピックに送信する役割を果たします。
    この関数にSNSのPublic権限を追加し、環境変数SNSのARN値(SNS_TOPIC_ARN)を設定してください。

    具体的な実装コードは以下のとおりで、ユーザーのメッセージ(event.body.user_msg)を取得し、ユーザー接続情報とメッセージ内容を含むSNSのトピックを発行します。

// aws-sdkパッケージをインポートします
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

const client = new SNSClient();
const topicArn = process.env.SNS_TOPIC_ARN;

export const handler = async(event) => {
    let body;
    if (typeof event.body === 'string') {
        body = JSON.parse(event.body);
    } else {
        body = event.body;
    } 
    // ユーザー接続情報とメッセージ内容を含むSNSのトピックを発行します
    const command = new PublishCommand({
        TopicArn:topicArn,
        Message:JSON.stringify({
            requestContext:event.requestContext,
            payload: body.user_msg,
        })
    });
    
    let response;
    try{
         await client.send(command);
    }catch (error) {
         console.log("Error:", JSON.stringify(error));
    }
};
  • meta_chat
    同様な手順でAWS Lambdaコンソールでmeta_chatという名前のLambda関数を作成します。
    この関数はLangchainを使用してOpenAIサーバーとのインタラクションする役割を果たします。
    DynamoDBおよびAPIGatewayへのアクセス権限を追加し、OpenAI API KEYを環境変数として設定してください。

    また、Lambdaではlangchainパッケージがないため、ローカルでダウンロードし、Layerとして関数にアップロードする必要があります。
    具体的な手順は以下のとおりです:

    1. ローカルで npm install langchain を実行し、langchainをインストールします。
    2. 生成されたnode_moduleフォルダをZIPのパッケージ化します。
    3. ZIPファイルをLayerにアップロードし、関数でlayer依存関係を設定します。

    注意:OpenAIサービスの実行時間は3秒を超える可能性があるため、関数の実行時間設定を調整し(3秒 → 1分 )、タイムアウトの問題を回避してください。

    「コード」タブにて、下記のコードをコピーしてください。

// Langchainとaws-sdkパッケージをインポートします
import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationChain } from "langchain/chains";
import { BufferMemory } from "langchain/memory";
import { DynamoDBChatMessageHistory } from "langchain/stores/message/dynamodb";
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";

export const handler = async (event) => {
  console.log(JSON.stringify(event));
  
  const body = JSON.parse(event.Records[0].Sns.Message);
  //SNSのトピックから必要な情報(クライアントの接続情報、ユーザーの質問内容のpayload)を読み取ります
  const requestContext = body.requestContext;
  const connectionId = requestContext.connectionId;
  const user_request = body.payload;
  // apigateway クライアントの初期化
  const apigateway_client = new ApiGatewayManagementApiClient({
    endpoint: 'https://'+requestContext.domainName + '/' + requestContext.stage,
  });
 // 文脈保存先としてのDynamoDBを設定し、DynamoDBChatMessageHistoryを初期化します
  const chatHistory = new DynamoDBChatMessageHistory({
    tableName: "Conversations",
    partitionKey: "ConnectionId",
    sessionId: connectionId,
    config: {
      region: "ap-northeast-1",
    },
  });
  //
  const memory = new BufferMemory({
    chatHistory: chatHistory,
  });
  
  // OpenAI model の初期化
  const model = new ChatOpenAI({ 
    modelName: "gpt-3.5-turbo",
    temperature: 0.5 
  });

  // 初期化されたメモリおよびモデルを利用して、ConversationChainを初期化します。
  const chain = new ConversationChain({
    memory: memory,
    llm: model,
    verbose: true 
  });

  try {
    // OpenAIサーバーにリクエストを送信します
    const response = await chain.call({
        input: user_request,
    });
    // OpenAIからの回答を受け取り、それをクライアントに返却します
    const api_gateway_input = {
      ConnectionId: connectionId,
      Data: JSON.stringify(response)
    };
  
    const command = new PostToConnectionCommand(api_gateway_input);
    await apigateway_client.send(command);
    
  } catch (error) {
    console.log("Error:", JSON.stringify(error));
  }
  
};
  • meta_disconnect_handler
    meta_disconnect_handlerという名前のLambda関数を作成し、主にユーザーの会話履歴をクリアするために使用されます。
    meta_chat と同様に、DynamoDBへのアクセス権限を追加してください。

    具体的なコードは以下のとおりです。

// Langchainパッケージをインポートします
import { BufferMemory } from "langchain/memory";
import { DynamoDBChatMessageHistory } from "langchain/stores/message/dynamodb";

export const handler = async (event) => {
  console.log(JSON.stringify(event));
  
  const requestContext = event.requestContext;
  const connectionId = requestContext.connectionId;
  
  const chatHistory = new DynamoDBChatMessageHistory({
    tableName: "Conversations",
    partitionKey: "ConnectionId",
    sessionId: connectionId,
    config: {
      region: "ap-northeast-1",
    },
  });

  const memory = new BufferMemory({
    chatHistory: chatHistory,
  });
  //DynamoDBから該当するユーザーの会話文脈を削除します
  await memory.chatHistory.clear();
};

手順2. DynamoDBの作成

AWS管理コンソール上でAmazon DynamoDBコンソールを開き、Conversationsという名前のテーブルとパーティションキーをConnectionIdに設定します。
通常、パーティションキーはユーザーIDにすべきですが、今回ではログイン機能がないため、クライアント接続IDをパーティションキーとして使用します。

手順3. WebSocket API Gatewayの作成

  1. コンソールからWebSocket API Gatewayを作成します。

  2. ルーティングキー $connect$disconnect、および sendprompt を追加します。

  3. Lambda関数の統合をそれぞれ設定します。

  4. WebSocket URL(wss://)を記録します。

手順4. SNSトピックの作成

  1. 標準のSNSトピックを作成します。

  2. サブスクリプションを作成し、ポリシーのフィールドでlambdaを選択し、meta_chatのARNリンクをエンドポイントのフィールドに貼り付けます。

  3. SNSトピックARNをコピーし、lambda_handle_chat環境変数SNS_TOPIC_ARNの値に貼り付けます。

機能検証

セッションテスト

  1. wscatツールのインストール

    • npm install -g wscatを実行し、wscatツールをインストールします。これはAWSが提供するWebSocket APIをテストするツールです。参考資料
  2. WebSocket URLへの接続

    • 以下のコマンドを使用してAPI Gatewayに接続します: wscat -c 「WebSocket URL(wss://)」
  3. セッションの開始

    • 質問1「what is your name?」および質問2「What did I ask you just now?」に対する回答の関連性を確認します(文脈確認)。
  4. DynamoDBのデータを確認

    • DynamoDBで会話履歴を確認します。
  5. セッションの終了

    • [ctrl + c]を押してセッションを終了し、再度DynamoDBを確認して、以前のセッション記録が削除されたことを確認します。

複数のクライアントが同時にセッションを持つテスト

2つのクライアントで実施している会話がそれぞれ独立して行われることを確認するためのシミュレーションを行います。

終わりに

この記事では、AWS上でChatGPTベースのインフラストラクチャの設計および構築について取り上げました。
今後、本インフラ構成において以下の点についてさらなる改善を行う予定です。

  • OpenAIのアクセスキーを保護するためにAWS Secret Serviceを使用します
  • ユーザー数が増えるにつれて、OpenAIの使用制限に達した場合に備えて、シームレスなアカウント切り替えを実装します
  • より正確な回答を提供するためにVectorDBを統合することを検討しています
  • より対話型のユーザーエクスペリエンスを提供するために、ストリーミング応答モデルに移行します

これからもChatGPTの応用シナリオについてさらに探求し、その結果をAIに興味を持つ読者と継続的に共有し続ける予定です。

現在ISIDはweb3領域のグループ横断組織を立ち上げ、Web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。
もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!
私たちと同じチームで働いてくれる仲間を、是非お待ちしております!
ISID採用ページ(Web3/メタバース/AI)

執筆:@chen.sun、レビュー:@wakamoto.ryosuke
Shodoで執筆されました