Ragas指標を用いたDify製RAGチャットボットの自動評価をやってみた

1. はじめに

NTT西日本の伏尾です。

Dify1のようなローコードでLLMアプリケーションを作れるツールが広がり、いわゆる市民開発によるアプリケーションが手軽に作れるようになってきました。
特にRAGチャットボットによる問い合わせ対応の省力化は有力なユースケースです。

一方で、このような市民開発ツールでは作成したツールの評価については後回しになりがちです。
今回はそのような背景を踏まえ、Difyで作成したRAGチャットボットを対象として自動評価の枠組みを取り入れることを検討します。具体的には、RAGシステムの自動評価のフレームワークであるRagas2の指標の一部をDify上に実装することを行います。

2. 想定読者

・Difyで作成したRAGチャットボットの品質を定量的に評価してみたい方
・エンジニアがいない現場で評価・改善のループを回したい方

3. RAGシステムによるアウトプット品質の評価フレームワーク:Ragas

RagasはRAGシステムによるアウトプットをLLMを用いて自動評価するためのフレームワークです。Pythonのモジュールとして利用が可能ですが、Difyのサンドボックス環境には導入されておらず、また調べた限り追加もできませんでした。本記事では、その指標の一部をDify上で実装することで、Dify上のみで完結するようにRagasの指標による評価を行えるようにします。なお、外部ツールと連携した場合はDify上で作成したRAGシステムの評価が可能なことがこの記事で報告されています。

Ragasではいくつかのメトリクスが提案されています。本記事では、Ragasの提案論文3や旧バージョンのドキュメント4を参照し、中心的な指標として下記をリストアップしました。いずれも0から1の範囲で1に近いほど精度がよいことを示します。

  1. Answer Relevancy(回答関連性):回答がユーザーの入力に対してどれだけ関連して答えているかを判別する指標です。5
  2. Faithfulness(忠実性):回答が与えられた検索結果にどれだけ事実的に整合しているかを示す指標です。6まず回答を主張に分解し、各主張が与えられた検索結果から主張可能かを判断する二段構えで実装されています。
  3. Context Recall(必要コンテキストの網羅性):検索結果の内容は、Reference(=理想的な回答)を回答するのに十分な内容があるかを判別する指標です。7
  4. Context Precision(コンテキスト精度):検索結果のうち、Referenceに関連しているものを上位に配置しているかを判別する指標です。8検索結果にReferenceに関連しない情報が存在したり、検索結果の上位に関連しない情報があると値が0に近づきます。

上記の指標は精度の分析や改善の一手を考察するための指標として有用です。本記事では、Ragasを参考に上記4つのメトリクスを自動で検出できるフローを作り上げていきます。

4. 実装図

実装した評価機能付きRAGチャットボットの全体図

上図がDify上での実装になります。
⓪はユーザー入力を受け付け知識検索を行い回答を作成する部分です。今回の評価には直接の関係はないため、知識検索と回答作成のブロックをつなげる最低限の実装にしています。
①~④は各評価指標を作成する部分で、入力はユーザーの質問と回答、使用した検索結果、さらに事前に用意したユーザーの質問に対するReferenceを使用します。それぞれ、①Answer Relevancy(回答関連性)②Faithfulness(忠実性)③Context Recall(必要コンテキストの網羅性)④Context Precision(コンテキスト精度)を計算しています。
黒で囲ったブロックはユーザーの質問に対応するReferenceが存在する場合にそれを取得するブロックです。

最後に、ユーザーの入力への回答に追記する形で各評価値を記載し回答します。

5. 実装方法

実装にあたっては以下の記事やドキュメントを参考にしました。

5.1 Answer Relevancy(回答関連性)

Answer Relevancyの計算の実装
Answer Relevancy(回答関連性)は、下記の手順で計算します。

(1)回答の情報からその回答を得るための質問を生成します。
プロンプトはRagasのコードを参考に日本語に翻訳し、下記を使用しています。Ragasの実装では複数回生成しますが、本記事では1回の生成を行う実装としています。

システムプロンプト

