云计算百科
云计算领域专业知识百科平台

用Spring Boot搭建AI评测与发布质量门禁:数据集、Judge、回归对比与CI/CD完整实战

文章摘要

传统软件可以通过单元测试和接口测试判断输出是否正确,AI应用却经常存在多个可接受答案、模型非确定性、RAG证据变化、Tool Calling轨迹差异和成本延迟波动。仅在CI中执行几条assertThat(answer).isNotBlank(),无法阻止Prompt、模型、知识索引或工具目录升级造成的质量退化。

本文从零实现一个Spring Boot AI评测与发布质量门禁服务。系统管理版本化数据集和Case,支持Baseline与Candidate双跑,记录完整Runtime Manifest;通过确定性Evaluator、Spring AI RelevancyEvaluator、事实检查器和自定义LLM-as-Judge对回答、RAG引用和工具轨迹评分;按业务Slice聚合通过率、最低分、成本和延迟;再根据硬失败与回归阈值生成Quality Gate结果。

最终评测服务以退出码、REST API和GitHub Actions检查三种方式接入CI/CD。受保护分支可将ai-quality-gate设置为必需状态检查,评测失败时禁止合并或发布。

一、项目目标

系统需要完成:

1. 管理版本化评测数据集
2. 支持黄金、合成、生产回放和事故样本
3. 固定Baseline与Candidate运行清单
4. 对每个Case重复执行并保存原始轨迹
5. 支持确定性、算法和LLM Judge
6. 支持RAG、结构化输出和Agent轨迹评测
7. 按Slice聚合质量、成本与延迟
8. 高风险硬失败零容忍
9. 输出机器可读Quality Gate结果
10. 接入Maven、GitHub Actions和发布流水线
11. 保存评测历史并比较趋势
12. 评测失败可定位到具体Case与组件

二、总体架构

┌──────────────────────────────────────────────┐
│ GitHub Actions / Jenkins / Release Pipeline │
└──────────────────────┬───────────────────────┘
│ Start Eval
┌──────────────────────▼───────────────────────┐
│ Evaluation API / CLI │
└──────────────┬─────────────────┬─────────────┘
│ │
┌──────────────▼───────┐ ┌───────▼────────────┐
│ Dataset Registry │ │ Release Manifest │
│ Dataset/Case/Tags │ │ Model/Prompt/Index │
└──────────────┬───────┘ └───────┬────────────┘
│ │
└────────┬────────┘

┌───────────────────────▼──────────────────────┐
│ Evaluation Runner │
│ Baseline / Candidate / Repetitions │
└───────┬───────────┬─────────────┬───────────┘
│ │ │
┌───────▼────┐ ┌────▼──────┐ ┌────▼──────────┐
│ Rules │ │ Spring AI │ │ LLM-as-Judge │
│ Schema/ACL │ │ Evaluator │ │ Pairwise │
└───────┬────┘ └────┬──────┘ └────┬──────────┘
│ │ │
└───────────┬─────────────┘

┌───────────────────▼──────────────────────────┐
│ Metric Aggregation / Slice Comparison │
└───────────────────┬──────────────────────────┘

┌───────────────────▼──────────────────────────┐
│ Quality Gate Engine │
│ PASS / FAIL / NEEDS_REVIEW / ERROR │
└───────────────────┬──────────────────────────┘

Report / Check Run / Artifacts

三、项目结构

