File size: 4,765 Bytes
06f35c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""
RAGAS evaluation script.
Dùng LLM-as-judge thay vì ragas library để tránh dependency conflicts.
Metrics: Faithfulness, Answer Relevancy, Context Recall
"""
print("Loading evaluate.py...")
import json
print("json ok")
import time
print("time ok")
from groq import Groq
print("groq ok")
from dotenv import load_dotenv
import os
load_dotenv()
client = Groq(api_key=os.environ["GROQ_API_KEY"])
JUDGE_MODEL = "llama-3.3-70b-versatile"
print("groq client ok")
from src.rag import answer as rag_answer
print("rag ok")


def judge(prompt: str) -> float:
    """Gọi LLM judge, trả về score 0.0-1.0."""
    response = client.chat.completions.create(
        model=JUDGE_MODEL,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=10,
        temperature=0.0,
    )
    text = response.choices[0].message.content.strip()
    try:
        score = float(text.split()[0])
        return max(0.0, min(1.0, score))
    except Exception:
        return 0.5


def eval_faithfulness(question: str, answer: str, contexts: list[str]) -> float:
    context_text = "\n".join(contexts)
    prompt = f"""Đánh giá xem câu trả lời có trung thực với ngữ cảnh không.
Chỉ trả về một số từ 0.0 đến 1.0 (1.0 = hoàn toàn trung thực, 0.0 = bịa đặt).

Ngữ cảnh: {context_text}
Câu trả lời: {answer}

Score:"""
    return judge(prompt)


def eval_answer_relevancy(question: str, answer: str) -> float:
    prompt = f"""Đánh giá xem câu trả lời có liên quan đến câu hỏi không.
Chỉ trả về một số từ 0.0 đến 1.0 (1.0 = rất liên quan, 0.0 = không liên quan).

Câu hỏi: {question}
Câu trả lời: {answer}

Score:"""
    return judge(prompt)


def eval_context_recall(question: str, ground_truth: str, contexts: list[str]) -> float:
    context_text = "\n".join(contexts)
    prompt = f"""Đánh giá xem ngữ cảnh có chứa đủ thông tin để trả lời câu hỏi theo đáp án chuẩn không.
Chỉ trả về một số từ 0.0 đến 1.0 (1.0 = đủ thông tin, 0.0 = thiếu hoàn toàn).

Câu hỏi: {question}
Đáp án chuẩn: {ground_truth}
Ngữ cảnh: {context_text}

Score:"""
    return judge(prompt)


def evaluate(golden_set_path: str = "data/golden_set.json", sample: int = 20):
    with open(golden_set_path, encoding="utf-8") as f:
        golden = json.load(f)

    # Lấy sample câu hỏi có ground_truth thực (bỏ out_of_scope)
    items = [q for q in golden if q.get("type") != "out_of_scope"][:sample]

    results = []
    print(f"Đánh giá {len(items)} câu hỏi...\n")

    for i, item in enumerate(items, 1):
        question = item["question"]
        ground_truth = item["answer"]

        print(f"[{i}/{len(items)}] {question[:60]}...")
        try:
            result = rag_answer(question, top_k=5)
            ans = result["answer"]
            context_texts = result["contexts"]

            faith = eval_faithfulness(question, ans, context_texts)
            relevancy = eval_answer_relevancy(question, ans)
            recall = eval_context_recall(question, ground_truth, context_texts)

            results.append({
                "question": question,
                "ground_truth": ground_truth,
                "answer": ans,
                "faithfulness": faith,
                "answer_relevancy": relevancy,
                "context_recall": recall,
            })

            print(f"  Faithfulness: {faith:.2f} | Relevancy: {relevancy:.2f} | Recall: {recall:.2f}")
            time.sleep(2)  # tránh rate limit

        except Exception as e:
            print(f"  Lỗi: {e}")
            time.sleep(5)

    if results:
        avg_faith = sum(r["faithfulness"] for r in results) / len(results)
        avg_rel = sum(r["answer_relevancy"] for r in results) / len(results)
        avg_recall = sum(r["context_recall"] for r in results) / len(results)

        print(f"\n{'='*50}")
        print(f"KẾT QUẢ ĐÁNH GIÁ ({len(results)} câu hỏi)")
        print(f"{'='*50}")
        print(f"Faithfulness:     {avg_faith:.3f}")
        print(f"Answer Relevancy: {avg_rel:.3f}")
        print(f"Context Recall:   {avg_recall:.3f}")
        print(f"Average:          {(avg_faith + avg_rel + avg_recall) / 3:.3f}")

        with open("eval_results.json", "w", encoding="utf-8") as f:
            json.dump({
                "summary": {
                    "faithfulness": avg_faith,
                    "answer_relevancy": avg_rel,
                    "context_recall": avg_recall,
                },
                "details": results,
            }, f, ensure_ascii=False, indent=2)
        print("\nĐã lưu chi tiết vào eval_results.json")


if __name__ == "__main__":
    evaluate(sample=20)