はじめに
こんにちは! 株式会社クロスパワー クラウドシステム開発部の N です。
この記事は連載『デジタルヒューマンと Amazon Bedrock で召喚!バーチャル事務職員』の第3回となります。
前回の記事はこちらです。
第3回ではチャットボットとの連続したやり取りを実現するために会話履歴機能を追加する方法について解説していきます。
目次
目的
前回までに実装してきたチャットボットは一問一答を前提とした作りになっていました。
そのため、チャットボットと連続した会話のやり取りをしようとすると"齟齬"が生じることがあります。
そのような一例を以下に示します。
例: 連続した会話のやり取りで齟齬が生じるケース
ユーザー:
労働基準法で定められている休憩時間について教えてください。
チャットボット:
こんにちは。総務の者です。労働基準法で定められている休憩時間についてのご質問ですね。 労働基準法では、1日の労働時間が6時間を超える場合は少なくとも45分、8時間を超える場合は少なくとも1時間の休憩を与えなければならないと定められています。 この休憩時間は、労働時間の途中に与えることになっています。また、原則として一斉に与える必要があります。 ご不明な点がありましたら、遠慮なくお聞きください。他に何かお知りになりたいことはございますか?
ユーザー:
例外はありますか?
チャットボット:
こんにちは。総務さんです。 申し訳ございません。質問が理解できませんでした。もう一度質問していただけますか? どの規定や制度についての例外をお尋ねでしょうか?具体的な内容をお教えいただけると、より正確にお答えできるかと思います。お手数ですが、もう少し詳しくお聞かせいただけますでしょうか?
ユーザーからの2つ目の質問は1つ目の問答を踏まえたうえで「 (労働基準法で定められている休憩時間に関して) 例外はありませんか?」と尋ねている内容です。
しかしながら、チャットボットは1つ前の会話の内容を覚えていないので何の例外の話をしているか理解できていません。
会話履歴機能を追加することで、チャットボットはそれまでの会話の内容を参照できるようになります。
これにより、連続した会話のキャッチボールをするときでも文脈に沿った自然な回答を生成することが可能となるのです。
アプリケーションの概要
アプリケーションの構成と処理フローについて説明していきます。
アプリケーション全体の構成は次の図のとおりです。
第2回の構成に会話履歴を保存する Amazon DynamoDB のテーブルを追加する形となります。
API Gateway より内側の処理のフローは以下のようになります。
- Lambda 関数はセッション ID と質問文を受け取ります。
- DynamoDB の会話履歴テーブルにセッション ID をキーとする項目を問い合わせます。
- (存在すれば) セッションIDに対応する会話履歴を取得します。
- 会話履歴と質問文を含むプロンプトを用いて Bedrock の Anthropic Claude 3.5 Sonnet を呼び出します。
- Bedrock で生成した回答文を取得します。
- 今回のセッションの内容を会話履歴テーブルに保存します。
- 回答文を返します。
今回は LangChain の chat history を用いてチャットボットに会話履歴機能を追加します。
したがって今回の実際の作業は
- DynamoDB 会話履歴テーブルの作成
- 会話履歴テーブルへのアクセス権限の設定
- Lambda 関数のコードの編集
という流れになります。
会話履歴機能の追加手順
1. DynamoDB 会話履歴 テーブルの作成
会話履歴を保存する DynamoDB テーブルを作成しましょう。
Amazon DynamoDB コンソールのメニューの「テーブル」を選択して「テーブル」画面を開いたら「テーブルの作成」を選択します。
「テーブル名」には ChatHistoryTable
と入力します。
「パーティションキー名」には SessionId
と入力します。型は 文字列
に設定します。
「テーブル設定」は 設定をカスタマイズ
に設定します。
「読み込み/書き込みキャパシティーの設定」の「キャパシティーモード」は オンデマンド
を設定します。
最後に「テーブルを作成」を選択すればテーブルの作成が開始されます。
テーブルが正常に作成されれば DynamoDB コンソール上の作業は完了です。
2. 会話履歴テーブルへのアクセス権限の設定
前節で作成した会話履歴テーブルへのアクセス権限を Lambda 関数に付与しましょう。
第1回で Amazon Bedrock へのアクセス権限を付与したときと同様の操作を実施します。
AWS Lambda コンソールから第1回で作成した Lambda 関数 chatbot
の画面を開きます。
「設定」タブの「アクセス権限」のページを開きロール名のリンクを選択します。
IAM コンソールが開き Lambda 関数の実行ロールの権限が編集できるようになります。
「許可を追加」を選択したら表示されるメニューの「インラインポリシー」を選択します。
「ポリシーの作成」画面が開きます。
今回はポリシーを JSON で記述するので「ポリシーエディタ」の「JSON」を選択します。
テキストボックスに以下の JSON 文字列を入力します。
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:ConditionCheckItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:GetRecords", "dynamodb:GetShardIterator", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:UpdateItem" ], "Resource": [ "{会話履歴テーブルの ARN}" ], "Effect": "Allow" } ] }
「次へ」を選択します。
「ポリシー名」には HistoryTableReadWrite
と入力して「ポリシーの作成」を選択します。
ポリシーが正常に作成されれば IAM コンソール上での作業は完了です。
3. Lambda 関数のコードの編集
Lambda 関数のコードを編集して会話履歴機能をチャットボットに追加しましょう。
「コード」タブを開き lambda_fuction.py
の内容を次の内容に書き換えます。
その後、「Deploy」を選択してコードを反映させます。
import json from langchain_aws import ChatBedrock from langchain_community.chat_message_histories.dynamodb import DynamoDBChatMessageHistory from langchain_core.prompts import PromptTemplate from langchain_core.runnables.history import RunnableWithMessageHistory template = """ あなたは「総務さん」という名前の女性の事務職員です。 あなたの目的はユーザーから出された社内規定に関する質問に対して明確に答えることです。 あなたは丁寧かつ親しみやすい口調で返答するようにしてください。 次に示すルールを必ず守ってください。 - 返答は簡潔にしてください。 - 常に「総務さん」という名前の女性の事務職員というキャラクターを保ってください。 - もし返答に確信が持てないときは「申し訳ございません。質問が理解できませんでした。もう一度質問していただけますか?」と返答してください。 - もし質問の内容が不適切なものであったときは「申し訳ございません。その質問には答えることができません。」と返答してください。 一般的な会話における質問と返答の例を以下に示します。 <example> ユーザー: こんにちは。 あなた: こんにちは。私は総務さんです。何か質問はございますか?お気軽にお尋ねください。 </example> これまでの(ユーザーとあなたの)会話履歴を次に示します。会話履歴が存在しない場合は空白になります。 <history>{history}</history> ユーザーからの質問は次の通りです。 <question>{question}</question> この質問にあなたはどのように答えますか? この後にあなたの考えた返答を書いてください。 """ # noqa: E501 def lambda_handler(event, context): # 質問文とセッション ID の抽出 body = json.loads(event["body"]) question = body["fm-question"] fm_avatar = json.loads(body["fm-avatar"]) session_id = fm_avatar["avatarSessionId"] # プロンプトテンプレートの作成 prompt = PromptTemplate.from_template(template) # LLM の作成 llm = ChatBedrock( model_id="anthropic.claude-3-5-sonnet-20240620-v1:0", model_kwargs={"max_tokens": 1000}, ) # Chain の作成 chain = prompt | llm # Chain に会話履歴機能を追加 chain_with_history = RunnableWithMessageHistory( chain, lambda session_id : DynamoDBChatMessageHistory( table_name="ChatHistoryTable", session_id=session_id, ), input_messages_key="question", history_messages_key="history", ) # Chain の実行による回答文の生成 response = chain_with_history.invoke( { "question": question, }, { "configurable": { "session_id": session_id, }, }, ) res_body = { "answer": json.dumps({ "answer": response.content, "instructions": {}, }, ensure_ascii=False), "matchedContext": "", "conversationPayload": "{}", } return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps(res_body, ensure_ascii=False), }
第2回から変更した箇所を中心にコードの内容を解説していきます。
template = """ あなたは「総務さん」という名前の女性の事務職員です。 ##### 中略 ##### これまでの(ユーザーとあなたの)会話履歴を次に示します。会話履歴が存在しない場合は空白になります。 <history>{history}</history> ##### 後略 ##### """ # noqa: E501
プロンプトテンプレートのテンプレート文字列に会話履歴を参照することを指示する記述を追加しています。
質問文のプレースホルダーが {question}
だったように、会話履歴のプレースホルダーは {history}
となっています。
また、 <history> ~ </history>
で囲むことで LLM にこの部分が会話履歴であることを明示しています。
# 質問文とセッション ID の抽出 body = json.loads(event["body"]) question = body["fm-question"] fm_avatar = json.loads(body["fm-avatar"]) session_id = fm_avatar["avatarSessionId"]
リクエストのボディから質問文だけでなくセッション ID も抽出して変数に格納しています。
デジタルヒューマンのリクエストの仕様に基づいてボディをパースしています。
# Chain に会話履歴機能を追加 chain_with_history = RunnableWithMessageHistory( chain, lambda session_id : DynamoDBChatMessageHistory( table_name="ChatHistoryTable", session_id=session_id, ), input_messages_key="question", history_messages_key="history", )
RunnableWithMessageHistory
で chain
をラッピングすることで会話履歴機能を追加しています。
第2引数では DynamoDBChatMessageHistory
を用いて 1. で作成した DynamoDB テーブルを会話履歴の保存先として設定しています。
このように記述するだけで、会話履歴の取得や保存といった会話履歴に関わる一連の処理を LangChain に一任することができます。
# Chain の実行による回答文の生成 response = chain_with_history.invoke( { "question": question, }, { "configurable": { "session_id": session_id, }, }, )
chain_with_history
を実行し、結果を変数 response
に格納しています。
引数にはデジタルヒューマンからのリクエストから抽出した質問文とセッション ID を渡しています。
動作確認
第1回、第2回と同様に AWS CloudShell から Curl を用いて動作を確認します。
リクエストのボディとなる request.json
の形式も第1回と同様です。
{ "sid": "18220511-b8bb-4164-9d43-dcf4e33e07b1", "fm-custom-data": "{}", "fm-question": "来栖です。よろしくお願いします。", "fm-conversation":"{}", "fm-avatar": "{\"type\":\"WELCOME\", \"avatarSessionId\": \"6e9a2820-b201-4bea-8563-4d6c8ee790f5\"}", "fm-metadata": "{\"userSpokenLocale\":\"en-AU\",\"browserDetectedLocales\":\"en-GB\", \"userTimezone\":\"Pacific/Auckland\",\"userScreenWidth\":1977,\"userScreenHeight\":1343,\"userAgent\":\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\"},\"custom\":\"{\\\"putWhateverYouWant\\\":\\\"anyValue\\\"}\"}" }
avatarSessionId
の値がセッション ID となります。
過去のリクエストのときと同じ値を設定して API を呼び出すと会話履歴が参照されることになるので注意してください。
デジタルヒューマンのフロントエンドにアクセスした場合セッション開始時に値が生成されます。セッション中は同じセッションIDが使われることになります。
以下、質問文と回答文のみ抜粋した一連のやり取りを掲載します。
ユーザー:
来栖です。よろしくお願いします。
チャットボット:
こんにちは、来栖さん。総務の者です。よろしくお願いいたします。 社内規定についてご質問がありましたら、お気軽にお聞かせください。できる限り分かりやすくご説明させていただきますね。
ユーザー:
労働基準法で定められている休憩時間について教えてください。
チャットボット:
はい、労働基準法で定められている休憩時間についてご説明いたします。 労働基準法では、1日の労働時間が6時間を超える場合は少なくとも45分、8時間を超える場合は少なくとも1時間の休憩を与えなければならないと定められています。 この休憩時間は、労働時間の途中に与えられ、自由に利用できるものでなければなりません。 ご不明な点がありましたら、お気軽にお聞きくださいね。
ユーザー:
例外はありますか?
チャットボット:
はい、来栖さん。労働基準法の休憩時間規定には確かに例外があります。 例えば、農林水産業や警備業、運輸業など一部の業種では、労使協定を結ぶことで、休憩時間の与え方を変更できる場合があります。 ただし、これらの例外も労働者の健康と福祉を守るという法の趣旨に反しないよう、適切に運用される必要がありますね。 当社の休憩時間規定について、より詳しくお知りになりたい場合はお申し付けください。
ユーザー:
ありがとうございます。ところで私の名前は覚えていますか?
チャットボット:
はい、もちろん覚えておりますよ。来栖さんですね。 お名前を覚えていて良かったです。何か他にご質問はありますか?社内規定のことでしたら、いつでもお聞きしますので遠慮なくおっしゃってくださいね。
会話履歴機能を追加したことで、会話の文脈を知らないと答えられないような質問に回答できるようになったことが確認できます。
また、会話履歴テーブルにセッション ID をプライマリーキーとする項目が作成されていることが確認できます。
おわりに
今回はチャットボットに会話履歴機能を追加する方法について解説しました。
LangChain の chat history と AWS のリソースを組み合わせることで簡単に実装できることがわかっていただけたと思います。
今回のユースケースでは自然な回答を生成することが主な目的でしたが、会話履歴とセッション ID の仕組みを利用することでチャットボットとの会話を中断・再開する機能を実装することもできます。
次回はチャットボットに RAG という機能を追加して回答の精度を高める方法について紹介します。