ai-quality-gate
├── pom.xml
├── src/main/java/com/example/eval
│ ├── api
│ │ ├── EvaluationRunController.java
│ │ ├── DatasetController.java
│ │ └── EvaluationReportController.java
│ ├── dataset
│ │ ├── EvaluationDataset.java
│ │ ├── EvaluationCase.java
│ │ ├── DatasetManifest.java
│ │ └── DatasetRepository.java
│ ├── release
│ │ ├── RuntimeManifest.java
│ │ ├── ReleaseTarget.java
│ │ └── ReleaseTargetRegistry.java
│ ├── runner
│ │ ├── EvaluationRunner.java
│ │ ├── CaseExecutionService.java
│ │ └── EvaluationRunContext.java
│ ├── evaluator
│ │ ├── EvaluationRule.java
│ │ ├── DeterministicEvaluator.java
│ │ ├── SpringAiEvaluatorAdapter.java
│ │ ├── LlmJudgeEvaluator.java
│ │ └── ToolTraceEvaluator.java
│ ├── metric
│ │ ├── MetricAggregator.java
│ │ ├── SliceMetric.java
│ │ └── StatisticalComparisonService.java
│ ├── gate
│ │ ├── QualityGateEngine.java
│ │ ├── QualityGatePolicy.java
│ │ └── QualityGateDecision.java
│ ├── report
│ │ ├── EvaluationReportService.java
│ │ ├── MarkdownReportRenderer.java
│ │ └── JsonReportRenderer.java
│ └── integration
│ ├── GitHubCheckPublisher.java
│ └── CliEvaluationCommand.java
└── src/test
├── DatasetVersionTest.java
├── EvaluationRunnerTest.java
├── JudgeCalibrationTest.java
└── QualityGateTest.java

四、Maven依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

五、核心数据库表

create table eval_dataset (
dataset_id varchar(128) not null,
version varchar(64) not null,
name varchar(256) not null,
status varchar(32) not null,
content_hash varchar(128) not null,
created_by varchar(128) not null,
created_at timestamptz not null,
primary key (
dataset_id,
version)
);

create table eval_case (
case_id varchar(128) not null,
case_version integer not null,
dataset_id varchar(128) not null,
dataset_version varchar(64) not null,
source varchar(32) not null,
task_type varchar(64) not null,
risk_level varchar(32) not null,
input jsonb not null,
expected jsonb not null,
provenance jsonb not null,
content_hash varchar(128) not null,
status varchar(32) not null,
primary key (
case_id,
case_version)
);

create table eval_case_tag (
case_id varchar(128) not null,
case_version integer not null,
tag varchar(128) not null,
primary key (
case_id,
case_version,
tag)
);

create table eval_run (
run_id varchar(64) primary key,
baseline_release varchar(128),
candidate_release varchar(128) not null,
dataset_manifest jsonb not null,
evaluator_manifest jsonb not null,
status varchar(32) not null,
started_at timestamptz not null,
completed_at timestamptz,
failure_code varchar(64)
);

create table eval_case_execution (
execution_id varchar(64) primary key,
run_id varchar(64) not null,
case_id varchar(128) not null,
case_version integer not null,
target varchar(32) not null,
repetition integer not null,
runtime_manifest jsonb not null,
input_hash varchar(128) not null,
output text,
evidence jsonb,
tool_trace jsonb,
usage jsonb,
latency_ms bigint,
status varchar(32) not null,
error_code varchar(64),
unique (
run_id,
case_id,
case_version,
target,
repetition)
);

create table eval_result (
result_id varchar(64) primary key,
execution_id varchar(64) not null,
evaluator_id varchar(128) not null,
evaluator_version varchar(64) not null,
status varchar(32) not null,
score numeric(10,6),
decision varchar(32),
details jsonb not null
);

create table eval_gate_result (
run_id varchar(64) primary key,
decision varchar(32) not null,
summary jsonb not null,
hard_failures jsonb not null,
generated_at timestamptz not null
);

六、数据集实体

public record EvaluationCase(
String caseId,
int caseVersion,
String taskType,
RiskLevel riskLevel,
EvaluationSource source,
JsonNode input,
ExpectedBehavior expected,
Set<String> tags,
DataProvenance provenance) {
}

