INDX
RAGの成果を"見える化"する評価フレームワーク
ブログ
データ活用

RAGの成果を"見える化"する評価フレームワーク

改善効果が測れない課題をRecall@k、MRR、忠実性等の指標導入で解決。Ragas、TruLens、DeepEvalの使い方と実践的な評価手法を解説します。

髙谷 謙介
COO
11

RAGの成果を"見える化"する評価フレームワーク

改善効果が測れない課題を解決

RAGシステムを導入したものの、「本当に精度は向上したのか?」「どの部分を改善すべきか?」という疑問に答えられない企業が多く存在します。定量的な評価指標なしには、システムの改善も投資対効果の証明も困難です。本記事では、RAGシステムの性能を多角的に評価し、継続的な改善を可能にする実践的なフレームワークを解説します。

なぜ評価フレームワークが必要なのか

従来の課題

RAGシステムの評価における典型的な問題:

1. 主観的な評価: 「なんとなく良くなった」という感覚的判断

2. 部分最適化: 検索精度は向上したが、回答品質は低下

3. 改善ポイントの不明確さ: どこを改善すべきか特定できない

4. ROIの証明困難: 投資効果を数値で示せない

評価フレームワークがもたらす変革

適切な評価フレームワークの導入により:

  • 改善効果の可視化: 各コンポーネントの性能を数値化
  • ボトルネックの特定: 問題箇所を即座に発見
  • 継続的改善: データドリブンな最適化サイクル
  • 投資判断の根拠: 明確な数値に基づく意思決定

主要評価指標の体系

1. 検索精度指標

RAGの第一段階である情報検索の評価:

python
1import numpy as np
2from typing import List, Dict, Tuple
3from sklearn.metrics import ndcg_score
4
5class RetrievalMetrics:
6    """検索精度を評価する指標群"""
7    
8    def calculate_recall_at_k(self, 
9                              retrieved_docs: List[str],
10                              relevant_docs: List[str],
11                              k: int) -> float:
12        """Recall@k: k件中の関連文書の再現率"""
13        retrieved_k = set(retrieved_docs[:k])
14        relevant_set = set(relevant_docs)
15        
16        if not relevant_set:
17            return 0.0
18        
19        intersection = retrieved_k.intersection(relevant_set)
20        recall = len(intersection) / len(relevant_set)
21        
22        return recall
23    
24    def calculate_precision_at_k(self,
25                                 retrieved_docs: List[str],
26                                 relevant_docs: List[str],
27                                 k: int) -> float:
28        """Precision@k: k件中の関連文書の精度"""
29        retrieved_k = retrieved_docs[:k]
30        relevant_set = set(relevant_docs)
31        
32        if not retrieved_k:
33            return 0.0
34        
35        hits = sum(1 for doc in retrieved_k if doc in relevant_set)
36        precision = hits / len(retrieved_k)
37        
38        return precision
39    
40    def calculate_mrr(self,
41                      retrieved_docs: List[str],
42                      relevant_docs: List[str]) -> float:
43        """MRR (Mean Reciprocal Rank): 最初の関連文書の順位の逆数"""
44        relevant_set = set(relevant_docs)
45        
46        for rank, doc in enumerate(retrieved_docs, 1):
47            if doc in relevant_set:
48                return 1.0 / rank
49        
50        return 0.0
51    
52    def calculate_map(self,
53                      queries: List[str],
54                      retrieved_results: List[List[str]],
55                      relevant_results: List[List[str]]) -> float:
56        """MAP (Mean Average Precision): 平均精度"""
57        ap_scores = []
58        
59        for retrieved, relevant in zip(retrieved_results, relevant_results):
60            if not relevant:
61                continue
62            
63            relevant_set = set(relevant)
64            ap = 0.0
65            relevant_count = 0
66            
67            for rank, doc in enumerate(retrieved, 1):
68                if doc in relevant_set:
69                    relevant_count += 1
70                    precision = relevant_count / rank
71                    ap += precision
72            
73            if relevant_count > 0:
74                ap /= len(relevant_set)
75            
76            ap_scores.append(ap)
77        
78        return np.mean(ap_scores) if ap_scores else 0.0
79    
80    def calculate_ndcg(self,
81                       retrieved_docs: List[str],
82                       relevance_scores: Dict[str, float],
83                       k: int) -> float:
84        """NDCG@k: 正規化割引累積利得"""
85        retrieved_k = retrieved_docs[:k]
86        
87        # 実際の利得スコア
88        actual_scores = [relevance_scores.get(doc, 0) for doc in retrieved_k]
89        
90        # 理想的な順序での利得スコア
91        ideal_scores = sorted(relevance_scores.values(), reverse=True)[:k]
92        
93        if not ideal_scores or sum(ideal_scores) == 0:
94            return 0.0
95        
96        # NDCGを計算
97        actual_dcg = self._calculate_dcg(actual_scores)
98        ideal_dcg = self._calculate_dcg(ideal_scores)
99        
100        return actual_dcg / ideal_dcg if ideal_dcg > 0 else 0.0
101    
102    def _calculate_dcg(self, scores: List[float]) -> float:
103        """DCG (Discounted Cumulative Gain) を計算"""
104        dcg = 0.0
105        for i, score in enumerate(scores, 1):
106            dcg += score / np.log2(i + 1)
107        return dcg

