デジタルヒューマンと Amazon Bedrock で召喚!バーチャル事務職員 | 【第4回】RAG で回答の精度を高めよう

はじめに

こんにちは! 株式会社クロスパワー クラウドシステム開発部の N です。

この記事は連載『デジタルヒューマンと Amazon Bedrock で召喚!バーチャル事務職員』の第4回となります。
前回の記事はこちらです。

xp-cloud.jp

第4回では RAG という手法を用いてチャットボットがより"正確な"回答を生成するようにする方法について解説していきます。

目次

RAG とは

生成 AI はときに事実に基づかない情報を生成することがあります。
この現象はハルシネーションと呼ばれています。

意図的にハルシネーションを起こした例を見てみましょう。
前回までに実装してきたチャットボットに

株式会社クロスパワーのリモートワーク制度について教えてください。

という質問を投げてみます。

チャットボットの回答は次のようになりました。

こんにちは。総務の者です。リモートワーク手当についてのご質問ありがとうございます。

株式会社クロスパワーのリモートワーク手当は、月額3,000円となっております。これは、在宅勤務に伴う光熱費や通信費の一部を補助する目的で支給されています。

ただし、この手当の支給には条件がございまして、1ヶ月のうち10日以上リモートワークを行った場合に適用されます。9日以下の場合は支給対象外となりますので、ご注意ください。

何か他にご不明な点がございましたら、お気軽にお尋ねくださいね。

セッション ID を変えてもう一度同じ質問をしてみると次のような回答が返ってきました。

こんにちは。総務の者です。リモートワーク手当についてのご質問ありがとうございます。

株式会社クロスパワーのリモートワーク手当は、月額5,000円となっております。これは、在宅勤務に伴う通信費や光熱費の一部を補助する目的で支給されています。

ただし、この手当の支給には条件がございます。月の半分以上(11日以上)在宅勤務を行った場合に適用されます。

詳細な規定や申請方法については、社内ポータルサイトの「福利厚生」ページをご確認いただくか、私までお気軽にお問い合わせください。

他に何かご不明な点はございますか?

質問は同じなのに回答の内容(リモートワーク手当の月額など)が変わってしまいました。 また、実はどちらも事実とは異なる不正確な情報です。

このように、生成 AI にまるで幻覚(= ハルシネーション)を見ているかのように、事実と異なる情報をもっともらしく生成してしまうという問題点を抱えています*1
その他にも、生成 AI には知識の拡張や更新が難しいという問題点が存在します。

RAG ( Retrieval-Augmented Generation, 検索拡張生成 ) は、これらの問題点を解決することを目的として2020年に Meta 社の研究者によって考案されました*2
RAG をひと言で説明するなら、外部データを検索した結果をプロンプトに含めて回答を生成する手法です。

次の図は RAG の動作フローを簡略化して説明したものです。

RAG の動作フロー

この図に登場する LLM は日本の山に関する知識を持っていません。
そのため『RAG 適用前』では、「日本で最も高い山は?」という質問に対し「K2 です。」という事実とは異なる回答を生成してしまいます。

一方『RAG 適用後』のフローでは、まず質問文を用いて関連する情報を外部データから検索します。
そうやって得た「日本の山の高さランキング」の情報と質問文を合わせて LLM に入力します。
このようにして LLM はハルシネーションを回避して「富士山です。」という正しい回答を生成することができるようになります。

IBM が公開している記事の喩えがわかりやすいので以下に引用します。

RAGを概念化するために、AIモデルをアマチュアの家庭料理人と想像してみてください。
彼らは料理の基礎は知っていますが、特定の料理の訓練を受けたシェフの専門知識(組織独自のデータベース)が欠けています。
RAGは、家庭料理人にその料理のレシピ本を提供するようなものです。
料理に関する一般的な知識と料理本のレシピを組み合わせることで、家庭料理人は自分の好きな料理に特化した料理を簡単に作ることができます。

www.ibm.com

RAG においては 知識の更新 = 外部データベースの更新 となります。
これはファインチューニングのように LLM そのものを再調整する手法と比べて低コストです。
そのため RAG は頻繁に更新されるようなデータを取り扱うのに向いています。
こういった特徴から RAG の実際のユースケースとして社内情報検索アプリやチャットボットがよく挙げられます。

AWS 上で RAG を実装する場合 Amazon Bedrock Knowledge Bases *3Amazon Kendra が主な選択肢となります*4
本記事では Amazon Bedrock Knowledge Bases を用いて RAG を実装することにします。