public record ExpectedBehavior(
JsonNode exactOutput,
List<String> requiredFacts,
List<String> forbiddenFacts,
Set<String> expectedDocumentIds,
Set<String> forbiddenDocumentIds,
ToolTraceExpectation toolTrace,
RefusalExpectation refusal,
OutputContract outputContract,
QualityRubric rubric,
OperationalBudget budget) {
}

七、Dataset Manifest

public record DatasetManifest(
List<DatasetReference> datasets,
String combinedHash,
Map<String, Long> tagCounts,
Instant frozenAt) {
}

public record DatasetReference(
String datasetId,
String version,
String contentHash) {
}

Runner启动后先重新计算Hash。如果数据库内容与Manifest不一致,直接停止评测。

八、Runtime Manifest

每次执行必须记录实际运行组件:

public record RuntimeManifest(
String releaseId,
String gitCommit,
String modelProfileId,
String requestedModel,
String resolvedModel,
String promptManifestHash,
String knowledgeIndexVersion,
String toolCatalogVersion,
String outputSchemaVersion,
String applicationConfigHash,
String environment) {
}

不能只记录代码Commit,因为Prompt、模型和知识索引可能独立更新。

九、Release Target抽象

public interface ReleaseTarget {

String releaseId();

RuntimeManifest manifest();

TargetExecutionResult execute(
EvaluationCase evaluationCase,
EvaluationExecutionContext context);
}

实现可以是:

  • 当前JVM内Service;
  • 远程HTTP环境;
  • Docker候选版本;
  • Kubernetes Preview;
  • 固定Baseline服务。

十、TargetExecutionResult

public record TargetExecutionResult(
String output,
JsonNode structuredOutput,
List<RetrievedEvidence> evidence,
List<ToolTraceEvent> toolTrace,
UsageSnapshot usage,
Duration latency,
RuntimeManifest runtimeManifest) {
}

质量评测必须保存中间轨迹,不能只有最终文本。

十一、评测运行配置

public record EvaluationRunConfig(
String runId,
String baselineReleaseId,
String candidateReleaseId,
DatasetManifest datasetManifest,
EvaluatorManifest evaluatorManifest,
int repetitions,
int concurrency,
boolean failFastOnHardFailure) {
}

十二、Case执行服务

@Service
public class CaseExecutionService {

private final ReleaseTargetRegistry targetRegistry;
private final EvalCaseExecutionRepository repository;

public TargetExecutionResult execute(
EvaluationRunContext run,
EvaluationCase evaluationCase,
EvaluationTarget target,
int repetition) {

ReleaseTarget releaseTarget =
targetRegistry.require(
target == EvaluationTarget.BASELINE
? run.baselineReleaseId()
: run.candidateReleaseId());

RuntimeManifest expected =
releaseTarget.manifest();

String executionId =
UUID.randomUUID()
.toString();

repository.insertRunning(
executionId,
run.runId(),
evaluationCase,
target,
repetition,
expected);

long started =
System.nanoTime();

try {
TargetExecutionResult result =
releaseTarget.execute(
evaluationCase,
run.executionContext());

assertManifestMatches(
expected,
result.runtimeManifest());

repository.completeSuccess(
executionId,
result,
elapsedMillis(started));

return result;
}
catch (Exception ex) {
repository.completeFailure(
executionId,
classify(ex),
elapsedMillis(started));
throw ex;
}
}
}

十三、重复执行

public List<TargetExecutionResult> executeRepeated(
EvaluationRunContext run,
EvaluationCase evaluationCase,
EvaluationTarget target) {

return IntStream.range(
0,
run.repetitions())
.mapToObj(repetition ->
execute(
run,
evaluationCase,
target,
repetition))
.toList();
}

高风险数据集可配置5次,普通PR快速集可配置1—3次。

十四、Evaluator接口

public interface EvaluationRule {

String evaluatorId();

String version();

boolean supports(
EvaluationCase evaluationCase);

EvaluationResult evaluate(
EvaluationContext context);
}

