Amazon Connect + Lex + BedrockのAIチャットボットで、発話から個人情報(名前、住所、生年月日)を正しく認識できるか試してみた

2023.10.27

はじめに

Amazon Connect + Lex + Bedrockで、発話から個人情報(番号や名前、住所、誕生日)を正しく認識できるか試してみました。

ConnectとLexによるAIチャットボットで、有人対応から無人対応に変更したいニーズが増えているように思います。

事前に登録したお客様情報に対して、AIチャットボットがお客様の認証を対応できるか気になったため、まず数字や英字、住所などをAIチャットボットが発話どおりに認識してくれるか検証しました。

今回の記事では、以下の5つの項目を発話し、AIチャットボットで正しく認識できるか確認します。

  • 住所
  • 名前
  • 英字
  • 数字
  • 生年月日

特に、数字に関しては、AIチャットボットで電話番号や会員番号、口座番号やクレジットカード番号を聞きとりたいニーズはあると思いますので、認識できるか確認します。

英字に関しては、英数字を絡めたパスワードや会員ID番号をヒアリングするケースがあると思いますので、まずは英字のみを認識できるか確認しました。

構成

構成としては、下記の通りです。

例として、発話で住所を認識させる処理の流れは以下のとおりです。

  1. ユーザーが電話をかけ、Connectのフローの一部であるLexのブロックタイプまで進むと、ユーザーが発話します
  2. ユーザーの発話内容は、Lexでテキスト化され、その内容をもとに、BedrockのClaude Instantで住所のみを抽出や整形します。
  3. 整形した住所名をConnectで音声出力します。

以下の図は、電話でのやり取りの流れを示しています。最後にConnect側で正確に住所名を伝えてくれるか確認します。(整形した住所名は、Lambdaのログからも確認します。)

前提

  • 2023年10月時点での検証内容です。今後のアップデートにより改善される可能性があります。恒久的な結果ではありません。
  • 今回は、数種類のサンプルで検証を行なったに過ぎません。他のサンプルでは同様の結果になるとは限りませんので、これらの結果は一例として参照ください。  

構築

Lambda、Lexボット、Connectフローの構築方法については、下記の記事を参照ください。

今回利用するLambdaのコードは下記の通りです。詳細な解説は前述の記事をご覧ください。

import json
import boto3
from decimal import Decimal
bedrock_runtime = boto3.client('bedrock-runtime')

def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

def elicit_slot(slot_to_elicit, intent_name, slots):
    return {
        'sessionState': {
            'dialogAction': {
                'type': 'ElicitSlot',
                'slotToElicit': slot_to_elicit,
            },
            'intent': {
                'name': intent_name,
                'slots': slots,
                'state': 'InProgress'
            }
        }
    }

def confirm_intent(message_content, intent_name, slots):
    return {
        'messages': [{'contentType': 'PlainText', 'content': message_content}],
        'sessionState': {
            'dialogAction': {
                'type': 'ConfirmIntent',
            },
            'intent': {
                'name': intent_name,
                'slots': slots,
                'state': 'Fulfilled'
            }
        }
    }

def close(fulfillment_state, message_content, intent_name, slots):
    return {
        'messages': [{'contentType': 'PlainText', 'content': message_content}],
        "sessionState": {
            'dialogAction': {
                'type': 'Close',
            },
            'intent': {
                'name': intent_name,
                'slots': slots,
                'state': fulfillment_state
            }
        }
    }

def get_bedrock_response(input_text):
    prompt = f"""\n\nHuman:
    - 依頼
    次の[# ルール]に遵守し、[# 文章]を修正後、住所のみを返してください。
    # ルール
    1. 無意味なスペースは削除してください
    2. 漢字やカタカナ、英語を適切な文字に修正してください
    3. 修正した文章から推測される住所の部分のみを返してください
        - 住所は、都道府県名、市区町村、丁目、番地、号、建物名、階数を含めてください。
        - 住所に使用する数字は、全てアラビア数字(1, 2, 3, ...)を使用してください。
    4. 「以下の文章の修正を行いました」などの文言は不要です
    # 文章
    {input_text}
    Assistant:
    """
    modelId = 'anthropic.claude-instant-v1' 
    # modelId = 'anthropic.claude-v2'
    accept = 'application/json'
    contentType = 'application/json'

    body = json.dumps({
        "prompt": prompt,
        "max_tokens_to_sample": 1000,
        "temperature": 0,
    })

    response = bedrock_runtime.invoke_model(
    	modelId=modelId,
    	accept=accept,
    	contentType=contentType,
        body=body
    )

    response_body = json.loads(response.get('body').read())
    
    return response_body.get('completion')

