Free lesson · GenAI Platform Engineering

Implement evaluation-gated deployment

You will build a deployment gate that blocks promotion if evaluations fail. Configure the promotion pipeline: merge to main triggers eval on the dev environment, passing eval auto-promotes to staging, staging eval requires manual approval before prod promotion. Store evaluation results in PostgreSQL: prompt_version, eval_dataset_version, accuracy, latency_p95, cost_per_request, timestamp, environment. Build a promotion controller that queries evaluation results and blocks promotion if: accuracy < 95%, latency_p95 > 2s, or cost_per_request > $0.05. Create a dashboard showing evaluation trends across prompt versions and environments.

Course: DevOps Foundations for GenAI Engineers · Chapter 8 · DevOps for AI Artifacts

Free to read — no subscription required.

Introduction

Production AI systems fail silently. A prompt change that passes unit tests can still degrade answer quality by 15% without triggering a single exception. Traditional CI/CD pipelines catch syntax errors and broken imports, but they have zero visibility into semantic regression—a model returning plausible-sounding but factually wrong answers. Evaluation-gated deployment solves this by inserting a quantitative quality checkpoint between every environment promotion. No artifact—prompt template, model config, guardrail rule, or evaluation dataset—advances from dev to staging to production unless it meets or exceeds accuracy thresholds measured against a curated evaluation suite.

This pattern draws directly from the concept of accuracy gates in CI pipelines, where evaluation CI runs a standardized battery of test cases against every candidate artifact, computes scoring metrics, and compares them against baseline thresholds stored in a prompt registry. The gate either approves promotion or blocks it with a detailed regression report. Unlike traditional deployment gates that check infrastructure health (CPU, memory, latency), evaluation gates check semantic health—the correctness, safety, and consistency of AI outputs. When combined with artifact promotion workflows and regression testing against versioned evaluation datasets, you get a deployment pipeline that treats AI quality as a first-class deployability criterion rather than an afterthought discovered through user complaints.

Key Terminology

  • Evaluation Gate — A pipeline checkpoint implemented as the EvaluationGate class that runs a candidate AI artifact against an EvalSuite, computes per-metric scores, and returns a GateResult that either approves or blocks environment promotion based on defined thresholds.
  • MetricThreshold — A dataclass that pairs a metric name with two promotion criteria: an absolute min_value floor that the measured score must meet, and a regression_tolerance (defaulting to 0.02) that caps how far the metric may drop from the recorded baseline before triggering a regression failure.
  • GateResult — The structured verdict returned by EvaluationGate.run_gate, whose passed field is True only when the failures list is empty after both the absolute-floor check and the regression comparison have run.
  • Baseline Snapshot — A stored set of metric values written to the prompt registry after each successful promotion, used by _compare_against_baseline as the reference point against which the next candidate's scores are compared for regression detection.
  • Artifact Promotion — The atomic operation that advances a versioned AI artifact (prompt template, model config, or guardrail) to the next environment by updating the prompt registry pointer, recording model config versioning metadata, and tagging the Git commit with the target environment label.
  • Ratchet Effect — The emergent property of baseline-recording promotion: every successful gate writes new baselines, so quality can only be acknowledged as lower through an explicit, auditable registry update—silent degradation across consecutive deployments is structurally prevented.

Concepts

Why Traditional CI/CD Is Blind to Semantic Regression

Conventional deployment pipelines excel at catching structural failures: a broken import, a container that refuses to start, a latency spike that exceeds an SLA. None of those checks have any visibility into whether a model is returning factually correct answers. A prompt change can pass every existing unit test while quietly dropping answer accuracy by 15%, because no exception is raised and no assertion is violated—the outputs are syntactically valid and plausible-sounding. Evaluation-gated deployment treats semantic quality as a first-class deployment criterion by inserting a scored, quantitative checkpoint between every environment promotion. An artifact is not promotable unless it meets a defined quality bar, measured against a curated evaluation dataset, not inferred from infrastructure telemetry.

Two-Layer Gate Logic: Absolute Floor and Regression Bound

Each gate runs two independent checks on every promotion attempt (see Code Walkthrough). The first is an absolute floor: every metric produced by the EvalSuite must meet or exceed the min_value defined in the corresponding MetricThreshold. This prevents a known-bad artifact from entering any environment regardless of its history. The second is a regression bound enforced by _compare_against_baseline: if a metric has dropped more than regression_tolerance from the last recorded baseline in the prompt registry, the gate appends a REGRESSION failure even when the score still clears the absolute floor.

This two-layer design closes a critical gap that a single threshold check cannot cover: an artifact that is "good enough" in absolute terms but has quietly degraded from what was previously deployed. The regression check catches drift the absolute floor alone would miss.

