ちょっと話題の記事

コールセンターへの問い合わせをAIチャットボットで種別を判定し、最適な担当者に自動振り分け[Amazon Connect + Lex + Bedrock]

2023.10.30

はじめに

Amazon Connect + LexでAIチャットボットを構築し、顧客のお問い合わせから、種別を判定し、カテゴリに合った担当者に自動振り分けしてみました。

コールセンターの担当者の負担軽減や人手不足を解消するために、AIチャットボットを使って有人対応から無人対応に切り替えたいというニーズが増えております。

今回は、顧客からのお問い合わせをAIチャットボットがヒアリングし、生成AIのAmazon BedrockのClaude Instanceによってお問い合わせの種別判定を行い、適切な担当者に振り分けを行ってみました。

今回検証するお問い合わせの種別は、以下の9種類です。

  • 在庫に関する問い合わせ
  • 配送/送料に関する問い合わせ
  • 返品/返金に関する問い合わせ
  • 不良品/交換に関する問い合わせ
  • リサイクル/廃棄に関する問い合わせ
  • 購入方法に関する問い合わせ
  • 販売店舗に関する問い合わせ
  • 営業日/営業時間に関する問い合わせ
  • 上記に当てはまらない場合、その他

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

1例として、処理の流れは以下の通りです。

  1. 顧客が電話をかけ、Amazon Connectのフローの一部であるLexのブロックに到達すると、顧客が発話します。
  2. 顧客が「昨日注文した商品の追跡番号は何ですか?」と発言すると、この内容がLexによってテキスト化され、その後BedrockのClaude Instantによって種別判定が行われます。
  3. 種別が「配送/送料に関する問い合わせ」と判定され、「配送/送料」担当者に電話をつなぎます。(今回は実際には電話をつなぎません)

以下の図は、電話での対話の流れを示しています。お問い合わせ内容から、最適な担当者に電話がつながります。

前提

  • 2023年10月時点での検証内容です。今後のアップデートにより改善される可能性があります。恒久的な結果ではありません。
  • 今回は、いくつかのサンプルで検証を行っただけであり、他のサンプルでも同様の結果となるとは限りません。これらの結果は一例として参照ください。
  • 検証では、ゆっくりと明瞭に発話していますので、早口であったり不明瞭な発話の場合、Lexの文字起こし精度に影響が出る可能性があります。

Lexボットの作成

ボットは日本語で作成します。

インテント名はfreeで、スロットはfreeinputtypeの2つ作成します。

スロットはどちらもAMAZON.FreeFormInputで設定し、プロンプトを適当に設定します。

スロットのtypeは、顧客からの発話を聞き取るのではなく、Claudeで種別判定した結果を挿入するために使用します。

Lambdaを使用するように設定しましょう。

後で作成しますが、LambdaをLexから呼び出すため、適切なLambdaを指定し保存します。

Lambdaを呼び出す設定は、[エイリアス]→対象のエイリアス(TestBotAlias)→[言語:Japanese (Japan)]を順にクリックすると、呼び出す設定画面がでます。

これでビルドするとLexのボットの構築は完了です。

Lambda

以下の記事を参考に、IAMロールやBedrockを利用するためのBoto3ライブラリをアップロードしてください。

実行時間は、デフォルトの3秒から1分に変更しました。

コードは下記の通りです。

import json
import boto3
from decimal import Decimal
bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name="ap-northeast-1")

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 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 input_slots_value(value):
    return {
        "shape": "Scalar",
        "value": {
            "originalValue": value,
            "resolvedValues": [value],
            "interpretedValue": value
        }
    }
    
