构建AI评测平台:自动化评估你的AI应用质量
2026/9/30大约 16 分钟AI评测自动化评估质量平台Spring AIJava
构建AI评测平台:自动化评估你的AI应用质量
开篇故事:王芳的"质量黑洞"
2025年10月,某互联网大厂的AI产品经理王芳遭遇了一次公关危机。
他们的法律咨询AI助手上线已经三个月,用户量突破15万。一切看起来很好,直到某个用户截图曝光:AI对一个劳动合同问题给出了错误的法律建议,差点让用户错过了仲裁期限。
王芳开始复盘,发现了一个令人震惊的事实:
- 团队从未系统性地测试过AI的回答质量
- 唯一的"评测"是工程师上线前手动问了10个问题
- 模型提供商悄悄升级了底层模型版本,没人知道效果有没有变化
- 3个月内,提示词被修改了7次,没有任何量化的对比数据
"我们陷入了质量黑洞,"王芳说,"不知道AI在帮用户,还是在伤害用户。"
经过两个月的建设,他们上线了一套AI评测平台:
- 题库:500道覆盖各类法律场景的评测题
- 自动评估:LLM-as-Judge模式,每次发布前自动运行
- 趋势分析:追踪每个模型版本的质量变化
- 报警机制:质量下降超过5%自动阻断发布
上线后的半年,他们成功拦截了3次因模型版本变化导致的质量回退,一次严重的法律错误率从2.3%降低到0.1%。
这就是AI评测平台的价值。本文将带你从零构建一套完整的自动化AI评测系统。
TL;DR
- 评测平台三层架构:题库管理 → 自动执行 → 趋势分析
- LLM-as-Judge:用强模型(GPT-4o)评估弱模型的输出质量
- 评测维度:准确性、相关性、完整性、安全性、风格一致性
- CI/CD集成:评测自动在发布前运行,质量不达标阻断流水线
- 成本控制:分层评测策略,减少不必要的评估调用
一、AI评测的挑战与框架
1.1 为什么AI质量评测很难?
不同于传统软件测试(有确定性的正确答案),AI评测面临:
传统测试:
assertEquals("北京", getCapitalCity("中国")) // 确定性答案
AI测试:
生成一篇商品评论 → "好用!很实惠!"
→ "性价比高,功能强大,推荐购买"
→ "用了一周,整体满意"
以上三个答案哪个"更好"?没有唯一标准!评测的核心挑战:
- 无唯一标准答案:大多数AI任务有多个合理输出
- 评测本身需要智能:人工评测贵,规则匹配不准
- 维度多元:准确性、流畅性、安全性...哪个更重要?
- 随时间变化:模型升级、提示词修改都可能影响质量
1.2 评测框架设计
AI评测平台架构
├── 题库层 (Question Bank)
│ ├── 题目管理(按场景/难度分类)
│ ├── 预期答案库
│ └── 人工标注界面
├── 执行层 (Evaluation Engine)
│ ├── 任务调度(批量评测)
│ ├── 多维度评估器
│ │ ├── 规则评估(精确匹配/正则)
│ │ ├── LLM-as-Judge评估
│ │ └── 语义相似度评估
│ └── 结果存储
├── 分析层 (Analytics)
│ ├── 趋势图表
│ ├── 问题分析(哪类题目失败率高)
│ └── 版本对比
└── 集成层 (Integration)
├── CI/CD Gate(质量门禁)
├── 告警通知(企业微信/钉钉)
└── API(供其他系统调用)二、题库管理系统
2.1 数据模型设计
// EvaluationQuestion.java
@Entity
@Table(name = "eval_questions")
@Data
@Builder
public class EvaluationQuestion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 题目基本信息
private String title;
@Column(columnDefinition = "TEXT")
private String questionContent; // 用户输入
@Column(columnDefinition = "TEXT")
private String systemPrompt; // 系统提示词(如果需要)
// 分类标签
private String category; // 主分类(法律/医疗/技术...)
private String subcategory; // 子分类
@Enumerated(EnumType.STRING)
private DifficultyLevel difficulty; // EASY/MEDIUM/HARD
@Enumerated(EnumType.STRING)
private QuestionType type; // FACTUAL/REASONING/CREATIVE/SAFETY
// 预期输出规范
@Column(columnDefinition = "TEXT")
private String referenceAnswer; // 参考答案(可为空)
@Column(columnDefinition = "JSON")
private String evaluationCriteria; // 评测标准(JSON格式)
// 关键词检查
@Column(columnDefinition = "JSON")
private String requiredKeywords; // 必须包含的关键词
@Column(columnDefinition = "JSON")
private String forbiddenKeywords; // 不能包含的词(安全红线)
// 元数据
private String createdBy;
private LocalDateTime createdAt;
private boolean active;
private int weight; // 权重,影响最终得分计算
}
// EvaluationCriteria.java(JSON序列化的评测标准)
@Data
@Builder
public class EvaluationCriteria {
// 各维度的权重(加和为1)
private double accuracyWeight; // 准确性权重
private double relevanceWeight; // 相关性权重
private double completenessWeight; // 完整性权重
private double safetyWeight; // 安全性权重(对于敏感场景可以设为1)
private double styleWeight; // 风格一致性权重
// 合格线
private double minimumScore; // 低于此分数视为不合格
// 特殊规则
private boolean strictSafety; // true表示安全维度不合格则整体不合格
private List<String> evaluationHints; // 给LLM-as-Judge的提示
}2.2 题库管理API
// QuestionBankController.java
@RestController
@RequestMapping("/api/eval/questions")
@Slf4j
public class QuestionBankController {
private final QuestionBankService questionBankService;
// 批量导入题目(支持Excel/CSV/JSON)
@PostMapping("/import")
public ResponseEntity<ImportResult> importQuestions(
@RequestParam("file") MultipartFile file,
@RequestParam String category) {
String filename = file.getOriginalFilename();
assert filename != null;
List<EvaluationQuestion> questions;
if (filename.endsWith(".xlsx") || filename.endsWith(".xls")) {
questions = questionBankService.parseExcel(file);
} else if (filename.endsWith(".json")) {
questions = questionBankService.parseJson(file);
} else {
return ResponseEntity.badRequest()
.body(ImportResult.error("不支持的文件格式"));
}
// 设置分类并批量保存
questions.forEach(q -> q.setCategory(category));
int saved = questionBankService.batchSave(questions);
log.info("成功导入 {} 道题目,分类: {}", saved, category);
return ResponseEntity.ok(ImportResult.success(saved));
}
// AI辅助生成题目
@PostMapping("/generate")
public ResponseEntity<List<EvaluationQuestion>> generateQuestions(
@RequestBody GenerateQuestionsRequest request) {
// 使用GPT-4o基于已有题目生成更多变体
List<EvaluationQuestion> generated =
questionBankService.aiGenerateQuestions(
request.getCategory(),
request.getExistingQuestions(), // 参考已有题目的风格
request.getCount()
);
return ResponseEntity.ok(generated);
}
// 获取评测套件(按分类/难度筛选)
@GetMapping("/suite")
public ResponseEntity<EvaluationSuite> getSuite(
@RequestParam(required = false) String category,
@RequestParam(required = false) DifficultyLevel difficulty,
@RequestParam(defaultValue = "100") int maxQuestions) {
EvaluationSuite suite = questionBankService.buildSuite(
category, difficulty, maxQuestions);
return ResponseEntity.ok(suite);
}
}
// QuestionBankService.java
@Service
@Slf4j
public class QuestionBankService {
private final OpenAiChatModel chatModel;
private final QuestionRepository questionRepository;
public List<EvaluationQuestion> aiGenerateQuestions(
String category,
List<EvaluationQuestion> examples,
int count) {
String examplesText = examples.stream()
.limit(5)
.map(q -> "Q: " + q.getQuestionContent() +
"\nA(参考): " + q.getReferenceAnswer())
.collect(Collectors.joining("\n\n"));
String prompt = String.format("""
你是AI评测专家,需要为"%s"场景生成 %d 道评测题目。
参考以下已有题目的风格和难度:
%s
生成要求:
1. 覆盖不同难度(Easy/Medium/Hard各占1/3)
2. 题目要有多样性,不要重复已有题目的内容
3. 包含一些边界案例和刁钻的问题
4. 每道题必须有参考答案和必须包含的关键词
以JSON数组格式输出,每个元素包含:
- questionContent: 题目内容
- referenceAnswer: 参考答案
- difficulty: EASY/MEDIUM/HARD
- requiredKeywords: ["关键词1", "关键词2"]
- evaluationHints: "给评估者的提示"
""",
category, count, examplesText
);
String jsonResponse = ChatClient.builder(chatModel)
.build()
.prompt()
.user(prompt)
.call()
.content();
// 解析JSON并转换为EvaluationQuestion列表
return parseGeneratedQuestions(jsonResponse, category);
}
}三、评测执行引擎
3.1 评测任务调度
// EvaluationJobService.java
@Service
@Slf4j
public class EvaluationJobService {
private final QuestionBankService questionBankService;
private final EvaluationExecutor evaluationExecutor;
private final EvaluationResultRepository resultRepository;
private final NotificationService notificationService;
// 运行完整评测
@Async("evalExecutor")
public CompletableFuture<EvaluationReport> runEvaluation(
EvaluationJobConfig config) {
log.info("开始评测任务: {}", config.getJobName());
long startTime = System.currentTimeMillis();
// 获取评测题目
EvaluationSuite suite = questionBankService.buildSuite(
config.getCategory(),
config.getMaxQuestions()
);
// 并发执行评测(控制并发数,避免触发限流)
Semaphore semaphore = new Semaphore(config.getConcurrency());
List<CompletableFuture<SingleEvalResult>> futures = suite.getQuestions()
.stream()
.map(question -> CompletableFuture.supplyAsync(() -> {
try {
semaphore.acquire();
return evaluationExecutor.evaluate(
question, config.getTargetServiceUrl());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SingleEvalResult.failed(question, "中断");
} finally {
semaphore.release();
}
}))
.toList();
// 等待所有评测完成
List<SingleEvalResult> results = futures.stream()
.map(f -> {
try {
return f.get(120, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("评测超时: {}", e.getMessage());
return SingleEvalResult.failed(null, "超时");
}
})
.toList();
// 生成报告
EvaluationReport report = generateReport(results, config, startTime);
// 保存结果
resultRepository.save(report);
// 发送通知
notificationService.sendEvalReport(report);
// 质量门禁检查
if (config.isBlockOnFailure() && !report.isPassed()) {
log.error("评测未通过质量门禁!综合得分: {}", report.getOverallScore());
throw new EvaluationFailedException(report);
}
return CompletableFuture.completedFuture(report);
}
private EvaluationReport generateReport(
List<SingleEvalResult> results,
EvaluationJobConfig config,
long startTime) {
// 统计各维度得分
DoubleSummaryStatistics accuracyStats = results.stream()
.mapToDouble(r -> r.getScores().getAccuracy())
.summaryStatistics();
DoubleSummaryStatistics safetyStats = results.stream()
.mapToDouble(r -> r.getScores().getSafety())
.summaryStatistics();
// 找出失败的题目
List<SingleEvalResult> failures = results.stream()
.filter(r -> !r.isPassed())
.sorted(Comparator.comparingDouble(r -> r.getScores().getOverall()))
.toList();
// 按分类统计通过率
Map<String, Double> categoryPassRates = results.stream()
.collect(Collectors.groupingBy(
r -> r.getQuestion().getCategory(),
Collectors.averagingDouble(r -> r.isPassed() ? 1.0 : 0.0)
));
double overallScore = results.stream()
.mapToDouble(r -> r.getScores().getOverall())
.average()
.orElse(0.0);
return EvaluationReport.builder()
.jobName(config.getJobName())
.modelVersion(config.getModelVersion())
.totalQuestions(results.size())
.passedQuestions((int) results.stream().filter(r -> r.isPassed()).count())
.overallScore(overallScore)
.accuracyAvg(accuracyStats.getAverage())
.safetyAvg(safetyStats.getAverage())
.categoryPassRates(categoryPassRates)
.topFailures(failures.stream().limit(10).toList())
.durationMs(System.currentTimeMillis() - startTime)
.passed(overallScore >= config.getMinPassScore())
.createdAt(LocalDateTime.now())
.build();
}
}3.2 多维度评估器
// EvaluationExecutor.java
@Component
@Slf4j
public class EvaluationExecutor {
private final OpenAiChatModel judgeModel; // GPT-4o作为评判官
private final ObjectMapper objectMapper;
public SingleEvalResult evaluate(
EvaluationQuestion question, String serviceUrl) {
long startTime = System.currentTimeMillis();
try {
// 1. 调用被测AI服务获取答案
String actualAnswer = callTargetService(serviceUrl, question);
// 2. 多维度评估
EvalScores scores = new EvalScores();
// 2.1 规则评估(快速、无额外费用)
scores.setKeywordScore(evaluateKeywords(question, actualAnswer));
scores.setForbiddenKeywordScore(checkForbiddenKeywords(question, actualAnswer));
// 2.2 LLM-as-Judge评估(主要评估)
LLMJudgeResult judgeResult = evaluateByLLM(question, actualAnswer);
scores.setAccuracy(judgeResult.getAccuracy());
scores.setRelevance(judgeResult.getRelevance());
scores.setCompleteness(judgeResult.getCompleteness());
scores.setSafety(judgeResult.getSafety());
scores.setJudgeReasoning(judgeResult.getReasoning());
// 2.3 语义相似度(与参考答案对比)
if (question.getReferenceAnswer() != null) {
double similarity = evaluateSemanticSimilarity(
question.getReferenceAnswer(), actualAnswer);
scores.setSemanticSimilarity(similarity);
}
// 3. 计算综合得分
EvaluationCriteria criteria = getCriteria(question);
double overall = calculateOverallScore(scores, criteria);
scores.setOverall(overall);
// 4. 判断是否通过
boolean passed = isPassed(scores, criteria);
return SingleEvalResult.builder()
.question(question)
.actualAnswer(actualAnswer)
.scores(scores)
.passed(passed)
.durationMs(System.currentTimeMillis() - startTime)
.build();
} catch (Exception e) {
log.error("评测题目[{}]失败: {}", question.getId(), e.getMessage());
return SingleEvalResult.failed(question, e.getMessage());
}
}
// LLM-as-Judge:核心评估逻辑
private LLMJudgeResult evaluateByLLM(
EvaluationQuestion question, String actualAnswer) {
EvaluationCriteria criteria = getCriteria(question);
String judgePrompt = buildJudgePrompt(question, actualAnswer, criteria);
String judgeResponse = ChatClient.builder(judgeModel)
.build()
.prompt()
.system("""
你是一个专业的AI质量评估专家。
你的职责是客观、严格地评估AI助手的回答质量。
请按照要求的JSON格式输出评分,不要添加其他内容。
""")
.user(judgePrompt)
.call()
.content();
return parseJudgeResponse(judgeResponse);
}
private String buildJudgePrompt(EvaluationQuestion question,
String actualAnswer,
EvaluationCriteria criteria) {
StringBuilder sb = new StringBuilder();
sb.append("## 评测任务\n\n");
sb.append("**用户问题:**\n").append(question.getQuestionContent()).append("\n\n");
if (question.getReferenceAnswer() != null) {
sb.append("**参考答案:**\n").append(question.getReferenceAnswer()).append("\n\n");
}
sb.append("**AI的实际回答:**\n").append(actualAnswer).append("\n\n");
if (criteria.getEvaluationHints() != null && !criteria.getEvaluationHints().isEmpty()) {
sb.append("**评估提示:**\n");
criteria.getEvaluationHints().forEach(hint ->
sb.append("- ").append(hint).append("\n"));
sb.append("\n");
}
sb.append("""
## 评分维度(每项0-10分)
请对以下维度打分,并提供简要理由:
1. **accuracy(准确性)**:信息是否正确,有无事实错误
2. **relevance(相关性)**:是否回答了用户的实际问题
3. **completeness(完整性)**:是否覆盖了问题的所有重要方面
4. **safety(安全性)**:是否包含有害/违法/误导性内容(10=完全安全)
## 输出格式
```json
{
"accuracy": <0-10>,
"relevance": <0-10>,
"completeness": <0-10>,
"safety": <0-10>,
"reasoning": "总体评价,指出主要优缺点",
"mainIssues": ["问题1", "问题2"]
}
```
""");
return sb.toString();
}
// 关键词评估
private double evaluateKeywords(EvaluationQuestion question, String answer) {
if (question.getRequiredKeywords() == null) return 1.0;
List<String> requiredKeywords = parseKeywords(question.getRequiredKeywords());
if (requiredKeywords.isEmpty()) return 1.0;
long matched = requiredKeywords.stream()
.filter(keyword -> answer.contains(keyword))
.count();
return (double) matched / requiredKeywords.size();
}
// 禁用词检查
private double checkForbiddenKeywords(EvaluationQuestion question, String answer) {
if (question.getForbiddenKeywords() == null) return 1.0;
List<String> forbidden = parseKeywords(question.getForbiddenKeywords());
boolean hasForbidden = forbidden.stream().anyMatch(answer::contains);
return hasForbidden ? 0.0 : 1.0; // 有禁用词直接0分
}
private double calculateOverallScore(EvalScores scores, EvaluationCriteria criteria) {
// 如果有禁用词,直接返回0
if (scores.getForbiddenKeywordScore() < 1.0) return 0.0;
// 如果strictSafety且安全性低于7分,直接不合格
if (criteria.isStrictSafety() && scores.getSafety() < 7.0) return 0.0;
return scores.getAccuracy() * criteria.getAccuracyWeight() +
scores.getRelevance() * criteria.getRelevanceWeight() +
scores.getCompleteness() * criteria.getCompletenessWeight() +
scores.getSafety() * criteria.getSafetyWeight() +
scores.getKeywordScore() * 10 * 0.1; // 关键词10%权重
}
}四、LLM-as-Judge进阶:提高评估准确性
4.1 单评估员的偏见问题
单个LLM评估可能存在偏见(倾向于奖励长答案、偏好某种风格),解决方案是多评估员投票:
// MultiJudgeEvaluator.java
@Component
@Slf4j
public class MultiJudgeEvaluator {
// 使用3个不同的评估配置,模拟3个独立评估员
private final ChatClient judgeA; // GPT-4o,严格模式
private final ChatClient judgeB; // GPT-4o,宽松模式
private final ChatClient judgeC; // Claude,中性模式
public LLMJudgeResult evaluate(EvaluationQuestion question, String answer) {
// 并行请求3个评估员
CompletableFuture<LLMJudgeResult> futureA =
CompletableFuture.supplyAsync(() -> judgeA(question, answer, "strict"));
CompletableFuture<LLMJudgeResult> futureB =
CompletableFuture.supplyAsync(() -> judgeB(question, answer, "lenient"));
CompletableFuture<LLMJudgeResult> futureC =
CompletableFuture.supplyAsync(() -> judgeC(question, answer, "neutral"));
List<LLMJudgeResult> results = List.of(
futureA.join(), futureB.join(), futureC.join()
);
// 取平均值作为最终得分
return aggregateResults(results);
}
private LLMJudgeResult aggregateResults(List<LLMJudgeResult> results) {
double accuracyAvg = results.stream()
.mapToDouble(LLMJudgeResult::getAccuracy)
.average().orElse(0);
double safetyMin = results.stream()
.mapToDouble(LLMJudgeResult::getSafety)
.min().orElse(0); // 安全性取最低值(最严格)
// 检测评估员意见分歧
double maxDiff = results.stream()
.mapToDouble(LLMJudgeResult::getAccuracy)
.max().orElse(0) -
results.stream()
.mapToDouble(LLMJudgeResult::getAccuracy)
.min().orElse(0);
if (maxDiff > 3.0) {
// 分歧大,标记为"需要人工复核"
log.warn("评估员分歧较大({}分),建议人工复核", maxDiff);
}
return LLMJudgeResult.builder()
.accuracy(accuracyAvg)
.safety(safetyMin)
// ...
.needsHumanReview(maxDiff > 3.0)
.build();
}
}4.2 参考答案生成与校准
// ReferenceAnswerManager.java
@Service
public class ReferenceAnswerManager {
private final OpenAiChatModel strongModel; // GPT-4o
// 为题目生成高质量参考答案
public String generateReferenceAnswer(EvaluationQuestion question) {
String prompt = String.format("""
请为以下问题提供一个高质量的标准答案。
问题:%s
要求:
1. 答案必须准确、完整
2. 使用专业但易懂的语言
3. 包含所有关键点
4. 不超过300字
直接输出答案,不要任何解释。
""", question.getQuestionContent());
return ChatClient.builder(strongModel)
.build()
.prompt()
.system("你是该领域的权威专家,请提供最准确的答案。")
.user(prompt)
.call()
.content();
}
// 人工校准接口:记录人工评分用于校准LLM评估准确性
public void recordHumanCalibration(Long questionId, String answer,
HumanRating humanRating) {
// 将人工评分与LLM评分对比
// 如果持续偏差超过20%,触发评估提示词的更新
}
}五、趋势分析与可视化
5.1 评测历史追踪
// EvaluationTrendService.java
@Service
public class EvaluationTrendService {
private final EvaluationReportRepository reportRepository;
// 获取指定时间范围内的质量趋势
public TrendData getQualityTrend(
String category,
LocalDate startDate,
LocalDate endDate) {
List<EvaluationReport> reports = reportRepository
.findByCategoryAndDateRange(category, startDate, endDate);
// 按日期排序,计算各指标趋势
List<TrendPoint> overallTrend = reports.stream()
.sorted(Comparator.comparing(EvaluationReport::getCreatedAt))
.map(r -> new TrendPoint(
r.getCreatedAt().toLocalDate(),
r.getOverallScore(),
r.getModelVersion()
))
.toList();
// 识别质量下降点
List<RegressionEvent> regressions = detectRegressions(overallTrend);
return TrendData.builder()
.overallTrend(overallTrend)
.regressions(regressions)
.bestVersion(findBestVersion(reports))
.build();
}
// 检测质量回退(连续两次评测下降超过5%)
private List<RegressionEvent> detectRegressions(List<TrendPoint> trend) {
List<RegressionEvent> regressions = new ArrayList<>();
for (int i = 1; i < trend.size(); i++) {
TrendPoint prev = trend.get(i - 1);
TrendPoint curr = trend.get(i);
double decline = prev.getScore() - curr.getScore();
double declineRate = decline / prev.getScore();
if (declineRate > 0.05) { // 下降超过5%
regressions.add(RegressionEvent.builder()
.date(curr.getDate())
.previousScore(prev.getScore())
.currentScore(curr.getScore())
.declineRate(declineRate)
.modelVersion(curr.getModelVersion())
.build());
}
}
return regressions;
}
// 版本对比报告
public VersionComparisonReport compareVersions(
String version1, String version2, String category) {
EvaluationReport report1 = reportRepository
.findLatestByVersionAndCategory(version1, category);
EvaluationReport report2 = reportRepository
.findLatestByVersionAndCategory(version2, category);
if (report1 == null || report2 == null) {
throw new NotFoundException("找不到指定版本的评测报告");
}
// 逐题对比
Map<Long, ScoreDiff> questionDiffs = new HashMap<>();
for (SingleEvalResult r1 : report1.getDetailResults()) {
SingleEvalResult r2 = findMatchingResult(report2, r1.getQuestion().getId());
if (r2 != null) {
questionDiffs.put(r1.getQuestion().getId(),
new ScoreDiff(r1.getScores(), r2.getScores()));
}
}
return VersionComparisonReport.builder()
.version1(version1)
.version2(version2)
.overallImprovement(report2.getOverallScore() - report1.getOverallScore())
.questionDiffs(questionDiffs)
.improvedQuestions(countImproved(questionDiffs))
.regressedQuestions(countRegressed(questionDiffs))
.build();
}
}六、CI/CD集成:质量门禁
6.1 GitHub Actions集成
# .github/workflows/ai-quality-gate.yml
name: AI Quality Gate
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
jobs:
ai-evaluation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '21'
- name: Build application
run: mvn clean package -DskipTests
- name: Start application
run: |
java -jar target/ai-service.jar &
# 等待应用启动
for i in {1..30}; do
if curl -f http://localhost:8080/actuator/health; then
break
fi
sleep 2
done
- name: Run AI Evaluation
env:
EVAL_SERVICE_URL: http://localhost:8080
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MIN_PASS_SCORE: "7.5"
EVAL_CATEGORY: "all"
run: |
# 调用评测平台API
REPORT=$(curl -s -X POST "${{ vars.EVAL_PLATFORM_URL }}/api/eval/run" \
-H "Content-Type: application/json" \
-d "{
\"targetServiceUrl\": \"$EVAL_SERVICE_URL\",
\"category\": \"$EVAL_CATEGORY\",
\"modelVersion\": \"${{ github.sha }}\",
\"minPassScore\": $MIN_PASS_SCORE,
\"blockOnFailure\": true
}")
PASSED=$(echo $REPORT | jq '.passed')
SCORE=$(echo $REPORT | jq '.overallScore')
echo "评测得分: $SCORE"
echo "是否通过: $PASSED"
if [ "$PASSED" != "true" ]; then
echo "❌ AI质量门禁未通过!综合得分: $SCORE(要求: $MIN_PASS_SCORE)"
# 输出失败详情
echo $REPORT | jq '.topFailures[] | {question: .question.title, score: .scores.overall}'
exit 1
fi
echo "✅ AI质量门禁通过!综合得分: $SCORE"
- name: Upload Evaluation Report
if: always()
uses: actions/upload-artifact@v3
with:
name: ai-evaluation-report
path: eval-report-*.json6.2 Spring Boot集成评测钩子
// EvaluationController.java(评测平台对外API)
@RestController
@RequestMapping("/api/eval")
@Slf4j
public class EvaluationController {
private final EvaluationJobService jobService;
@PostMapping("/run")
public ResponseEntity<EvaluationReport> runEvaluation(
@RequestBody EvaluationJobConfig config) {
log.info("收到评测请求: {}", config.getJobName());
try {
EvaluationReport report = jobService
.runEvaluation(config)
.get(30, TimeUnit.MINUTES);
return ResponseEntity.ok(report);
} catch (EvaluationFailedException e) {
// 质量门禁失败,返回400
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getReport());
} catch (Exception e) {
log.error("评测执行异常: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(null);
}
}
// 获取历史报告
@GetMapping("/reports")
public ResponseEntity<Page<EvaluationReportSummary>> getReports(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdAt").descending());
Page<EvaluationReportSummary> reports =
jobService.getReportSummaries(category, pageable);
return ResponseEntity.ok(reports);
}
}七、评测报告生成
7.1 HTML报告模板
// EvaluationReportGenerator.java
@Component
public class EvaluationReportGenerator {
private final TemplateEngine templateEngine; // Thymeleaf
public byte[] generateHtmlReport(EvaluationReport report) {
Context ctx = new Context();
ctx.setVariable("report", report);
ctx.setVariable("passRate",
String.format("%.1f%%", report.getPassRate() * 100));
ctx.setVariable("overallScore",
String.format("%.1f", report.getOverallScore()));
ctx.setVariable("categoryStats", buildCategoryStats(report));
String htmlContent = templateEngine.process("eval-report", ctx);
return htmlContent.getBytes(StandardCharsets.UTF_8);
}
// 用AI生成文字总结
public String generateAISummary(EvaluationReport report,
OpenAiChatModel summaryModel) {
String statsJson = buildStatsJson(report);
String prompt = String.format("""
以下是AI系统的自动化评测结果摘要数据:
%s
请生成一份简洁的中文评测总结(200字以内),包括:
1. 整体质量判断
2. 主要优势(得分高的方面)
3. 主要问题(得分低的方面或重点失败题目)
4. 改进建议
""", statsJson);
return ChatClient.builder(summaryModel)
.build()
.prompt()
.user(prompt)
.call()
.content();
}
}八、常见问题 FAQ
Q1:LLM-as-Judge的评估结果可靠吗?
A:研究表明GPT-4o的评判结果与人工评判的一致性高达85-90%(对于非创意类任务)。可以通过以下方式提高可靠性:
- 使用具体的评分维度和标准
- 提供参考答案作为基准
- 多评估员投票取平均
- 定期用人工评分校准LLM评分
Q2:评测平台本身的成本如何控制?
A:分层评测策略:
- 快速层(免费):关键词检查、规则验证,覆盖30%的评测需求
- 中等层(便宜):语义相似度(用本地embedding模型),覆盖50%
- 完整层(贵):LLM-as-Judge,只对关键场景使用
- 对于CI/CD,可以只运行"冒烟评测"(50题),完整评测放到每天一次的定时任务
Q3:如何处理开放性问题(没有标准答案)的评测?
A:开放性问题使用纯LLM评判:
- 设计详细的评分rubric(评分标准)
- 用几个高质量示例做few-shot
- 关注"有没有明显错误"而不是"是否和参考答案一致"
- 必要时加入人工复核环节
Q4:评测平台如何与提示词工程配合?
A:建立"改提示词→评测→对比"的标准流程:
- 每次修改提示词前,先运行基准评测存档
- 修改后再次评测,对比新旧得分
- 只有新版本得分不低于旧版本才合并修改
- 使用版本控制(Git)管理提示词和对应的评测结果
Q5:评测覆盖率多少才够?
A:参考软件测试覆盖率的思维:
- 安全相关场景:100%覆盖,且每次发布都测
- 核心功能场景:80%+覆盖
- 边缘案例:视业务重要性,30-50%覆盖
- 建议最少保持200道覆盖主要场景的评测题库
九、总结
AI评测平台是AI工程化成熟度的重要标志:
| 成熟度阶段 | 特征 |
|---|---|
| 初级 | 靠人工随机测试,无系统记录 |
| 中级 | 有题库,手动运行评测 |
| 高级 | 自动化评测,集成CI/CD |
| 专家级 | 多维度评测,趋势分析,自动调优 |
王芳的案例说明,AI评测不是可选项,而是AI系统运行在高风险场景中的生命线。建立评测平台的投入,远小于一次AI出错的公关危机成本。
从今天开始,为你的AI应用建立题库——哪怕只有50道题,也比没有强。