public record EvaluationContext(
EvaluationCase evaluationCase,
TargetExecutionResult baseline,
TargetExecutionResult candidate,
int repetition,
EvaluatorManifest manifest) {
}

十五、EvaluationResult

public record EvaluationResult(
String evaluatorId,
String evaluatorVersion,
EvaluationStatus status,
Double score,
EvaluationDecision decision,
JsonNode details,
boolean hardFailure) {
}

状态:

public enum EvaluationStatus {
COMPLETED,
EVALUATOR_ERROR,
SKIPPED
}

决策:

public enum EvaluationDecision {
PASS,
FAIL,
BASELINE_WINS,
CANDIDATE_WINS,
TIE,
NEEDS_REVIEW
}

十六、确定性Evaluator

JSON Schema

@Component
public class JsonSchemaEvaluator
implements EvaluationRule {

@Override
public EvaluationResult evaluate(
EvaluationContext context) {

OutputContract contract =
context.evaluationCase()
.expected()
.outputContract();

if (contract == null) {
return skipped();
}

ValidationReport report =
schemaValidator.validate(
contract.schema(),
context.candidate()
.structuredOutput());

return report.valid()
? pass(1.0, report)
: fail(
0.0,
report,
contract.hardGate());
}
}

禁止证据

@Component
public class ForbiddenEvidenceEvaluator
implements EvaluationRule {

public EvaluationResult evaluate(
EvaluationContext context) {

Set<String> forbidden =
context.evaluationCase()
.expected()
.forbiddenDocumentIds();

Set<String> actual =
context.candidate()
.evidence()
.stream()
.map(
RetrievedEvidence
::documentId)
.collect(
Collectors.toSet());

Set<String> violations =
new HashSet<>(actual);

violations.retainAll(
forbidden);

return violations.isEmpty()
? pass()
: hardFail(
"命中禁止文档",
violations);
}
}

Tool调用次数

@Component
public class ToolCallCountEvaluator
implements EvaluationRule {

public EvaluationResult evaluate(
EvaluationContext context) {

Map<String, Long> counts =
context.candidate()
.toolTrace()
.stream()
.filter(
ToolTraceEvent
::isInvocation)
.collect(
Collectors.groupingBy(
ToolTraceEvent::toolName,
Collectors.counting()));

return toolExpectationValidator
.validate(
context.evaluationCase()
.expected()
.toolTrace(),
counts);
}
}

十七、Spring AI Evaluator适配

Spring AI提供相关性和事实检查等Evaluator。

@Component
public class SpringAiRelevancyEvaluatorAdapter
implements EvaluationRule {

private final RelevancyEvaluator evaluator;

public SpringAiRelevancyEvaluatorAdapter(
@Qualifier("judgeChatModel")
ChatModel judgeChatModel) {

this.evaluator =
new RelevancyEvaluator(
ChatClient.builder(
judgeChatModel));
}

@Override
public EvaluationResult evaluate(
EvaluationContext context) {

EvaluationRequest request =
new EvaluationRequest(
context.evaluationCase()
.input()
.toString(),
context.candidate()
.evidence()
.stream()
.map(
RetrievedEvidence
::toDocument)
.toList(),
context.candidate()
.output());

try {
EvaluationResponse response =
evaluator.evaluate(
request);

return response.isPass()
? pass(
response.getScore(),
response)
: fail(
response.getScore(),
response,
false);
}
catch (Exception ex) {
return evaluatorError(
"SPRING_AI_RELEVANCY_ERROR",
ex);
}
}
}

Evaluator异常不能直接算候选失败。

十八、事实检查适配

