AWS でも生成 AI のサービスとして Amazon Bedrock が一般利用可能になりました。AWS の一貫した API で様々なモデルを実行できるのが特徴で、気軽に生成 AI を利用したサービスが構築できるのが嬉しい点かと思います。その Bedrock を使って AI とチャットができる LINE ボットを作ってみました。今回は AWS Step Functions で新しく使えるようになった機能を駆使してなるべくコードを書かずに、かつサーバレスで実現してみたいと思います。

全くコードを書かずに実装できればと思ったのですが、LINE から Webhook によるリクエストの署名を検証するところでどうしてもコード実装が必要になりそうでしたので、Webhook を受けるところで Lambda を利用しました。

アーキテクチャ

(今回作成したものは CDK でデプロイできるようにし、GitHub に置いておきました。)

Architecture diagram

以降では考慮点などを紹介したいと思います。

API Gateway - Lambda - SQS の部分

LINE の Messaging API を利用したチャネルでは、メッセージを送った際に Webhook URL に対して HTTP POST リクエストを送ることができます。そのリクエストを API Gateway で受け、Lambda を実行します。Lambda ではリクエストされた署名の検証などを実行し、受け取った Webhook イベントオブジェクトを SQS にエンキューします。

API Gateway から直接 SQS に Webhook イベントオブジェクトをエンキューすることで Lambda なし (つまりはコーディングなし)でできそうですが、LINE Developers のドキュメントでは署名の検証がセキュリティ上の理由から警告されていますので、Lambda 内で署名を検証します。

https://developers.line.biz/ja/docs/messaging-api/receiving-messages/

セキュリティ上の警告

ボットサーバーが受信したHTTP POSTリクエストは、LINEプラットフォームから送信されていない危険なリクエストの可能性があります。

必ず署名を検証してから、Webhookイベントオブジェクトを処理してください。

検証方法は同じページ内に紹介されていますが、LINE の Bot SDK を利用するとシンプルに書くことができます。
(Bot SDK を使った検証方法は GitHub を参照くださいませ。)

また、API Gateway は応答時間の制限として 29秒までとなり、また Bedrock でのモデル実行が 29秒以上かかる可能性も考慮して、SQS へのエンキューが完了したらすぐに LINE に対して応答を返すようにし、API Gateway としては処理を終わらせます。応答を返すと言ってもメッセージを返すわけではなく、Webhook を受信したよと返すだけです(ステータスコード 202 (Accepted) を返します)。言い換えれば非同期化しておきます。LINE Developers でも非同期化が推奨されています。

イベント処理を非同期化することを推奨します

HTTP POST リクエストの処理が後続のイベントの処理に遅延を与えないよう、イベント処理を非同期化することを推奨します。

ちなみに、Lambda が実行される際、1回の Webhook で Webhook イベントオブジェクトが複数含まれる可能性があり (それぞれ送信者が異なることもある)、とのことなので Webhook イベントオブジェクトを1つずつ個別に SQS にエンキューしていきます。

https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects

1つのWebhookに複数のWebhookイベントオブジェクトが含まれる場合があります

LINEプラットフォームから送信されるWebhookには、複数のWebhookイベントオブジェクトが含まれる場合があります。また1つのWebhookにつき一人のユーザーとは限らず、Aさんからのメッセージイベントと、Bさんからのフォローイベントが同じWebhookに入ることもあります。

複数のWebhookイベントオブジェクトを含むWebhookを受信した場合も、ボットサーバーは適切な処理を行えるようにしてください。詳しくは、Webhookのリクエストボディを参照してください。

SQS - EventBridge Pipes - Step Functions の部分

SQS のメッセージをトリガーに Step Functions ステートマシンを実行するために EventBridge Pipes を利用します。Pipes を利用すると Step Functions の呼び出しに成功したら SQS からメッセージを削除、といったことも AWS サービス側でやってくれます。Pipes ではイベントメッセージのフィルタやエンリッチなども可能ですが今回は利用しませんでした。

SQS から取得するメッセージの数を Pipes では BatchSize として設定が可能ですが、Step Functions 側での処理を簡単にするために 1つずつ取得するようにします。

Step Functions での処理フロー