2. 生成品質指標

LLMによる回答生成の品質評価:

python
1from rouge_score import rouge_scorer
2from bert_score import score as bert_score
3import torch
4from transformers import AutoTokenizer, AutoModelForSequenceClassification
5
6class GenerationMetrics:
7    """生成品質を評価する指標群"""
8    
9    def __init__(self):
10        self.rouge_scorer = rouge_scorer.RougeScorer(
11            ['rouge1', 'rouge2', 'rougeL'], use_stemmer=True
12        )
13        # 日本語対応のBERTモデル
14        self.tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese')
15        self.model = AutoModelForSequenceClassification.from_pretrained(
16            'cl-tohoku/bert-base-japanese'
17        )
18    
19    def calculate_rouge_scores(self, 
20                              generated: str,
21                              reference: str) -> Dict[str, float]:
22        """ROUGE スコア: n-gram重複に基づく評価"""
23        scores = self.rouge_scorer.score(reference, generated)
24        
25        return {
26            'rouge1_f1': scores['rouge1'].fmeasure,
27            'rouge2_f1': scores['rouge2'].fmeasure,
28            'rougeL_f1': scores['rougeL'].fmeasure,
29            'rouge1_precision': scores['rouge1'].precision,
30            'rouge1_recall': scores['rouge1'].recall
31        }
32    
33    def calculate_bert_score(self,
34                            generated: List[str],
35                            references: List[str]) -> Dict[str, float]:
36        """BERTScore: 文脈を考慮した意味的類似度"""
37        P, R, F1 = bert_score(
38            generated, 
39            references,
40            lang='ja',
41            model_type='cl-tohoku/bert-base-japanese'
42        )
43        
44        return {
45            'bert_precision': P.mean().item(),
46            'bert_recall': R.mean().item(),
47            'bert_f1': F1.mean().item()
48        }
49    
50    def calculate_fluency_score(self, text: str) -> float:
51        """流暢性スコア: 文章の自然さを評価"""
52        # パープレキシティベースの評価
53        inputs = self.tokenizer(text, return_tensors='pt', truncation=True)
54        
55        with torch.no_grad():
56            outputs = self.model(**inputs)
57            # logitsから流暢性スコアを計算
58            fluency_score = torch.softmax(outputs.logits, dim=-1).max().item()
59        
60        return fluency_score
61    
62    def calculate_coherence_score(self, text: str) -> float:
63        """一貫性スコア: 文章の論理的一貫性を評価"""
64        sentences = text.split('。')
65        if len(sentences) < 2:
66            return 1.0
67        
68        coherence_scores = []
69        
70        for i in range(len(sentences) - 1):
71            if not sentences[i] or not sentences[i+1]:
72                continue
73            
74            # 隣接する文の意味的類似度を計算
75            inputs1 = self.tokenizer(sentences[i], return_tensors='pt', truncation=True)
76            inputs2 = self.tokenizer(sentences[i+1], return_tensors='pt', truncation=True)
77            
78            with torch.no_grad():
79                emb1 = self.model.bert(**inputs1).last_hidden_state.mean(dim=1)
80                emb2 = self.model.bert(**inputs2).last_hidden_state.mean(dim=1)
81                
82                # コサイン類似度
83                similarity = torch.cosine_similarity(emb1, emb2).item()
84                coherence_scores.append(similarity)
85        
86        return np.mean(coherence_scores) if coherence_scores else 0.0

3. RAG特有の評価指標

