6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Amazon Lex #3】Lambda連携で動的な応答を返せるようにしてみよう

6
Posted at

はじめに

こんばんは、mirukyです。
Amazon Lex シリーズ第3回です。

前回(#2)では、AWSコンソールからLexボットを構築し、インテント・スロット・サンプル発話の基本を学びました。
今回は、AWS Lambdaと連携して動的な応答を返すボットに進化させます。

#2で作成したボットは「注文番号を聞いて定型文を返す」だけでしたが、実際の業務では「注文番号をもとにデータベースを検索し、リアルタイムの注文状況を返す」ことが求められます。この動的処理を実現するのがLambda連携です。

出典:AWS Lambda関数を Amazon Lex V2 ボットに統合する - AWS

目次

  1. Lambda連携の仕組み
  2. Lambda関数の作成
  3. ダイアログコードフックの設定
  4. フルフィルメントコードフックの設定
  5. ボットへのLambda関数の紐付け
  6. テストと動作確認
  7. エラーハンドリング
  8. 料金について
  9. おわりに

1. Lambda連携の仕組み

1-1. コードフックとは

Amazon Lexでは、会話の途中やインテント完了時にLambda関数を呼び出す仕組みコードフックと呼びます。コードフックには2種類あります。

コードフック 呼び出しタイミング 主な用途
ダイアログコードフック スロット引き出し中(会話の途中) 入力値のバリデーション、条件に応じた会話分岐
フルフィルメントコードフック 全スロットが埋まった後(インテント完了時) DB検索、外部API呼び出し、最終応答の生成

1-2. 処理フロー

スクリーンショット 2026-03-10 22.28.47.png

ユーザー:「注文の状況を確認したい」
  ↓
Amazon Lex:インテント認識(CheckOrderStatus)
  ↓
Lex:「注文番号をお教えください」(スロットプロンプト)
  ↓
ユーザー:「ORD-001」
  ↓
Lambda(ダイアログコードフック):注文番号の形式を検証
  ↓ 検証OK
Lex:「注文番号 ORD-001 で確認します。よろしいですか?」
  ↓
ユーザー:「はい」
  ↓
Lambda(フルフィルメント):DB検索 → 注文情報を取得
  ↓
Lex → ユーザー:「ORD-001は現在配送中です。3月7日到着予定です。」

2. Lambda関数の作成

2-1. Lambda関数の作成手順

スクリーンショット 2026-03-11 21.40.52.png

  1. AWSマネジメントコンソール(東京リージョン)で Lambda を開く
  2. 「関数の作成」「一から作成」
設定項目
関数名 lex-order-lookup
ランタイム Python 3.14
アーキテクチャ x86_64

2-2. Lambda関数のコード

スクリーンショット 2026-03-11 21.41.57.png

コードソースに下記のコードを入力します。

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# サンプルの注文データ(本番ではDynamoDB等から取得)
ORDERS = {
    "ORD-001": {
        "customerName": "田中太郎",
        "product": "ワイヤレスイヤホン",
        "status": "配送中",
        "estimatedDelivery": "2026年3月10日"
    },
    "ORD-002": {
        "customerName": "佐藤花子",
        "product": "モバイルバッテリー",
        "status": "出荷準備中",
        "estimatedDelivery": "2026年3月12日"
    },
    "ORD-003": {
        "customerName": "鈴木一郎",
        "product": "USBケーブル",
        "status": "お届け済み",
        "estimatedDelivery": "2026年3月5日"
    }
}


def lambda_handler(event, context):
    logger.info(f"Received event: {json.dumps(event, ensure_ascii=False)}")

    intent_name = event["sessionState"]["intent"]["name"]
    invocation_source = event["invocationSource"]  # DialogCodeHook or FulfillmentCodeHook

    if intent_name == "CheckOrderStatus":
        if invocation_source == "DialogCodeHook":
            return handle_dialog(event)
        elif invocation_source == "FulfillmentCodeHook":
            return handle_fulfillment(event)

    # その他のインテントはそのまま通過
    return delegate(event)


def handle_dialog(event):
    """ダイアログコードフック:スロット値のバリデーション"""
    slots = event["sessionState"]["intent"]["slots"]
    order_id_slot = slots.get("OrderId")

    # スロットがまだ埋まっていない場合はLexに委任
    if not order_id_slot or not order_id_slot.get("value"):
        return delegate(event)

    order_id = order_id_slot["value"]["interpretedValue"]

    # 注文番号の形式チェック(ORD-で始まるか)
    if not order_id.startswith("ORD-"):
        return elicit_slot(
            event,
            "OrderId",
            "注文番号はORD-で始まる形式です(例:ORD-001)。もう一度お教えください。"
        )

    return delegate(event)


def handle_fulfillment(event):
    """フルフィルメントコードフック:注文情報の検索と応答"""
    slots = event["sessionState"]["intent"]["slots"]
    order_id = slots["OrderId"]["value"]["interpretedValue"]

    if order_id in ORDERS:
        order = ORDERS[order_id]
        message = (
            f"注文番号 {order_id} の情報です。\n"
            f"商品:{order['product']}\n"
            f"ステータス:{order['status']}\n"
            f"お届け予定日:{order['estimatedDelivery']}"
        )
    else:
        message = f"注文番号 {order_id} は見つかりませんでした。番号をご確認の上、もう一度お試しください。"

    return close(event, "Fulfilled", message)


def delegate(event):
    """Lexにダイアログ管理を委任する"""
    return {
        "sessionState": {
            "dialogAction": {"type": "Delegate"},
            "intent": event["sessionState"]["intent"]
        }
    }


def elicit_slot(event, slot_name, message):
    """特定のスロットを再度引き出す"""
    return {
        "sessionState": {
            "dialogAction": {
                "type": "ElicitSlot",
                "slotToElicit": slot_name
            },
            "intent": event["sessionState"]["intent"]
        },
        "messages": [
            {"contentType": "PlainText", "content": message}
        ]
    }


def close(event, fulfillment_state, message):
    """会話を終了する"""
    return {
        "sessionState": {
            "dialogAction": {"type": "Close"},
            "intent": {
                "name": event["sessionState"]["intent"]["name"],
                "state": fulfillment_state
            }
        },
        "messages": [
            {"contentType": "PlainText", "content": message}
        ]
    }

「Deploy」 をクリックして関数をデプロイします。

Lambda関数のレスポンス形式
Amazon Lex V2向けのLambdaレスポンスは、sessionStatedialogActionで次のアクションを指定します。主なアクションタイプは以下の3つです。

dialogAction.type 動作
Delegate 会話の制御をLexに委任する
ElicitSlot 指定したスロットの再入力を求める
Close 会話を終了し応答を返す

出典:Lambda 関数の入出力形式 - AWS

3. ダイアログコードフックの設定

3-1. ダイアログコードフックとは

会話の途中で呼び出されるコードフックです。ユーザーがスロット値を入力するたびに呼び出されるため、入力値のバリデーション条件に応じた会話分岐に使用します。

3-2. 今回の実装内容

def handle_dialog(event):
    """ダイアログコードフック:スロット値のバリデーション"""
    slots = event["sessionState"]["intent"]["slots"]
    order_id_slot = slots.get("OrderId")

    # スロットがまだ埋まっていない場合はLexに委任
    if not order_id_slot or not order_id_slot.get("value"):
        return delegate(event)

    order_id = order_id_slot["value"]["interpretedValue"]

    # 注文番号の形式チェック(ORD-で始まるか)
    if not order_id.startswith("ORD-"):
        return elicit_slot(
            event,
            "OrderId",
            "注文番号はORD-で始まる形式です(例:ORD-001)。もう一度お教えください。"
        )

    return delegate(event)

上記のLambdaコードでは、handle_dialog()関数で以下の処理を行っています。

処理 内容
スロット未入力チェック OrderIdがまだ入力されていなければLexに委任
形式チェック 注文番号がORD-で始まっていなければ再入力を要求
検証OK 検証に問題なければLexに委任して会話を続行

4. フルフィルメントコードフックの設定

4-1. フルフィルメントコードフックとは

すべての必須スロットが埋まり、ユーザーが確認を終えた後に呼び出されるコードフックです。ビジネスロジックの実行(データベース検索、API呼び出し、レスポンス生成など)を行います。

4-2. 今回の実装内容

def handle_fulfillment(event):
    """フルフィルメントコードフック:注文情報の検索と応答"""
    slots = event["sessionState"]["intent"]["slots"]
    order_id = slots["OrderId"]["value"]["interpretedValue"]

    if order_id in ORDERS:
        order = ORDERS[order_id]
        message = (
            f"注文番号 {order_id} の情報です。\n"
            f"商品:{order['product']}\n"
            f"ステータス:{order['status']}\n"
            f"お届け予定日:{order['estimatedDelivery']}"
        )
    else:
        message = f"注文番号 {order_id} は見つかりませんでした。番号をご確認の上、もう一度お試しください。"

    return close(event, "Fulfilled", message)

handle_fulfillment()関数では、以下の処理を行っています。

  1. OrderIdスロットから注文番号を取得
  2. 注文データ(サンプル)を検索
  3. 見つかれば注文情報を整形して応答、見つからなければエラーメッセージを返す

本番環境ではDynamoDBを使いましょう
今回はサンプルとしてLambdaコード内に注文データをハードコードしていますが、本番環境ではDynamoDBなどのデータベースから取得するのが一般的です。

5. ボットへのLambda関数の紐付け

5-1. エイリアスにLambda関数を設定

スクリーンショット 2026-03-11 21.44.56.png

  1. Amazon Lexコンソールで CustomerSupportBot を開く
  2. 左メニューの 「エイリアス」 を選択
  3. 「TestBotAlias」 をクリック
  4. 「言語」 セクションの 「Japanese(Japan)」 をクリック
  5. Lambda関数のプルダウンで lex-order-lookup を選択
  6. Lambda関数のバージョンまたはエイリアスは $LATEST を選択
  7. 「保存」 をクリック

5-2. インテントでコードフックを有効化

スクリーンショット 2026-03-11 21.47.06.png

  1. CheckOrderStatusインテントの編集画面を開く
  2. 「フルフィルメント」 セクションをアクティブ にする
  3. 「コードフック」 セクションでチェックマーク をつける
  4. 「インテントを保存」「ビルド」 をクリック

ビルドを忘れずに
Lambda関数の紐付けやコードフックの設定を変更した後は、必ずビルドを実行してください。ビルドしないと変更が反映されません。

6. テストと動作確認

6-1. テスト①:正常な注文確認

ユーザー:注文の状況を確認したい
ボット  :注文番号をお教えください。(例:ORD-001)
ユーザー:ORD-001
ボット  :注文番号 ORD-001 の情報です。
          商品:ワイヤレスイヤホン
          ステータス:配送中
          お届け予定日:2026年3月10日

スクリーンショット 2026-03-11 21.48.52.png
あらかじめLambdaで渡しているデータを出力してくれました。

6-2. テスト②:不正な注文番号形式

ユーザー:注文の確認をお願いします
ボット  :注文番号をお教えください。(例:ORD-001)
ユーザー:12345
ボット  :注文番号はORD-で始まる形式です(例:ORD-001)。もう一度お教えください。
ユーザー:ORD-002
ボット  :注文番号 ORD-002 の情報です。
          商品:モバイルバッテリー
          ステータス:出荷準備中
          お届け予定日:2026年3月12日

スクリーンショット 2026-03-11 21.50.05.png

6-3. テスト③:存在しない注文番号

ユーザー:注文を確認したい
ボット  :注文番号をお教えください。(例:ORD-001)
ユーザー:ORD-999
ボット  :注文番号 ORD-999 は見つかりませんでした。番号をご確認の上、もう一度お試しください。

スクリーンショット 2026-03-11 21.50.33.png
きちんと動いてくれましたね。

7. エラーハンドリング

7-1. Lambda関数のエラー時の動作

Amazon Lex V2はLambda関数を同期呼び出し(RequestResponse)で実行するため、Lambda関数がエラー(例外、タイムアウト等)で失敗した場合、自動リトライは行われません。エラーが発生すると、Lexはインテントに設定されたフルフィルメント失敗応答をユーザーに返します。フルフィルメント失敗応答が未設定の場合は、汎用的なエラーメッセージが表示されます。

7-2. エラーハンドリングのベストプラクティス

プラクティス 説明
try-exceptで例外をキャッチ Lambda関数内で例外を適切にハンドリングし、ユーザーフレンドリーなエラーメッセージを返す
CloudWatch Logsでモニタリング Lambda関数のログを確認し、エラーの原因を特定する
タイムアウト値の設定 Lambdaのデフォルトタイムアウトは3秒。DB接続やAPI呼び出しがある場合は適切に延長する
フォールバック応答の用意 エラー時でも「担当者に繋ぎます」のような応答を返して会話を終了する
def handle_fulfillment(event):
    """エラーハンドリング付きフルフィルメント"""
    try:
        slots = event["sessionState"]["intent"]["slots"]
        order_id = slots["OrderId"]["value"]["interpretedValue"]

        # DB検索処理(実際にはDynamoDBなどを使用)
        order = lookup_order(order_id)

        if order:
            message = format_order_response(order)
        else:
            message = f"注文番号 {order_id} は見つかりませんでした。"

    except Exception as e:
        logger.error(f"Error: {str(e)}")
        message = "申し訳ございません。システムエラーが発生しました。お手数ですが、しばらく時間をおいてから再度お試しください。"

    return close(event, "Fulfilled", message)

8. 料金について

サービス 料金 備考
Amazon Lex テキスト: $0.00075/リクエスト、音声: $0.004/リクエスト 各ユーザー入力ごとに課金
AWS Lambda リクエスト数 + 実行時間課金 無料利用枠あり(月100万リクエスト)

コードフックが有効な場合のLambda呼び出し回数
ダイアログコードフックを有効にすると、ユーザーの入力ごとにLambdaが呼び出されます。例えば1回の会話で3回のやり取りがあれば、Lambda呼び出しも3回以上発生します。コスト見積もりの際はこの点を考慮してください。

最新の料金は公式ページをご確認ください。
出典:Amazon Lex の料金 - AWS

9. おわりに

ここまでお読みいただきありがとうございます。
今回は、Lambda関数と連携して、注文データを動的に検索・応答できるボットを構築しました。

コードフック(ダイアログ・フルフィルメント)の仕組みを理解すれば、入力値のバリデーションからデータベース検索外部API連携まで、あらゆるビジネスロジックをボットに組み込めるようになります。

次回#4では、構築したLexボットをAmazon Connectに統合し、電話で操作できる音声ボットを構築します。実際の電話回線からLexボットと会話する仕組みを作っていきましょう。

ではまた、お会いしましょう。

参考リンク

Amazon Lex V2 Lambda連携 公式ドキュメント

6
6
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
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?