【AI Shift Advent Calendar 2023】RAGの自動評価の活用|第14回対話システムシンポジウムで発表します

DALLE-3で生成

こんにちは、AIチームの二宮です。

この記事はAI Shift Advent Calendar 2023の12日目の記事です。

前回の記事では、RAG(Retrieval Augmented Generation)の概要と実装、設計について書かせていただきました。今回の記事では、RAGの自動評価の活用について実装を交えながらお話させていただきます。

そして、宣伝になりますが、2023年の12月13日から12月14日の2日間にかけて第14回対話システムシンポジウム(人工知能学会 言語・音声理解と対話処理研究会 SIG-SLUD)が国立国語研究所にて開催されます。AI Shiftからは二宮がインダストリーセッションにおいて「カスタマーサポートにおけるLLMを用いたRAGベースの対話システムの評価と事業活用に向けた取り組み」について発表いたします。今回の記事はその内容を元にした概説になります。当日は是非ともRAGの事業活用やLLMのことについて議論させていただけますと幸いです。

タイトル : カスタマーサポートにおけるLLMを用いたRAGベース対話システムの評価と事業活用に向けた取り組み
著者 : 二宮 大空(株式会社AI Shift)
日時 : 12月14日(木)13:40-15:20 インダストリーセッション

背景

RAGの設計

カスタマーサポート分野ではRAGを用いたチャットボットの導入検討が積極的に行われています。弊社でもRAGを用いたチャットボットの動作検証と開発を行っております。

まず、チャットボットは導入先ごとに作り込みが必要なリソースがいくつかあります。これは導入先ごとに参照する情報や適切な応対が異なるためです。RAGをベースとしたチャットボットでは、導入先ごとにプロンプトと社内ドキュメントなどの任意の情報(本記事ではKnowledge DBと記します)の2つをリソースとして利用します。

RAGの動作検証

リソースが準備できたら、動作検証を開始します。もし導入先に既にカスタマーサポートがある場合は、ユーザーからよく寄せられる質問がわかっている場合が多いので、それらの質問に対して適切に回答できるか検証します。さらに、Knowledge DBに記載されていない無関係な質問に対して不適切な回答をしていないかも同時に検証します。ここで、望ましくない回答が生成された場合は、プロンプトやKnowledge DBの修正を行います。また、もしユーザーからどのような質問がなされるのかわからない場合は、類似したドメインの企業に寄せられる質問を収集したり、Knowledge DBを元にLLMで生成したりすることが考えられます。

しかし、この動作検証は非常に多くの時間と労力が必要です。例えば、「質問Aに正しく回答できなかったためプロンプトを修正すると、これまで正しく回答できていた質問Bに回答できなくなる」といったことは多々ありました。そして、いつまでも動作検証が終わらない状態になる可能性があります。そこで、RAGの自動評価を用いて、この状態を改善できないかと考えました。

RAGの自動評価

自動評価の目的

RAGの自動評価の目的を、以下の二つに定めました。

  • 動作検証の終了条件の明確化
  • プロンプトとKnowledge DBの改善点の列挙

まず1点目は、自動評価でスコアを出力することで「90点を超えたら動作検証は終わり」といったように動作検証の終了条件を明確にできます。これによって、作業者が目標を持って安心して作業できることを狙いとしています。次に2点目は、自動評価を通してRAGの望ましくない回答を収集し、改善に活かすことを狙いとしています。

自動評価の概要

企業のカスタマーサポートによく寄せられる質問を事前に収集または作成し、それらに対して上手く回答できているかを評価します。今回はチャットボットとしての利用を考慮して、一問一答ではなく、複数ターンの対話に対する評価を行います。具体的には、最初のターンは事前に用意した質問をユーザーの発話として利用し、2ターン目よりユーザー役のLLMを用いて合計3ターンの対話を行いました。RAGの評価は、各ターンごとに評価用のプロンプトを与えたLLMにより行いました。

OSSのチャットボット評価ツール

本記事で紹介するデモでは自作したプロンプトを利用しております。しかし、OSSで提供されているツールは妥当性に関する議論や検証が頻繁に行われているため、これから評価用のツールを作成する方にはそちらをおすすめします。現在、RAGをはじめとしたチャットボットの評価に関するツールはたくさん存在しており、例えば、ragasLangChain AutoEvaluatorLM vs LMChatbot Arenaが挙げられます。