RAGシステム固有の性能評価:

python
1class RAGSpecificMetrics:
2    """RAG特有の評価指標"""
3    
4    def __init__(self, llm_client=None):
5        self.llm_client = llm_client
6    
7    def calculate_faithfulness(self,
8                              answer: str,
9                              context: str,
10                              use_llm: bool = True) -> float:
11        """忠実性: 回答がコンテキストに基づいているか"""
12        if use_llm and self.llm_client:
13            prompt = f"""
14            以下の回答がコンテキストの情報に忠実に基づいているか評価してください。
15            
16            コンテキスト: {context}
17            回答: {answer}
18            
19            評価基準:
20            1.0: 完全に忠実(すべての情報がコンテキストに基づく)
21            0.8: ほぼ忠実(わずかな推論を含むが妥当)
22            0.5: 部分的に忠実(一部の情報が不正確)
23            0.0: 不忠実(コンテキストと矛盾または無関係)
24            
25            スコアのみを数値で回答してください。
26            """
27            
28            response = self.llm_client.generate(prompt)
29            try:
30                return float(response.strip())
31            except:
32                return 0.0
33        else:
34            # 簡易的なルールベース評価
35            context_words = set(context.split())
36            answer_words = set(answer.split())
37            
38            if not answer_words:
39                return 0.0
40            
41            overlap = len(context_words.intersection(answer_words))
42            return min(overlap / len(answer_words), 1.0)
43    
44    def calculate_answer_relevance(self,
45                                   question: str,
46                                   answer: str,
47                                   use_llm: bool = True) -> float:
48        """回答関連性: 質問に対する回答の適切さ"""
49        if use_llm and self.llm_client:
50            prompt = f"""
51            質問に対する回答の関連性を評価してください。
52            
53            質問: {question}
54            回答: {answer}
55            
56            評価基準:
57            1.0: 完全に関連(質問に直接的かつ完全に回答)
58            0.7: 高い関連性(質問の主要部分に回答)
59            0.4: 部分的関連(一部のみ回答)
60            0.0: 無関連(質問に答えていない)
61            
62            スコアのみを数値で回答してください。
63            """
64            
65            response = self.llm_client.generate(prompt)
66            try:
67                return float(response.strip())
68            except:
69                return 0.0
70        else:
71            # 簡易的なキーワードマッチング
72            question_words = set(question.lower().split())
73            answer_words = set(answer.lower().split())
74            
75            overlap = len(question_words.intersection(answer_words))
76            return min(overlap / max(len(question_words), 1), 1.0)
77    
78    def calculate_context_precision(self,
79                                    retrieved_contexts: List[str],
80                                    relevant_contexts: List[str]) -> float:
81        """コンテキスト精度: 取得した文脈の正確性"""
82        if not retrieved_contexts:
83            return 0.0
84        
85        relevant_set = set(relevant_contexts)
86        precision_scores = []
87        
88        for i, context in enumerate(retrieved_contexts):
89            if context in relevant_set:
90                # 順位を考慮した精度
91                precision_scores.append(1.0 / (i + 1))
92        
93        return sum(precision_scores) / len(retrieved_contexts)
94    
95    def calculate_hallucination_score(self,
96                                     answer: str,
97                                     context: str) -> float:
98        """幻覚スコア: 存在しない情報の生成度合い"""
99        # 回答に含まれる固有名詞や数値を抽出
100        import re
101        
102        # 数値の抽出
103        answer_numbers = set(re.findall(r'd+.?d*', answer))
104        context_numbers = set(re.findall(r'd+.?d*', context))
105        
106        # 固有名詞の簡易抽出(大文字で始まる単語)
107        answer_entities = set(re.findall(r'[A-Z][a-z]+', answer))
108        context_entities = set(re.findall(r'[A-Z][a-z]+', context))
109        
110        hallucination_count = 0
111        total_count = 0
112        
113        # 数値の幻覚チェック
114        for num in answer_numbers:
115            total_count += 1
116            if num not in context_numbers:
117                hallucination_count += 1
118        
119        # 固有名詞の幻覚チェック
120        for entity in answer_entities:
121            total_count += 1
122            if entity not in context_entities:
123                hallucination_count += 1
124        
125        if total_count == 0:
126            return 0.0
127        
128        # 幻覚スコア(低いほど良い)
129        return hallucination_count / total_count

評価ツールの実装