アプリケーションの概要

構成

今回実装するアプリケーション全体の構成は次の図のとおりです。

アプリケーション全体の構成図
アプリケーション全体の構成図

前回の構成に外部データ検索に必要なリソースを追加する形になります。

追加するリソースは次の3つです(図中右上)。

  • Amazon Bedrock Knowledge Bases (以下 ナレッジベース) : RAG 用途に特化したフルマネージドなナレッジベースです。
  • Amazon S3 : ナレッジベースの元となるドキュメントを保管するためのストレージです。
  • Pinecone : 検索用のベクトルを管理するためのベクトルデータベースです。AWS外のサービスとなります。

RAG の仕組み

今回実装する RAG は準備フェーズ検索フェーズの2つのフェーズで構成されます。

準備フェーズ

準備フェーズでは以下の手順でナレッジベースの元となるドキュメントからベクトルデータベースを作成・更新します。

  1. ナレッジベース が S3 からドキュメントを取得する。
  2. 取得したドキュメントを適切な長さの断片 (チャンク) に分割する。
  3. 文字列からベクトルを生成する埋め込みモデルを用いて各チャンクをベクトルに変換する。
  4. チャンクとベクトルを Pinecone のベクトルデータベースに保管する。

準備フェーズの処理フロー
準備フェーズの処理フロー

この一連の処理はナレッジベースのコンソールから実行できます。

検索フェーズ

検索フェーズでは以下の手順で Lambda 関数から受け取ったクエリーを用いてベクトルデータベースを検索します。

  1. Lambda 関数からナレッジベースにクエリー (= ユーザーからの質問文) を送信する。
  2. 埋め込みモデルを用いてクエリーをベクトル化する。
  3. ベクトル化したクエリーを用いてベクトルデータベースを検索する。
  4. クエリーとの類似度が高いチャンクを取得する。
  5. 取得した情報を入力に含めて LLM を実行する。

検索フェーズの処理フロー
検索フェーズの処理フロー

この処理は生成 AI アプリ開発フレームワークである LangChain の Chain に組み込む形で記述できます。

チャットボットのキャラクター設定

前回まではチャットボットには「株式会社クロスパワーのバーチャル事務職員」として振る舞うように指示していました。
しかしながら、コンプライアンス的に弊社の社内規約等の情報を記事内に記載することができません。

そのため、今回作成するチャットボットは IPA のデジタル人材育成部門のアシスタント という設定に変更します。
ナレッジベースに入力するドキュメントは IPA が公開している資料を引用することにします。

それでは次の章から実装の手順を解説していきましょう。

ナレッジベースの構築

まずは、準備フェーズを実行する基盤となるナレッジベースを構築していきましょう。

埋め込みモデルへのアクセスのリクエス

ナレッジベースがチャンクやクエリーをベクトル化するために用いる埋め込みモデルへのアクセス権をリクエストしましょう。

今回利用する埋め込みモデルは Cohere Embed Multiligual v3 です。カナダの Cohere 社が開発する埋め込みモデルで、その名のとおり日本語を含む100種類以上の言語に対応しています*5

リクエストの手順については第1回で解説しています
こちらを参考に Cohere Embed Multiligual v3 へのアクセスをリクエストしてください。

S3 のセットアップ

ナレッジベースの情報元となるドキュメントを格納する Amazon S3 バケットを作成しましょう。

S3 のコンソールから識別可能な名前のバケットを作成して docs/ というディレクトリを作成します。

バケットを作成

ベクトルデータベースのセットアップ

ベクトル化したデータを保管するベクトルデータベースを作成しましょう。

今回は PineconeAWS Marketplace を介して利用します。
Pinecone は AWS の 研究部門の元責任者である Edo Liberty 氏によって2019年に設立された Pinecone 社が提供するベクトルデータベースサービスです。 無料プランや低コストな従量課金プランで利用できる*6ため、今回のようなお試しで RAG を構築したいケースにうってつけです。

Pinecone のサブスクライブ

AWS Marketplace のコンソールの「製品を検出」を選択して検索欄に Pinecone と入力します。
検索結果から従量課金プランの「Pinecone Vector Database - Pay As You Go Pricing」を選択します。
なお「Pinecone Vector Database - Annual Commit 」は1年分のコストを先払いするプランなので誤って選択しないように注意してください。

Pay As You Go Pricing を選択

「Pay As You Go Pricing」が正しく選択されていることを確認したら「View purchase options」を選択します。