Step Functions の各ステートの詳細について紹介する前に全体の方針、考慮点を簡単に紹介します。

  • 生成 AI を利用したチャットなので、会話の履歴を生成 AI モデル実行時のプロンプトに含めることで文脈、会話のやり取りを意識した自然な会話が可能になりそうです。そのための履歴の保管先として DynamoDB のテーブルを利用します
  • 生成 AI 実行時 (Bedrock の呼び出し時) に必要なプロンプトは適切に記述することでより精度の高い結果を得られるようになります。そのため、プロンプト自体を Step Functions 内に埋め込むことも可能ですが、よりカスタマイズしやすいように Systems Manager のパラメータストアからテンプレートとして参照するようにしました
  • Bedrock から応答結果を取得した後、メッセージを LINE で返信する際に Step Functions の新しいステートである HTTP Endpoint 呼び出しを利用することで Lambda なしで実装することができるので今回はこちらを利用します
  • 並列処理が可能なところは Parallel ステートを利用し、ちょっとでも全体の実行時間が短くなるようにします
  • 各ステートの出力にはステート名を利用して ResultPath を設定し、元の入力を維持しつつ後段のステートの入力からは $.<ステート名>.xxx でその出力結果を参照できるようにしておきます
  • Step Functions は標準フローとして実行します

また、履歴を保存する DynamoDB テーブルのスキーマについてもあらかじめ紹介しておきたいと思います。

履歴テーブルのスキーマ

DynamoDB のテーブルはシンプルに以下のようにしました。

属性名 パーティションキー 備考
chat_id O 文字列 (S) LINE のユーザ ID もしくはグループ ID をハッシュ化したもの
history - 文字列 (S) チャットの履歴。\n\nHuman: xxx\n\nAssistant: yyy\n\nHuman: xxx... という形式
ttl - 数値 (N) このアイテムの有効期限。今回は1時間ぐらいで設定

chat_id は 1対1 のトークルームであればユーザ ID を、複数ユーザのトークルームであればグループ ID をハッシュ化して利用しました。LINE Developers によると、ユーザ ID は同じユーザであってもプロバイダごとに異なる値が発行されるようなのでそこまで神経質にならなくても良いかもしれませんが、ID をそのまま DB に保管しておくのも少々気持ち悪かったのでハッシュ化して保存することにしました。
ちなみに、ID のハッシュ化は Step Functions の組み込み関数でもできますが、今回は Webhook リクエストを受け取った時に実行される Lambda で算出しました (この部分(GitHub))。

history に対話の履歴を保管しています。Claude 前提の形式ですが、以下のように新しいメッセージを追記していく形で格納しています (見やすさのため改行させていますが、項目値としては改行コードで入っています)。



Human: (ユーザからのメッセージ1)

Assistant: (Bedrock からの応答1)

Human: (ユーザからのメッセージ2)

Assistant: (Bedrock からの応答2)

ttl でアイテムの有効期限を設定しています。履歴もあまり長くなるとプロンプトとしても長くなってしまうので、最後の会話から 1時間ほど経過したら削除するようにしました。

Step Functions の各ステート (1. 最初の Parallel ステート)

DynamoDB からは GetItem API を利用してこれまでの対話履歴を取得します。履歴は DynamoDB テーブルの TTL を利用した削除を設定しているので経過時間によってアイテムの有無が変わってきます。履歴の有無によって DynamoDB の GetItem の結果が異なってきますので、その後の Choice ステートで条件分岐させ、Pass ステートで JSON を整形します。

さらに並列で Systems Manager パラメータストアからはプロンプトのテンプレートを取得します。最低限のプロンプトテンプレートとして以下のように設定しています。

{}

Human: {}

Assistant:

{} は後のステートで States.Format 組み込み関数を利用してチャットの履歴やユーザからのメッセージに置き換えるようにするためのプレースホルダーです。

Step Functions の各ステート (2. Pass ステート (Flatten))

前段の Parallel ステートにより出力の JSON が全体として配列構造になるのと重複など不要なデータも結構入ったままになるので、この先扱いやすくするために Pass ステートの Parameters を使って整形しています。イメージとしては Pass ステートへの以下の入力が、、、

[
  {
    "LoadHistory": { ... },       // 履歴取得の結果
    "body": { ... }               // SQS (Webhook イベント)からの入力
  },
  {
    "LoadPromptTemplate": {...},  // 取得したプロンプトテンプレートの結果
    "body": { ... },              // SQS (Webhook イベント)からの入力
    // その他不要なデータ
    //  :
    //  :
  }
]

Pass ステートで設定した以下の Parameters により、、、

{
  "body.$": "$[0].body",
  "LoadHistory.$": "$[0].LoadHistory",
  "LoadPromptTemplate.$": "$[1].LoadPromptTemplate"
}

結果として下記のように整形させています。