def get_bedrock_response(input_text):
    prompt = f"""\n\nHuman:あなたは、コールセンター担当者です。
    お客様からのお問い合わせ内容を元に、ルール内のリストからもっとも適切な種別を選んでください。
    存在しない問い合わせ種別に遭遇した場合は、「その他」を回答してください。

    <rule>
    1. 返信には、「以下のように変換しました」等の文言は含めないでください。
    2. お客様のお問い合わせ内容に最も適した種別を次のリストから選択してください:
    - 在庫に関する問い合わせ
    - 配送/送料に関する問い合わせ
    - 返品/返金に関する問い合わせ
    - 不良品/交換に関する問い合わせ
    - リサイクル/廃棄に関する問い合わせ
    - 購入方法に関する問い合わせ
    - 販売店舗に関する問い合わせ
    - 営業日/営業時間に関する問い合わせ
    3. 回答は、選択した種別のみを返してしてください。
    </rule>
    お客さんのお問い合わせ内容は次のとおりです。
    <question>
    {input_text}
    </question>
    \n\nAssistant:
    """
    modelId = 'anthropic.claude-instant-v1' 
    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)

    response_text = get_bedrock_response(input_text)
    print("Received response_text:" + response_text)
    
    freeinput = slots['freeinput']['value']['interpretedValue']
    if slots['type'] is None:
        # 種別をスロット名typeに挿入し、確認プロンプトに移行
        slots = {
            "freeinput": input_slots_value(freeinput),
            "type": input_slots_value(response_text.strip())
            }

    return close("Fulfilled", 'それでは、担当者にお繋ぎします', 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には、お問い合わせから種別判定を行い、種別のみを返すプロンプトが設定されています。
  • スロットfreeinputには、顧客のお問い合わせの発話が入り、スロットtypeにはお問い合わせの種別のみが入ります
  • temperatureは、毎回同じ出力が得られるように0にしました
  • 東京リージョンでも利用でき、レスポンスが高速であるClaude Instantを採用しました

Connectのコンタクフロー

コンタクフローは下記の通りです。

ポイントとしては、Lexスロットtypeの値には種別(〇〇に関する問い合わせ)が入っていますので、ブロックタイプであるコンタクト属性の設定に以下の設定をすると、種別ごとに別のフローへ振り分けが可能です。

  • 確認する属性
    • 名前空間:Lex
    • キー:スロット
    • スロット名:type
  • チェックする条件
    • 条件:次と等しい
    • 〇〇に関する問い合わせ(種別ごとに設定すること

フローでは、種別は2つしかありませんが、実際には種別が9つなので、チェックする条件も9つ必要です。

次のブロックでは、プロンプトの再生のフローが組まれていますが、実際には該当する担当者に繋げるため、適切なキューを設定することが良いです。

試してみた

それでは実際に電話をかけて、お問い合わせ内容から、下記の種別が正しく判断してくれるか確認します。

  • 在庫に関する問い合わせ
  • 配送/送料に関する問い合わせ
  • 返品/返金に関する問い合わせ
  • 不良品/交換に関する問い合わせ
  • リサイクル/廃棄に関する問い合わせ
  • 購入方法に関する問い合わせ
  • 販売店舗に関する問い合わせ
  • 営業日/営業時間に関する問い合わせ
  • 上記に当てはまらない場合、その他

顧客が発話する内容には、在庫,返品などの種別名と同じワードが入らないようにします。同じワードが入ると、判定が簡単になってしまうからです。

判定:◯

こちらは、正しく判定されたものです。

在庫に関する問い合わせ

下記を3点を説明すると、私の発話(発話内容)からLexで文字起こしした内容、そしてその内容からお問い合わせの種別判定(Claude Instantの種別判定)を記載しています。

下記のLexでの文字起こしClaude Instantの種別結果は、Lambdaのログから確認できます。

1つめ

  • 発話内容:「特大サイズのヘルメットは揃っていますか?」
  • Lexでの文字起こし:「サイズ の ヘルメット は 揃っ て ます か」
  • Claude Instantの種別判定:「在庫に関する問い合わせ」

2つめ

  • 発話内容:「来週までに入手可能な服はありますでしょうか?」
  • Lexでの文字起こし:「来週 まで に 入手 可能 な 服 は あり ます でしょう か」
  • Claude Instantの種別判定:「在庫に関する問い合わせ」

3つめ

  • 発話内容:「M1のMacBook Proってすぐに買える?」
  • Lexでの文字起こし:「m i の macbook pro って 週 買える」
  • Claude Instantの種別判定:「在庫に関する問い合わせ」

製品名も認識しますね。

配送/送料に関する問い合わせ

  • 発話内容:「昨日頼んだ商品の追跡番号は?」
  • Lexでの文字起こし:「昨日 頼ん だ 商品 の 追跡 番号 は」
  • Claude Instantの種別判定:「配送/送料に関する問い合わせ」

返品/返金に関する問い合わせ

  • 発話内容:「違う商品が届きました。どうすればよいでしょうか?」
  • Lexでの文字起こし:「違う 商品 が 届き まし た どう すれ ば 良い でしょう か」
  • Claude Instantの種別判定:「返品/返金に関する問い合わせ」

不良品/交換に関する問い合わせ

  • 発話内容:「洗濯機に異音がするのですが、見てもらいたいです」
  • Lexでの文字起こし:「洗濯 機 に イオン が する の です が みて もらい たい です」
  • Claude Instantの種別判定:「不良品/交換に関する問い合わせ」

リサイクル/廃棄に関する問い合わせ

  • 発話内容:「昔に買ったエアコンを処分したいんですが。」
  • Lexでの文字起こし:「昔 に 買っ た エアコン を 処分 し たい ん です が」
  • Claude Instantの種別判定:「リサイクル/廃棄に関する問い合わせ」

購入方法に関する問い合わせ

  • 発話内容:「クレジットカード以外の支払い方法はありますか?」
  • Lexでの文字起こし:「クレジット カード 以外 の 支払い 方法 は あり ます か」
  • Claude Instantの種別判定:「購入方法に関する問い合わせ」

販売店舗に関する問い合わせ

  • 発話内容:「あのー商品は直接店頭で見ることはできますか?」
  • Lexでの文字起こし:「商品 は 直接 店頭 で 見る こと は でき ます か」
  • Claude Instantの種別判定:「販売店舗に関する問い合わせ」

営業日/営業時間に関する問い合わせ

  • 発話内容:「祝日でもカスタマーサービスに連絡は可能ですか?」
  • Lexでの文字起こし:「祝日 で も カスタマー サービス に 連絡 は 可能 です か」
  • Claude Instantの種別判定:「営業日/営業時間に関する問い合わせ」

その他

  • 発話内容:「いたずら電話してます」
  • Lexでの文字起こし:「いたずら 電話 し て ます」
  • Claude Instantの種別判定:「その他」

判定:×

種別の判定が間違っていたパターンです。

  • 発話内容:「こんにちは」
  • Lexでの文字起こし:「こんにちは」
  • Claude Instantの種別判定:「購入方法に関する問い合わせ」

検証結果

発話内容からLexの文字起こし精度は想定よりも高く、お問い合わせ内容の種別も正しく判断されておりました。

「その他」の判定は難しいのか、一部で「その他」の判断で間違っていることはありました。

ちなみに、お問い合わせの発話から、レスポンスまでは5秒程度と早いため、顧客体験もよいです。(Lexの文字起こしや生成AIの処理、音声出力でのレスポンス含む)

ただし、以前下記の記事で検証しましたが、名前や住所の建物名などの固有名詞は、Lexでは正しく認識されにくい傾向がありましたので、最新の製品名やマイナーな商品名だと認識されない可能性はあります。

参考