Environment-Stratified Thresholds

The architecture depicted in the Code Walkthrough runs the same EvalSuite at both promotion boundaries—dev→staging and staging→production—but deliberately allows the MetricThreshold values to differ per environment. Staging→production gates are typically stricter, using a tighter min_value and a narrower regression_tolerance, because the blast radius of a semantic regression scales with user traffic. Dev→staging gates can tolerate more variance while an artifact is still being iterated. This mirrors the principle behind guardrails as code: quality and safety constraints tighten as artifacts approach user-facing production traffic, and the same underlying test corpus drives both checkpoints.

The Ratchet: Quality Can Only Be Deliberately Lowered

After a successful promotion, the pipeline writes the measured metric snapshot as the new baseline for the next deployment cycle. Each successful gate therefore raises the implicit floor: successor artifacts must match or beat what the current production deployment achieved within the regression_tolerance band. There is no background process that silently drifts the baseline downward—lowering it requires a deliberate, auditable write to the prompt registry. This ratchet property is what transforms a one-time quality check into a compounding deployment discipline, where gradual semantic erosion across consecutive releases is structurally prevented rather than discovered through user complaints.

Code Walkthrough

Architecture of an Evaluation-Gated Pipeline

Before examining code, you need a mental model of how artifacts flow through the gated pipeline. The system has three environments (dev, staging, production), two gates (dev→staging, staging→production), and an evaluation service that runs against each environment independently.

Loading diagram...

Each yellow node represents an evaluation gate. The critical design choice here is that the same evaluation suite runs in both gates, but the thresholds can differ—staging→production gates are typically stricter than dev→staging gates. This mirrors the well-established pattern in guardrails as code, where safety policies tighten as artifacts approach production. The promotion step itself is an atomic operation: it updates the prompt registry to point the target environment at the new artifact version, updates model config versioning records, and tags the Git commit with the environment label.

Building the Evaluation Gate

The core abstraction is an evaluation gate that accepts an artifact version, runs it against an evaluation dataset, computes metrics, and returns a pass/fail verdict with detailed scoring. The following implementation defines an EvaluationGate class that orchestrates this process. It depends on an EvalSuite that encapsulates the evaluation dataset and scoring logic, and a MetricThreshold dataclass that defines per-metric pass criteria. The run_gate method iterates through all configured evaluation suites, collects results, and determines whether the artifact meets promotion criteria. The _compare_against_baseline method handles regression testing by loading the previous production baseline from the prompt registry and flagging any metric that dropped below the allowed regression tolerance.

Code snippet python
1from dataclasses import dataclass, field 2from typing import Optional 3import json 4import logging 5 6logger = logging.getLogger(__name__) 7 8@dataclass 9class MetricThreshold: 10 name: str 11 min_value: float 12 regression_tolerance: float = 0.02 # Allow 2% drop from baseline 13 14@dataclass 15class GateResult: 16 passed: bool 17 metrics: dict[str, float] 18 failures: list[str] = field(default_factory=list) 19 baseline_comparison: Optional[dict] = None 20 21class EvaluationGate: 22 def __init__(self, thresholds: list[MetricThreshold], registry_client): 23 self.thresholds = {t.name: t for t in thresholds} 24 self.registry = registry_client 25 26 def run_gate(self, artifact_id: str, artifact_version: str, 27 eval_suite, environment: str) -> GateResult: 28 logger.info(f"Running eval gate for {artifact_id}@{artifact_version}") 29 measured = eval_suite.evaluate(artifact_id, artifact_version) 30 failures = [] 31 for metric_name, value in measured.items(): 32 threshold = self.thresholds.get(metric_name) 33 if threshold is None: 34 continue 35 if value < threshold.min_value: 36 failures.append( 37 f"{metric_name}: {value:.4f} < {threshold.min_value:.4f}" 38 ) 39 baseline_cmp = self._compare_against_baseline( 40 artifact_id, environment, measured 41 ) 42 if baseline_cmp and baseline_cmp.get("regressions"): 43 for reg in baseline_cmp["regressions"]: 44 failures.append(f"REGRESSION {reg}") 45 46 passed = len(failures) == 0 47 return GateResult( 48 passed=passed, metrics=measured, 49 failures=failures, baseline_comparison=baseline_cmp, 50 ) 51 52 def _compare_against_baseline(self, artifact_id, environment, measured): 53 baseline = self.registry.get_baseline_metrics(artifact_id, environment) 54 if baseline is None: 55 return None 56 regressions = [] 57 for metric_name, current_val in measured.items(): 58 baseline_val = baseline.get(metric_name) 59 if baseline_val is None: 60 continue 61 threshold = self.thresholds.get(metric_name) 62 if threshold is None: 63 continue 64 drop = baseline_val - current_val 65 if drop > threshold.regression_tolerance: 66 regressions.append( 67 f"{metric_name}: dropped {drop:.4f} from {baseline_val:.4f}" 68 ) 69 return {"baseline": baseline, "regressions": regressions}
  • Lines 1–4: Import standard library modules. The dataclass decorator eliminates boilerplate for the value objects. The Optional type hint signals that baseline comparison can return None when no previous baseline exists.
  • Lines 8–10: MetricThreshold defines a named metric, its absolute minimum, and a regression_tolerance defaulting to 0.02. This tolerance means a metric can drop by at most 2% from the last known baseline before triggering a regression failure.
  • Lines 12–15: GateResult is the return type from every gate evaluation. The passed field is True only when failures is empty. The baseline_comparison field is None for first-time deployments where no previous baseline exists in the registry.
  • Lines 17–20: EvaluationGate.init accepts a list of thresholds and converts them to a dictionary keyed by metric name for O(1) lookup. The registry_client is an abstraction over the prompt registry that stores baselines and artifact metadata.
  • Lines 22–24: run_gate is the primary entry point. It takes the artifact identifier, version string, an evaluation suite instance, and the target environment name.
  • Lines 25–31: The method calls eval_suite.evaluate() to get measured metrics, then checks each against the absolute minimum threshold. Any metric falling below its min_value is appended to the failures list with a formatted message showing the actual vs. required value.
  • Lines 32–39: After absolute threshold checks, the method runs regression comparison against the stored baseline. If regressions are detected, they are added to failures. The final passed boolean is True only if both absolute and regression checks produced zero failures.
  • Lines 41–56: _compare_against_baseline retrieves the last known production metrics from the registry. If no baseline exists (first deployment), it returns None. Otherwise, it iterates through measured metrics, computes the drop from baseline, and flags any drop exceeding the configured regression_tolerance.

