LLMチャットボットをAWS Lambdaで構築する完全ガイド【構成図・コード付き】

最終更新:2025年6月 / 対象:AWS Lambda / Python 3.12 / OpenAI API or Amazon Bedrock

目次

1. 結論:最適構成はこれだ

LLMチャットボットをAWSで低コスト・低運用負荷で動かすなら、API Gateway + AWS Lambda + DynamoDB(会話履歴)+ Bedrock or OpenAI APIの構成が現時点での最適解です。

  • サーバー管理不要(EC2・ECSのインスタンス管理ゼロ)
  • リクエストベース課金でアイドル時コストゼロ
  • IAMロールでAPIキーをコードに書かずセキュアに管理
  • DynamoDBにセッションIDで会話履歴を保持し、文脈付き応答を実現

「とりあえずEC2で動かした」→月数千円の無駄コスト発生。Lambdaなら月100万リクエストまで無料枠。まず Lambda から始めるのが鉄則。

2. そもそも何か?用語と全体像(初心者向け)

2-1. LLMチャットボットとは

LLM(Large Language Model)とはGPT-4やClaude、Amazon Titanなどの大規模言語モデルです。これをバックエンドに持つチャットボットは、ルールベースのbotと違い、自然な日本語で質疑応答・文章生成・要約が可能です。

2-2. AWS Lambdaとは

コードをアップロードするだけで動くAWSのサーバーレス実行環境です。リクエストが来た時だけ起動し、終わったら停止します。常時稼働のサーバー(EC2)と違い、使った分だけ課金される点が最大の特徴です。

2-3. 今回の構成で登場するAWSサービス一覧

サービス役割無料枠
API GatewayHTTPリクエストの受け口(エンドポイント)100万回/月
AWS Lambdaチャットボットのロジック実行100万回/月
DynamoDB会話履歴の保存(セッション管理)25GB/月
Amazon BedrockLLMのAPI呼び出し(Claude / Titan)なし(従量課金)
IAMLambda→Bedrock/DynamoDBへの権限管理無料
CloudWatch LogsLambdaのログ収集・監視5GB/月
Secrets ManagerOpenAI APIキーの安全な保管30日間無料

3. アーキテクチャ解説と構成図

3-1. リクエストの流れ

  1. ユーザーがフロントエンド(React等)からPOSTリクエスト送信
  2. API GatewayがリクエストをLambdaへプロキシ転送
  3. LambdaがDynamoDBから過去の会話履歴を取得
  4. 履歴+今回のメッセージをBedrockまたはOpenAI APIへ送信
  5. LLMの応答をDynamoDBに保存し、ユーザーへ返却

3-2. システム構成図(PlantUML)

PlantUML Syntax:<br />
@startuml<br />
left to right direction</p>
<p>skinparam rectangle {<br />
BackgroundColor<<user>> #E3F2FD<br />
BackgroundColor<<app>> #FFF3E0<br />
BackgroundColor<<condition>> #E8F5E9<br />
BackgroundColor<<external>> #FCE4EC<br />
BackgroundColor<<output>> #E0F7FA<br />
BorderColor #888888<br />
FontSize 13<br />
}</p>
<p>skinparam rectangle<<condition>> {<br />
BackgroundColor #E8F5E9<br />
BorderColor #888888<br />
}</p>
<p>skinparam shadowing false<br />
skinparam backgroundColor #FFFFFF<br />
skinparam defaultFontName “Noto Sans JP”</p>
<p>rectangle “User (Browser / App)” <<user>> as User<br />
rectangle “API Gateway (REST API)” <<app>> as APIGW<br />
rectangle “AWS Lambda (Python 3.12)” <<app>> as Lambda<br />
rectangle “DynamoDB (Conversation History Table)” <<external>> as DDB</p>
<p>rectangle “LLM Provider Selection” <<condition>> as LLMChoice</p>
<p>rectangle “Amazon Bedrock (Claude / Titan)” <<external>> as Bedrock<br />
rectangle “OpenAI API (GPT-4o)” <<external>> as OpenAI</p>
<p>rectangle “CloudWatch Logs (Logs & Metrics)” <<external>> as CW<br />
rectangle “LLM Response Output” <<output>> as Response</p>
<p>User –> APIGW : POST /chat {session_id, message}<br />
APIGW –> Lambda : Proxy integration<br />
Lambda –> DDB : Get conversation history<br />
Lambda –> LLMChoice : Select provider</p>
<p>LLMChoice –> Bedrock : Use Bedrock<br />
LLMChoice –> OpenAI : Use OpenAI</p>
<p>Bedrock –> Lambda : LLM response<br />
OpenAI –> Lambda : LLM response</p>
<p>Lambda –> DDB : Save history<br />
Lambda –> CW : Send logs<br />
Lambda –> Response : JSON response<br />
Response –> User : assistant_message</p>
<p>@enduml<br />