設計時の自動評価として利用するときには、使い勝手がよいかを観点とすることをおすすめします。例えば、弊社の社内検証では、推論時間が短いか、低コストであるか、複数の企業におけるデータで安定した評価を行うことができるかをツール選定時に考慮しました。

自動評価の実装

前回の記事で作成したRAGと、ユーザー役のLLMで対話させ、それをLLMに評価させてみます。その過程の生成結果は全てスプレッドシートに書き出します。

まず、自動評価で利用するプロンプトは以下になります。

  • ユーザー役のLLMに与えるプロンプト
# 指示
あなたは最近リリースされたサービスのユーザーです。
そのサービスを使用中に、予期しない挙動に遭遇しました。ユーザーとして、問題を解決するために会社のサポートデスクに相談してください。
相談内容は以下に記載されていますので、それに基づいて会話を進めてください。
ただし、以下に記載された要件を守ってください。

# 相談内容
#QUERY#

# 要件
#PERSONA#
- 相談内容が解決したら、"**DONE**"のみを出力して会話をすみやかに終了してください。
- 相談内容の解決に向けて、相手の発言に対する適切な感情や反応を表現してください。
- 問題が解決しない場合は、追加の簡潔で的確な質問をしてください。解決した場合は、それ以上の行動を取る必要はありません
日本語で答えてください。

ここで#QUERY#はユーザーの質問文、#PERSONA#はユーザーの特徴に後ほど置換します。#PERSONA#を設定する理由は、多様なユーザーを想定した動作検証を行うためです。

  • 評価用のLLMに与えるプロンプト
# 指示
与えられた[Context]、[Question]、[Response]に基づいて対話を評価してください。以下の基準に基づいて評価してください:
- **Understanding**::応答は質問の本質を理解していますか?
- **Relevance**:応答はコンテキストと質問に関連していますか?
- **Completeness**:応答は質問の全ての側面に対応していますか?
- **Correctness**:応答の情報は正確ですか?
- **Coherence**:応答は論理的に構築され、容易に理解できますか?

それぞれのカテゴリーについて1(悪い)から5(優れている)のスコアを付けてください。

# 対話システムの振る舞いに関するガイドライン:
対話システムは次のように振る舞うべきであると設定されています。
- 提供されたコンテキストだけに基づいて回答してください。
- 回答するための十分な情報がない場合は、「わかりません」と回答してください。
- あなたは会社として応答することが期待されているので、他の会社やその製品について言及したり、他の会社の製品と比較したりしないでください。
- 必要に応じて、ユーザーの手続きに適した連絡先をユーザーに通知してください。
- 応答はできるだけ正確であるようにしてください。

# 出力形式:
- "Comment"セクションでは、[Japanese_Comment...]とマークされた部分に記入し、思考過程をステップバイステップで詳細に説明し、最後に結論を述べてください。
- 各アンダースコア(_)を適切な情報またはスコアで置き換えてください。
{{
  "Comment": {{"Step1": [Japanese_Comment_1], "Step2": [Japanese_Comment_2], "Step3": [Japanese_Comment_3], "...": [Japanese_Comment_N], "Conclusion": [Japanese_Comment_Conclusion]}},
  "Understanding": _,
  "Relevance": _,
  "Completeness": _,
  "Correctness": _,
  "Coherence": _,
  "Overall": _
}}
# 評価対象:
### CONTEXT BEGIN ###
#CONTEXT#
### CONTEXT END ###

### QUESTION BEGIN ###
#QUESTION#
### QUESTION END ###

### RESPONSE BEGIN ###
#RESPONSE#
### RESPONSE END ###

評価を開始し、JSON形式で出力してください。

評価用のLLMの出力は、JSON形式を期待しています。細かな工夫として、スコアを生成する前にCommentで段階的にLLMに判断根拠を生成させることで、Chain of Thoughtを活用しています。ただし、この評価用のプロンプトの妥当性は十分に検証されていないため、必要に応じてragasなどのOSSの評価ツール等を利用することをおすすめします。

それでは評価スクリプトの実装になります。まず、必要なライブラリをインストールします。

pip install gspread
pip install oauth2client
pip install openai

