第1953篇:大模型评估基准的工程实现——在自己的数据集上评测模型表现
第1953篇:大模型评估基准的工程实现——在自己的数据集上评测模型表现
每次有新模型发布,各家都会晒MMLU、HumanEval、C-Eval这些跑分,数字看起来很漂亮。然后你把这个模型接到自己的业务场景里,发现效果差得一塌糊涂。
这不是模型在骗你,是你在用错评估基准。
MMLU测的是通识知识,HumanEval测的是代码生成,C-Eval测的是中文知识理解——这些都是通用基准,告诉你模型在"平均水平"上怎么样。但你的业务场景不是平均水平,你可能是一个法律文书智能助手,或者是一个工厂设备故障诊断系统,这些场景需要用自己的数据集来评测。
今天这篇,我来讲如何从零构建一套业务专属的模型评估框架。
通用基准的局限性在哪里
先说清楚问题本质。
通用基准有三个根本性的局限:
第一,领域分布不匹配。 你的业务有自己的专有词汇、表达习惯、知识结构。通用基准里根本没有这些,所以即使模型在通用基准上得了高分,在你的领域里也可能答非所问。
第二,评估维度不对。 MMLU主要测"知识准确性",但你的业务可能更关心"格式规范性"(比如法律文书必须用特定格式)、"拒绝回答能力"(不该回答的问题要拒绝)、"一致性"(同一问题多次回答不应该矛盾)。这些在通用基准里都没有。
第三,输入分布不一样。 基准测试的问题是精心设计的、边界清晰的。但真实用户的输入充满了错别字、歧义、口语化表达。
我自己的经验是:在通用基准上,A模型比B模型高10分,但接到我们的法律助手系统后,B模型明显更好。原因后来分析出来了:A模型更"博学",但在格式规范上很随意;B模型知识面略窄,但严格遵循指令,输出格式一致性极高,而我们的业务最需要的恰好是格式一致性。
评估框架的核心设计
整个框架围绕四个核心问题:评什么、用什么数据、怎么打分、怎么决策。
数据集构建
构建高质量的评估数据集是最难、最重要的环节。很多团队在这里偷懒,用随便几十个case凑数,最后评估结果完全不可信。
@Service
public class EvalDatasetBuilder {
private final ProductionLogService productionLogService;
private final ManualAnnotationService annotationService;
private final DataAugmentationService augmentationService;
/**
* 三种方式构建数据集,各有侧重
*/
public EvalDataset buildDataset(DatasetConfig config) {
List<EvalCase> cases = new ArrayList<>();
// 1. 从生产日志里采样真实用户输入(最有代表性)
if (config.isIncludeProductionSamples()) {
List<EvalCase> prodCases = sampleFromProduction(
config.getProductionSampleCount(),
config.getSamplingStrategy()
);
cases.addAll(prodCases);
}
// 2. 手工构造边界case(覆盖重要但罕见的场景)
if (config.isIncludeEdgeCases()) {
List<EvalCase> edgeCases = loadEdgeCases(config.getEdgeCaseFile());
cases.addAll(edgeCases);
}
// 3. 数据增强(在现有case基础上变换,增加多样性)
if (config.isIncludeAugmented()) {
List<EvalCase> augmented = augmentationService.augment(
cases, config.getAugmentationRatio()
);
cases.addAll(augmented);
}
// 验证数据集质量
DatasetQualityReport quality = validateDataset(cases);
if (quality.getDuplicateRate() > 0.05) {
log.warn("数据集重复率过高: {}%,建议去重",
quality.getDuplicateRate() * 100);
}
return EvalDataset.builder()
.id(UUID.randomUUID().toString())
.cases(deduplicateAndShuffle(cases))
.metadata(buildMetadata(cases, config))
.qualityReport(quality)
.build();
}
/**
* 分层采样,保证各场景类别都有代表
*/
private List<EvalCase> sampleFromProduction(int count, SamplingStrategy strategy) {
// 按场景类型分层
Map<String, List<ProductionLog>> byCategory =
productionLogService.getRecentLogs(30) // 最近30天
.stream()
.collect(Collectors.groupingBy(ProductionLog::getCategory));
List<EvalCase> sampled = new ArrayList<>();
// 确保每个类别都有足够的样本
int perCategory = count / byCategory.size();
for (Map.Entry<String, List<ProductionLog>> entry : byCategory.entrySet()) {
List<ProductionLog> logs = entry.getValue();
// 按质量分层采样:一部分高质量(用户满意),一部分低质量(用户抱怨)
// 这样测试集能同时测"能做好的"和"容易出错的"
List<ProductionLog> highQuality = logs.stream()
.filter(l -> l.getUserFeedbackScore() >= 4)
.limit(perCategory / 2)
.collect(Collectors.toList());
List<ProductionLog> lowQuality = logs.stream()
.filter(l -> l.getUserFeedbackScore() <= 2)
.limit(perCategory / 2)
.collect(Collectors.toList());
Stream.concat(highQuality.stream(), lowQuality.stream())
.map(this::toEvalCase)
.forEach(sampled::add);
}
return sampled;
}
}数据集的分层采样很关键。 如果只用好的case,测试集就成了"模型秀场",没什么区分度。要刻意加入那些边界case、历史上出过问题的case,这样评估结果才有价值。
评估指标体系
不同业务场景的评估指标不一样,但有几类是通用的:
// 评估指标的枚举定义
public enum EvalMetric {
// 准确性类
FACTUAL_ACCURACY, // 事实准确性
ANSWER_COMPLETENESS, // 回答完整性
// 格式类
FORMAT_COMPLIANCE, // 格式规范符合度
STRUCTURE_VALIDITY, // 结构有效性(JSON/XML等)
// 安全类
REFUSAL_ACCURACY, // 不该回答的问题是否正确拒绝
HALLUCINATION_RATE, // 幻觉比率
SENSITIVE_CONTENT, // 有害内容检测
// 一致性类
SELF_CONSISTENCY, // 同一问题多次回答的一致性
CONTEXT_COHERENCE, // 多轮对话的上下文一致性
// 效率类
CONCISENESS, // 是否简洁(没有废话)
LATENCY_P50, // 50分位延迟
LATENCY_P95, // 95分位延迟
TOKEN_EFFICIENCY // 同等质量下Token消耗
}@Service
public class CompositeEvaluator {
private final Map<EvalMetric, MetricEvaluator> evaluatorMap;
public EvalResult evaluate(EvalCase evalCase, String modelOutput,
EvalConfig config) {
Map<EvalMetric, Double> scores = new HashMap<>();
Map<EvalMetric, String> explanations = new HashMap<>();
for (EvalMetric metric : config.getMetrics()) {
MetricEvaluator evaluator = evaluatorMap.get(metric);
if (evaluator == null) {
log.warn("No evaluator found for metric: {}", metric);
continue;
}
try {
MetricScore score = evaluator.evaluate(evalCase, modelOutput);
scores.put(metric, score.getValue());
explanations.put(metric, score.getExplanation());
} catch (Exception e) {
log.error("Evaluator failed for metric {}: {}", metric, e.getMessage());
scores.put(metric, 0.0);
}
}
// 加权综合得分
double overallScore = calculateWeightedScore(scores, config.getWeights());
return EvalResult.builder()
.evalCaseId(evalCase.getId())
.modelOutput(modelOutput)
.metricScores(scores)
.explanations(explanations)
.overallScore(overallScore)
.passedThresholds(checkThresholds(scores, config.getThresholds()))
.build();
}
private double calculateWeightedScore(Map<EvalMetric, Double> scores,
Map<EvalMetric, Double> weights) {
double totalWeight = 0;
double weightedSum = 0;
for (Map.Entry<EvalMetric, Double> entry : scores.entrySet()) {
double weight = weights.getOrDefault(entry.getKey(), 1.0);
weightedSum += entry.getValue() * weight;
totalWeight += weight;
}
return totalWeight > 0 ? weightedSum / totalWeight : 0;
}
}评估器的实现
评估器分三类:规则评估器(最快)、模型评估器(中等)、人工评估器(最贵)。
// 规则评估器:格式规范
@Component
public class FormatComplianceEvaluator implements MetricEvaluator {
@Override
public EvalMetric getMetric() {
return EvalMetric.FORMAT_COMPLIANCE;
}
@Override
public MetricScore evaluate(EvalCase evalCase, String output) {
FormatSpec spec = evalCase.getExpectedFormat();
if (spec == null) {
return MetricScore.notApplicable();
}
List<String> violations = new ArrayList<>();
switch (spec.getType()) {
case JSON -> {
try {
new ObjectMapper().readTree(output);
// JSON格式正确,进一步检查字段完整性
violations.addAll(checkJsonFields(output, spec.getRequiredFields()));
} catch (JsonProcessingException e) {
violations.add("输出不是有效的JSON格式");
}
}
case MARKDOWN_LIST -> {
if (!output.contains("- ") && !output.contains("* ")) {
violations.add("期望Markdown列表格式,但未找到列表项");
}
}
case NUMBERED_LIST -> {
if (!output.matches("(?s).*\\d+\\..*")) {
violations.add("期望有序列表格式,但未找到编号");
}
}
case CUSTOM -> {
violations.addAll(checkCustomFormat(output, spec.getPattern()));
}
}
double score = violations.isEmpty() ? 1.0 :
Math.max(0, 1.0 - violations.size() * 0.25);
return MetricScore.builder()
.value(score)
.explanation(violations.isEmpty() ? "格式符合规范" :
"格式违规: " + String.join("; ", violations))
.build();
}
}// 模型评估器:用一个LLM评估另一个LLM的输出质量
@Component
public class LLMJudgeEvaluator implements MetricEvaluator {
private final LLMClient judgeModel;
private static final String JUDGE_PROMPT = """
你是一个严格的AI输出质量评估专家。
用户问题:{{question}}
参考答案(如果有):{{reference}}
模型输出:{{output}}
请从以下维度评估模型输出的质量(每项0-10分):
1. 准确性:信息是否正确
2. 相关性:是否切题,没有废话
3. 完整性:是否覆盖了问题的关键点
4. 可读性:表达是否清晰流畅
请以JSON格式回答:
{
"accuracy": <0-10>,
"relevance": <0-10>,
"completeness": <0-10>,
"readability": <0-10>,
"reasoning": "<简短的评估理由>"
}
只返回JSON,不要有其他内容。
""";
@Override
public MetricScore evaluate(EvalCase evalCase, String output) {
String prompt = JUDGE_PROMPT
.replace("{{question}}", evalCase.getInput())
.replace("{{reference}}", evalCase.getReferenceAnswer() != null ?
evalCase.getReferenceAnswer() : "无")
.replace("{{output}}", output);
try {
String judgeResponse = judgeModel.complete(prompt);
JudgeResult result = parseJudgeResponse(judgeResponse);
// 四个维度加权平均
double score = (result.getAccuracy() * 0.4 +
result.getRelevance() * 0.2 +
result.getCompleteness() * 0.3 +
result.getReadability() * 0.1) / 10.0;
return MetricScore.builder()
.value(score)
.explanation(result.getReasoning())
.rawScores(Map.of(
"accuracy", result.getAccuracy(),
"relevance", result.getRelevance(),
"completeness", result.getCompleteness(),
"readability", result.getReadability()
))
.build();
} catch (Exception e) {
log.warn("Judge evaluation failed: {}", e.getMessage());
return MetricScore.error("评估执行失败: " + e.getMessage());
}
}
}这里用LLM评估LLM,俗称"LLM-as-Judge",是目前业界最流行的做法之一。但有几个已知问题要注意:
- 位置偏差:把两个输出放在一起比较时,Judge模型倾向于选第一个(没为什么,就是有这个偏向)。解决方法是swap位置重复两次,取一致的结果
- 长度偏差:Judge模型倾向于认为长答案更好,即使长答案充满废话
- 自我偏好:如果你的Judge用GPT-4,它倾向于打高分给GPT-4生成的内容
在我的实践中,对抗这些偏差的方法是:对同一case做3次独立评估,取中位数;同时配合规则评估器做交叉验证。
批量评估引擎
@Service
public class BatchEvaluationEngine {
private final ThreadPoolExecutor executor;
private final LLMClientPool llmClientPool;
private final CompositeEvaluator evaluator;
public BatchEvaluationEngine() {
// 控制并发,避免打爆LLM API限流
this.executor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
public EvalReport runEvaluation(EvalDataset dataset,
ModelConfig modelConfig,
EvalConfig evalConfig) {
log.info("开始评估: model={}, cases={}",
modelConfig.getModelId(), dataset.getCases().size());
List<CompletableFuture<EvalResult>> futures = dataset.getCases().stream()
.map(evalCase -> CompletableFuture.supplyAsync(
() -> evaluateSingleCase(evalCase, modelConfig, evalConfig),
executor
))
.collect(Collectors.toList());
// 带进度追踪的等待
List<EvalResult> results = new ArrayList<>();
AtomicInteger completed = new AtomicInteger(0);
int total = futures.size();
for (CompletableFuture<EvalResult> future : futures) {
try {
EvalResult result = future.get(30, TimeUnit.SECONDS);
results.add(result);
int count = completed.incrementAndGet();
if (count % 50 == 0) {
log.info("评估进度: {}/{}", count, total);
}
} catch (TimeoutException e) {
log.warn("单个case评估超时,跳过");
results.add(EvalResult.timeout());
} catch (Exception e) {
log.error("单个case评估失败", e);
results.add(EvalResult.error(e.getMessage()));
}
}
return buildReport(dataset, modelConfig, evalConfig, results);
}
private EvalResult evaluateSingleCase(EvalCase evalCase,
ModelConfig modelConfig,
EvalConfig evalConfig) {
long startTime = System.currentTimeMillis();
try {
// 限流:避免太快打到限流
RateLimiter rateLimiter = getRateLimiter(modelConfig.getModelId());
rateLimiter.acquire();
// 调用模型
LLMClient client = llmClientPool.getClient(modelConfig);
String output = client.complete(
buildPrompt(evalCase, evalConfig),
modelConfig.getGenerationParams()
);
long latency = System.currentTimeMillis() - startTime;
// 评估输出质量
EvalResult result = evaluator.evaluate(evalCase, output, evalConfig);
result.setLatencyMs(latency);
return result;
} catch (Exception e) {
return EvalResult.error(e.getMessage());
}
}
private EvalReport buildReport(EvalDataset dataset, ModelConfig modelConfig,
EvalConfig evalConfig, List<EvalResult> results) {
// 按指标聚合统计
Map<EvalMetric, DescriptiveStats> metricStats = new HashMap<>();
for (EvalMetric metric : evalConfig.getMetrics()) {
List<Double> scores = results.stream()
.filter(r -> r.getMetricScores().containsKey(metric))
.map(r -> r.getMetricScores().get(metric))
.collect(Collectors.toList());
metricStats.put(metric, computeStats(scores));
}
// 找出失败的case
List<EvalResult> failures = results.stream()
.filter(r -> r.getOverallScore() < evalConfig.getPassThreshold())
.sorted(Comparator.comparingDouble(EvalResult::getOverallScore))
.collect(Collectors.toList());
return EvalReport.builder()
.datasetId(dataset.getId())
.modelId(modelConfig.getModelId())
.totalCases(results.size())
.successRate((double) (results.size() - failures.size()) / results.size())
.metricStatistics(metricStats)
.failureCases(failures)
.overallScore(results.stream()
.mapToDouble(EvalResult::getOverallScore)
.average().orElse(0))
.evaluatedAt(LocalDateTime.now())
.build();
}
}多模型横向对比
@Service
public class ModelComparisonService {
private final BatchEvaluationEngine evalEngine;
/**
* 同一数据集,对多个候选模型做横向对比
*/
public ComparisonReport compareModels(EvalDataset dataset,
List<ModelConfig> candidates,
EvalConfig evalConfig) {
Map<String, EvalReport> reports = new LinkedHashMap<>();
for (ModelConfig model : candidates) {
log.info("评估模型: {}", model.getModelId());
EvalReport report = evalEngine.runEvaluation(dataset, model, evalConfig);
reports.put(model.getModelId(), report);
}
// 找出在各指标上的最优模型
Map<EvalMetric, String> bestModelByMetric = new HashMap<>();
for (EvalMetric metric : evalConfig.getMetrics()) {
String bestModel = reports.entrySet().stream()
.max(Comparator.comparingDouble(e ->
e.getValue().getMetricStatistics()
.getOrDefault(metric, DescriptiveStats.zero()).getMean()))
.map(Map.Entry::getKey)
.orElse("unknown");
bestModelByMetric.put(metric, bestModel);
}
// 成本效益分析
Map<String, CostEfficiencyScore> costEfficiency = reports.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> calculateCostEfficiency(e.getKey(), e.getValue(), candidates)
));
return ComparisonReport.builder()
.modelReports(reports)
.bestModelByMetric(bestModelByMetric)
.overallRanking(rankModels(reports))
.costEfficiencyAnalysis(costEfficiency)
.recommendation(makeRecommendation(reports, bestModelByMetric, costEfficiency))
.build();
}
private String makeRecommendation(Map<String, EvalReport> reports,
Map<EvalMetric, String> bestByMetric,
Map<String, CostEfficiencyScore> costEfficiency) {
// 找出综合排名第一的
String topPerformer = reports.entrySet().stream()
.max(Comparator.comparingDouble(e -> e.getValue().getOverallScore()))
.map(Map.Entry::getKey)
.orElse("unknown");
EvalReport topReport = reports.get(topPerformer);
// 找出性价比最好的
String bestValue = costEfficiency.entrySet().stream()
.max(Comparator.comparingDouble(e -> e.getValue().getScore()))
.map(Map.Entry::getKey)
.orElse("unknown");
if (topPerformer.equals(bestValue)) {
return String.format("推荐使用 %s,综合质量最高且性价比最佳,总分 %.2f",
topPerformer, topReport.getOverallScore());
} else {
return String.format("质量最优: %s(总分%.2f);性价比最优: %s;" +
"建议根据业务对成本的敏感度做最终选择",
topPerformer, topReport.getOverallScore(), bestValue);
}
}
}我遇到的几个反直觉发现
做这套评估框架之后,我在不同项目里发现了几个反直觉的结论,值得分享:
发现1:更大的模型不一定更适合你的场景。 我们做了一次评测,GPT-4在通识类问题上远超GPT-3.5,但在格式合规这个指标上,两者差距很小。而我们的业务最重要的是格式合规,所以从成本效益角度,3.5完全够用。
发现2:低延迟的模型在用户满意度上有时更好。 一个响应速度快一倍但质量略低一点的模型,用户满意度反而更高——因为用户等待超过某个阈值之后,即使答案很好,体验也已经打了折扣。这个阈值在我们的场景是大约12秒。
发现3:测试集分布比测试集大小更重要。 100个精心挑选的case,比1000个随机采样的case能给出更有指导意义的评估结果。因为随机采样的case里,90%都是模型能轻松处理的普通case,这些case对区分模型能力没什么贡献。
小结
自建评估基准不是一件轻松的事,但它是把AI工程做严肃的必要前提。
你用通用基准做模型选型,就像用全国统一考试成绩来招募某个特殊岗位的专业人才——有一定参考价值,但远远不够。
花时间构建你自己的数据集和评估体系,才能真正回答那个最关键的问题:这个模型,适不适合我的业务?