3-3. DynamoDBのデータ設計

属性名役割
session_id(PK)String会話セッションの識別子
timestamp(SK)Numberメッセージの時刻(エポック秒)
roleString“user” または “assistant”
contentStringメッセージ本文
ttlNumber自動削除タイムスタンプ(24時間後)

4. 実装手順(ステップ形式)

4-1. ディレクトリ構成

llm-chatbot-lambda/
├── lambda/
│   ├── handler.py          # Lambdaエントリーポイント
│   ├── llm_client.py       # Bedrock/OpenAI呼び出しロジック
│   ├── dynamodb_client.py  # 会話履歴CRUD
│   └── requirements.txt    # 依存ライブラリ
├── terraform/              # (オプション)IaC
│   └── main.tf
└── README.md

4-2. Step 1:DynamoDBテーブルを作成する

AWSコンソール → DynamoDB → テーブルの作成

  • テーブル名:chat_history
  • パーティションキー:session_id(文字列)
  • ソートキー:timestamp(数値)
  • TTL属性を有効化:ttl
  • キャパシティモード:オンデマンド(本番低負荷ならコスト最適)

4-3. Step 2:IAMロールを作成する

Lambda実行ロールに以下のポリシーをアタッチします。

// ファイル名:lambda_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:YOUR_ACCOUNT_ID:table/chat_history"
    },
    {
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel"
      ],
      "Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

⚠️ 現場注意"Resource": "*"は絶対に使わないこと。最小権限の原則(Principle of Least Privilege)を徹底し、テーブルARNとモデルARNを明示します。

4-4. Step 3:Lambdaコードを実装する

ファイル:lambda/dynamodb_client.py

import boto3
import time
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")
table = dynamodb.Table("chat_history")

def get_history(session_id: str, limit: int = 10) -> list[dict]:
    """直近limit件の会話履歴を取得する"""
    response = table.query(
        KeyConditionExpression=Key("session_id").eq(session_id),
        ScanIndexForward=False,  # 降順取得
        Limit=limit
    )
    items = sorted(response["Items"], key=lambda x: x["timestamp"])
    return [{"role": item["role"], "content": item["content"]} for item in items]

def save_message(session_id: str, role: str, content: str) -> None:
    """メッセージをDynamoDBに保存する(TTL:24時間)"""
    table.put_item(Item={
        "session_id": session_id,
        "timestamp": int(time.time() * 1000),  # ミリ秒エポック
        "role": role,
        "content": content,
        "ttl": int(time.time()) + 86400  # 24時間後に自動削除
    })

ファイル:lambda/llm_client.py

import boto3
import json
import os

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

SYSTEM_PROMPT = """あなたは親切なAIアシスタントです。
日本語で丁寧に回答してください。"""

def call_bedrock(messages: list[dict]) -> str:
    """Amazon Bedrock (Claude 3 Sonnet) を呼び出す"""
    body = {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 1024,
        "system": SYSTEM_PROMPT,
        "messages": messages
    }
    response = bedrock.invoke_model(
        modelId="anthropic.claude-3-sonnet-20240229-v1:0",
        body=json.dumps(body),
        contentType="application/json",
        accept="application/json"
    )
    result = json.loads(response["body"].read())
    return result["content"][0]["text"]

ファイル:lambda/handler.py

import json
import uuid
from dynamodb_client import get_history, save_message
from llm_client import call_bedrock

def lambda_handler(event, context):
    """API Gatewayからのリクエストを処理するエントリーポイント"""
    try:
        body = json.loads(event.get("body", "{}"))
        user_message = body.get("message", "").strip()
        session_id = body.get("session_id") or str(uuid.uuid4())

        if not user_message:
            return _response(400, {"error": "message is required"})

        # 1. 会話履歴の取得
        history = get_history(session_id)

        # 2. 今回のメッセージを追加
        history.append({"role": "user", "content": user_message})

        # 3. LLM呼び出し
        assistant_message = call_bedrock(history)

        # 4. 履歴保存
        save_message(session_id, "user", user_message)
        save_message(session_id, "assistant", assistant_message)

        return _response(200, {
            "session_id": session_id,
            "assistant_message": assistant_message
        })

    except Exception as e:
        print(f"ERROR: {str(e)}")  # CloudWatch Logsへ出力
        return _response(500, {"error": "Internal server error"})