@Component
public class ClaimFactCheckEvaluator
implements EvaluationRule {

private final FactCheckingEvaluator evaluator;
private final ClaimExtractor claimExtractor;

public List<EvaluationResult> evaluateClaims(
EvaluationContext context) {

List<AnswerClaim> claims =
claimExtractor.extract(
context.candidate()
.output());

String document =
context.candidate()
.evidence()
.stream()
.map(
RetrievedEvidence
::text)
.collect(
Collectors.joining(
"\\n\\n"));

return claims.stream()
.map(claim ->
evaluateOne(
claim,
document))
.toList();
}

private EvaluationResult evaluateOne(
AnswerClaim claim,
String document) {

EvaluationResponse response =
evaluator.evaluate(
new EvaluationRequest(
claim.text(),
List.of(
new Document(
document)),
claim.text()));

return response.isPass()
? pass()
: fail(
0.0,
response,
claim.highRisk());
}
}

对数字、日期和枚举应先运行确定性Evaluator,再运行语义事实检查。

十九、自定义LLM Judge

@Component
public class LlmJudgeEvaluator
implements EvaluationRule {

private final ChatClient judgeClient;
private final JudgePromptRegistry promptRegistry;

public EvaluationResult evaluate(
EvaluationContext context) {

JudgePrompt prompt =
promptRegistry.require(
context.evaluationCase()
.taskType(),
context.manifest()
.rubricVersion());

try {
JudgeResult result =
judgeClient.prompt()
.system(
prompt.systemPrompt())
.user(
prompt.render(
context))
.call()
.entity(
JudgeResult.class);

judgeResultValidator.validate(
result,
context);

return map(result);
}
catch (Exception ex) {
return evaluatorError(
"LLM_JUDGE_ERROR",
ex);
}
}
}

Judge模型、Prompt、Rubric、重复次数和实现Hash都进入Evaluator Manifest。

二十、Pairwise双向比较

public PairwiseEvaluationResult evaluatePairwise(
EvaluationContext context) {

PairwiseJudgeResult normal =
judgePair(
context,
context.baseline()
.output(),
context.candidate()
.output(),
CandidatePosition.SECOND);

PairwiseJudgeResult reversed =
judgePair(
context,
context.candidate()
.output(),
context.baseline()
.output(),
CandidatePosition.FIRST);

return pairwiseCalibrator
.aggregate(
normal,
reversed);
}

若两个顺序结论冲突,返回:

NEEDS_REVIEW

而不是强行决定胜负。

二十一、Evaluator编排

@Service
public class EvaluationService {

private final List<EvaluationRule> rules;
private final EvaluationResultRepository resultRepository;

public List<EvaluationResult> evaluate(
EvaluationContext context) {

List<EvaluationResult> results =
new ArrayList<>();

for (EvaluationRule rule :
rules) {

if (!rule.supports(
context.evaluationCase())) {
continue;
}

EvaluationResult result =
rule.evaluate(context);

resultRepository.save(
context,
result);

results.add(result);

if (result.hardFailure()) {
break;
}
}

return results;
}
}

硬失败可以终止当前Case后续昂贵Judge,但整个Run是否Fail Fast由配置决定。

二十二、Evaluation Runner

@Service
public class EvaluationRunner {

private final DatasetService datasetService;
private final CaseExecutionService executionService;
private final EvaluationService evaluationService;
private final MetricAggregator metricAggregator;
private final QualityGateEngine qualityGateEngine;

public EvaluationRunResult run(
EvaluationRunConfig config) {

EvaluationRunContext context =
runLifecycle.start(config);

try {
List<EvaluationCase> cases =
datasetService.loadApproved(
config.datasetManifest());

ExecutorService executor =
Executors.newFixedThreadPool(
config.concurrency());

List<Future<CaseRunResult>> futures =
cases.stream()
.map(evaluationCase ->
executor.submit(() ->
executeCase(
context,
evaluationCase)))
.toList();

List<CaseRunResult> results =
collect(futures);

AggregatedMetrics metrics =
metricAggregator.aggregate(
context,
results);

QualityGateDecision gate =
qualityGateEngine.evaluate(
context,
metrics,
results);

runLifecycle.complete(
context,
metrics,
gate);

return new EvaluationRunResult(
context.runId(),
metrics,
gate);
}
catch (Exception ex) {
runLifecycle.fail(
context,
ex);
throw ex;
}
}
}