Orchestrating the Promotion Pipeline

With the gate built, you need a pipeline orchestrator that wires together the deploy→evaluate→promote flow across environments. The following PromotionPipeline class manages the ordered sequence of environment promotions. Its promote_through method deploys the artifact to the first environment, runs the evaluation gate, and upon success, advances to the next environment. The _deploy_artifact method interacts with the prompt registry to update the active version for a given environment, and _record_baseline persists the passing metrics as the new baseline for future regression testing comparisons. This design supports both prompt versioning and model config versioning through a unified artifact abstraction.

Code snippet python
1@dataclass 2class PromotionStage: 3 environment: str 4 eval_suite: object # EvalSuite instance 5 gate: EvaluationGate 6 7class PromotionPipeline: 8 def __init__(self, stages: list[PromotionStage], registry_client): 9 self.stages = stages 10 self.registry = registry_client 11 12 def promote_through(self, artifact_id: str, version: str) -> dict: 13 results = {} 14 for stage in self.stages: 15 logger.info(f"Stage: {stage.environment}") 16 self._deploy_artifact(artifact_id, version, stage.environment) 17 gate_result = stage.gate.run_gate( 18 artifact_id, version, stage.eval_suite, stage.environment 19 ) 20 results[stage.environment] = gate_result 21 if not gate_result.passed: 22 logger.error( 23 f"Blocked at {stage.environment}: {gate_result.failures}" 24 ) 25 self._rollback_artifact(artifact_id, stage.environment) 26 return {"status": "blocked", "blocked_at": stage.environment, 27 "results": results} 28 self._record_baseline(artifact_id, stage.environment, 29 gate_result.metrics) 30 return {"status": "promoted", "results": results} 31 32 def _deploy_artifact(self, artifact_id, version, environment): 33 self.registry.set_active_version(artifact_id, environment, version) 34 logger.info(f"Deployed {artifact_id}@{version} to {environment}") 35 36 def _rollback_artifact(self, artifact_id, environment): 37 prev = self.registry.get_previous_version(artifact_id, environment) 38 if prev is not None: 39 self.registry.set_active_version(artifact_id, environment, prev) 40 logger.info(f"Rolled back {artifact_id} in {environment} to {prev}") 41 42 def _record_baseline(self, artifact_id, environment, metrics): 43 self.registry.store_baseline_metrics(artifact_id, environment, metrics) 44 logger.info(f"Recorded baseline for {artifact_id} in {environment}") 45 46# === Wiring the gate into CI/CD === 47# The final piece connects the promotion pipeline to your CI/CD system. 48# run_promotion_from_ci constructs the full pipeline from environment 49# variables and config files typical of a CI context, reads threshold 50# definitions from a YAML config versioned alongside the artifact 51# (accuracy gates as code), and returns a CI-suitable exit code: 52# zero on success, non-zero on gate failure. 53import sys 54import yaml 55 56def run_promotion_from_ci(artifact_id: str, version: str, 57 config_path: str, registry_client) -> int: 58 with open(config_path) as f: 59 config = yaml.safe_load(f) 60 61 stages = [] 62 for env_cfg in config["environments"]: 63 thresholds = [ 64 MetricThreshold( 65 name=m["name"], 66 min_value=m["min_value"], 67 regression_tolerance=m.get("regression_tolerance", 0.02), 68 ) 69 for m in env_cfg["thresholds"] 70 ] 71 gate = EvaluationGate(thresholds, registry_client) 72 suite = load_eval_suite(env_cfg["eval_suite_path"]) 73 stages.append(PromotionStage( 74 environment=env_cfg["name"], eval_suite=suite, gate=gate, 75 )) 76 77 pipeline = PromotionPipeline(stages, registry_client) 78 result = pipeline.promote_through(artifact_id, version) 79 80 if result["status"] == "blocked": 81 blocked_env = result["blocked_at"] 82 gate_result = result["results"][blocked_env] 83 print(f"BLOCKED at {blocked_env}:", file=sys.stderr) 84 for failure in gate_result.failures: 85 print(f" - {failure}", file=sys.stderr) 86 return 1 87 print(f"Promoted {artifact_id}@{version} through all environments") 88 return 0
  • Lines 1–2: Import sys for stderr output and exit codes, and yaml for parsing the threshold configuration file.
  • Lines 4–7: run_promotion_from_ci accepts the artifact identifier, version (typically the Git SHA or a semantic version tag), the path to the gate configuration YAML, and a registry client. It loads the YAML config that defines environments and their thresholds.
  • Lines 9–17: For each environment defined in the config, the function constructs MetricThreshold instances. The dict.get call with a default of 0.02 makes regression tolerance optional in the config file—teams that omit it get the standard 2% tolerance.
  • Lines 18–23: Each environment gets its own EvaluationGate and evaluation suite loaded from a path specified in the config. The load_eval_suite function (not shown) reads the versioned evaluation dataset from disk—this dataset is itself a versioned artifact tracked in Git alongside the prompts and model configs.
  • Lines 25–26: The pipeline is constructed and executed. The promote_through call may take several minutes as it deploys and evaluates across each environment sequentially.
  • Lines 28–34: On failure, the function writes a structured error report to stderr listing every failing metric and returns exit code 1. This non-zero exit code causes the CI job to fail, which blocks the merge or deployment depending on your branch protection configuration.
  • Lines 35–36: On success, it prints a confirmation and returns 0. The CI job succeeds, and downstream workflows (such as A/B testing configs for gradual rollout) can proceed.

