AI评估框架:指标不够用了之后
去年在做一个 RAG(检索增强生成)系统时,这个问题特别明显——本地评估分数看着不错,上线后用户反馈「检索不准」「答非所问」。
AI 这东西有个很反直觉的特点:训练集表现好,真实环境里可能烂得要命。
评估框架到底在做什么
先澄清一个容易被混淆的点:评估框架和测试框架不是一回事。测试框架通常回答「代码有没有bug」,评估框架回答「模型在任务上表现如何」。
一个完整的评估框架至少要包含这几块:
数据集构建负责收集和组织评估样本,指标体系设计决定从哪些维度评估模型表现,评估流程负责执行并收集结果,结果分析则把原始分数变成可行动的洞察。
它们之间的关系是循环的:分析结果通常会反过来影响数据集或指标体系的选择。
从单指标开始,但别停在那
最早的时候我也贪图省事,只选几个常见的指标:BLEU-4、ROUGE-L、EM(精确匹配)。代码大概是这样:
from nltk.translate.bleu_score import corpus_bleu
from rouge import Rouge
import numpy as np
def evaluate_metrics(predictions, references):
"""基础的文本生成评估指标"""
# BLEU-4
bleu = corpus_bleu(
[[ref.split()] for ref in references],
[pred.split() for pred in predictions],
weights=(0.25, 0.25, 0.25, 0.25)
)
# ROUGE-L
rouge = Rouge()
rouge_scores = rouge.get_scores(
predictions,
references,
avg=True
)
# EM(Exact Match)
em = np.mean([
1 if pred.strip() == ref.strip() else 0
for pred, ref in zip(predictions, references)
])
return {
'bleu_4': bleu,
'rouge_l': rouge_scores['rouge-l']['f'],
'em': em
}
看着挺正常,但实际问题很快就暴露了。
RAG 场景里,答案往往很长,用户更关心的是关键信息有没有被覆盖。但 BLEU 对词序和常见词敏感,ROUGE 又过度依赖 n-gram 重叠。两者都没法准确反映「核心信息准确性」。
举个例子,模型回答「北京在北方」,标准答案「北京是中国北部城市」,BLEU 分数会很低(词序和短语不匹配),但信息其实是准确的。反过来,模型乱说一通但恰巧词序匹配上了,分数可能还很高。
这就是「指标-真实感受」之间的gap。
重新设计指标体系
后来我重新思考了评估目标:RAG 系统的核心是「检索+生成」,评估也应该拆成两部分。检索部分看有没有找到相关文档,生成部分看答案质量。
from typing import List, Dict, Any
from dataclasses import dataclass
@dataclass
class EvaluationSample:
"""单个评估样本"""
query: str
retrieved_docs: List[str]
generated_answer: str
reference_answer: str
categories: List[str] # 样本类别,如"事实型"、"观点型"
@dataclass
class MetricResult:
"""单个指标结果"""
name: str
value: float
confidence: float # 置信区间
details: Dict[str, Any] # 额外细节
class EvaluationFramework:
"""完整的评估框架"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.retrieval_metrics = []
self.generation_metrics = []
def add_metric(self, metric_type: str, metric_func):
"""添加指标"""
if metric_type == 'retrieval':
self.retrieval_metrics.append(metric_func)
elif metric_type == 'generation':
self.generation_metrics.append(metric_func)
def evaluate(self, samples: List[EvaluationSample]) -> Dict[str, List[MetricResult]]:
"""执行完整评估"""
results = {
'retrieval': [],
'generation': []
}
# 评估检索质量
for metric in self.retrieval_metrics:
result = metric(samples)
results['retrieval'].append(result)
# 评估生成质量
for metric in self.generation_metrics:
result = metric(samples)
results['generation'].append(result)
return results
这样拆分后,检索评估可以单独看 Recall、MRR、NDCG 这些信息检索指标,生成评估则可以更关注语义和结构。
检索评估的一个例子:
def retrieval_recall(samples: List[EvaluationSample], k: int = 5) -> MetricResult:
"""Top-k 召回率"""
hits = 0
total = len(samples)
for sample in samples:
# 简单的判断:如果任一检索结果包含参考答案的关键词,就算命中
# 实际应用中可以用更复杂的相关性判断
keywords = extract_keywords(sample.reference_answer)
for doc in sample.retrieved_docs[:k]:
if any(kw in doc for kw in keywords):
hits += 1
break
# 计算置信区间(Wilson区间)
p = hits / total
z = 1.96 # 95%置信
margin = z * ((p * (1 - p)) / total) ** 0.5
return MetricResult(
name=f'recall@{k}',
value=p,
confidence=margin,
details={'hits': hits, 'total': total}
)
def extract_keywords(text: str) -> List[str]:
"""简单的关键词提取,实际项目可用更复杂的方法"""
# 这里只是示意,可以用jieba、TF-IDF等
import re
words = re.findall(r'[一-龥]{2,}', text)
return list(set(words))
生成评估则可以引入语义相似度、事实一致性等指标:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
class SemanticSimilarity:
"""语义相似度评估"""
def __init__(self, model_name: str = 'paraphrase-multilingual-MiniLM-L12-v2'):
self.model = SentenceTransformer(model_name)
def __call__(self, samples: List[EvaluationSample]) -> MetricResult:
"""计算语义相似度"""
similarities = []
for sample in samples:
# 计算生成答案和参考答案的语义相似度
answer_emb = self.model.encode([sample.generated_answer])
ref_emb = self.model.encode([sample.reference_answer])
sim = cosine_similarity(answer_emb, ref_emb)[0][0]
similarities.append(sim)
avg_sim = np.mean(similarities)
std_sim = np.std(similarities)
return MetricResult(
name='semantic_similarity',
value=avg_sim,
confidence=1.96 * std_sim / np.sqrt(len(samples)),
details={
'similarities': similarities,
'std': std_sim
}
)
数据集构建的关键问题
指标再好,数据集有问题也会白搭。在实践中踩过几个明显的坑。
第一个是「样本不均衡」。最早的数据集里,「简单问答」占了 80%,模型在这些样本上表现很好,拉高了整体分数。但复杂场景的样本太少,模型表现差但被淹没了。
后来改用分层采样,强制保证各类别数量平衡:
import random
from collections import defaultdict
def balanced_sample(
samples: List[EvaluationSample],
target_per_category: int = 50
) -> List[EvaluationSample]:
"""按类别平衡采样"""
category_dict = defaultdict(list)
# 按类别分组
for sample in samples:
for category in sample.categories:
category_dict[category].append(sample)
balanced = []
# 从每个类别随机抽取目标数量
for category, category_samples in category_dict.items():
if len(category_samples) >= target_per_category:
selected = random.sample(category_samples, target_per_category)
else:
selected = category_samples # 样本不足就全用
balanced.extend(selected)
return balanced
第二个是「参考答案质量参差不齐」。有的参考答案本身就很不完整或存在事实错误,导致模型回答其实不错,但分数很低。解决方案是用多个人工标注者的答案,取一致性较高的作为「黄金标准」,或用模型生成的答案作为候选,再由人工审核。
第三个是「样本过期」。领域知识变化很快,几个月前还是准确的答案,现在可能已经错了。需要建立数据集版本管理和更新机制,定期标注并替换过期样本。
评估流程的自动化
手动跑评估很麻烦,尤其是模型迭代快的时候。后来搭了一套自动化流程,每次模型更新后自动触发评估:
import subprocess
from datetime import datetime
import json
def run_evaluation_pipeline(
model_path: str,
dataset_path: str,
output_dir: str
) -> Dict[str, Any]:
"""运行完整评估流程"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_file = f"{output_dir}/eval_{timestamp}.json"
# 1. 加载数据集
samples = load_dataset(dataset_path)
# 2. 运行模型生成答案
print("Running inference...")
generated_answers = run_inference(model_path, [s.query for s in samples])
# 3. 更新样本
for sample, answer in zip(samples, generated_answers):
sample.generated_answer = answer
# 4. 运行评估
print("Running evaluation...")
framework = EvaluationFramework({'model_path': model_path})
# 添加指标
framework.add_metric('retrieval', lambda s: retrieval_recall(s, k=5))
framework.add_metric('retrieval', lambda s: retrieval_recall(s, k=10))
framework.add_metric('generation', SemanticSimilarity())
framework.add_metric('generation', lambda s: factual_consistency(s))
results = framework.evaluate(samples)
# 5. 保存结果
report = {
'timestamp': timestamp,
'model_path': model_path,
'dataset_path': dataset_path,
'results': {
'retrieval': [
{'name': r.name, 'value': r.value, 'confidence': r.confidence}
for r in results['retrieval']
],
'generation': [
{'name': r.name, 'value': r.value, 'confidence': r.confidence}
for r in results['generation']
]
},
'sample_count': len(samples)
}
with open(output_file, 'w') as f:
json.dump(report, f, indent=2)
# 6. 可选:触发可视化或告警
if results['generation'][0].value < 0.7: # 假设阈值
send_alert(f"Semantic similarity dropped: {results['generation'][0].value}")
return report
这套流程可以集成到 CI/CD 里,每次模型训练完成自动跑一遍,分数不达标就阻止发布。
分析结果比跑出分数更重要
评估报告不应该只是一堆数字。真正有价值的是从数字里读出问题。
比如某次评估里,semantic_similarity 看着不错,但 factual_consistency(事实一致性)很低。进一步分析发现,模型在「观点型」问题上表现很好(语义相关性强),但在「事实型」问题上经常编造信息。
这说明模型在风格和语气上模仿到位,但知识获取能力有问题。解决方案是加强检索模块,或者引入外部知识库验证。
def detailed_analysis(
samples: List[EvaluationSample],
metric_results: Dict[str, List[MetricResult]]
) -> Dict[str, Any]:
"""细粒度分析"""
analysis = {
'by_category': {},
'by_difficulty': {},
'failure_cases': []
}
# 按类别分析
by_category = defaultdict(list)
for sample in samples:
for category in sample.categories:
by_category[category].append(sample)
for category, cat_samples in by_category.items():
# 只计算当前类别的指标
cat_results = framework.evaluate(cat_samples)
analysis['by_category'][category] = cat_results
# 找出表现最差的样本(failure cases)
for sample in samples:
# 这里可以自定义判定「差」的标准
if sample.generated_answer == "" or "抱歉" in sample.generated_answer:
analysis['failure_cases'].append({
'query': sample.query,
'generated': sample.generated_answer,
'reference': sample.reference_answer,
'categories': sample.categories
})
return analysis
踩过的几个大坑
这套体系不是一开始就这么完善的,中间踩了不少坑。
坑一:过度依赖单指标。 早期只看 BLEU 分数,模型学会「投机取巧」——输出一些高概率词但语义空洞。后来引入语义相似度和事实一致性才缓解。
坑二:置信区间被忽略。 评估报告只给平均值,没有置信区间。某次发现模型分数「提升」了 2%,但数据量太少,置信区间就有 ±3%,实际上是噪声。
坑三:评估集泄露到训练集。 为了提高分数,有人把评估集的样本悄悄混进了训练集。这是典型的数据泄露,需要严格的权限和流程控制。
坑四:指标和业务目标不一致。 业务关心「用户满意度」,但评估系统只看文本相似度。后来引入用户反馈数据,和评估指标做相关性分析,发现某些指标和真实满意度关联很强,有些几乎无关。
一些实用建议
基于这些经验,整理几个实用建议:
指标要和业务目标对齐。 不是指标越多越好,而是要回答「用户真正关心什么」。
持续监控和迭代。 评估体系本身也需要评估——它的分数变化和真实业务指标是否一致。
保留历史结果。 建立评估结果的时序数据库,可以看模型长期表现趋势,避免某次偶然提升被误判为「突破」。
人工抽样必不可少。 自动化再完善,也要定期人工抽查样本,看评估系统有没有「幻觉」或系统性偏差。
简单可复现优先。 指标选常见的、开源的、社区认可度高的,除非有特殊需求,否则不要自己发明新指标。
收住
评估框架不是银弹,它更像一面镜子——照出模型的问题,也照出我们对问题理解不足的地方。从单指标到体系化的过程,本质上是「从想当然到有证据」的转变。
现在回头看,这套体系还在不断演进。新模型出来要有新指标,新场景要有新样本,甚至「什么是好」的标准也在变。唯一不变的是:如果你不肯花功夫认真评估,模型上线后一定会让你花更多功夫去擦屁股。
AI 这东西,账面成绩再漂亮,也得看考场表现。
版权声明: 本文首发于 指尖魔法屋-AI评估框架:指标不够用了之后(https://blog.thinkmoon.cn/post/215-practice-ai-evaluation-metrics-framework/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。