Ragasを使った自動評価

python
1from ragas import evaluate
2from ragas.metrics import (
3    faithfulness,
4    answer_relevancy,
5    context_precision,
6    context_recall,
7    answer_correctness,
8    answer_similarity
9)
10from datasets import Dataset
11import pandas as pd
12
13class RagasEvaluator:
14    """Ragasを使用した包括的評価"""
15    
16    def __init__(self):
17        self.metrics = [
18            faithfulness,
19            answer_relevancy,
20            context_precision,
21            context_recall,
22            answer_correctness,
23            answer_similarity
24        ]
25    
26    def prepare_dataset(self, 
27                        questions: List[str],
28                        answers: List[str],
29                        contexts: List[List[str]],
30                        ground_truths: List[str] = None) -> Dataset:
31        """評価用データセットの準備"""
32        data = {
33            'question': questions,
34            'answer': answers,
35            'contexts': contexts
36        }
37        
38        if ground_truths:
39            data['ground_truth'] = ground_truths
40        
41        df = pd.DataFrame(data)
42        return Dataset.from_pandas(df)
43    
44    def evaluate_rag_system(self, dataset: Dataset) -> Dict[str, float]:
45        """RAGシステムの総合評価"""
46        results = evaluate(
47            dataset,
48            metrics=self.metrics
49        )
50        
51        # 結果の整形
52        evaluation_results = {
53            'faithfulness': results['faithfulness'],
54            'answer_relevancy': results['answer_relevancy'],
55            'context_precision': results['context_precision'],
56            'context_recall': results['context_recall'],
57            'answer_correctness': results.get('answer_correctness', None),
58            'answer_similarity': results.get('answer_similarity', None),
59            'overall_score': self._calculate_overall_score(results)
60        }
61        
62        return evaluation_results
63    
64    def _calculate_overall_score(self, results: Dict) -> float:
65        """総合スコアの計算"""
66        weights = {
67            'faithfulness': 0.25,
68            'answer_relevancy': 0.25,
69            'context_precision': 0.20,
70            'context_recall': 0.20,
71            'answer_correctness': 0.10
72        }
73        
74        score = 0.0
75        total_weight = 0.0
76        
77        for metric, weight in weights.items():
78            if metric in results and results[metric] is not None:
79                score += results[metric] * weight
80                total_weight += weight
81        
82        return score / total_weight if total_weight > 0 else 0.0
83    
84    def generate_report(self, results: Dict[str, float]) -> str:
85        """評価レポートの生成"""
86        report = "=" * 50 + "
87"
88        report += "RAGシステム評価レポート
89"
90        report += "=" * 50 + "
91
92"
93        
94        for metric, value in results.items():
95            if value is not None:
96                status = self._get_status(metric, value)
97                report += f"{metric:20s}: {value:.3f} [{status}]
98"
99        
100        report += "
101" + "-" * 50 + "
102"
103        report += "推奨改善アクション:
104"
105        report += self._generate_recommendations(results)
106        
107        return report
108    
109    def _get_status(self, metric: str, value: float) -> str:
110        """メトリクスのステータス判定"""
111        thresholds = {
112            'excellent': 0.9,
113            'good': 0.7,
114            'fair': 0.5,
115            'poor': 0.0
116        }
117        
118        if value >= thresholds['excellent']:
119            return "優秀"
120        elif value >= thresholds['good']:
121            return "良好"
122        elif value >= thresholds['fair']:
123            return "要改善"
124        else:
125            return "要対策"
126    
127    def _generate_recommendations(self, results: Dict) -> str:
128        """改善推奨事項の生成"""
129        recommendations = []
130        
131        if results.get('faithfulness', 1.0) < 0.7:
132            recommendations.append(
133                "- 忠実性が低い: グラウンディング強化、プロンプト改善を検討"
134            )
135        
136        if results.get('answer_relevancy', 1.0) < 0.7:
137            recommendations.append(
138                "- 回答関連性が低い: クエリ理解の改善、意図分類の導入を検討"
139            )
140        
141        if results.get('context_precision', 1.0) < 0.7:
142            recommendations.append(
143                "- コンテキスト精度が低い: 検索アルゴリズムの改善、リランキング導入を検討"
144            )
145        
146        if results.get('context_recall', 1.0) < 0.7:
147            recommendations.append(
148                "- コンテキスト再現率が低い: インデックス戦略の見直し、チャンクサイズ調整を検討"
149            )
150        
151        return "
152".join(recommendations) if recommendations else "現在、良好な性能です。"