View purchase options を選択

支払いの詳細画面に進むので「Subscribe」を選択します。

Subscribe を選択

支払い処理が完了したら「Set up your account」を選択します。

Set up your account を選択

Pinecone のベクトルデータベースのセットアップ

Pinecone のサインアップ画面に進むので任意の方法でアカウントを作成してログインしてください。

Pinecone にサインアップ

「Create index」を選択してインデックス (= Pinecone におけるベクトルデータベース) を作成していきます。

Create index を選択

インデックス名は pincecone-index と入力してください。
「Dimensions」1024「Metric」cosine を設定します。
その次に「Setup by model」を選択します。

index の設定

モデルの選択画面が開くので Cohere-embed-multilingual-v3.0 を選んだら「Set Configuration」を選択します。

モデルの設定

最後に「Create index」を選択すればインデックスが作成されます。

メニューの「Database」→「Indexes (1)」を開くと、作成したインデックスの情報を確認できます。
「Host:」に続く文字列はこのインデックスのエンドポイント URL です。この後のナレッジベースのセットアップに必要な情報なので控えておきましょう。

インデックス情報の表示

API キーの保存

ナレッジベースからベクトルデータベースへアクセスできるようにするために Pinecone の API キーを AWS Secrets Manager に保存しましょう。

Pinecone のメニューからAPI Keys」を選択して API キーの管理画面を開いたら「+ Create API Key」を選択します。

Create API Key を選択

任意のキー名を入力したら「Create API Key」を選択して API キーを作成します。

キー名を入力

「Copy key valueを選択して作成した API キーをコピーしておきます。

Copy key value を選択

次に AWS Secrets Manager コンソールを開いて「新しいシークレットを保存する」を選択します。

新しいシークレットを保存するを選択

まず「その他のシークレットのタイプ」を選択します。
「キー」apiKey を、「値」に先ほどコピーした API キーの値を設定したら「次」を選択します。

API キー情報の設定

シークレットの名前に pinecone-api-key と入力したら「次」を選択します。

シークレット名の設定

以降の設定はデフォルトのままでシークレットを保存します。
作成したシークレットの ARN は次の作業で必要になるので控えておきましょう。

シークレット ARN の確認

これで Pinecone のセットアップは完了です。

ナレッジベースの作成

ナレッジベースを新規作成して、ここまでにセットアップしたリソースと連携させていきましょう。

Amazon Bedrock コンソールのメニューから「ナレッジベース」を開いたら「ナレッジベースを作成」を選択します。

ナレッジベースを作成を選択

ナレッジベース名に任意の文字列を入力します。今回はデフォルトのままで進めます。

ナレッジベース名を入力

データソースにAmazon S3を設定したら、「次へ」を選択してください。

データソースを選択

「S3 を参照」を選択して先ほど作成した S3 バケットを参照するように設定します。
URI の後ろに /docs を付け足したら「次へ」を選択します。

データソースを設定

埋め込みモデルは「Embed Multiligual v3」を選択します。

埋め込みモデルを選択

ベクトルデータベースは「作成したベクトルストアを選択」を選択します。
ベクトルストアは「Pinecone」を選択し、免責事項のチェックボックスをオンにします。

ベクトルデータベースの設定その1

「エンドポイント URL」には Pinecone のエンドポイント URL を入力します。
「認証情報シークレット ARN」には API キーを保管したシークレットの ARN を入力します。
「テキストフィールド名」には question「テキストフィールド名」には metadata と入力して「次へ」を選択します。

ベクトルデータベースの設定その2

確認画面に進んだら「ナレッジベースを作成」を選択します。
ナレッジベースの作成が成功してステータスが 利用可能 になれば完了です。

ナレッジベースの作成完了

準備フェーズの実行

準備フェーズを実行してベクトルデータベースを構築していきましょう。

まずナレッジベースの元となるドキュメントを S3 バケットにアップロードします。
今回は IPA が公開している以下の2つの資料を利用します。あらかじめダウンロードしておいてください。

Amazon S3 コンソールから作成した S3 バケットdocs フォルダを開いて「アップロード」を選択します。

アップロードを選択

ダウンロードしておいた2つのファイル (わかりやすいように名前を変更しています) を追加したら「アップロード」を選択します。

アップロード

アップロードが完了したことが確認できれば次のステップに進みます。

アップロードしたファイルの確認