{
  "body": { ... },              // SQS (Webhook イベント)からの入力
  "LoadHistory": { ... },       // 履歴取得の結果
  "LoadPromptTemplate": { ... } // 取得したプロンプトテンプレートの結果
}

Step Functions の各ステート (3. Bedrock の InvokeModel 実行)

Step Functions から Bedrock を最適化された統合で呼び出せようになっています (公式アナウンス)。実行するモデルの ID や Temperature などのパラメータなども指定します。

モデルは日本語でもより自然な応答を得られると評判の Anthropic 社の Claude v2.1 を利用しました (CDK で指定する方法は GitHub を参照ください)。

arn:aws:bedrock:<リージョン>::foundation-model/anthropic.claude-v2:1

Anthropic Claude v2.1 model ID

Systems Manager のパラメータストアから取得したプロンプトテンプレートには States.Format 組み込み関数を利用して、履歴やユーザメッセージを設定していきます。

{
  //   :
  // (snip)
  //   :
  "Parameters": {
    //   :
    // (snip : モデル ID の指定など)
    //   :
    "Body": {
      //   :
      // (snip : モデルに渡す temperature パラメータなど)
      //   :
      "prompt.$": "States.Format($.LoadPromptTemplate.Value, $.LoadHistory.history.S, $.body.message)"
    }
  }
}

前述した以下のプロンプトテンプレートは、

{}

Human: {}

Assistant:

例えば以下のようにプロンプトとして置き換えられます(履歴があった場合)。



Human: こんにちは

Assistant: はい、こんにちは。

Human: あなたは誰ですか?

Assistant:

\n\nHuman: こんにちは\n\nAssistant: はい、こんにちは。 が履歴で、あなたは誰ですか? がユーザからの新しいメッセージになります。

Step Functions の各ステート (3. 最後の Parallel ステート)

Bedrock からの応答結果を LINE で返信するにあたり、HTTP Endpoint ステートを使って LINE の Reply API を呼び出します。ユーザからのメッセージ受信時の Webhook イベントオブジェクトに replyToken が含まれていますので、リクエストボディとして返信メッセージと一緒に送ります。また、LINE API 利用時に以下のように認証ヘッダを HTTP ヘッダに設定する必要があります。
<チャネルアクセストークン> は LINE Developers の “Messaging API 設定” から発行・参照できます。

Authorization: Bearer <チャネルアクセストークン>

並列処理として、これまでの履歴と新しいユーザからのメッセージ、Bedrock からの応答結果を結合して新たな履歴として DynamoDB テーブルに PutItem しておきます (更新しています)。保存するデータは States.Format 組み込み関数を利用して整形します。

States.Format('{}\n\nHuman: {}\n\nAssistant: {}', $.LoadHistory.history.S, $.body.message, $.InvokeModel.Body.completion)

JSON Path には以下の通り設定されています。

  • $.LoadHistory.history.S : これまでの履歴 (DynamoDB テーブルから GetItem したアイテムの history 属性の値)
  • $.body.message : ユーザからのメッセージ (LINE の Webhook イベントに含まれている Step Functions への入力値)
  • $.InvokeModel.Body.completion : Bedrock からの応答結果

先の例ですと以下を履歴として保存します (見やすさのため改行コードを改行させてます)。



Human: こんにちは

Assistant: はい、こんにちは。

Human: あなたは誰ですか?

Assistant: 私は Anthropic 社が開発した AI アシスタントです。

やや余談ですが、HTTP Endpoint はまだ CDK では Construct がないため CustomState を利用しました。また、履歴保存のところでは DynamoPutItem を使おうと思ったのですが、値として States.Format を指定するとエラーとなってしまったのでやむなく、CallAwsService を利用しました。
CDK コードについては以下から GitHub を参照ください。

今回作成したもの

今回作成したものは CDK でデプロイできるようにして、GitHub に置いておきました。デプロイ手順は README にも書いておきましたが、こちらでは日本語で簡単に紹介しておきたいと思います。

前提条件

  • AWS マネジメントコンソールから Bedrock の “Model access” より、Anthropic - Claude を利用できるようにしておいてください。詳しくはドキュメントを参照ください。
  • AWS CDK は事前に利用できるようにしておいてください。導入方法はドキュメント等を参考にしてください。

1. LINE の Messaging API を利用したチャネルの作成

LINE Developers コンソールから、プロバイダを作成し、Messaging API を利用したチャネルを作成します。

Create channel with Messaging API

2. Webhook の利用を有効化