二十三、单Case双跑

private CaseRunResult executeCase(
EvaluationRunContext run,
EvaluationCase evaluationCase) {

List<RepeatedComparisonResult> repetitions =
new ArrayList<>();

for (int repetition = 0;
repetition < run.repetitions();
repetition++) {

TargetExecutionResult baseline =
executionService.execute(
run,
evaluationCase,
EvaluationTarget.BASELINE,
repetition);

TargetExecutionResult candidate =
executionService.execute(
run,
evaluationCase,
EvaluationTarget.CANDIDATE,
repetition);

EvaluationContext context =
new EvaluationContext(
evaluationCase,
baseline,
candidate,
repetition,
run.evaluatorManifest());

List<EvaluationResult> evaluation =
evaluationService.evaluate(
context);

repetitions.add(
new RepeatedComparisonResult(
repetition,
baseline,
candidate,
evaluation));
}

return repeatedResultAggregator
.aggregate(
evaluationCase,
repetitions);
}

如果没有Baseline,可只运行Candidate绝对门禁。

二十四、成本控制

评测可能消耗大量Token。

需要:

  • 最大并发;
  • 每Run预算;
  • 每Case预算;
  • Judge缓存;
  • 失败快速停止;
  • 数据集分层;
  • Batch或离线运行。

public record EvaluationBudget(
BigDecimal maximumRunCost,
BigDecimal maximumCaseCost,
int maximumJudgeCalls,
Duration maximumDuration) {
}

预算超出:

RUN_ABORTED_BUDGET

不能生成一个不完整但显示PASS的报告。

二十五、Judge缓存

相同:

Judge Manifest
+Case
+输出
+Evidence

可以缓存Judge结果。

public record JudgeCacheKey(
String evaluatorManifestHash,
String caseHash,
String outputHash,
String evidenceHash) {
}

Baseline与Candidate输出相同,避免重复收费。

二十六、Slice Metric

public record SliceMetric(
String slice,
int caseCount,
double passRate,
double meanScore,
double minimumScore,
double candidateWinRate,
double baselineWinRate,
double evaluatorErrorRate,
double p95LatencyMs,
BigDecimal meanCost) {
}

Slice来源于Case标签:

contract-review
high-risk
rag
citation
security
tool-side-effect
long-context

二十七、指标聚合

@Service
public class MetricAggregator {

public AggregatedMetrics aggregate(
EvaluationRunContext run,
List<CaseRunResult> results) {

Map<String, List<CaseRunResult>> slices =
sliceResolver.group(results);

Map<String, SliceMetric> metrics =
slices.entrySet()
.stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
entry ->
calculate(
entry.getKey(),
entry.getValue())));

return new AggregatedMetrics(
metrics.get("global"),
metrics,
hardFailureCollector
.collect(results));
}
}

二十八、统计比较

Candidate与Baseline是同一Case的配对数据。

可以计算:

  • 通过率Delta;
  • 平均分Delta;
  • Pairwise Win Rate;
  • Bootstrap区间;
  • McNemar;
  • 重复一致率。

public record StatisticalComparison(
double delta,
double lowerConfidenceBound,
double upperConfidenceBound,
boolean meaningful) {
}

质量门禁不应因为极小随机波动频繁抖动。

二十九、Quality Gate Policy

quality-gate:
global:
minimum-pass-rate: 0.92
maximum-pass-rate-regression: 0.02
minimum-mean-score: 4.20
maximum-evaluator-error-rate: 0.01

slices:
contract-review:
minimum-pass-rate: 0.95
maximum-regression: 0.00

rag-citation:
minimum-claim-support-rate: 0.97
maximum-forbidden-document-rate: 0.00

security:
allowed-failures: 0