与えられた回答に対して、その回答に対応する質問文を生成し、さらにその回答がnoncommittalかどうかを判定してください。noncommittalであればnoncommittalを1、committal(noncommittal ではない)であれば0を与えてください。noncommittalな回答とは、はぐらかしていたり、曖昧だったり、あいまいで断定を避けている回答のことです。たとえば「わかりません」や「確かではありません」といった回答はnoncommittalに該当します。
出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください:
{"properties": {"question": {"title": "Question", "type": "string"}, "noncommittal": {"title": "Noncommittal", "type": "integer"}}, "required": ["question", "noncommittal"], "title": "ResponseRelevanceOutput", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。

--------例-----------
例 1
入力: {
    "response": "アルバート・アインシュタインはドイツで生まれました。"
}
出力: {
    "question": "アルバート・アインシュタインはどこで生まれましたか?",
    "noncommittal": 0
}

例 2
入力: {
    "response": "2022年以降の情報は知らないので、2023年に発明されるスマートフォンの画期的な機能についてはわかりません。"
}
出力: {
    "question": "2023年に発明されたスマートフォンの画期的な機能は何だったでしょうか?",
    "noncommittal": 1
}

ユーザープロンプト

それでは、次の入力について同じことを行ってください。
Input: <回答>
Output: 

(2)生成した回答全体から質問部分を抽出
パラメータ抽出ブロックを使用して与えられたJSON形式のテキストから必要なパラメータを自然言語で指定して抽出します。

パラメータ抽出の設定画面

(3)生成した質問と元の質問をエンベディング
Difyにはモデル設定でエンベディングモデルを設定できますが、チャットフローやワークフローで提供されているブロックからそれらのエンベディングモデルを呼び出すことができません。したがって、ここではHTTPリクエストブロックでエンベディングモデルを直接呼び出しています。API_KEYは環境変数を使用して導入しています。

HTTPリクエストの設定画面

(4)エンベディングした二つのベクトルからAnswer Relevancy(回答関連性)のスコア)を得ます
(3)で作成したブロックのコサイン類似度がスコアになります。

5.2 Faithfulness(忠実性)

Faithfulnessの計算の実装
Faithfulnessの計算は2回のLLM問い合わせが必要です。

(1)与えられた回答を主張に分解する。
回答は複数の主張を組み合わせてできているため、まずは個別の主張に分解します。

システムプロンプト

