144 lines
5.0 KiB
Python
144 lines
5.0 KiB
Python
"""
|
||
RAG评估指标计算工具
|
||
简化版:只保留Recall/F1(检索)和 LLM Judge(答案准确性)
|
||
"""
|
||
|
||
import textwrap
|
||
from typing import Any
|
||
|
||
import json_repair
|
||
|
||
from yuxi.utils import logger
|
||
|
||
|
||
class RetrievalMetrics:
|
||
"""检索评估指标计算"""
|
||
|
||
@staticmethod
|
||
def precision_at_k(retrieved_ids: list[str], relevant_ids: list[str], k: int) -> float:
|
||
"""计算Precision@K"""
|
||
if not retrieved_ids[:k]:
|
||
return 0.0
|
||
retrieved_set = set(retrieved_ids[:k])
|
||
relevant_set = set(relevant_ids)
|
||
return len(retrieved_set & relevant_set) / k
|
||
|
||
@staticmethod
|
||
def recall_at_k(retrieved_ids: list[str], relevant_ids: list[str], k: int) -> float:
|
||
"""计算Recall@K"""
|
||
if not relevant_ids:
|
||
return 0.0
|
||
retrieved_set = set(retrieved_ids[:k])
|
||
relevant_set = set(relevant_ids)
|
||
return len(retrieved_set & relevant_set) / len(relevant_set)
|
||
|
||
@staticmethod
|
||
def f1_score_at_k(retrieved_ids: list[str], relevant_ids: list[str], k: int) -> float:
|
||
"""计算F1@K"""
|
||
precision = RetrievalMetrics.precision_at_k(retrieved_ids, relevant_ids, k)
|
||
recall = RetrievalMetrics.recall_at_k(retrieved_ids, relevant_ids, k)
|
||
if precision + recall == 0:
|
||
return 0.0
|
||
return 2 * precision * recall / (precision + recall)
|
||
|
||
|
||
class AnswerMetrics:
|
||
"""答案评估指标计算"""
|
||
|
||
@staticmethod
|
||
async def judge_correctness(query: str, generated_answer: str, gold_answer: str, judge_llm: Any) -> dict[str, Any]:
|
||
"""
|
||
使用LLM判断生成的答案是否正确
|
||
"""
|
||
if not generated_answer:
|
||
return {"score": 0.0, "reasoning": "未生成答案"}
|
||
if not gold_answer:
|
||
return {"score": 0.0, "reasoning": "无参考答案"}
|
||
|
||
prompt = textwrap.dedent(f"""你是一个公正的评判者,请评估AI生成的答案相对于标准答案的准确性。
|
||
|
||
问题:{query}
|
||
|
||
标准答案:
|
||
{gold_answer}
|
||
|
||
AI生成的答案:
|
||
{generated_answer}
|
||
|
||
请判断AI生成的答案是否在事实层面与标准答案一致。
|
||
忽略措辞、标点符号或格式上的细微差异。
|
||
只关注核心事实是否准确包含。
|
||
|
||
请返回以下JSON格式的结果(不要包含其他文本、Markdown 或注释):
|
||
{{
|
||
"score": 1.0,
|
||
"reasoning": "简要说明判定理由"
|
||
}}
|
||
score 只能是 1.0 或 0.0。
|
||
""")
|
||
try:
|
||
response = await judge_llm.call(prompt, stream=False)
|
||
content = response.content.strip()
|
||
|
||
# 尝试清理可能的 markdown 代码块
|
||
if content.startswith("```json"):
|
||
content = content[7:]
|
||
if content.endswith("```"):
|
||
content = content[:-3]
|
||
content = content.strip()
|
||
|
||
result = json_repair.loads(content)
|
||
return {"score": float(result.get("score", 0.0)), "reasoning": result.get("reasoning", "")}
|
||
except Exception as e:
|
||
logger.error(f"LLM 评判失败: {e}")
|
||
return {"score": 0.0, "reasoning": f"评判出错: {str(e)}"}
|
||
|
||
|
||
class EvaluationMetricsCalculator:
|
||
"""综合评估指标计算器"""
|
||
|
||
@staticmethod
|
||
def calculate_retrieval_metrics(
|
||
retrieved_chunks: list[dict[str, Any]], gold_chunk_ids: list[str], k_values: list[int] = [1, 3, 5, 10]
|
||
) -> dict[str, float]:
|
||
"""计算检索指标 (Recall, F1)"""
|
||
if not retrieved_chunks or not gold_chunk_ids:
|
||
return {}
|
||
|
||
# 提取 ID
|
||
retrieved_ids = []
|
||
for chunk in retrieved_chunks:
|
||
chunk_id = chunk.get("chunk_id") or chunk.get("metadata", {}).get("chunk_id")
|
||
retrieved_ids.append(str(chunk_id) if chunk_id else "")
|
||
|
||
metrics = {}
|
||
for k in k_values:
|
||
metrics[f"recall@{k}"] = RetrievalMetrics.recall_at_k(retrieved_ids, gold_chunk_ids, k)
|
||
metrics[f"f1@{k}"] = RetrievalMetrics.f1_score_at_k(retrieved_ids, gold_chunk_ids, k)
|
||
|
||
return metrics
|
||
|
||
@staticmethod
|
||
async def calculate_answer_metrics(
|
||
query: str, generated_answer: str, gold_answer: str, judge_llm: Any = None
|
||
) -> dict[str, Any]:
|
||
"""计算答案指标 (LLM Judge)"""
|
||
if not judge_llm:
|
||
return {}
|
||
|
||
return await AnswerMetrics.judge_correctness(query, generated_answer, gold_answer, judge_llm)
|
||
|
||
@staticmethod
|
||
def calculate_overall_score(
|
||
retrieval_metrics_list: list[dict[str, float]], answer_metrics_list: list[dict[str, Any]]
|
||
) -> float | None:
|
||
"""计算整体平均分,只聚合检索指标。"""
|
||
retrieval_scores = []
|
||
|
||
# 计算检索平均分
|
||
for m in retrieval_metrics_list:
|
||
if m:
|
||
retrieval_scores.append(sum(m.values()) / len(m))
|
||
|
||
return sum(retrieval_scores) / len(retrieval_scores) if retrieval_scores else None
|