tool-side-effect:
duplicate-tool-call-rate: 0.00

operations:
maximum-p95-latency-regression: 0.15
maximum-cost-regression: 0.20

三十、Gate规则模型

public record GateRule(
String ruleId,
String slice,
MetricName metric,
ComparisonOperator operator,
BigDecimal threshold,
GateSeverity severity,
boolean compareWithBaseline) {
}

public record GateViolation(
String ruleId,
String slice,
BigDecimal actual,
BigDecimal threshold,
GateSeverity severity,
List<String> affectedCaseIds) {
}

三十一、Quality Gate Engine

@Service
public class QualityGateEngine {

public QualityGateDecision evaluate(
EvaluationRunContext context,
AggregatedMetrics metrics,
List<CaseRunResult> cases) {

List<GateViolation> violations =
policy.rules()
.stream()
.map(rule ->
evaluateRule(
rule,
metrics,
cases))
.flatMap(
Optional::stream)
.toList();

boolean blocking =
violations.stream()
.anyMatch(violation ->
violation.severity()
== GateSeverity.BLOCKING);

boolean needsReview =
cases.stream()
.anyMatch(
CaseRunResult
::needsHumanReview);

if (blocking) {
return QualityGateDecision.fail(
violations);
}

if (needsReview) {
return QualityGateDecision
.needsReview(
violations);
}

return QualityGateDecision.pass();
}
}

三十二、REST API

启动:

POST /api/v1/evaluation-runs

{
"baselineReleaseId": "release-v41",
"candidateReleaseId": "release-v42",
"datasets": [
{
"datasetId": "enterprise-rag",
"version": "2026.08.06-v3"
}
],
"repetitions": 3,
"concurrency": 8,
"gatePolicy": "production-release-v5"
}

响应:

{
"runId": "RUN-1001",
"status": "QUEUED"
}

查询:

GET /api/v1/evaluation-runs/RUN-1001

三十三、CLI模式

@Component
@Profile("eval-cli")
public class CliEvaluationCommand
implements CommandLineRunner {

public void run(
String... args) {

EvaluationRunResult result =
runner.run(
configLoader
.fromArgs(args));

reportWriter.write(
result,
Path.of(
"build/eval-report"));

int exitCode =
switch (result.gate()
.decision()) {
case PASS -> 0;
case NEEDS_REVIEW -> 2;
case FAIL, ERROR -> 1;
};

SpringApplication.exit(
applicationContext,
() -> exitCode);
}
}

CI使用退出码阻断。

三十四、GitHub Actions

name: AI Quality Gate

on:
pull_request:
workflow_dispatch:

jobs:
ai-quality-gate:
runs-on: ubuntulatest
timeout-minutes: 45

permissions:
contents: read
checks: write

steps:
uses: actions/checkout@v4

uses: actions/setupjava@v4
with:
distribution: temurin
java-version: "21"
cache: maven

name: Build
run: ./mvnw B DskipTests package

name: Run AI evaluation
env:
EVAL_API_KEY: ${{ secrets.EVAL_API_KEY }}
BASELINE_RELEASE: production
CANDIDATE_RELEASE: ${{ github.sha }}
run: |
./mvnw -B spring-boot:run \\
-Dspring-boot.run.profiles=eval-cli \\
-Dspring-boot.run.arguments="\\
–baseline=${BASELINE_RELEASE} \\
–candidate=${CANDIDATE_RELEASE} \\
–dataset=pr-fast@2026.08.06-v1 \\
–report-dir=build/eval-report"

name: Upload evaluation report
if: always()
uses: actions/uploadartifact@v4
with:
name: aievaluationreport
path: build/evalreport

将ai-quality-gate配置为受保护分支的Required Status Check后,失败的检查会阻止合并。

三十五、报告内容

Markdown报告:

# AI Quality Gate

Decision: FAIL