def _response(status_code: int, body: dict) -> dict:
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*"  # 本番はドメイン指定に変更
        },
        "body": json.dumps(body, ensure_ascii=False)
    }

ファイル:lambda/requirements.txt

boto3>=1.34.0

boto3はLambdaランタイムに同梱されていますが、バージョン固定のためrequirements.txtに明記することを推奨します。

4-5. Step 4:Lambdaにデプロイする

# 依存ライブラリをパッケージに同梱
cd lambda/
pip install -r requirements.txt -t ./package/
cp *.py ./package/

# ZIPファイルを作成
cd package/
zip -r ../function.zip .
cd ..

# Lambdaへデプロイ(CLIで実行)
aws lambda create-function \
  --function-name llm-chatbot \
  --runtime python3.12 \
  --handler handler.lambda_handler \
  --role arn:aws:iam::YOUR_ACCOUNT_ID:role/llm-chatbot-role \
  --zip-file fileb://function.zip \
  --timeout 30 \
  --memory-size 256 \
  --region ap-northeast-1

⚠️ タイムアウト設定:LLMの応答に最大10〜15秒かかるため、デフォルト3秒では必ず失敗します。最低30秒を設定してください。

4-6. Step 5:API Gatewayを設定する

  1. AWSコンソール → API Gateway → REST APIを作成
  2. リソース /chat を作成 → メソッド POST を追加
  3. 統合タイプ:Lambda関数プロキシ統合を選択
  4. Lambda関数:llm-chatbotを指定
  5. ステージ名 prod でデプロイ

エンドポイントURL例:https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/chat

4-7. Step 6:動作確認

curl -X POST https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "AWSのLambdaとは何ですか?", "session_id": "test-session-001"}'

# 期待するレスポンス
{
  "session_id": "test-session-001",
  "assistant_message": "AWS Lambdaは、サーバーを管理せずにコードを実行できるサーバーレスコンピューティングサービスです。..."
}

5. 実務ユースケース

5-1. 社内FAQボット(最頻出ユースケース)

Confluenceや社内ドキュメントのテキストをS3に保存し、Lambda内でRAG(Retrieval-Augmented Generation)を組み合わせることで、「会社固有の知識に答えるボット」を実現できます。

  • 追加コンポーネント:S3(ドキュメント格納)+ Amazon OpenSearch Serverless(ベクター検索)
  • Lambda関数をRAG処理用とチャット用に分割し、Step Functionsでオーケストレーション

5-2. カスタマーサポートボット(SLA対応)

API Gatewayの使用量プラン・APIキー認証を設定し、顧客ごとのリクエスト制限を実装します。LLM応答に3秒以上かかる場合はSQSキュー経由の非同期処理に切り替え、WebSocket API GatewayでプッシュするとUX向上に繋がります。

5-3. Kubernetes(EKS)との併用パターン

既存のEKSクラスター上のマイクロサービスからLambdaのチャットボットAPIを呼び出す構成も現場でよく使われます。EKSのPodにIRSA(IAM Roles for Service Accounts)を設定し、API GatewayのエンドポイントをKubernetes ExternalNameサービス経由で名前解決する方法が定番です。

6. メリット・デメリット比較(技術選定表)

観点Lambda構成EC2 + FastAPIECS Fargate
初期構築コスト◎ 低い(1〜2時間)△ 中程度(半日)○ 中程度(3〜4時間)
月額コスト(低負荷)◎ 無料〜数十円✕ 最低3,000円〜(t3.small常時起動)○ 数百円〜(タスク数による)
スケーラビリティ◎ 自動(最大1,000同時実行)△ 手動スケール要◎ Auto Scaling対応
コールドスタート✕ 初回300ms〜1秒の遅延◎ なし○ 軽微(タスク起動に数十秒)
実行時間上限△ 最大15分(ストリーミング制約あり)◎ 制限なし◎ 制限なし
サーバー管理◎ 不要✕ OS・パッチ管理必要◎ 不要
デバッグのしやすさ△ CloudWatch必須◎ SSHでリアルタイム確認可○ CloudWatch + ECS Exec
本番向け推奨度◎ 低〜中負荷○ 高負荷・常時接続◎ 中〜高負荷

選定指針:月間リクエスト数が10万件未満ならLambda一択。10万〜100万件はFargate、100万件超かつストリーミング必須ならECS on EC2を検討してください。

7. よくあるエラーと対策

エラー①:Task timed out after 3.00 seconds

事象:

