文章摘要
传统软件可以通过单元测试和接口测试判断输出是否正确,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: ubuntu–latest
timeout-minutes: 45
permissions:
contents: read
checks: write
steps:
– uses: actions/checkout@v4
– uses: actions/setup–java@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/upload–artifact@v4
with:
name: ai–evaluation–report
path: build/eval–report
将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应用才真正拥有与传统软件测试相当的工程控制力。
网硕互联帮助中心

评论前必须登录!
注册