def Bedrock_intent(event):
    intent_name = event['sessionState']['intent']['name']
    slots = event['sessionState']['intent']['slots']
    input_text = event['inputTranscript']

    if slots['freeinput'] is None:
        return elicit_slot('freeinput', intent_name, slots)

    confirmation_status = event['sessionState']['intent']['confirmationState']

    if confirmation_status == "Confirmed":
        return close("Fulfilled", 'それでは、電話を切ります', intent_name, slots)

    elif confirmation_status == "Denied":
        return close("Failed", 'お力になれず、申し訳ありません。電話を切ります', intent_name, slots)

    # confirmation_status == "None"
    response_text = get_bedrock_response(input_text)
    print("Received response_text:" + response_text)

    return confirm_intent(
        f'それでは、回答します。{response_text}。以上が回答になります。回答に納得したかたは、はい、とお伝え下さい。納得いかない場合、いいえ、とお伝え下さい',
        intent_name, slots)

def lambda_handler(event, context):
    print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))

    intent_name = event['sessionState']['intent']['name']

    if intent_name == 'free':
        return Bedrock_intent(event)
  • 変数名promptは、住所や名前などの個人情報ごとにプロンプトを変えます
  • temperatureは、毎回同じ出力が得られるように0にしました
  • ClaudeV2ではなく、東京リージョンでも利用できるClaude Instantを採用しました

採用したLexのスロットタイプ

LexではAWSで用意されている組み込みスロットタイプがあります。

今回の検証では、自由形式の入力を受け付けるAMAZON.FreeFormInputを利用しています。

AMAZON.FreeFormInputを採用した理由は、他のスロットタイプの場合、ノイズになる言葉を受け付けないためです。

例えば、数字のスロットタイプAMAZON.Numberの場合、「1111です」と発話すると、数字ではない「です」の部分によってエラーになります。

事前に指定した言葉(「です」)を設定することで、エラーを避けることはできますが、「あのー」「えーっと」などノイズになる言葉はたくさんあるので、全てを設定することは難しいです。

また、住所に関係するスロットタイプAMAZON.CityAMAZON.Stateはありますが、日本語の場合、県名のみや、市のみしか認識しなかったので、採用しませんでした。(ブログの検証時点での話です)

そのため、他の方法もあると思いますが、今回の検証では、AMAZON.FreeFormInputで自由入力にし、BedrockのClaudeで必要な文字のみを抽出したり整形するようにしてみました。

試してみた

5つの項目に対して認識するか確認します。

  • 住所
  • 名前
  • 英字
  • 数字
  • 生年月日

前提として、サンプル数は少ないので、他のサンプルでは同じ結果とは限らない可能性が十分にあります。これらの結果は一例として参考にしてください。

また、ゆっくりと明瞭に発話していますので、早口であったり不明瞭な発話の場合、Lexの文字起こし精度に影響が出る可能性があります。

住所

住所は、クラスメソッド各拠点の住所で確認しました。

利用したプロンプトは以下の通りです。input_textにLexで文字起こしされた文章が入ります。

    prompt = f"""\n\nHuman:
    - 依頼
    次の[# ルール]に遵守し、[# 文章]を修正後、住所のみを返してください。
    # ルール
    1. 無意味なスペースは削除してください
    2. 漢字やカタカナ、英語を適切な文字に修正してください
    3. 修正した文章から推測される住所の部分のみを返してください
        - 住所は、都道府県名、市区町村、丁目、番地、号、建物名、階数を含めてください。
        - 住所に使用する数字は、全てアラビア数字(1, 2, 3, ...)を使用してください。
    4. 「以下の文章の修正を行いました」などの文言は不要です
    # 文章
    {input_text}
    Assistant:
    """