[ERROR] Task timed out after 3.00 seconds

原因:LambdaのデフォルトタイムアウトはHTTPリクエストの完了まで3秒であるが、BedrockやOpenAI APIの応答に平均5〜15秒かかることによって、レスポンスが返る前にLambdaが強制終了されています。

対策手順:

  1. AWSコンソール → Lambda → 該当関数 → 「設定」タブ → 「一般設定」を開く
  2. 「タイムアウト」を 0分30秒 以上(推奨:1分)に変更して保存する
  3. API GatewayもデフォルトタイムアウトがHTTP統合で29秒のため、Lambda側を29秒以内に設定するか、WebSocket APIへ移行する

エラー②:AccessDeniedException on Bedrock

事象:

botocore.exceptions.ClientError: An error occurred (AccessDeniedException)
when calling the InvokeModel operation:
User: arn:aws:sts::123456789:assumed-role/llm-chatbot-role/llm-chatbot
is not authorized to perform: bedrock:InvokeModel
on resource: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0

原因:IAMポリシーにBedrockのInvokeModel権限が付与されていないか、Bedrockのモデルアクセスがアカウントで有効化されていないことによって、API呼び出しが拒否されています。

対策手順:

  1. AWSコンソール → Amazon Bedrock → 「モデルアクセス」を開く
  2. 使用するモデル(例:Claude 3 Sonnet)の「アクセスをリクエスト」をクリックして有効化する(反映まで最大1分)
  3. IAM → ロール → llm-chatbot-role → アタッチ済みポリシーに bedrock:InvokeModel が含まれているか確認する
  4. リソースARNのリージョンが us-east-1(Bedrockが利用可能なリージョン)になっているか確認する

エラー③:DynamoDB ValidationException(型不一致)

事象:

botocore.exceptions.ClientError: An error occurred (ValidationException)
when calling the Query operation:
Value provided in ExpressionAttributeValues is incompatible with ConditionExpression operator

原因:DynamoDBテーブル作成時にソートキーtimestamp文字列(String)で定義したが、コードでは数値(Number)型を渡していることによって、型の不一致が発生しています。

対策手順:

  1. DynamoDBコンソール → chat_historyテーブル → 「概要」タブでソートキーの型を確認する
  2. 型が「String」になっている場合、テーブルを削除して再作成する(キーの型は変更不可)
  3. 再作成時にソートキー timestamp の型を必ず 数値(Number) に設定する
  4. コード側でも int(time.time() * 1000) のように必ず整数型で渡していることを確認する

エラー④:コールドスタートによるレスポンス遅延

事象:初回リクエストが5〜10秒かかりユーザーからタイムアウトと誤認される

原因:Lambdaはアイドル状態から起動する際にコンテナ初期化(コールドスタート)が発生し、Python実行環境のロードとboto3の初期化で最大2〜3秒かかることによって、LLMの応答時間と合算され全体遅延が目立っています。

対策手順:

  1. Lambda → 「設定」→「プロビジョニングされた同時実行数」を開く
  2. 同時実行数を 1 以上に設定する(コスト:月約15〜30ドル/1インスタンス)
  3. コストを抑えたい場合は、CloudWatch EventsでLambdaを10分ごとにウォームアップするPing関数を設定する
# lambda/warmer.py: ウォームアップ用ダミーハンドラー
def lambda_handler(event, context):
    if event.get("source") == "aws.events":
        print("Warm-up ping received")
        return {"statusCode": 200}

8. まとめ:実務でどう使うか

本記事で構築したLLMチャットボットのポイントを整理します。

フェーズやること注意点
開発初期Lambda + DynamoDB + Bedrock で最小構成を作るタイムアウト30秒・IAM最小権限を最初に設定する
本番移行前API Gatewayに使用量プラン・APIキー認証を追加するCORSは * から本番ドメインに変更する
スケールアップ時Lambda Powertuningでメモリ最適化する負荷が月10万超えならFargateへの移行を検討する
コスト管理CloudWatch Budgetsで月額アラートを設定するDynamoDBのTTLで古い履歴を自動削除しストレージ費用を抑える
運用監視CloudWatch Dashboardでエラー率・レイテンシを可視化するLLMコストはBedrockのCost Explorerで週次確認する

LLMチャットボットをAWS Lambdaで構築する最大のメリットは、インフラ管理ゼロ・従量課金・IAMによるセキュアなAPI呼び出しの3点です。まず本記事の最小構成で動かし、ユーザー数の増加に応じてFargate→ECSへ段階的に移行するのが現場では現実的な進め方です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次