TruLensによる詳細分析

python
1from trulens_eval import Tru, Feedback, TruLlama
2from trulens_eval.feedback import Groundedness
3from trulens_eval.feedback.provider import OpenAI as TruOpenAI
4import numpy as np
5
6class TruLensAnalyzer:
7    """TruLensを使用した詳細分析"""
8    
9    def __init__(self, openai_key: str):
10        self.tru = Tru()
11        self.provider = TruOpenAI(api_key=openai_key)
12        self.groundedness = Groundedness(groundedness_provider=self.provider)
13    
14    def setup_feedback_functions(self):
15        """フィードバック関数の設定"""
16        # コンテキスト関連性
17        f_context_relevance = Feedback(
18            self.provider.relevance_with_cot_reasons
19        ).on_input().on(
20            TruLlama.select_source_nodes().node.text
21        ).aggregate(np.mean)
22        
23        # 回答関連性
24        f_answer_relevance = Feedback(
25            self.provider.relevance_with_cot_reasons
26        ).on_input_output()
27        
28        # グラウンディング
29        f_groundedness = Feedback(
30            self.groundedness.groundedness_measure_with_cot_reasons
31        ).on(
32            TruLlama.select_source_nodes().node.text.collect()
33        ).on_output().aggregate(self.groundedness.grounded_statements_aggregator)
34        
35        return [f_context_relevance, f_answer_relevance, f_groundedness]
36    
37    def analyze_conversation(self, 
38                            query: str,
39                            response: str,
40                            contexts: List[str]) -> Dict:
41        """会話の詳細分析"""
42        feedbacks = self.setup_feedback_functions()
43        
44        # 各フィードバック関数を実行
45        results = {}
46        for feedback in feedbacks:
47            score = feedback.run(
48                input=query,
49                output=response,
50                contexts=contexts
51            )
52            results[feedback.name] = score
53        
54        # 詳細な分析結果
55        analysis = {
56            'scores': results,
57            'weak_points': self._identify_weak_points(results),
58            'suggestions': self._generate_suggestions(results)
59        }
60        
61        return analysis
62    
63    def _identify_weak_points(self, scores: Dict) -> List[str]:
64        """弱点の特定"""
65        weak_points = []
66        
67        for metric, score in scores.items():
68            if score < 0.6:
69                weak_points.append(f"{metric}: {score:.2f}")
70        
71        return weak_points
72    
73    def _generate_suggestions(self, scores: Dict) -> List[str]:
74        """改善提案の生成"""
75        suggestions = []
76        
77        if scores.get('context_relevance', 1.0) < 0.7:
78            suggestions.append("検索クエリの改善またはインデックス戦略の見直し")
79        
80        if scores.get('answer_relevance', 1.0) < 0.7:
81            suggestions.append("プロンプトエンジニアリングの改善")
82        
83        if scores.get('groundedness', 1.0) < 0.7:
84            suggestions.append("ハルシネーション対策の強化")
85        
86        return suggestions

A/Bテストフレームワーク

実験的評価の実装