Do's and Don'ts

Do's

  1. Do version your threshold configs alongside your artifacts — Store gate configuration YAML in the same repository as your prompts and model configs. This ensures that threshold changes go through the same code review process as artifact changes, and you can trace exactly which thresholds were active for any historical deployment.

  2. Do set different thresholds per environment — Dev gates should be permissive enough to allow experimentation (e.g., 0.80 accuracy), while production gates should enforce your SLA targets (e.g., 0.92 accuracy). This gradient gives engineers fast feedback in dev without allowing low-quality artifacts to reach users.

  3. Do include both absolute and regression checks — Absolute thresholds catch catastrophic failures, but regression checks catch slow degradation. An artifact scoring 0.91 passes a 0.90 absolute gate, but if the baseline was 0.95, that 4-point drop signals a real problem that regression testing will catch.

Don'ts

  1. Don't skip the evaluation gate for "small" changes — A single-word prompt edit can cause a 20% accuracy swing. Every artifact version must pass the gate regardless of the diff size. If gate execution is too slow for rapid iteration, invest in faster evaluation suites rather than gate bypasses.

  2. Don't use the same evaluation dataset for training and gating — If your prompt was optimized against the same examples used in the gate, you are measuring memorization, not generalization. Maintain a held-out evaluation dataset that prompt authors cannot access during development.

  3. Don't hardcode thresholds in pipeline code — Thresholds embedded in Python files require code deployments to adjust. Keep them in external config files (YAML, JSON) so that ML engineers can update quality bars without modifying pipeline infrastructure and waiting for CI to rebuild.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.

From · cancel anytime · Already a subscriber? Sign in →

More free lessons in DevOps Foundations for GenAI Engineers

All free lessons in GenAI Platform Engineering