質問と回答が与えられた場合、回答に含まれる各文の複雑さを分析します。各文を1つ以上の完全に理解可能な文に分解します。どの文にも代名詞が使用されていないことを確認してください。出力はJSON形式で出力してください。
出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください:
{"properties": {"statements": {"description": "The generated statements", "items": {"type": "string"}, "title": "Statements", "type": "array"}}, "required": ["statements"], "title": "StatementGeneratorOutput", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。

--------例-----------
例 1
入力: {
"question": "アルバート・アインシュタインとは誰で、どのような業績で最もよく知られていますか?",
"answer": "彼はドイツ生まれの理論物理学者で、史上最も偉大で影響力のある物理学者の一人として広く認められています。彼は相対性理論の開発で最もよく知られていますが、量子力学理論の発展にも重要な貢献をしました。"
}
出力: {
"statements": [
"アルバート・アインシュタインはドイツ生まれの理論物理学者でした。",
"アルバート・アインシュタインは、史上最も偉大で影響力のある物理学者の一人として認められています。",
"アルバート・アインシュタインは相対性理論の開発で最もよく知られています。",
"アルバート・アインシュタインは量子力学理論の発展にも重要な貢献をしました。"
]
}

ユーザープロンプト

それでは、次の入力について同じことを行ってください。
Input: <回答>
Output: 

(2)各主張の判定
生成した各主張が検索結果から推論できるかを判断します。推論可能なら1、そうでなければ0を出力します。

システムプロンプト

あなたの課題は、与えられた文脈に基づいて、一連の文の忠実性を判断することです。各文について、文脈から直接推論できる場合は1、文脈から直接推論できない場合は0を判定として返してください。
出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください:
{"$defs": {"StatementFaithfulnessAnswer": {"properties": {"statement": {"description": "the original statement, word-by-word", "title": "Statement", "type": "string"}, "reason": {"description": "the reason of the verdict", "title": "Reason", "type": "string"}, "verdict": {"description": "the verdict(0/1) of the faithfulness.", "title": "Verdict", "type": "integer"}}, "required": ["statement", "reason", "verdict"], "title": "StatementFaithfulnessAnswer", "type": "object"}}, "properties": {"statements": {"items": {"$ref": "#/$defs/StatementFaithfulnessAnswer"}, "title": "Statements", "type": "array"}}, "required": ["statements"], "title": "NLIStatementOutput", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。

--------例-----------
例 1
入力: {
"context": "ジョンはXYZ大学の学生です。コンピュータサイエンスの学位取得を目指しています。今学期は、データ構造、アルゴリズム、データベース管理など、複数のコースを受講しています。ジョンは勤勉な学生で、多くの時間を勉強と課題の完了に費やしています。プロジェクトに取り組むため、図書館に遅くまでいることがよくあります。",
"statements": [
"ジョンは生物学を専攻しています。",
"ジョンは人工知能のコースを受講しています。",
"ジョンは熱心な学生です。",
"ジョンはパートタイムの仕事をしています。"
]
}
出力: {
"statements": [
{
"statement": "ジョンは生物学を専攻しています。",
"reason": "ジョンの専攻はコンピュータサイエンスと明示的に記載されています。彼が生物学を専攻していることを示す情報はありません。",
"verdict": 0
},
{
"statement": "ジョンは人工知能に関するコースを受講しています。",
"reason": "文脈にはジョンが現在受講しているコースが記載されており、人工知能については言及されていません。したがって、ジョンがAIに関するコースを受講しているとは推測できません。",
"verdict": 0
},
{
"statement": "ジョンは熱心な学生です。",
"reason": "文脈には、彼がかなりの時間を勉強と課題の完了に費やしていることが記載されています。さらに、彼はプロジェクトに取り組むために図書館に遅くまで残ることが多いと記載されており、これは彼の献身を示唆しています。",
"verdict": 1
},
{
"statement": "ジョンはパートタイムのjob.",
"reason": "ジョンがパートタイムの仕事をしていることについては、文脈から何も得られていない。",
"verdict": 0
}
]
}

例2
入力: {
"context": "光合成は、植物、藻類、および特定の細菌が光エネルギーを化学エネルギーに変換するプロセスです。",
"statements": [
"アルバート・アインシュタインは天才でした。"
]
}
出力: {
"statements": [
{
"statement": "アルバート・アインシュタインは天才でした。",
"reason": "文脈と記述は無関係です。",
"verdict": 0
}
]
}

ユーザープロンプト

それでは、次の入力について同じことを行ってください。
Input: {
    "context": <検索結果>,
    "statements": <主張の生成で生成した主張のリスト>
}
Output: 

(3)Faithfulnessの計算
各主張に対する推論結果の平均がFaithfulnessの値です。

5.3 Context Recall(必要コンテキストの網羅性)

Context Recallの計算の実装

(0)Referenceの取得

取得用コード

ユーザーからの質問に対して事前に用意した理想的な回答を取得します。
Difyではデータセットとしてユーザーからの質問とReferenceのセットを保存できなかったためコードブロックに直接記述する形で質問とReferenceのセットを用意しました。

(1)理想的な回答が検索結果に沿っているか判断します。
理想的な回答の各主張が検索結果の検索結果に帰属するかどうかを判断します。帰属する場合は1、そうでない場合は0です。

システムプロンプト

文脈と回答が与えられます。回答に含まれる各文を分析し、その文が与えられたコンテキストに帰属するかどうかを分類します。「はい」(1)または「いいえ」(0)のいずれかの二値分類のみを使用します。理由をJSON形式で出力します。
出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください:
{"$defs": {"ContextRecallClassification": {"properties": {"statement": {"title": "Statement", "type": "string"}, "reason": {"title": "Reason", "type": "string"}, "attributed": {"title": "Attributed", "type": "integer"}}, "required": ["statement", "reason", "attributed"], "title": "ContextRecallClassification", "type": "object"}}, "properties": {"classifications": {"items": {"$ref": "#/$defs/ContextRecallClassification"}, "title": "Classifications", "type": "array"}}, "required": ["classifications"], "title": "ContextRecallClassifications", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。

--------例-----------
例 1
入力: {
    "question": "アルバート・アインシュタインについて教えてください。",
    "context": "アルバート・アインシュタイン(1879年3月14日 - 1955年4月18日)はドイツ生まれの理論物理学者で、歴史上最も偉大で影響力のある科学者の一人と広く考えられています。相対性理論の考案で最もよく知られていますが、量子力学にも重要な貢献を果たし、20世紀初頭に現代物理学が成し遂げた自然に対する科学的理解の革命的な再構築において中心人物となりました。相対性理論から導き出された質量とエネルギーの等価性を示す式 E = mc2 は、「世界で最も有名な式」と呼ばれています。彼は1921年に「理論物理学への貢献、特に時空の法則の発見」によりノーベル物理学賞を受賞しました。アインシュタインは、量子論の発展における極めて重要な一歩である「光電効果」を発見しました。彼の研究は科学哲学にも影響を与えたことで知られています。1999年に英国の物理学誌『Physics World』が世界中の著名な物理学者130人を対象に行った投票で、アインシュタインは史上最高の物理学者に選ばれました。彼の知的業績と独創性により、アインシュタインは天才の代名詞となっています。",
    "answer":"アルバート・アインシュタインは1879年3月14日に生まれたドイツ生まれの理論物理学者であり、史上最も偉大で影響力のある科学者の一人と広く考えられています。彼は理論物理学への貢献により、1921年にノーベル物理学賞を受賞しました。彼は1905年に4本の論文を発表しました。アインシュタインは1895年にスイスに移住しました。"
}
出力: {
"classifications": [
{
"statement": "アルバート・アインシュタインは1879年3月14日に生まれたドイツ生まれの理論物理学者であり、史上最も偉大で影響力のある科学者の一人と広く考えられています。",
"reason": "アインシュタインの生年月日は文脈の中で明確に言及されています。",
"attributed": 1
},
{
"statement": "彼は理論物理学への貢献により1921年のノーベル物理学賞を受賞しました。",
"reason": "指定された文脈には正確な文が含まれています。",
"attributed": 1
},
{
"statement": "彼は1905年に4本の論文を発表しました。",
"reason": "指定された文脈には彼が執筆した論文についての言及はありません。",
"attributed": 0
},
{
"statement": "アインシュタインは1895年にスイスに引っ越しました。"
"reason": "この文脈では、これを裏付ける証拠はありません。"
"attributed": 0
}
]
}

ユーザープロンプト

それでは、次の入力について同じことを行ってください。
Input: {
    "question": <ユーザーからの質問>,
    "context": <検索結果>,
    "answer": <回答>
}
Output: 

(2)Context Recallの計算
(1)で得た各主張の帰属するかどうかの値の平均がContext Recallになります。

5.4 Context Precision(コンテキスト精度)

Context Precisionは上述した通りReferenceの内容に関係のない検索結果が含まれていたり、関係のない情報が検索結果の上位にある場合はスコアが0に近づきます。
Context Precisionは以下の数式で与えられます。


\displaystyle
\text{Context Precision@K} = \frac{\sum_{k=1}^{K} (\text{Precision@k} \times v_k)}{\text{true positives@K}}


\displaystyle
\text{Precision@k} = \frac{\text{true positives@k}}{\text{k}}

\text{true positives@k}は上位\text{k}番目までの検索結果のうち、Referenceの内容に関連のあった検索結果の個数です。
v_k\text{k}番目の検索結果がReferenceの内容に関連があった場合は1、なかった場合は0になります。
実装上は\text{Context Precision@K}の分母が0にならないよう、微小な定数を足すことで回避します。

Context Precisionの計算の実装

(0)Referenceの取得
Context Recallの処理と同じ。

(1)検索結果を各結果に分割し、配列に保存
知識検索ブロックの結果を受け取り、各検索結果を要素とする配列にします。

実装図

(2)各検索結果がReferenceに関連しているかどうかを判定
Referenceを回答するのに各検索結果が役立つかどうかを判定します。役立つ場合は1、そうでない場合は0です。
検索結果ごとにイテレーションを回す処理をしています。

システムプロンプト

与えられた質問、回答、文脈から、文脈が与えられた回答に至る上で役立ったかどうかを検証します。役立った場合は「1」、役に立たない場合は「0」と判定し、JSON形式で出力します。
出力は、以下の JSON Schema で指定されたスキーマに準拠する JSON 形式で返してください:
{"properties": {"reason": {"description": "Reason for verification", "title": "Reason", "type": "string"}, "verdict": {"description": "Binary (0/1) verdict of verification", "title": "Verdict", "type": "integer"}}, "required": ["reason", "verdict"], "title": "Verification", "type": "object"}レスポンスではシングルクォート(')は使わず、ダブルクォート(")を使ってください。必要な場合はバックスラッシュで適切にエスケープしてください。

--------例-----------
例 1
入力: {
    "question": "アルバート・アインシュタインについて教えてください。",
    "context": "アルバート・アインシュタイン(1879年3月14日 - 1955年4月18日)は、ドイツ生まれの理論物理学者であり、歴史上最も偉大で影響力のある科学者の一人と広く考えられています。相対性理論の考案で最もよく知られていますが、量子力学にも重要な貢献を果たし、20世紀初頭に現代物理学が成し遂げた自然に対する科学的理解の革命的な再構築において中心人物となりました。相対性理論から導き出された質量とエネルギーの等価性の式 E = mc2 は、「世界で最も有名な式」と呼ばれています。彼は1921年に「理論物理学への貢献、特に光電効果の法則の発見」によりノーベル物理学賞を受賞しました。量子論の発展における極めて重要な一歩である「量子効果」を提唱しました。彼の研究は科学哲学への影響でも知られています。1999年に英国の物理学誌『Physics World』が世界中の著名な物理学者130人を対象に行った投票で、アインシュタインは史上最高の物理学者に選ばれました。彼の知的業績と独創性により、アインシュタインは天才の代名詞となっています。",
    "answer":"アルバート・アインシュタインは1879年3月14日に生まれたドイツ生まれの理論物理学者であり、史上最も偉大で影響力のある科学者の一人と広く考えられています。彼は理論物理学への貢献により、1921年にノーベル物理学賞を受賞しました。"
}
出力: {
    "reason": "提供された文脈は、与えられた回答を導き出す上で非常に役立ちました。文脈には、アルバート・アインシュタインの生涯と貢献に関する重要な情報が含まれており、回答にも反映されています。",
    "verdict": 1
}

例2
入力: {
    "question": "2020 ICCワールドカップの優勝者は誰ですか?",
    "context": "2022年10月16日から11月13日までオーストラリアで開催された2022 ICC男子T20ワールドカップは、同大会の第8回大会でした。当初は2020年に開催予定でしたが、COVID-19パンデミックの影響で延期されました。決勝戦ではイングランドがパキスタンを5ウィケット差で破り、2度目のICC男子T20ワールドカップ優勝を果たしました。",
    "answer": "イングランド"
}
出力: {
    "reason": "文脈は、2020 ICCワールドカップの状況を明確にするのに役立ちました。 2020年に開催予定だったが実際には2022年に開催された大会で、イングランドが優勝したことを示しています。",
    "verdict": 1
}

例3
入力: {
    "question": "世界で最も高い山は何ですか?",
    "context": "アンデス山脈は、南アメリカに位置する世界最長の大陸山脈です。7か国にまたがり、西半球の最高峰の多くがそびえ立っています。この山脈は、高地のアンデス高原やアマゾンの熱帯雨林など、多様な生態系で知られています。",
    "answer": "エベレスト山です。"
}
出力: {
    "reason": "提供された文脈はアンデス山脈について論じていますが、アンデス山脈は印象的ですが、エベレスト山を含んでおらず、世界最高峰に関する質問にも直接関連していません。",
    "verdict": 0
}

ユーザープロンプト

それでは、次の入力について同じことを行ってください。
Input: {
    "question": <ユーザーからの質問>,
    "context": <検索結果>,
    "answer": <理想的な回答>
}
Output: 

(3)Context Precisionの計算
前述した式に基づき、各検索結果に対する関連するかどうかの値(0, 1)からContext Precisionを求めます。

Context Precisionの計算

6. 動作確認

実行結果

質問に対して回答した後、各指標が計算されていることが確認できました。
事前に質問と理想的な回答を整備しておけば、作成者は作成したチャットボットに質問を投げ続けるだけで、各種指標を得ることができます。

よりRagasの実装に近づけるには、事前に質問と理想的な回答のリストを保存しておき、一度の実行で用意したリストの質問すべてに対して評価を実行することが望ましいです。しかしDify上では外部から呼び出しても、応答と検索結果を別の変数として受け取ることができず実現が難しかったです。

7. まとめと今後の展望

Difyで作成したRAGチャットボットに対してRagasで使用する一部指標をDify上で実装しました。
実装を通して、Ragasで評価したい観点をどのように実装しているのか把握でき、今後のRAGチャットボットの構築や評価設計において活用できる知見が得られました。
今後の展望としては、まずRagasの評価結果と本記事で実装したDify上での評価結果を比較することでRagasの実装にどの程度合致しているか検証することが考えられます。また、Difyではワークフローを別のワークフローやチャットフローからツールとして呼び出せるため、本記事で実装した評価の部分をツール化することで、別のRAGチャットボットからでも容易に利用できる仕組みの追加を目指していきたいと考えています。

執筆者

伏尾 佳悟(NTT西日本 技術革新部)
AIエンジニアとして主にLLM関連のAIプロジェクトの技術支援を担当

商標

DifyはLangGenius, Inc.の登録商標です。

© NTT WEST, Inc.