python
1import scipy.stats as stats
2from datetime import datetime
3import json
4
5class RAGABTester:
6    """RAGシステムのA/Bテスト"""
7    
8    def __init__(self):
9        self.experiments = {}
10        self.results = {}
11    
12    def create_experiment(self,
13                         experiment_id: str,
14                         variant_a_config: Dict,
15                         variant_b_config: Dict,
16                         metrics_to_track: List[str]):
17        """実験の作成"""
18        self.experiments[experiment_id] = {
19            'id': experiment_id,
20            'variant_a': variant_a_config,
21            'variant_b': variant_b_config,
22            'metrics': metrics_to_track,
23            'start_time': datetime.now(),
24            'data_a': [],
25            'data_b': []
26        }
27    
28    def record_result(self,
29                      experiment_id: str,
30                      variant: str,
31                      metrics: Dict):
32        """結果の記録"""
33        if experiment_id not in self.experiments:
34            raise ValueError(f"Experiment {experiment_id} not found")
35        
36        data_key = f'data_{variant}'
37        self.experiments[experiment_id][data_key].append({
38            'timestamp': datetime.now(),
39            'metrics': metrics
40        })
41    
42    def analyze_experiment(self, 
43                          experiment_id: str,
44                          confidence_level: float = 0.95) -> Dict:
45        """実験結果の分析"""
46        exp = self.experiments[experiment_id]
47        
48        results = {}
49        for metric in exp['metrics']:
50            # 各バリアントのデータを抽出
51            data_a = [d['metrics'][metric] for d in exp['data_a'] 
52                     if metric in d['metrics']]
53            data_b = [d['metrics'][metric] for d in exp['data_b'] 
54                     if metric in d['metrics']]
55            
56            if len(data_a) < 2 or len(data_b) < 2:
57                results[metric] = {
58                    'status': 'insufficient_data',
59                    'message': 'データ不足'
60                }
61                continue
62            
63            # t検定の実施
64            t_stat, p_value = stats.ttest_ind(data_a, data_b)
65            
66            # 効果量(Cohen's d)の計算
67            cohens_d = self._calculate_cohens_d(data_a, data_b)
68            
69            # 信頼区間の計算
70            ci_a = stats.t.interval(
71                confidence_level, 
72                len(data_a)-1,
73                loc=np.mean(data_a),
74                scale=stats.sem(data_a)
75            )
76            ci_b = stats.t.interval(
77                confidence_level,
78                len(data_b)-1,
79                loc=np.mean(data_b),
80                scale=stats.sem(data_b)
81            )
82            
83            results[metric] = {
84                'mean_a': np.mean(data_a),
85                'mean_b': np.mean(data_b),
86                'std_a': np.std(data_a),
87                'std_b': np.std(data_b),
88                'p_value': p_value,
89                'cohens_d': cohens_d,
90                'ci_a': ci_a,
91                'ci_b': ci_b,
92                'significant': p_value < (1 - confidence_level),
93                'winner': 'A' if np.mean(data_a) > np.mean(data_b) else 'B',
94                'improvement': abs(np.mean(data_b) - np.mean(data_a)) / np.mean(data_a) * 100
95            }
96        
97        return results
98    
99    def _calculate_cohens_d(self, group1: List[float], group2: List[float]) -> float:
100        """Cohen's d(効果量)の計算"""
101        n1, n2 = len(group1), len(group2)
102        var1, var2 = np.var(group1, ddof=1), np.var(group2, ddof=1)
103        
104        # プールされた標準偏差
105        pooled_std = np.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2))
106        
107        # Cohen's d
108        d = (np.mean(group1) - np.mean(group2)) / pooled_std
109        
110        return d
111    
112    def generate_experiment_report(self, experiment_id: str) -> str:
113        """実験レポートの生成"""
114        results = self.analyze_experiment(experiment_id)
115        exp = self.experiments[experiment_id]
116        
117        report = f"""
118A/Bテスト結果レポート
119実験ID: {experiment_id}
120期間: {exp['start_time']} - {datetime.now()}
121サンプル数: A={len(exp['data_a'])}, B={len(exp['data_b'])}
122
123結果サマリー:
124"""
125        
126        for metric, result in results.items():
127            if result.get('status') == 'insufficient_data':
128                report += f"
129{metric}: データ不足により評価不可"
130                continue
131            
132            report += f"""
133メトリクス: {metric}
134  バリアントA: {result['mean_a']:.3f} ± {result['std_a']:.3f}
135  バリアントB: {result['mean_b']:.3f} ± {result['std_b']:.3f}
136  p値: {result['p_value']:.4f}
137  効果量: {result['cohens_d']:.3f}
138  統計的有意性: {'あり' if result['significant'] else 'なし'}
139  優勢: バリアント{result['winner']}
140  改善率: {result['improvement']:.1f}%
141"""
142        
143        # 推奨事項
144        report += "
145推奨事項:
146"
147        significant_improvements = [
148            m for m, r in results.items() 
149            if r.get('significant') and r.get('winner') == 'B'
150        ]
151        
152        if significant_improvements:
153            report += f"バリアントBが以下のメトリクスで有意に優れています: {', '.join(significant_improvements)}
154"
155            report += "バリアントBの採用を推奨します。"
156        else:
157            report += "現時点で明確な優位性は確認されていません。実験の継続を推奨します。"
158        
159        return report

継続的モニタリング

リアルタイムダッシュボード