最初に試した拠点は以下の通り:

  • 発話内容:「東京都港区西新橋1-1-1 日比谷フォートタワー26階」
  • Lexでの文字起こし:「東京 都 港 区 西新橋 一 の 一 の 一 期 は ポート タワー に 十 六 回」
  • Claude Instantの整形結果:「東京都港区西新橋1丁目1-1」

上記を3点を説明すると、私の発話(発話内容)からLexで文字起こしした内容、そしてその内容を整形した結果をClaude Instantの整形結果として記載しています。

上記のLexでの文字起こしClaude Instantの整形結果は、Lambdaからログ出力された内容を記載してます。

Lexの文字起こしの時点で、建物名がうまく認識できておりませんでした。

Lexの文字起こしの建物名を認識させるには、カスタム語彙による方法がありますが、日本に存在する建物名を全てカスタム語彙に設定するのは現実的ではありません。また、Lexのカスタム語彙は、日本語をサポートしていません。

日比谷フォートタワーは、竣工予定が2021年6月のため、Lexが認知していない可能性がありますね。

別の住所(クラスメソッドの札幌オフィス)の場合、Lexでは建物名も認識してくれました。

  • 発話内容:「北海道札幌市中央区北3条西1-1-1 札幌ブリックキューブ10階です」
  • Lexでの文字起こし:「北海道 札幌 市 中央 区 北 三条 西 一 の 一 の 一 札幌 ブリック キューブ 十 階 です」
  • Claude Instantの整形結果:「北海道札幌市中央区北三条西1-1-1札幌ブリックキューブ10階」

札幌ブリックキューブは、1981年が竣工であり、30年以上前だったため、Lexも認識できたと推測します。

住所に関しては、番地や号までは認識できていますが、建物名などの固有名詞(特に最近のもの)は、Lexだと難しいようです。

名前

名前は自分の名前で検証しました。

プロンプトは以下の通りです。

    prompt = f"""\n\nHuman:
    - 依頼
    次の[# ルール]に遵守し、[# 文章]を修正後、人名を返してください。
    # ルール
    1. 無意味なスペースは削除してください
    2. 人名のみを抽出してください。
    3. 「以下のように変換しました」などの文言は不要です。
    # 文章
    {input_text}
    Assistant:
    """
  • 発話内容:「ひらいゆうじです」
  • Lexでの文字起こし:「ヒライ いう 中 です」
  • Claude Instantの整形結果:「申し訳ありません。いいえ、人名は見つかりませんでした」

Lexの文字起こしの時点で、名前がうまく認識できておりませんでした。

先程の建物名と同様に、固有名詞は、Lexだと難しいようです。

ちなみに、下記のように「人名」ではなく「名前」してみました。

    prompt = f"""\n\nHuman:
    - 依頼
    次の[# ルール]に遵守し、[# 文章]を修正後、名前を返してください。
    # ルール
    1. 無意味なスペースは削除してください
    2. 名前のみを抽出してください。
    3. 「以下のように変換しました」などの文言は不要です。
    # 文章
    {input_text}
    Assistant:
    """
  • 発話内容:「ひらいゆうじです」
  • Lexでの文字起こし:「ヒライ いう 中 です」
  • Claude Instantの整形結果:「ヒライ」

名字のみが抽出されました。

やはり難しいですね。

英字

英数字を絡めた会員IDを想定して、まず英字のみで認識できるか確認しました。

結論を言うと、Lexの認識精度もしくは私の発音が原因で、Lexは期待通りには認識しませんでした。

1つめ

  • 発話内容:「ABCDEFGHIJKLMN」
  • Lexでの文字起こし:「a b c d f t t h l n」

2つめ

  • 発話内容:「OPQRSTUVWXYZ」
  • Lexでの文字起こし:「e q r a t u r i w a y a r」

数字

続いては、数字の認識を確認します。

数字の場合、電話番号や口座番号、会員IDやクレジットカード番号などがありますね。

0~9までの数字を発話をして検証しました。