次に、評価に必要な情報を定義します。プロンプト、書き出すスプレッドシート用のサービスアカウント、OpenAIのAPIが必要になります。また、今回は前回の記事で記述したRAGを利用しています。もしRAGが既に実装されている方は、必要に応じて変更していただければと思います。

import datetime
import json
import random

import gspread
from oauth2client.service_account import ServiceAccountCredentials
from openai import OpenAI

# 前回の記事で作成したRAGをimportします (同ディレクトリ内にrag.pyが配置されている想定)
from rag import rag


random.seed(42)


with open("resources/evaluate_prompt.txt", "r") as f:
    EVALUATE_PROMPT = f.read()
with open("resources/rag_prompt.txt", "r") as f:
    RAG_PROMPT = f.read()
with open("resources/user_prompt.txt", "r") as f:
    USER_PROMPT = f.read()

client = OpenAI(api_key="<YOUR_OPENAI_API_KEY>")

scope = [
    "https://spreadsheets.google.com/feeds",
    "https://www.googleapis.com/auth/drive",
]
credentials = ServiceAccountCredentials.from_json_keyfile_name("<YOUR_CREDENTIALS_PATH>", scope)

MAX_TURN = 3
THRESHOLD = 0.7

queries = [
    "本当に役立たず。イライラするんだけど。",
    "複数のアカウントを持っていた場合、どのアカウントで操作すれば良いのか分からない場合はどうしたらいいですか?",
    "どのようにAmebaと外部サービスのID連携ができるのか?",
]

personas = [
    ("angry", "怒り口調で高圧的な態度をとる。大声で捲し立てるように相談する。"),
    ("calm", "論理的で、事実に基づいて冷静な言葉遣いで話す。問題解決に向けて合理的な提案をする。"),
    ("beginner", "基本的な内容に関する質問が多く、専門用語には不慣れである。"),
]

企業のよくある質問としてqueriesを、ユーザー役の振る舞いとしてpersonasを定義しています。

それでは、ユーザー役のLLM、評価用のLLMの振る舞いを関数にします。

def user_completion(history, persona):
    original_query = history[0][0]
    prompt = USER_PROMPT.replace("#QUERY#", original_query).replace("#PERSONA#", persona[1])
    messages = [
        {"role": "system", "content": prompt},
        {"role": "user", "content": "こちらは相談窓口です。本日はどのようなご相談でしょうか?"},
    ]
    for h in history:
        messages.append({"role": "assistant", "content": h[0]})
        messages.append({"role": "user", "content": h[1]})
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        temperature=0.7,
        messages=messages,
    )
    return response.choices[0].message.content

def evaluate(query, documents, response):
    context = "\n---\n".join([doc.page_content for doc in documents])
    prompt = (
        EVALUATE_PROMPT.replace("#CONTEXT#", context)
        .replace("#QUESTION#", query)
        .replace("#RESPONSE#", response)
    )
    messages = [{"role": "system", "content": prompt}]
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        temperature=0.2,
        messages=messages,
    )
    return response.choices[0].message.content

def parse_evaluate_output(response):
    try:
        response = (
            response.replace("```json", "")
            .replace("```", "")
            .replace("{{", "{")
            .replace("}}", "}")
        )
        response_json = json.loads(response)
        score = float(int(response_json["Overall"]) / 5)  # 1~5点を0.0~1.0点に変換
    except:
        score = 0.0

    return score

ユーザー役のLLMは設定した課題が解決したら、DONEの固定文字を生成するように指示しています。この場合、対話は3ターン未満で終了します。

評価用LLMの出力はJSON形式を期待しており、スコア箇所をparse_evaluate_output関数で抽出しています。補足ですが、先日のOpenAIのアップデートで必ずJSON出力を受け取れるJSON modeが利用可能になりましたので、そちらを利用しても良いと思います。

最後に、スプレッドシートに書き出す部分です。