python
1from datetime import datetime, timedelta
2import plotly.graph_objects as go
3from plotly.subplots import make_subplots
4
5class RAGMonitoringDashboard:
6    """RAGシステムのモニタリングダッシュボード"""
7    
8    def __init__(self):
9        self.metrics_history = []
10        self.alert_thresholds = {
11            'faithfulness': 0.7,
12            'answer_relevancy': 0.7,
13            'latency': 3.0,  # 秒
14            'error_rate': 0.05
15        }
16    
17    def collect_metrics(self, rag_system) -> Dict:
18        """メトリクスの収集"""
19        current_metrics = {
20            'timestamp': datetime.now(),
21            'faithfulness': rag_system.get_faithfulness(),
22            'answer_relevancy': rag_system.get_answer_relevancy(),
23            'latency': rag_system.get_average_latency(),
24            'throughput': rag_system.get_throughput(),
25            'error_rate': rag_system.get_error_rate(),
26            'user_satisfaction': rag_system.get_user_satisfaction()
27        }
28        
29        self.metrics_history.append(current_metrics)
30        
31        # アラートチェック
32        alerts = self.check_alerts(current_metrics)
33        if alerts:
34            self.send_alerts(alerts)
35        
36        return current_metrics
37    
38    def check_alerts(self, metrics: Dict) -> List[str]:
39        """アラート条件のチェック"""
40        alerts = []
41        
42        for metric, threshold in self.alert_thresholds.items():
43            if metric in metrics:
44                if metric in ['latency', 'error_rate']:
45                    # これらは低い方が良い
46                    if metrics[metric] > threshold:
47                        alerts.append(
48                            f"⚠️ {metric}が閾値を超えています: "
49                            f"{metrics[metric]:.3f} > {threshold}"
50                        )
51                else:
52                    # これらは高い方が良い
53                    if metrics[metric] < threshold:
54                        alerts.append(
55                            f"⚠️ {metric}が閾値を下回っています: "
56                            f"{metrics[metric]:.3f} < {threshold}"
57                        )
58        
59        return alerts
60    
61    def create_dashboard(self) -> go.Figure:
62        """ダッシュボードの作成"""
63        if len(self.metrics_history) < 2:
64            return None
65        
66        # データの準備
67        timestamps = [m['timestamp'] for m in self.metrics_history]
68        
69        # サブプロットの作成
70        fig = make_subplots(
71            rows=3, cols=2,
72            subplot_titles=(
73                '忠実性スコア', '回答関連性',
74                'レイテンシ (秒)', 'スループット (req/min)',
75                'エラー率 (%)', 'ユーザー満足度'
76            )
77        )
78        
79        # 忠実性
80        fig.add_trace(
81            go.Scatter(
82                x=timestamps,
83                y=[m['faithfulness'] for m in self.metrics_history],
84                name='忠実性',
85                line=dict(color='blue')
86            ),
87            row=1, col=1
88        )
89        
90        # 回答関連性
91        fig.add_trace(
92            go.Scatter(
93                x=timestamps,
94                y=[m['answer_relevancy'] for m in self.metrics_history],
95                name='関連性',
96                line=dict(color='green')
97            ),
98            row=1, col=2
99        )
100        
101        # レイテンシ
102        fig.add_trace(
103            go.Scatter(
104                x=timestamps,
105                y=[m['latency'] for m in self.metrics_history],
106                name='レイテンシ',
107                line=dict(color='red')
108            ),
109            row=2, col=1
110        )
111        
112        # スループット
113        fig.add_trace(
114            go.Scatter(
115                x=timestamps,
116                y=[m['throughput'] for m in self.metrics_history],
117                name='スループット',
118                line=dict(color='purple')
119            ),
120            row=2, col=2
121        )
122        
123        # エラー率
124        fig.add_trace(
125            go.Scatter(
126                x=timestamps,
127                y=[m['error_rate'] * 100 for m in self.metrics_history],
128                name='エラー率',
129                line=dict(color='orange')
130            ),
131            row=3, col=1
132        )
133        
134        # ユーザー満足度
135        fig.add_trace(
136            go.Scatter(
137                x=timestamps,
138                y=[m['user_satisfaction'] for m in self.metrics_history],
139                name='満足度',
140                line=dict(color='teal')
141            ),
142            row=3, col=2
143        )
144        
145        # レイアウトの更新
146        fig.update_layout(
147            title='RAGシステム パフォーマンスダッシュボード',
148            showlegend=False,
149            height=900
150        )
151        
152        return fig
153    
154    def generate_daily_report(self) -> str:
155        """日次レポートの生成"""
156        today = datetime.now().date()
157        today_metrics = [
158            m for m in self.metrics_history
159            if m['timestamp'].date() == today
160        ]
161        
162        if not today_metrics:
163            return "本日のデータがありません。"
164        
165        report = f"""
166RAGシステム 日次パフォーマンスレポート
167日付: {today}
168
169【サマリー】
170総リクエスト数: {len(today_metrics)}
171平均忠実性: {np.mean([m['faithfulness'] for m in today_metrics]):.3f}
172平均関連性: {np.mean([m['answer_relevancy'] for m in today_metrics]):.3f}
173平均レイテンシ: {np.mean([m['latency'] for m in today_metrics]):.2f}174エラー率: {np.mean([m['error_rate'] for m in today_metrics]) * 100:.2f}%
175
176【時間帯別パフォーマンス】
177"""
178        
179        # 時間帯別の集計
180        hourly_stats = {}
181        for metric in today_metrics:
182            hour = metric['timestamp'].hour
183            if hour not in hourly_stats:
184                hourly_stats[hour] = []
185            hourly_stats[hour].append(metric)
186        
187        for hour in sorted(hourly_stats.keys()):
188            hour_metrics = hourly_stats[hour]
189            report += f"{hour:02d}:00-{hour+1:02d}:00 - "
190            report += f"リクエスト: {len(hour_metrics)}, "
191            report += f"平均レイテンシ: {np.mean([m['latency'] for m in hour_metrics]):.2f}192"
193        
194        return report