プロンプトは以下の通りです。

    prompt = f"""\n\nHuman:
    - 依頼
    次の[# ルール]に遵守し、[# 文章]を修正後、すべての数字を順番に続けて返してください。
    # ルール
    1. スペースは削除してください
    2. 漢数字は、次のようにアラビア数字に変換してください
      - 〇: 0
      - 一: 1
      - 二: 2
      - 三: 3
      - 四: 4
      - 五: 5
      - 六: 6
      - 七: 7
      - 八: 8
      - 九: 9
    3. アラビア数字を1桁ずつカンマで区切ってください
      - 例:〇,〇,〇,〇,〇,〇
    4. 「以下のように数字を変換しました」などの文言は不要です。
    # 文章
    {input_text}
    Assistant:
    """

1つめ

  • 発話内容:「1023456789です。」(数字は一文字ずつ伝えています。)
  • Lexでの文字起こし:「一 〇 二 三 四 五 六 七 八 九」
  • Claude Instantの整形結果:「1,0,2,3,4,5,6,7,8,9」

2つめ

  • 発話内容:「6023548791ですね。」(数字は一文字ずつ伝えています。)
  • Lexでの文字起こし:「六 〇 二 三 五 四 八 七 九 一ですね」
  • Claude Instantの整形結果:「6,0,2,3,5,4,8,7,9,1」

Claude Instantの整形結果では、数字の間にカンマを意図的に挿入しています。

理由は、Connectで「6023548791」を音声出力する場合、「六十億二千三百五十四万八千七百九十一」と音声が流れます。

電話などの番号では、一文字ずつ伝える方が分かりやすいので、カンマを入れています。

Claude Instantの整形結果でカンマを使って音声出力をする一方、Lexのスロット内ではカンマがない番号を保持しておくほうが良いでしょう。

今回のプロンプトでは、数字の桁数までは指定してません。例えば、口座番号であれば、番号の桁数は決まっていますので、桁数を指定することで、より精度が上がるかと思います。

生年月日

最後に、西暦の年月日を発話し、その認識精度を確認します。

プロンプトは以下の通りです。

    prompt = f"""\n\nHuman:
    - 依頼
    次の[# ルール]に遵守し、[# 文章]を修正後、日付の形式で返してください。
    # ルール
    1. スペースは削除してください
    2. 漢数字は、次のようにアラビア数字に変換してください
      - 〇: 0
      - 一: 1
      - 二: 2
      - 三: 3
      - 四: 4
      - 五: 5
      - 六: 6
      - 七: 7
      - 八: 8
      - 九: 9
    3. 「以下のように変換しました」などの文言は不要です。
    4. 誕生日の日付を以下のフォーマットで答えてください。
     - 例:〇〇〇〇年〇〇月〇〇日
    # 文章
    {input_text}
    Assistant:
    """
  • 発話内容:「誕生日は、2003年12月20日です」
  • Lexでの文字起こし:「誕生 日 は 二 千 三 年 十 二 月 二十 日 です」
  • Claude Instantの整形結果:「2003年12月20日」

20日を「はつか」と発話しましたが、認識してくれています。

  • 発話内容:「誕生日は、2002年1月1日です」
  • Lexでの文字起こし:「誕生 日 は 二 千 二 年 一 月 一日 です」
  • Claude Instantの整形結果:「2002年1月1日」

プロンプトの例は、例:〇〇〇〇年〇〇月〇〇日のようにしています。

例:2000年1月1日といった具体的な数字を入れると、例にしている値がClaude Instantの整形結果に影響を与えたためです。

問題なく認識してくれました。

結果

今回のチャットボットの認識とその評価は、以下の結果となりました。

項目 認識 備考
住所 号までは可能。建物名(固有名詞)が難しい
名前 × 固有名詞が難しい
英字 × 特定の英字が難しい
数字 問題なく認識した
生年月日 問題なく認識した

数字や生年月日に関しては、認識されました。

数字であれば、プッシュボタンの入力でも実現できますが、一部で発話が必要であれば、発話で統一した方が使い勝手は良いと思います。

住所に関しては、都道府県から号までは、認識されましたが、建物名などの固有名詞が難しいです。名前も同様です。

どなたかの参考になれば幸いです

参考