def main():
    # authorization
    gc = gspread.authorize(credentials)
    sh = gc.open_by_key("<YOUR_SPREADSHEET_ID>")
    worksheet_title = "hoge"
    sheets = [sheet.title for sheet in sh.worksheets()]
    if worksheet_title in sheets:
        worksheet = sh.worksheet(worksheet_title)
    else:
        worksheet = sh.add_worksheet(title=worksheet_title, rows=100, cols=20)
    worksheet.append_row(
        [
            "config",
            "user1",
            "assistant1",
            "check1",
            "user2",
            "assistant2",
            "check2",
            "user3",
            "assistant3",
            "check3",
        ]
    )

    # begin evaluation
    overall_scores = []
    empty_row = len(worksheet.get_all_values()) + 1
    for i, query in enumerate(queries):
        curr_col = "A"
        curr_row = empty_row + i

        persona = random.choice(personas)
        metadata = {
            "date": (datetime.datetime.utcnow() + datetime.timedelta(hours=9)).strftime(
                "%Y/%m/%d %H:%M:%S"
            ),
            "persona": persona[0],
        }
        worksheet.update(f"{curr_col}{curr_row}", json.dumps(metadata, ensure_ascii=False))
        curr_col = chr(ord(curr_col) + 1)

        # begin conversation
        history = []
        scores = []
        while len(history) < MAX_TURN:
            # generate user query
            if len(history) > 0:
                query = user_completion(history, persona)
            # finish conversation if user says "DONE"
            if "DONE" in query:
                history.append([query, ""])
                break
            worksheet.update(f"{curr_col}{curr_row}", query)
            curr_col = chr(ord(curr_col) + 1)

            # RAG
            _, history, docs_str = rag(query, history, RAG_PROMPT)
            answer = history[-1][1]
            worksheet.update(f"{curr_col}{curr_row}", answer)
            rag_output_cell = f"{curr_col}{curr_row}"
            curr_col = chr(ord(curr_col) + 1)

            # evaluation
            turn_response = evaluate(query, docs_str, answer)
            turn_score = parse_evaluate_output(turn_response)
            worksheet.update(f"{curr_col}{curr_row}", turn_response)
            curr_col = chr(ord(curr_col) + 1)
            scores.append(turn_score)

            # alert if turn score is low
            if turn_score < THRESHOLD:
                worksheet.format(
                    rag_output_cell,
                    {
                        "backgroundColor": {
                            "red": 250 / 255,
                            "green": 219 / 255,
                            "blue": 218 / 255,
                            "alpha": 1,
                        }
                    },
                )

        # ターンごとのスコアを平均して対話に対するスコアとする
        # scoring
        avg_score = sum(scores) / len(scores)
        overall_scores.append(avg_score)

    score = sum(overall_scores) / len(overall_scores)
    print(f"{score=}")

    return


if __name__ == "__main__":
    main()

スコアリングの方法として、今回はターンごとのスコアを平均した値をその対話におけるスコアとします。最終的に、対話におけるスコアを平均した値を、このRAGに対するスコアとして算出しています。上記のPythonスクリプトを実行し、スプレッドシートに書き出した結果が以下のようになります。

自動評価の結果

誤りが含まれる可能性の高いセルを赤色で表示しています。動作検証の作業者はこの部分をチェックして、プロンプト等の改善に活かすことを期待しています。

自動評価の活用における課題

自動評価の目的の節で述べた2点から考えると、自動評価が本当に価値あるものなのかを知るためには、以下の2点を確認する必要があります。

  • 自動評価の妥当性
  • 発見した改善点の数

つまり、如何に評価の妥当性を保ったまま改善点を発見できるかが重要になります。また、作業者にとって使いやすいツールでなければならないので、これらに加えて以下も考慮する必要があります。

  • 自動評価にかかる時間
  • 自動評価に伴うUX(出力の見やすさ・自動評価を用いた設計手順など)

現在のLLMの生成は完全に正しいわけではないため、「絶対に自動評価で90点以上を出さなければ終了できない」といったルールは作るべきではありません。まずは、実際に自動評価ツールを使ってもらい、フィードバックを受け、改善していく必要があると考えています。

対話システムシンポジウムでの発表

本記事でお伝えした内容を元にしたRAGの設計支援アプリケーションについて、第14回対話システムシンポジウムで発表いたします。サイバーエージェントからは、CyberAgent公式ブログでアナウンスされている通り、本発表を含めて計3件の発表を行います。当日会場にお越しの方は、是非とも沢山議論させていただけますと幸いです。

第14回対話システムシンポジウムポスター

おわりに

ここまでお読みいただきましてありがとうございました!

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)
面談フォームはこちら

明日は開発チームの須永よりAI Shift Voicebotのアーキテクチャについてご紹介いたします。

今後ともどうぞよろしくお願いいたします。

PICK UP

TAG