INDXでの実践事例

金融機関Iでの評価フレームワーク導入

課題: RAGシステムの効果を定量的に証明できない

導入した評価体系:

python
1# I社向けカスタム評価システム
2class FinancialRAGEvaluator:
3    def __init__(self):
4        self.domain_metrics = {
5            'regulatory_compliance': 0.95,  # 規制遵守精度
6            'numerical_accuracy': 0.99,     # 数値精度
7            'source_traceability': 1.0      # ソース追跡性
8        }
9    
10    def evaluate_financial_rag(self, qa_pairs):
11        results = {
12            'standard_metrics': self.calculate_standard_metrics(qa_pairs),
13            'domain_metrics': self.calculate_domain_metrics(qa_pairs),
14            'risk_assessment': self.assess_risks(qa_pairs)
15        }
16        return results

成果:

  • 評価時間: 手動2日 → 自動2時間
  • 改善サイクル: 月1回 → 週2回
  • ROI証明: 投資対効果3.2倍を数値で証明

製造業J社での継続的改善

課題: どこを改善すべきか分からない

導入プロセス:

1. 包括的な評価指標の設定

2. A/Bテストによる改善検証

3. 自動モニタリングシステム構築

成果:

  • ボトルネック特定: 2週間 → 即座
  • 精度向上: 65% → 89%(6ヶ月)
  • ダウンタイム: 月10時間 → 月30分

ベストプラクティス

評価指標の選定基準

ユースケース重要指標推奨閾値
カスタマーサポート回答関連性、応答速度>0.8, <2秒
法務文書検索忠実性、精度>0.95, >0.9
技術ドキュメントコンテキスト再現率、正確性>0.85, >0.9
医療情報ハルシネーション率、忠実性<0.01, >0.98

段階的導入アプローチ

1. フェーズ1: 基本指標の確立(1-2週間)

- Recall@k、Precision@k

- 基本的な生成品質メトリクス

2. フェーズ2: RAG特有指標の追加(2-4週間)

- 忠実性、回答関連性

- コンテキスト精度

3. フェーズ3: 自動化とモニタリング(1-2ヶ月)

- 自動評価パイプライン

- リアルタイムダッシュボード

4. フェーズ4: 継続的最適化(継続)

- A/Bテスト

- 自動アラート・改善提案

まとめ

RAGシステムの評価フレームワークは、単なる測定ツールではなく、継続的な改善を可能にする重要な基盤です。適切な評価指標の選定、自動化ツールの活用、そして継続的なモニタリングにより、RAGシステムの性能を客観的に把握し、データドリブンな改善が可能になります。

INDXでは、クライアントのビジネス要件に合わせた評価フレームワークを設計・実装し、「見える化」から「継続的改善」まで、RAGシステムの価値最大化を支援しています。

タグ

評価指標
Ragas
TruLens
DeepEval
Recall@k