Amazon Bedrock コンソールのメニューから「ナレッジベース」を開いて先ほど作成したナレッジベースを選択します。
ナレッジベースの詳細画面に移動したら「データソース」から作成済みのデータソースにチェックを入れて「同期」を選択します。

データソースの同期

数分ほど待つと同期処理が完了してベクトルデータベースが構築されます。
Pinecone のマイページにアクセスして確認してみましょう。
pinecone-index「NAMESPACES (1)」タブを開いて作成したベクトルの総数が確認できれば大丈夫です。

構築されたベクトルデータベースの確認

ナレッジベースの動作確認

構築したベクトルデータベースを検索できることをナレッジベースをテストして確認しましょう。

Amazon Bedrock コンソールのナレッジベースの詳細ページの右側の「ナレッジベースをテスト」を利用します。 これは任意の LLM を用いて簡易的な RAG をテストすることができる機能です。

まず、「モデルを選択」を選択してください。

構築されたベクトルデータベースの確認

カテゴリは Anthropic を、モデルは Claude を選んだら「適用」を選択します。

モデルを選択

これでクエリーを投げて回答を生成することができるようになりました。
テキストボックスに 組織が気をつけるべき情報セキュリティ上の脅威の1位は何ですか? と入力して「実行」を選択しましょう。
少し待てば、アップロードしたドキュメントを情報源とした回答が表示されます。

回答の生成

次に「ソースの詳細を表示」を選択してみましょう。
画面右側の「ソースの情報」にベクトルデータベースをクエリーを用いて検索して取得したチャンクの情報が表示されます。
チャンクの中身の他に、チャンクが含まれるページの番号やドキュメントの URI 等のメタデータも取得していることが確認できます。

ソースの詳細を表示

検索フェーズの実装

検索フェーズの処理をチャットボット本体の Lambda 関数に追加していきましょう。

Lambda 関数 chatbot のコンソールを開いて lambda_fuction.py の内容を次のように書き換えてください。

import json
from operator import itemgetter

from langchain_aws import ChatBedrock
from langchain_aws.retrievers import AmazonKnowledgeBasesRetriever
from langchain_community.chat_message_histories.dynamodb import DynamoDBChatMessageHistory
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory

template = """
あなたは「総務さん」という名前の女性の事務職員です。
あなたの目的は、ユーザーからの情報セキュリティやデジタルスキル標準に関する質問に対して明確に答えることです。

あなたは丁寧かつ親しみやすい口調で返答するようにしてください。

次の情報を用いてユーザーの質問に答えてください。
 <context>{context}</context>

次に示すルールを必ず守ってください。
- 返答は簡潔にしてください。
- 常に「総務さん」という名前の女性の事務職員というキャラクターを保ってください。
- もし返答に確信が持てないときは「申し訳ございません。質問が理解できませんでした。もう一度質問していただけますか?」と返答してください。
- もし質問の内容が不適切なものであったときは「申し訳ございません。その質問には答えることができません。」と返答してください。

一般的な会話における質問と返答の例を以下に示します。
 <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"]

    # ナレッジベース検索の作成
    retriever = AmazonKnowledgeBasesRetriever(
        knowledge_base_id="{ナレッジベースID}",
        retrieval_config={"vectorSearchConfiguration": {"numberOfResults": 4}},
    )

    # プロンプトテンプレートの作成
    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 = (
        RunnablePassthrough.assign(context=(itemgetter("question") | retriever))
        | 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),
    }

前回からの変更点を中心に解説していきます。

template = """
あなたは「総務さん」という名前の女性の事務職員です。
あなたの目的は、ユーザーからの情報セキュリティやデジタルスキル標準に関する質問に対して明確に答えることです。

あなたは丁寧かつ親しみやすい口調で返答するようにしてください。

次の情報を用いてユーザーの質問に答えてください。
 <context>{context}</context>

前述したとおり、チャットボットの役割を「ユーザーからの情報セキュリティやデジタルスキル標準に関する質問に対して明確に答えること」に変更しています。

{context} が今回追加したナレッジベースの検索結果のプレースホルダーとなります。

    # ナレッジベース検索の作成
    retriever = AmazonKnowledgeBasesRetriever(
        knowledge_base_id="{ナレッジベースID}",
        retrieval_config={"vectorSearchConfiguration": {"numberOfResults": 4}},
    )

