15.2K Views
May 20, 23
スライド概要
https://linedevelopercommunity.connpass.com/event/281444/
広島市のプログラマ
AWSサーバレス上にChatGPT LINEボットを構築する 2023/05/20(土) LINE Developers Community dyoshikawa
自己紹介🧑🔧 https://github.com/dyoshikawa 京都市出身、広島市在住 趣味 ジョギング、ジム 最近始めてハマってきた 30になるとずっと座ってると体痛い・・・ 4月に子供が産まれました👶 AWSインフラ, Laravel, Ruby on Rails, Node.js, Reactなど経験 AWSサーバレスでLIFF/LINEミニアプリを作っています
ChatGPT LINEボットがどんどん増えている🤖 AIチャットくん | 話題のChatGPTがLINEで使える! 友達登録100万人以上 ChatGPTもStable Diffusionもこれ一つでOK。LINE版ChatGPTの画像生成モードを新規追加しました!|株 式会社Emposyのプレスリリース 友達登録12万人以上 ChatGPT APIベースのLINEボット「くらにゃんAI」を無料公開します | DevelopersIO 発表者が作成 友達登録約1600人 世間的にChatGPTの注目度が高いので使ってもらいやすい → 作ってみよう💪
今回紹介する構成の技術スタック🔧 APIGateway+Lambda+DynamoDB AWS CDK Node.js+TypeScript AWS SDK v3
構成の特長🐍 AWSサーバレス 固定費がほぼないので始めやすい 個人開発者の財布にも優しい スケーラビリティが高い
構成の特長🐍 APIGatewayから非同期でLambda関数を起動 プロキシ統合+同期実行はしない コンスタントに1秒以内にレスポンスできるようにする 1秒を超過するとリクエストタイムアウトが発生するため メッセージ(Webhook)を受信する | LINE Developers Lambda関数内で先にレスポンスして処理継続という手もあるが、コールドスタートで1秒超える場合 もある
構成の特長🐍 DynamoDBに会話を保存 過去発言を踏まえた会話ができる 項目 id content userId typedAt 型 string string string string 説明 発言ID。UUID V4で採番 発言内容 LINEユーザID チャットの発言時刻。ISO8601、UTC、ナノ秒まで
構成の特長🐍 AWS WAFをアタッチ 過度に使われて課金爆発しないようにレート制限する OpenAI APIは課金のハードリミット設定できるので比較的安全ではある
アーキテクチャ🚃 LINE Platform Webhook 呼び出し MessagingAPI AWS APIGateway 非同期起動 Lambda 返信メッセージ送信 R/W 1. LINEユーザがボットに対して発言する 2. LINEプラットフォームからAPIGWにWebhookリクエストが送信される 3. APIGWからLambdaを非同期起動する 4. DynamoDBから過去会話を取得し、ChatGPT APIに会話を送信して返信メッセージを得る 5. ChatGPTの返信メッセージをユーザに送信 6. DynamoDBに今回発生した新規会話を保存 DynamoDB
やっていく💪 環境 事前準備 LINE{チャネルシークレット,アクセストークン} OpenAI APIキー AWS CDKコードを書く 各種インフラリソースを定義 APIGateway Lambda DynamoDB AWS WAF SSM Parameter Store Lambda関数コードを書く SSM Parameter Storeに各種シークレットをセット デプロイ
環境 typescript 4.9.5 aws-cdk-lib 2.67.0 constructs 10.1.266 lodash-es 4.17.21 source-map-support 0.5.21 uuid 9.0.0 @aws-sdk/* 3.282.0 dayjs 1.11.7
事前準備⚙️ LINE DevelopersにてMessagingAPIチャネル作成 → チャネルシークレットとチャネルアクセストークン払 い出して控えておく OpenAIのAPIキーを払い出して控えておく 詳細は割愛 参考 薬を飲むまで飲んだか聞いてくるLINE botアプリを作ってみた[前編] | DevelopersIO GPT-3 を LINE チャットボットに組み込んでみた | DevelopersIO
CDKコードを書く
📝
要所をピックアップ
DynamoDBテーブル定義
userIdでクエリできるようGSIを貼る
const messagesTable = new cdk.aws_dynamodb.Table(this, "messagesTable", {
tableName: "chatGptLineBotSample-messages",
partitionKey: {
name: "id",
type: cdk.aws_dynamodb.AttributeType.STRING,
},
billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
messagesTable.addGlobalSecondaryIndex({
indexName: "chatGptLineBotSample-userIdIndex",
partitionKey: {
name: "userId",
type: cdk.aws_dynamodb.AttributeType.STRING,
},
});
CDKコードを書く📝 SSM Parameter Storeから各種シークレットを読み取る const lineMessagingApiChannelSecret = cdk.aws_ssm.StringParameter.valueForStringParameter( this, "chatGptLineBotSample-lineMessagingApiChannelSecret" ); const lineMessagingApiChannelAccessToken = cdk.aws_ssm.StringParameter.valueForStringParameter( this, "chatGptLineBotSample-lineMessagingApiChannelAccessToken" ); const openAiApiKey = cdk.aws_ssm.StringParameter.valueForStringParameter( this, "chatGptLineBotSample-openAiApiKey" );
CDKコードを書く📝
Lambda関数
環境変数にSSM Parameter Storeから読み取った各種シークレットをセットする
const apiFn = new cdk.aws_lambda_nodejs.NodejsFunction(this, "apiFn", {
runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
entry: "../server/src/handler.ts",
environment: {
環境変数にシークレットと キーをセット
//
API
CHANNEL_SECRET: lineMessagingApiChannelSecret,
CHANNEL_ACCESS_TOKEN: lineMessagingApiChannelAccessToken,
OPEN_AI_API_KEY: openAiApiKey,
},
bundling: {
sourceMap: true,
},
timeout: cdk.Duration.minutes(5),
});
messagesTable.grantReadWriteData(apiFn);
CDKコードを書く📝
APIGateway マッピングテンプレート
JSONパースされたbodyとは別にJSONパース前のrawBodyを送信している
リクエスト署名検証に使いたいため
requestTemplates: {
// AWS
APIGW > "
" > "
" > "application/json" > "
" > "
"application/json": `
## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.ht
## This template will pass through all parameters including path, querystring, header, stage variables, and
#set($allParams = $input.params())
{
"body" : $input.json('$'),
"rawBody": "$util.escapeJavaScript($input.body)",
マネコンの
統合リクエスト
マッピングテンプレート
"params" : {
#foreach($type in $allParams.keySet())
#set($params = $allParams.get($type))
"$type" : {
#foreach($paramName in $params.keySet())
"$paramName" : "$util.escapeJavaScript($params.get($paramName))"
#if($foreach.hasNext),#end
#end
}
テンプレートの生成
メソッ
CDKコードを書く📝 AWS WAF 5分あたり100リクエストのレート制限 設定可能値 100〜20,000,000(2千万) AWS WAF でリクエストにレート制限を適用する | AWS re:Post { name: "RateLimit", priority: 0, statement: { rateBasedStatement: { limit: 100, aggregateKeyType: "IP", }, }, action: { block: {}, }, visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: "RateLimit", },
Lambdaコードを書く
📝
要所をピックアップ
リクエスト署名検証を行う
invalidな場合はエラーを投げて処理を落とす
const isValid = validateSignature(
event.rawBody,
process.env.CHANNEL_SECRET,
event.params.header["x-line-signature"]
);
if (!isValid) {
throw new Error("Invalid signature");
}
Lambdaコードを書く📝
ChatGPTが文脈を踏まえられるよう、会話中ユーザとの過去会話をDDBからクエリ取得
最新3件のみを抽出する。OpenAI API Maxトークン制限を超過しない範囲で調整可能
アプリケーション側でソート
扱う件数が少ないためちょっと横着している
const { Items: messages = [] } = await ddbDocClient.send(
new QueryCommand({
TableName: messagesTableName,
IndexName: messagesTableUserIdIndexName,
KeyConditionExpression: "#userId = :userId",
ExpressionAttributeNames: {
"#userId": "userId",
},
ExpressionAttributeValues: {
":userId": userId,
},
})
);
const orderedMessages = orderBy(messages, "typedAt", "asc");
const queriedMessages = orderedMessages.splice(-3);
Lambdaコードを書く📝
LINEユーザの最新発言をDDBに保存する
await ddbDocClient.send(
new PutCommand({
TableName: messagesTableName,
Item: {
id: v4(),
content: userMessageContent,
userId: userId,
typedAt: dayjs().format(nanoSecondFormat),
role: "user",
},
})
);
Lambdaコードを書く📝
ChatGPT APIに過去会話+最新発言を送信し、返信を受け取る
文字数が多いと数十秒かかる場合があるため、Lambdaのタイムアウト時間設定に注意する
const completion = await openAiApi.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
...queriedMessages.map(
(message) =>
({
role: message.role,
content: message.content,
} as ChatCompletionRequestMessage)
),
{
role: "user",
content: userMessageContent,
},
],
});
const chatGptMessageContent = completion.data.choices[0].message?.content!;
Lambdaコードを書く📝
ユーザに返信を送る
LINE Bot SDK
const repliedMessage: TextMessage = {
type: "text",
text: chatGptMessageContent,
};
await lineBotClient.replyMessage(event.replyToken, repliedMessage);
Lambdaコードを書く📝
ChatGPTの最新発言をDDBに保存する
await ddbDocClient.send(
new PutCommand({
TableName: messagesTableName,
Item: {
id: v4(),
content: chatGptMessageContent,
userId: userId,
typedAt: dayjs().format(nanoSecondFormat),
role: "assistant",
},
})
);
ソースコード全体はこちら👇 dyoshikawa/chatgpt-api-line-bot-aws-serverless
SSM Parameter Storeに各種シークレットをセット🔐 AWS CLIでセット マネコン操作でも可 チャネルアクセストーク チャネルシークレット}" キー}" aws ssm put-parameter --name "chatGptLineBotSample-channelAccessToken" --type "String" --value "{LINE aws ssm put-parameter --name "chatGptLineBotSample-channelSecret" --type "String" --value "{LINE aws ssm put-parameter --name "chatGptLineBotSample-openAiApiKey" --type "String" --value "{OpenAI API
デプロイ🚀 cdk deploy 数分かかるので完了まで待つ 完了したらLINE DevelopersのQRコードを実機で読み取り、LINEボットと友達になる
実機で会話してみる🗣 返信が来る✅ 過去の会話内容を踏まえている✅
ご清聴ありがとうございました✋
本スライドはSlidevで作成しました🔧 Home | Slidev
参考📚 [ChatGPT API][AWSサーバーレス]ChatGPT APIであなたとの会話・文脈を覚えてくれるLINEボットを作る 方法まとめ | DevelopersIO AWS WAF でアクセス数が一定回数を超えた IP アドレスを自動的にブラックリストに追加させる方法 | DevelopersIO CloudFormationでAWS WAFを構築してみた(2022年1月版) | DevelopersIO AWS WAF でアクセス数が一定回数を超えた IP アドレスを自動的にブラックリストに追加させる方法 | DevelopersIO レートベースのルールステートメント - AWS WAF、AWS Firewall Manager、および AWS Shield Advanced 【AWS】API GatewayからLambdaを非同期で実行する - Qiita Messaging APIリファレンス | LINE Developers