LINE Developers コンソールの “Messaging API 設定” で Webhookの利用 を有効化します。

Enable Webhook URL

3. チャネルシークレットとチャネルアクセストークンを発行

LINE Developers コンソールの “チャネル基本設定” からチャネルシークレット、“Messaging API 設定” からチャネルアクセストークン (長期) を発行して控えておきます。

4. CDK コンテキストパラメータの設定

ホームディレクトリにある ~/.cdk.json にパラメータとして 3. で発行した LINE のチャネルシークレット等を設定します。ファイルがなければ作成してください。 ホームディレクトリの方は .(ドット) で始まっている点にご注意を。
リポジトリにある cdk.json に設定しても動作はしますが、誤って push してシークレットが公開されてしまうリスクがあるのでホームディレクトリの方のファイルに設定した方が安全でしょう。

以下のように "line-bot-with-amazon-bedrock": {...} を追加します。

{
  //   :
  // (snip)
  //   :
  "line-bot-with-amazon-bedrock": {
    "secret": "...(チャネルシークレット)...",
    "accessToken": "...(チャネルアクセストークン)..."
  }
}

5. GitHub からリポジトリをクローン

します。

git clone https://github.com/msysh/aws-sample-line-bot-with-amazon-bedrock.git

6. AWS リソースのデプロイ

AWS のリソースをデプロイしていきます。

cdk deploy --all

2つの Stack をデプロイします。

  • LineBotWithAmazonBedrockParameter
    • LINE のチャネルシークレット等を Systems Manager のパラメータストアに格納する Stack
  • LineBotWithAmazonBedrock
    • API Gateway から Step Functions までをデプロイするメインの Stack

7. Webhook URL の設定

6.の実行結果として API Gateway のエンドポイントが出力されます。

LineBotWithAmazonBedrock.ApiGatewayEndpointXXX = https://xxxxx.execute-api.us-east-1.amazonaws.com/prod/

この URL を LINE Developers コンソールの “Messaging API 設定” の Webhook URL に設定します。

Configure Webhook URL

8. LINE で友達に追加

LINE Developers コンソールの “Messaging API 設定” で表示されている QR コードを使って友達に追加します。

ここまでできたらボットと会話してみましょう!

9. プロンプトのカスタマイズ

プロンプトをカスタマイズしたい場合は AWS マネジメントコンソールから Systems Manager のパラメータストアに /line-bot-with-amazon-bedrock/prompt-template というパラメータが作成されていますのでこちらを変更します。1つ目の {} には会話履歴が、2つ目の {} がユーザからのメッセージに置き換えられます。

かなりシンプルなカスタマイズの例をご紹介すると (モデルとして Claude を前提としています)



Human: あなたは AI アドバイザとして振る舞います。ユーザの悩みに親身になって回答します。
以下はユーザとあなたの会話履歴です。履歴がない場合は空でもかまいません:
<history>{}</history>
ここにユーザーからの相談があります: <question>{}</question>
ユーザーの相談にどう答えますか?

Assistant:

リソースの削除

AWS リソースを削除したい場合は以下のようにします。

cdk destroy --all

パフォーマンス

Step Functions では X-Ray によるトレーシングが可能なので、どこの処理でどのぐらい時間がかかっているか可視化することができます。参考までに履歴がない状態での Step Functions のステートマシンを実行した際のトレーシング結果を載せておきます。

このケースでは Step Functions ステートマシンの開始から終了までで 3.2秒ほどでした (他に API Gateway - Lambda で 1.5秒ほど、Pipes 部分で 0.1秒ほどかかっています)。

履歴が増えたり、プロンプトをカスタマイズするとプロンプトも長くなるので Bedrock (Invoke Model) の部分の実行時間は延びてくるのではないかと思います。

Tracing with X-Ray

まとめ

Bedrock と Step Functions を駆使して、ローコードで AI ボットを作ってみました。Step Functions は AWS SDK や HTTP Endpoint を呼び出すことで様々なユースケースにコードを書かずに適用できるので、今後の機能拡張にも注目したいサービスです。

クラウドのサービスを活用してアプリを作ってみる時、フロントの UI まで作り出すと結構手間になって億劫になるのですが、今回のように UI を LINE に全部お任せできるとハードルが下がるので良いですよね。

おまけ

プロンプトを工夫して、あのキャラクタさながらの AI ボットにしてみました。プロンプトだけでもそれっぽくなりますね。

Sample1 Sample2

最後に・・・

この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。
※本サンプルは、自己責任の範囲でご利用ください。