Global:
– Pass rate: 93.1% → 91.7%
– Mean score: 4.31 → 4.18
– P95 latency: +8%
– Mean cost: +12%

Blocking:
– security: 1 failure
– contract-review pass rate: 94% < 95%

Top regressions:
– CASE-CONTRACT-108
– CASE-RAG-CITATION-022

JSON报告用于机器读取。

三十六、GitHub Check注释

可以将失败Case映射到Prompt、配置或测试文件,发布Check Run Annotation。

但不要在公开PR中输出:

  • 生产用户输入;
  • 租户文档;
  • 敏感Evidence;
  • 密钥;
  • 完整合同。

报告使用脱敏Case ID和授权详情页。

三十七、运行分层

PR Fast

  • 50—200条;
  • 低成本;
  • 关键硬失败;
  • 1—3次重复。

Nightly Full

  • 完整回归;
  • 多Judge;
  • 生产Trace回放;
  • 统计分析。

Release Acceptance

  • 隐藏集;
  • 高风险;
  • 人工抽样;
  • 影子环境。

不同Run使用不同Gate Policy。

三十八、失败诊断

报告按组件定位:

Generation Regression
Retrieval Regression
Citation Regression
Tool Regression
Permission Regression
Judge Infrastructure
Operational Regression

Case保存的Evidence和Tool Trace可以找到第一次分叉。

三十九、线上失败回流

发布后:

用户点踩
安全告警
人工修正
工具失败
低Judge分

进入Eval Candidate Pool

脱敏与审核

生成新Case版本

加入Regression/Incident

四十、监控指标

ai_eval_run_total{
status,
policy
}

ai_eval_case_total{
status,
task_type
}

ai_eval_pass_rate{
slice
}

ai_eval_score{
evaluator,
slice
}

ai_eval_evaluator_error_rate{
evaluator
}

ai_eval_candidate_win_rate{
slice
}

ai_eval_latency_seconds{
target
}

ai_eval_cost_total{
target,
evaluator
}

ai_eval_gate_violation_total{
rule,
severity
}

ai_eval_dataset_stale_case_total

四十一、集成测试

数据集Hash变化

Manifest与实际内容不一致时,Run必须失败。

Runtime Manifest不一致

候选声明索引V42,实际回显V41时停止。

Judge异常

Judge 5xx不能直接算候选失败。

硬失败

跨租户Case失败立即生成Blocking Violation。

Slice回归

全局提升但合同Slice下降时仍FAIL。

GitHub退出码

PASS=0,FAIL=1,NEEDS_REVIEW按团队策略设2或阻断。

四十二、上线检查清单

□ 数据集和Case均版本化并有Hash
□ 只运行APPROVED Case
□ Baseline与Candidate保存Runtime Manifest
□ 每个Case保存Evidence、Tool Trace、Usage和延迟
□ 高风险Case重复执行
□ 确定性Evaluator优先
□ Spring AI Evaluator异常单独统计
□ Pairwise Judge执行位置互换
□ Judge使用固定Manifest
□ 指标按业务Slice聚合
□ 安全和副作用零容忍
□ Gate同时检查质量、成本和延迟
□ CI输出机器可读退出码
□ 报告不泄露生产敏感数据
□ PR、Nightly和Release使用不同数据集
□ Required Status Check已启用
□ 生产失败持续回流数据集

总结

AI质量门禁不是在CI中再调用一次模型,而是建立可复现的版本比较系统:

固定数据集
+Runtime Manifest
+Baseline/Candidate双跑
+确定性规则
+Spring AI Evaluator
+校准Judge
+Slice指标
+硬失败
+质量门禁

当候选版本只有在关键业务、安全、引用、工具、成本和延迟全部满足标准后才能合并或发布,AI应用才真正拥有与传统软件测试相当的工程控制力。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 用Spring Boot搭建AI评测与发布质量门禁:数据集、Judge、回归对比与CI/CD完整实战
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!