RAGシステムを運用していて、こんな状況になったことはないでしょうか。
チューニングを繰り返すうちに「なんとなく良くなった気がする」という感覚で判断を進め、ある日突然ユーザーから「回答の精度が落ちた」と報告が来る。原因を追いかけると、数週間前のプロンプト変更が引き金だったことが判明する——。
このサイクルから抜け出すために私が整備したのが、評価フレームワークの本番組み込みです。感覚的な改善からデータ駆動の改善サイクルへ移行したことで、変更のたびに「改悪していないか」を定量的に確認できるようになりました。
ここではGemini APIをジャッジモデルとして使いながら、RAGAS・LLM-as-Judge・カスタム指標を統合したRAG評価システムを構築する方法を、実際に動くコードとともに解説します。
RAGシステムが「感覚的改善」を脱せない理由
RAGの評価が難しい理由は、正解の定義が曖昧なことにあります。
検索エンジンのランキングなら「関連するドキュメントが上位に来ているか」という明確な指標があります。しかしRAGは、検索・拡張・生成の3段階が絡み合い、それぞれに品質の問題が潜んでいます。
- 検索の問題: 正しいコンテキストを取得できているか
- 拡張の問題: 取得したコンテキストを正しく利用できているか
- 生成の問題: 質問に対して適切に回答できているか
これを1つの「回答が良かったか悪かったか」でしか評価しない限り、どこに問題があるかが見えません。
評価フレームワークの目的は、この3段階を分解して測定可能にすることです。
評価指標の全体像:4つの軸で品質を測る
RAGの品質評価でよく使われる指標を整理します。私が実際の運用で重視している4つを紹介します。
Faithfulness(忠実性) 生成された回答が、取得されたコンテキストに基づいているかを評価します。コンテキストに存在しない情報を生成(ハルシネーション)していないかを検出するために使います。
スコアは「回答に含まれるすべての主張のうち、コンテキストによって裏付けられているものの割合」で計算されます。1.0が理想で、0.7を下回ると実用上問題が出始めます。
Answer Relevancy(回答関連性) 生成された回答が、元の質問に対して適切かを評価します。Faithfulnessが「コンテキストとの整合性」を見るのに対し、こちらは「質問への回答性」を見ます。
計算方法は少し特殊で、「生成された回答から逆算的に生成した質問」と「元の質問」のコサイン類似度で算出します。
Context Precision(コンテキスト精度) 取得されたコンテキストのうち、回答生成に実際に役立ったものの割合です。無駄なコンテキストを取得しすぎていないかを測ります。過大なコンテキスト取得はコストとレイテンシの無駄につながるため、最適化の指標として使います。
Context Recall(コンテキスト再現率) 正解を生成するために必要なすべての情報が取得できているかを測ります。これはGroundTruth(正解)が必要なため、評価データセットの準備が必要です。
# 評価指標の実装例(概念コード)
from dataclasses import dataclass
from typing import Optional
@dataclass
class RAGEvalMetrics:
faithfulness: float # 0.0 - 1.0
answer_relevancy: float # 0.0 - 1.0
context_precision: float # 0.0 - 1.0
context_recall: Optional[float] = None # ground_truth が必要
@property
def overall_score(self) -> float:
"""コンテキスト再現率を除く総合スコア"""
scores = [self.faithfulness, self.answer_relevancy, self.context_precision]
return sum(scores) / len(scores)
def is_acceptable(self, threshold: float = 0.7) -> bool:
return self.overall_score >= thresholdRAGASの実装:Python環境での完全セットアップ
RAGASはこれらの指標を自動計算してくれるOSSフレームワークです。Gemini APIをバックエンドとして使うように設定します。
# requirements.txt
# ragas>=0.2.0
# google-generativeai>=0.8.0
# langchain-google-genai>=2.0.0
import os
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from ragas.llms import LangchainLLMWrapper
from langchain_google_genai import ChatGoogleGenerativeAI
# Gemini APIをRAGASのジャッジモデルとして設定
def setup_gemini_evaluator():
"""Gemini Flash 2.5をコスト効率の良いジャッジとして設定"""
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash-preview-05-20", # 評価用なら Flash が最適
google_api_key=os.environ["GOOGLE_API_KEY"],
temperature=0.0, # 評価は決定論的に
)
return LangchainLLMWrapper(llm)
evaluator_llm = setup_gemini_evaluator()
# 各メトリクスにGeminiを設定
for metric in [faithfulness, answer_relevancy, context_precision]:
metric.llm = evaluator_llm評価を実行するには、テストデータセットと推論結果が必要です。
from datasets import Dataset
import json
def run_rag_evaluation(
rag_pipeline,
test_cases: list[dict],
) -> dict:
"""
RAGパイプラインを評価する
Args:
rag_pipeline: evaluate(question) -> {"answer": str, "contexts": list[str]} を返す関数
test_cases: [{"question": str, "ground_truth": str}, ...] のリスト
Returns:
評価結果の辞書
"""
questions = []
answers = []
contexts = []
ground_truths = []
for case in test_cases:
question = case["question"]
result = rag_pipeline(question)
questions.append(question)
answers.append(result["answer"])
contexts.append(result["contexts"])
ground_truths.append(case.get("ground_truth", ""))
# RAGASのDataset形式に変換
eval_dataset = Dataset.from_dict({
"question": questions,
"answer": answers,
"contexts": contexts,
"ground_truth": ground_truths,
})
# 評価実行
result = evaluate(
dataset=eval_dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
],
)
return result.to_pandas().to_dict(orient="records")
# 使用例
# test_cases = [
# {"question": "Gemini APIの無料枠の制限は?", "ground_truth": "..."},
# ...
# ]
# results = run_rag_evaluation(my_rag_pipeline, test_cases)実際に動かしてみると、特にFaithfulnessのスコアが想定より低いことに驚くケースが多いです。これは「コンテキストに書いてあること以上のことを回答が主張している」状態で、システムプロンプトの調整で改善できます。
LLM-as-Judgeによる意味的品質評価
RAGASの指標だけでは拾いきれない品質側面があります。たとえば「回答のトーンが適切か」「ユーザーにとって理解しやすい表現か」といった側面です。これをカバーするのがLLM-as-Judgeパターンです。
Gemini 2.5 Proをジャッジとして使い、構造化出力でスコアと判定理由を返させます。
import google.generativeai as genai
from pydantic import BaseModel, Field
import json
class JudgmentResult(BaseModel):
score: float = Field(ge=0.0, le=1.0, description="品質スコア(0.0〜1.0)")
reasoning: str = Field(description="スコアの根拠(200文字以内)")
passed: bool = Field(description="品質ゲートを通過したか")
issues: list[str] = Field(default_factory=list, description="発見された問題点")
JUDGE_PROMPT = """
あなたはRAGシステムの回答品質を評価する厳格な審査員です。
以下の基準で回答を評価してください。
【評価基準】
1. コンテキストとの整合性(ハルシネーションがないか)
2. 質問への回答性(的外れな回答になっていないか)
3. 情報の過不足(必要十分な情報が含まれているか)
4. 表現の適切さ(ユーザーが理解しやすい表現か)
【入力】
質問: {question}
取得コンテキスト: {contexts}
生成された回答: {answer}
以下のJSON形式で評価結果を返してください:
{{
"score": <0.0から1.0の数値>,
"reasoning": "<スコアの根拠>",
"passed": <true/false, スコア0.75以上でtrue>,
"issues": ["<問題点1>", "<問題点2>"]
}}
"""
def llm_as_judge(
question: str,
contexts: list[str],
answer: str,
model: str = "gemini-2.5-pro-preview-05-06",
) -> JudgmentResult:
"""
Gemini 2.5 Proを使って回答品質をジャッジする
高品質な評価が必要な場面ではProを、大量評価ではFlashを使い分けることを推奨
"""
client = genai.GenerativeModel(model)
prompt = JUDGE_PROMPT.format(
question=question,
contexts="\n---\n".join(contexts[:3]), # 上位3件のコンテキストのみ渡す
answer=answer,
)
response = client.generate_content(
prompt,
generation_config=genai.GenerationConfig(
temperature=0.0,
response_mime_type="application/json",
),
)
raw = json.loads(response.text)
return JudgmentResult(**raw)
# 使用例
# result = llm_as_judge(
# question="Gemini 2.5 Proの入力コンテキスト上限は?",
# contexts=["Gemini 2.5 Proは最大1Mトークンのコンテキストウィンドウを持ちます"],
# answer="Gemini 2.5 Proは100万トークンまでの入力をサポートします",
# )
# print(f"Score: {result.score}, Passed: {result.passed}")
# # Score: 0.92, Passed: TrueLLM-as-Judgeの一番の落とし穴は同じモデルで生成と評価を行うことです。GPT-4で生成した回答をGPT-4で評価すると、自己肯定的なバイアスが生じます。生成にGemini Flash、評価にGemini Proという組み合わせを使うことで、このバイアスを軽減できます。
カスタム評価指標の設計:ドメイン固有の品質基準
汎用的な指標では捉えられないドメイン固有の品質基準があります。私が法律文書を対象としたRAGシステムを構築した際には、「条文番号が正確に引用されているか」という指標が必要でした。
import re
from typing import Callable
def build_custom_metric(
name: str,
evaluator: Callable[[str, str, list[str]], float],
description: str,
) -> dict:
"""カスタム評価指標を定義する"""
return {
"name": name,
"description": description,
"evaluator": evaluator,
}
# 例1: 数値精度チェック(金融ドキュメントRAG向け)
def numeric_accuracy_check(
question: str,
answer: str,
contexts: list[str],
) -> float:
"""
回答に含まれる数値がコンテキストと一致しているかチェック
Returns:
一致率(0.0〜1.0)
"""
# 回答から数値を抽出
answer_numbers = set(re.findall(r'\d+(?:\.\d+)?', answer))
if not answer_numbers:
return 1.0 # 数値がなければ満点
# コンテキストに存在する数値を確認
all_context_text = " ".join(contexts)
context_numbers = set(re.findall(r'\d+(?:\.\d+)?', all_context_text))
matched = answer_numbers & context_numbers
return len(matched) / len(answer_numbers)
# 例2: 引用品質チェック(技術ドキュメントRAG向け)
def citation_quality_check(
question: str,
answer: str,
contexts: list[str],
) -> float:
"""
回答が具体的なソースを適切に参照しているかチェック
"""
citation_indicators = [
"によると", "では", "に記載されている", "から引用",
"according to", "as stated in", "per the documentation",
]
has_citation = any(indicator in answer for indicator in citation_indicators)
# コンテキストが1件以上ある場合、引用があると加点
if len(contexts) > 0 and has_citation:
return 1.0
elif len(contexts) == 0:
return 0.5 # コンテキストなしの場合は中間点
else:
return 0.6 # 引用なしは若干減点
numeric_metric = build_custom_metric(
name="numeric_accuracy",
evaluator=numeric_accuracy_check,
description="回答に含まれる数値がコンテキストに裏付けられているかを測定",
)カスタム指標は「このシステムが最終的にユーザーに何を提供するか」から逆算して設計するのが重要です。汎用指標が高くても、ドメイン固有の品質基準を満たしていなければ意味がありません。
評価データセットの収集と管理
評価フレームワークは、評価データセットの質で大きく左右されます。最初は手動で20〜30件の質問・正解ペアを作成し、徐々に拡充していくアプローチを推奨しています。
import json
from pathlib import Path
from datetime import datetime
class EvalDatasetManager:
"""評価データセットを管理するクラス"""
def __init__(self, dataset_path: str = "eval_dataset.json"):
self.path = Path(dataset_path)
self.dataset = self._load()
def _load(self) -> list[dict]:
if self.path.exists():
return json.loads(self.path.read_text(encoding="utf-8"))
return []
def add_case(
self,
question: str,
ground_truth: str,
category: str = "general",
difficulty: str = "medium",
) -> None:
"""テストケースを追加する"""
case = {
"id": f"{len(self.dataset) + 1:04d}",
"question": question,
"ground_truth": ground_truth,
"category": category,
"difficulty": difficulty,
"created_at": datetime.now().isoformat(),
}
self.dataset.append(case)
self._save()
def _save(self) -> None:
self.path.write_text(
json.dumps(self.dataset, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def get_by_category(self, category: str) -> list[dict]:
return [c for c in self.dataset if c["category"] == category]
def get_summary(self) -> dict:
from collections import Counter
categories = Counter(c["category"] for c in self.dataset)
difficulties = Counter(c["difficulty"] for c in self.dataset)
return {
"total": len(self.dataset),
"by_category": dict(categories),
"by_difficulty": dict(difficulties),
}
# データセット管理の例
# manager = EvalDatasetManager("data/eval_dataset.json")
# manager.add_case(
# question="Gemini APIの無料枠のRPMは?",
# ground_truth="Gemini 2.5 Flashの無料枠では毎分10リクエストまで利用できます",
# category="api_limits",
# difficulty="easy",
# )データセット構築で特に意識していることが1つあります。「実際にユーザーが困った質問」を優先的に収録することです。サポート問い合わせのログや、検索クエリのデータがある場合はそこから抽出すると、現実に即した評価ができます。
CI/CDへの統合:品質ゲートの自動化
評価フレームワークをGitHub Actionsに組み込み、RAGパイプラインの変更がマージされるたびに自動で品質チェックが走るようにします。
# .github/workflows/rag-quality-check.yml
name: RAG Quality Gate
on:
pull_request:
paths:
- 'src/rag/**'
- 'prompts/**'
jobs:
evaluate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install ragas langchain-google-genai datasets
- name: Run RAG Evaluation
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: python scripts/eval_rag.py --threshold 0.72 --output eval_report.json
- name: Check Quality Gate
run: |
python -c "
import json, sys
report = json.load(open('eval_report.json'))
score = report['overall_score']
threshold = 0.72
print(f'Overall Score: {score:.3f} (threshold: {threshold})')
if score < threshold:
print('❌ Quality gate FAILED')
sys.exit(1)
print('✅ Quality gate PASSED')
"
- name: Comment PR
uses: actions/github-script@v7
if: always()
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('eval_report.json'));
const body = `## RAG Quality Report
| Metric | Score |
|--------|-------|
| Faithfulness | ${report.faithfulness.toFixed(3)} |
| Answer Relevancy | ${report.answer_relevancy.toFixed(3)} |
| Context Precision | ${report.context_precision.toFixed(3)} |
| **Overall** | **${report.overall_score.toFixed(3)}** |
Threshold: 0.72 | Status: ${report.passed ? '✅ PASSED' : '❌ FAILED'}
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});# scripts/eval_rag.py
import argparse
import json
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--threshold", type=float, default=0.70)
parser.add_argument("--output", default="eval_report.json")
args = parser.parse_args()
# テストケースのロード
test_cases = json.loads(
Path("data/eval_dataset.json").read_text(encoding="utf-8")
)[:20] # CI環境ではコスト抑制のため20件に限定
# RAGパイプラインのインポート(プロジェクト固有)
from src.rag.pipeline import run_rag
# 評価実行
results = run_rag_evaluation(run_rag, test_cases)
# スコア集計
overall = sum(r.get("faithfulness", 0) for r in results) / len(results)
report = {
"overall_score": overall,
"faithfulness": sum(r.get("faithfulness", 0) for r in results) / len(results),
"answer_relevancy": sum(r.get("answer_relevancy", 0) for r in results) / len(results),
"context_precision": sum(r.get("context_precision", 0) for r in results) / len(results),
"passed": overall >= args.threshold,
"sample_count": len(results),
}
Path(args.output).write_text(json.dumps(report, indent=2), encoding="utf-8")
print(json.dumps(report, indent=2))
sys.exit(0 if report["passed"] else 1)
if __name__ == "__main__":
main()CI/CDに組み込んで最初に気づいたのが、「プロンプトを少し変更するだけでFaithfulnessが0.1前後変動する」という事実でした。これがなければ気づかなかったはずで、評価の仕組みを持つことの価値を実感しました。
本番モニタリング:ダッシュボードと異常検知
本番環境では、すべてのリクエストを評価するのはコストが高すぎます。代わりにサンプリング評価を採用し、5〜10%のトラフィックに対して自動評価を実行します。
import random
import asyncio
from datetime import datetime
import google.generativeai as genai
class ProductionRAGMonitor:
"""本番RAGシステムのモニタリング"""
def __init__(
self,
sample_rate: float = 0.05, # 5%をサンプリング
alert_threshold: float = 0.65,
):
self.sample_rate = sample_rate
self.alert_threshold = alert_threshold
self.metrics_buffer: list[dict] = []
def should_evaluate(self) -> bool:
"""このリクエストを評価対象にするか判定"""
return random.random() < self.sample_rate
async def evaluate_async(
self,
question: str,
answer: str,
contexts: list[str],
request_id: str,
) -> None:
"""
非同期でバックグラウンド評価を実行する
ユーザーのレスポンスを遅らせないよう、評価は非同期で行う
"""
if not self.should_evaluate():
return
# バックグラウンドで評価実行(ユーザーレスポンスとは独立)
asyncio.create_task(
self._run_evaluation(question, answer, contexts, request_id)
)
async def _run_evaluation(
self,
question: str,
answer: str,
contexts: list[str],
request_id: str,
) -> None:
try:
result = llm_as_judge(question, contexts, answer)
metric = {
"request_id": request_id,
"timestamp": datetime.utcnow().isoformat(),
"score": result.score,
"passed": result.passed,
"issues": result.issues,
}
self.metrics_buffer.append(metric)
# スコアが閾値を下回った場合にアラート
if result.score < self.alert_threshold:
await self._send_alert(metric, question, answer)
except Exception as e:
# 評価失敗はユーザーへの影響なし(ログのみ)
print(f"Evaluation failed for {request_id}: {e}")
async def _send_alert(self, metric: dict, question: str, answer: str) -> None:
"""品質低下アラートを送信(実装は使用する通知サービスに合わせる)"""
print(f"⚠️ Quality alert: score={metric['score']:.2f}")
print(f" Question: {question[:100]}...")
print(f" Issues: {metric['issues']}")
# 実際の実装では Slack・PagerDuty・BigQuery への書き込みなどを行う
def get_rolling_stats(self, window: int = 100) -> dict:
"""直近N件の統計を返す"""
recent = self.metrics_buffer[-window:]
if not recent:
return {}
scores = [m["score"] for m in recent]
return {
"avg_score": sum(scores) / len(scores),
"pass_rate": sum(1 for m in recent if m["passed"]) / len(recent),
"sample_count": len(recent),
"min_score": min(scores),
}Before/After:リファクタリング事例3選
評価フレームワークを導入してから見えてきた典型的な改善パターンを3つ紹介します。
事例1:システムプロンプトの曖昧さによるFaithfulness低下
Before: 「ユーザーの質問に親切に回答してください。関連する背景情報も補足してください。」
→ Faithfulness: 0.58(コンテキスト外の情報を「補足」として生成)
After: 「取得されたコンテキストに含まれる情報のみを使って回答してください。コンテキストに記載のない事項については、その旨を明示してください。」
→ Faithfulness: 0.89(31ptの改善)
事例2:コンテキスト取得数の過剰によるContext Precision低下
Before: TOP-10のチャンクを取得 → Context Precision: 0.41 After: 関連度スコアでフィルタしてTOP-3に絞る → Context Precision: 0.78
不要なコンテキストはモデルを混乱させ、回答品質を下げます。少ないが高品質なコンテキストの方が優れていることが多いです。
事例3:質問のリライトによるAnswer Relevancy改善
Before: ユーザーの質問をそのままベクトル検索に使用 → Answer Relevancy: 0.71 After: Gemini APIで質問を「検索に最適な形」にリライトしてから検索 → Answer Relevancy: 0.87
def rewrite_query_for_retrieval(question: str) -> str:
"""
ユーザーの質問を検索に最適化された形にリライトする
例: 「あの機能ってどう使うの?」
→ 「Gemini API Function Calling 使い方 実装例」
"""
client = genai.GenerativeModel("gemini-2.5-flash-preview-05-20")
prompt = f"""
以下のユーザーの質問を、技術的なドキュメント検索に最適化された形に変換してください。
変換後のクエリのみを返してください(説明不要)。
元の質問: {question}
"""
response = client.generate_content(prompt)
return response.text.strip()評価コストの現実的な管理
最後に、見落とされがちなコスト管理について触れておきます。
RAGASの各評価指標は内部でLLMを複数回呼び出します。1テストケースあたりの評価コストは、使用するモデルによって大きく異なります。
- Gemini 2.5 Pro でジャッジ: 約$0.003〜$0.01/ケース
- Gemini 2.5 Flash でジャッジ: 約$0.0003〜$0.001/ケース
CI/CDではFlashをジャッジモデルとして使い、週次の詳細評価のみProを使うという分離が費用対効果の観点から合理的です。
評価フレームワーク自体も「継続的に改善すべき成果物」です。指標の閾値も初期設定のままにせず、3〜4週間ごとに実際の品質感と照らし合わせて調整することをお勧めします。
RAGシステムの品質管理は、一度整備すれば終わりではなく、システムと一緒に育てていくものです。その土台を作る段階で、この記事が参考になれば幸いです。
RAGの構築については Gemini APIを使ったRAGシステムの完全実装 も参考にしてください。LLM評価の詳細については Gemini API LLM-as-Judge 評価システムの本番実装 も合わせてご覧ください。
RAG評価の実装に取り組む際は、まず小さなデータセット(20〜30件)で試行錯誤を始めることをお勧めします。完璧なデータセットを整備してから始めると、着手が遅れがちです。評価システムそのものを反復して改善していく姿勢で取り組んでみてください。