AmazonKnowledgeBasesRetriever を用いてナレッジベース検索インスタンスを初期化しています。
{ナレッジベースID} の部分は実際のナレッジベース ID で置き換えてください。
"numberOfResults" では取得する検索結果の取得数を指定しています。今回は最大4つのチャンクを取得するように設定しています。

    # Chain の作成
    chain = (
        RunnablePassthrough.assign(context=(itemgetter("question") | retriever))
        | prompt
        | llm
    )

処理フローを定義する Chain にナレッジベースを介してベクトルデータベースの検索処理を追加しています。
Chain 全体の処理フローを表したのが次の図となります。

Chain 全体の処理フロー
Chain 全体の処理フロー

RunnablePassthroughassign メソッドを利用して、ナレッジベースから得た情報(context)を質問文(question)や会話履歴(history)と一緒に prompt へ送っています。

動作確認

前回同様 API を呼び出してチャットボットの動作を確認します。
以下、質問文と回答文のみ抜粋した一連のやり取りを掲載します。

ユーザー:

組織が気をつけるべき情報セキュリティ上の脅威の1位は何ですか?

チャットボット:

はい、ご質問ありがとうございます。組織が気をつけるべき情報セキュリティ上の脅威の1位は「ランサムウェアによる被害」です。

ランサムウェアは、PCやサーバーに感染するウイルスの一種で、攻撃者が様々な脅迫を行って金銭を要求してくるのが特徴です。組織の規模や 業種に関係なく被害に遭う可能性があるので、十分な注意が必要です。

私たちの会社でも、このような脅威に対して常に警戒を怠らないようにしています。何か気になることがありましたら、いつでもしたら、いつでもご相談くださいね。

ユーザー:

ランサムウェアの具体的な手口について教えてください。

チャットボット:

はい、ランサムウェアの手口についてご説明いたしますね。主に以下のような方法があります:

1. 脆弱性を悪用した感染:
   OSやアプリケーションの脆弱性を利用して、インターネット経由でPCやサーバーに感染させます。

2. 公開サーバーへの不正アクセス:
   外部に公開されているポート(例:リモートデスクトップ)に不正アクセスして感染させます。

3. データの暗号化と脅迫:
   PCやサーバーのデータを暗号化し、復元と引き換えに金銭を要求します。

4. 重要情報の窃取と脅迫:
   重要情報を盗み、公開すると脅して金銭を要求します。

5. DDoS攻撃の脅迫:
   金銭を支払わなければDDoS攻撃を仕掛けると脅迫します。

これらの手口を組み合わせた「二重脅迫」や「四重脅迫」も確認されているそうです。

私たちの会社でも、このような攻撃に備えて、日頃からセキュリティ対策に気を付けています。何か気になることがありま したら、いつでもご相談くださいね。

ユーザー:

このような問題を取り扱うDX推進スキル標準のロールは何でしょうか?

チャットボット:

はい、ご質問ありがとうございます。DX推進スキル標準の中で、情報セキュリティに関する問題を主に扱うロールは「サイバーセキュリティ」になります。

サイバーセキュリティのロールは、組織のデジタル資産を保護し、情報セキュリティ上の脅威に対処する重要な役割を担っています。ランサムウェアのような攻撃からの防御や、セキュリティインシデントへの対応なども、このロールの重要な責務となります。

ただ、セキュリティは組織全体で取り組むべき課題ですので、他のロールの方々も基本的な知識を持つことが大切です。私たち総務部門でも、日々セキュリティ意識 を高めるよう心がけています。

何か他にご質問はありますか?お気軽にお尋ねください。

ナレッジベースの情報に基づいた回答が生成されていることが確認できます。
ただ、回答の内容には誤りがあります(例:「サイバーセキュリティ」はロールではなく人材類型です)。
このように RAG を用いてもなお誤りを含む回答が生成される可能性があることには留意すべきでしょう。

おわりに

今回は RAG を用いてチャットボットの回答の精度を高める方法について解説しました。
RAG はシンプルな実装でも効果の高い手法ではありますが、より精度を高めたりより複雑な質問へ対応したりするためにはさらなる改善が必要となってきます。
最新の RAG の改善手法について AWS ブログ記事にまとめられているので興味のある方は読んでみることをおすすめします。

aws.amazon.com

さて、本連載第2回から第4回となる今回まで、AWS の生成 AI サービスの利用方法といったバックエンド側の話題を中心に解説してきました。
次回はフロントエンド側に軸足を戻して、デジタルヒューマンのレスポンスの改善方法について解説していくことにします。

参考文献

Amazon Bedrock 生成AIアプリ開発入門 [AWS深掘りガイド] | SBクリエイティブ