はじめに
RAGを構築して社内ドキュメント検索やFAQボットをリリースした。ユーザーからは「まあまあ使える」という声もあれば「的外れな回答が返ってくる」という声もある。
プロンプトを修正してみた。チャンク分割の方法を変えてみた。Embeddingモデルを変えてみた。でも、改善されたのかどうかが分からない。
こうした「なんとなく良さそう」「なんとなく悪くなった気がする」という状態でRAGを運用していませんか?
本記事では、LLM-as-a-Judgeという手法とRagasというOSSフレームワークを使い、Amazon Bedrock上のRAGパイプラインを定量的に評価する方法を解説します。
なぜRAGの評価が難しいのか
従来のNLP評価手法(BLEU, ROUGEなど)は、正解文と生成文の単語レベルの一致度を測るものです。
しかしRAGの回答は自然言語であり、同じ意味でも表現が異なることが多いため、これらの指標では正確に品質を測れません。
さらにRAGには検索と生成の2つのステップがあり、問題の切り分けが困難です。
回答の品質が低い
├── 検索が悪い? → 関連文書を取得できていない
└── 生成が悪い? → 文書はあるのに回答に活かせていない
「回答がイマイチ」という事象だけでは、チャンク分割を見直すべきなのか、プロンプトを改善すべきなのか判断できません。
LLM-as-a-Judgeとは
LLM-as-a-Judgeは、LLM自身に回答の品質を採点させる手法です。
人間の評価者の代わりにLLMが「この回答は質問に対して適切か」「根拠に基づいているか」を判定します。
従来の評価手法との比較
| 手法 | メリット | デメリット |
|---|---|---|
| 人手評価 | 最も正確 | コストが高い、スケールしない |
| BLEU/ROUGE | 自動化できる | 意味的な一致を測れない |
| LLM-as-a-Judge | 意味レベルで自動評価 | Judge自体のバイアスに注意が必要 |
LLM-as-a-Judgeは人手評価との相関が高いことが複数の研究で報告されており、RAG評価の実用的なアプローチとして注目されています。
注意点
- Judgeモデルの品質が評価精度に直結する(高性能なモデルを使うべき)
- 自身が生成した回答を高く評価しやすいバイアスがある(Self-Enhancement Bias)
- 回答の順番によるバイアスがある(Position Bias)
これらのバイアスを理解した上で活用することが重要です。
Ragasとは ― RAG評価を「分解」するフレームワーク
Ragas(Retrieval Augmented Generation Assessment)は、RAGパイプラインの品質をコンポーネント単位で評価するPython OSSフレームワークです。
Ragasの最大の強みは、検索の品質と生成の品質を分離して評価できる点です。
主要メトリクス
| メトリクス | 評価対象 | 何を測るか | 値の意味 |
|---|---|---|---|
| Faithfulness | 生成 | 回答が検索された文書に基づいているか | 低い→ハルシネーションの可能性 |
| Answer Relevancy | 生成 | 回答が質問に対して的確か | 低い→質問と無関係な回答 |
| Context Precision | 検索 | 関連性の高い文書が上位に来ているか | 低い→ランキングの問題 |
| Context Recall | 検索 | 正解に必要な情報がすべて検索できているか | 低い→検索漏れ |
この4つのメトリクスにより、「検索が悪いのか、生成が悪いのか」を定量的に切り分けられます。
評価結果から改善アクションへ
| スコアパターン | 推定される問題 | 改善アクション |
|---|---|---|
| Faithfulnessだけ低い | 生成時のハルシネーション | プロンプト改善、temperatureの調整 |
| Context Recallが低い | 検索漏れ | チャンク戦略の見直し、Embeddingモデル変更 |
| Context Precisionが低い | 不要な文書が混入 | top_kの調整、リランキングの導入 |
| Answer Relevancyが低い | 回答が質問とずれている | プロンプトの指示を明確化 |
構成概要 ― BedrockとRagasの組み合わせ
アーキテクチャ
なぜこの組み合わせか?
- Bedrock Knowledge Baseだけでは「検索品質」と「生成品質」の切り分けができない
- RagasがBedrockの回答とコンテキストを受け取り、コンポーネント別に評価を行う
- 評価のJudgeモデルとしてもBedrock上のClaudeを利用できるため、AWS内で完結する
ハンズオン ― Bedrock Knowledge BaseのRAGをRagasで評価する
前提条件
- AWSアカウントとクレデンシャル設定済み
- Bedrock Knowledge Baseが構築済み(未構築の場合は公式ガイドを参照)
- Bedrockモデルアクセスが有効(Claude, Titanなど)
- Python 3.10以上
Bedrockの一部モデルは推論プロファイルID(例: apac.anthropic.claude-3-5-sonnet-20240620-v1:0)での呼び出しが必要です。従来のモデルARN形式(arn:aws:bedrock:...)ではValidationExceptionが発生する場合があります。利用可能な推論プロファイルはaws bedrock list-inference-profilesで確認できます。
Step 1: 環境セットアップ
mkdir bedrock-ragas-eval
cd bedrock-ragas-eval
python3 -m venv .venv
source .venv/bin/activate
pip install ragas langchain-aws boto3 pandas
Step 2: テストデータセットを準備する
評価には「質問」「正解(Ground Truth)」のペアが必要です。実際のユースケースに合わせて作成してください。
# test_dataset.py
"""テストデータセットの定義"""
test_questions = [
"Amazon S3のストレージクラスにはどのような種類がありますか?",
"S3のバージョニング機能とは何ですか?",
"S3のライフサイクルポリシーで何ができますか?",
"S3のクロスリージョンレプリケーションの設定方法は?",
"S3 Glacierからデータを復元する方法は?",
]
ground_truths = [
"S3にはS3 Standard、S3 Intelligent-Tiering、S3 Standard-IA、S3 One Zone-IA、S3 Glacier Instant Retrieval、S3 Glacier Flexible Retrieval、S3 Glacier Deep Archiveの7つのストレージクラスがあります。",
"バージョニングはオブジェクトの複数バージョンを同一バケットに保持する機能です。誤削除や上書きからの復旧が可能になります。",
"ライフサイクルポリシーを使用すると、オブジェクトのストレージクラスの自動移行や、有効期限切れオブジェクトの自動削除ができます。",
"レプリケーション元バケットでバージョニングを有効にし、レプリケーションルールで送信先バケットとIAMロールを指定して設定します。",
"復元リクエストをAPIまたはコンソールから発行します。Glacier Flexible Retrievalでは迅速取り出し(1-5分)、標準取り出し(3-5時間)、大容量取り出し(5-12時間)の3つのオプションがあります。",
]
テストデータセットの品質が評価精度に直結します。実際のユーザーが投げそうな質問を10〜50件程度用意するのが実用的です。
Step 3: Bedrock Knowledge Baseから回答を取得する
# retrieve_answers.py
"""Bedrock Knowledge Baseから回答とコンテキストを取得"""
import boto3
def retrieve_and_generate(knowledge_base_id, model_arn, question, custom_prompt=None):
"""Knowledge Baseに質問してRAGの回答とコンテキストを取得する"""
client = boto3.client("bedrock-agent-runtime", region_name="ap-northeast-1")
kb_config = {
"knowledgeBaseId": knowledge_base_id,
"modelArn": model_arn,
"retrievalConfiguration": {
"vectorSearchConfiguration": {
"numberOfResults": 5
}
},
}
# カスタムプロンプトが指定された場合は生成設定に追加
if custom_prompt:
kb_config["generationConfiguration"] = {
"promptTemplate": {
"textPromptTemplate": custom_prompt
}
}
response = client.retrieve_and_generate(
input={"text": question},
retrieveAndGenerateConfiguration={
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": kb_config,
},
)
# 回答テキスト
answer = response["output"]["text"]
# 検索されたコンテキスト(引用元)
contexts = []
for citation in response.get("citations", []):
for ref in citation.get("retrievedReferences", []):
text = ref.get("content", {}).get("text", "")
if text:
contexts.append(text)
# カスタムプロンプト使用時はcitationsにcontextが含まれない場合がある
# その場合はretrieve APIで別途取得する
if not contexts:
retrieve_response = client.retrieve(
knowledgeBaseId=knowledge_base_id,
retrievalQuery={"text": question},
retrievalConfiguration={
"vectorSearchConfiguration": {
"numberOfResults": 5
}
},
)
for result in retrieve_response.get("retrievalResults", []):
text = result.get("content", {}).get("text", "")
if text:
contexts.append(text)
return answer, contexts
def collect_rag_responses(knowledge_base_id, model_arn, questions, custom_prompt=None):
"""全テスト質問に対してRAGの回答を収集する"""
answers = []
contexts = []
for i, question in enumerate(questions):
print(f"[{i+1}/{len(questions)}] {question[:40]}...")
answer, ctx = retrieve_and_generate(
knowledge_base_id, model_arn, question, custom_prompt
)
answers.append(answer)
contexts.append(ctx)
return answers, contexts
Step 4: Ragasで評価を実行する
# evaluate_rag.py
"""RagasによるRAG評価の実行"""
from ragas import evaluate
from ragas.metrics import (
Faithfulness,
ResponseRelevancy,
LLMContextPrecisionWithoutReference,
LLMContextRecall,
)
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from ragas.dataset_schema import SingleTurnSample, EvaluationDataset
from langchain_aws import ChatBedrock, BedrockEmbeddings
from test_dataset import test_questions, ground_truths
from retrieve_answers import collect_rag_responses
def main():
# ===== 設定 =====
knowledge_base_id = "<YOUR_KNOWLEDGE_BASE_ID>"
model_arn = "<YOUR_INFERENCE_PROFILE_ID>" # 例: apac.anthropic.claude-3-5-sonnet-20240620-v1:0
region = "ap-northeast-1"
# ===== Step 1: RAG回答を収集 =====
print("RAG回答を収集中...")
answers, contexts = collect_rag_responses(
knowledge_base_id, model_arn, test_questions
)
# ===== Step 2: Ragasデータセットを構築 =====
samples = []
for q, a, c, gt in zip(test_questions, answers, contexts, ground_truths):
sample = SingleTurnSample(
user_input=q,
response=a,
retrieved_contexts=c,
reference=gt,
)
samples.append(sample)
eval_dataset = EvaluationDataset(samples=samples)
# ===== Step 3: Judge用のLLMとEmbeddingを設定 =====
judge_llm = LangchainLLMWrapper(
ChatBedrock(
model_id="<YOUR_INFERENCE_PROFILE_ID>", # 例: apac.anthropic.claude-3-5-sonnet-20241022-v2:0
region_name=region,
model_kwargs={"max_tokens": 4096},
)
)
judge_embeddings = LangchainEmbeddingsWrapper(
BedrockEmbeddings(
model_id="amazon.titan-embed-text-v2:0",
region_name=region,
)
)
# ===== Step 4: 評価メトリクスを定義 =====
metrics = [
Faithfulness(),
ResponseRelevancy(),
LLMContextPrecisionWithoutReference(),
LLMContextRecall(),
]
# ===== Step 5: 評価実行 =====
print("Ragas評価を実行中...")
results = evaluate(
dataset=eval_dataset,
metrics=metrics,
llm=judge_llm,
embeddings=judge_embeddings,
)
# ===== 結果表示 =====
print("\n" + "=" * 50)
print("評価結果")
print("=" * 50)
df = results.to_pandas()
metric_columns = [
"faithfulness",
"answer_relevancy",
"llm_context_precision_without_reference",
"context_recall",
]
for col in metric_columns:
if col in df.columns:
print(f" {col}: {df[col].mean():.3f}")
# 質問ごとの詳細
print("\n質問ごとの詳細:")
print(df[["user_input"] + metric_columns].to_string())
# CSVに保存
df.to_csv("evaluation_results.csv", index=False)
print("\n結果をevaluation_results.csvに保存しました")
if __name__ == "__main__":
main()
Step 5: 評価を実行する
python evaluate_rag.py
Ragas v0.4系ではragas.metricsからのimportやLangchainLLMWrapperに対してDeprecationWarningが表示されますが、動作に問題はありません。将来のv1.0でragas.metrics.collectionsへの移行が予定されています。
評価結果の例
==================================================
評価結果
==================================================
faithfulness: 0.858
answer_relevancy: 0.673
llm_context_precision_without_reference: 0.700
context_recall: 0.800
この結果から以下のことが読み取れます。
| メトリクス | スコア | 解釈 |
|---|---|---|
| Faithfulness | 0.858 | 回答は概ね検索文書に基づいている(ハルシネーション少) |
| Answer Relevancy | 0.673 | 回答が質問に対してやや冗長・不正確な傾向 |
| Context Precision | 0.700 | 検索結果の上位に関連文書が来ているが改善余地あり |
| Context Recall | 0.800 | 必要な情報は概ね取得できている |
→ この場合、検索よりも生成側に改善余地があると判断できます。特にAnswer Relevancyが0.673と低く、回答が冗長になっている可能性があります。またFaithfulnessも0.858で、検索結果にない情報を補足してしまうケースがありそうです。プロンプトを調整して改善を試みましょう。
Step 6: 改善して再評価する(評価サイクル)
生成側のスコア(Faithfulness, Answer Relevancy)を改善するため、プロンプトテンプレートをカスタマイズします。
Step 3で作成したcollect_rag_responsesはcustom_prompt引数を受け取れるようになっています。カスタムプロンプトを定義して渡すだけです。
IMPROVED_PROMPT = """あなたはAWSの技術ドキュメントに基づいて質問に回答するアシスタントです。
以下のルールに従って回答してください:
- 検索結果に含まれる情報のみを使用し、推測や補足は行わないでください
- 質問に対して簡潔かつ的確に回答してください
- 箇条書きを活用し、要点を明確にしてください
検索結果:
$search_results$
ユーザーの質問: $query$
上記の検索結果のみに基づいて、簡潔に回答してください。"""
# custom_prompt引数にプロンプトを渡して再実行
answers, contexts = collect_rag_responses(
knowledge_base_id, model_arn, test_questions, custom_prompt=IMPROVED_PROMPT
)
同じスクリプトで再評価した結果:
メトリクス 改善前 改善後 差分
faithfulness 0.858 0.906 +0.048 ↑
answer_relevancy 0.673 0.689 +0.016 ↑
llm_context_precision_without_reference 0.700 0.800 +0.100 ↑
context_recall 0.800 0.800 0.000 →
Faithfulnessが0.858→0.906に改善しました。「検索結果に含まれる情報のみを使用」という指示によりハルシネーションが減少したことが数値に表れています。Context Precisionも0.700→0.800に向上しました。
一方、Answer Relevancyは0.673→0.689とほぼ横ばいでした。回答の簡潔さはまだ改善の余地があります。この場合、次の改善サイクルとして「回答は3文以内で」といったより具体的な制約を加える、あるいはモデル自体を変更するなどのアプローチが考えられます。
このように、どの施策がどのメトリクスに効いたか(あるいは効かなかったか)を定量的に把握できるのがRagas評価の最大の価値です。
カスタムプロンプト使用時はretrieveAndGenerateのcitationsにコンテキストが含まれない場合があります。その際はretrieve APIで別途コンテキストを取得してください。
どこに導入できるか ― 実践ユースケース
1. 開発フェーズ: パラメータチューニングのA/Bテスト
- チャンクサイズ、top_k、プロンプトなどのパラメータ変更の効果を定量比較
- 「なんとなく良くなった」ではなく「Faithfulnessが0.85→0.92に改善」と説明できる
2. リリース判定: 品質ゲートとしての活用
CI/CDパイプラインに評価を組み込み、品質スコアが閾値を下回る場合はデプロイをブロックします。
# ci_quality_gate.py
"""CI/CDパイプラインでの品質ゲート(例)"""
THRESHOLDS = {
"faithfulness": 0.80,
"answer_relevancy": 0.80,
"llm_context_precision_without_reference": 0.70,
"context_recall": 0.70,
}
def check_quality_gate(results):
"""評価結果が閾値を満たすか判定する"""
df = results.to_pandas()
passed = True
for metric, threshold in THRESHOLDS.items():
if metric not in df.columns:
continue
score = df[metric].mean()
status = "PASS" if score >= threshold else "FAIL"
if score < threshold:
passed = False
print(f" {metric}: {score:.3f} (threshold: {threshold}) [{status}]")
return passed
3. 運用監視: 定期評価による品質劣化検知
ナレッジベースのドキュメントが追加・更新されると、検索品質が変動する可能性があります。週次や月次で評価を回し、スコアの推移をモニタリングします。
| 対象 | 検知できること |
|---|---|
| ドキュメント追加後 | 新規文書がノイズになっていないか |
| モデル更新後 | 生成品質が維持されているか |
| プロンプト変更後 | 意図した改善が実現されているか |
4. 社内RAG: FAQボット・ナレッジ検索の継続改善
社内RAGではユーザーからのフィードバック(👍/👎)を収集し、低評価の質問をテストデータセットに追加していくことで、評価の精度と網羅性を継続的に高められます。
ユーザーフィードバック → テストデータセット追加 → Ragas評価 → 改善 → 再評価
まとめ
本記事では、Amazon BedrockのKnowledge BaseとRagasを組み合わせたRAG評価パイプラインを紹介しました。
ポイント:
- LLM-as-a-JudgeはLLMが回答を自動採点する手法で、人手評価に近い精度を自動化できる
- Ragasは検索と生成を分離して評価でき、問題の原因特定に直結する
- Bedrock + Ragasの組み合わせにより、AWS内で完結する評価パイプラインが構築できる
- 開発時のA/Bテスト、リリース判定、運用監視などRAGのライフサイクル全体で活用できる
RAGの品質管理を「なんとなく」から「定量的」に変えることで、改善サイクルが回り始めます。まずは小さなテストデータセットから始めてみてください。