Knowledge BaseContinuous AI EvaluationMonitoring

Continuous AI Evaluation: Testing Doesn't Stop at Deploy

JK
James Kim · April 2026 · 8 min read

TL;DR

Pre-deploy testing is a gate, not a guarantee. Modern AI systems need continuous, always-on evaluation in production. Use shadow testing, canary evaluations, real-time quality monitoring, regression detection, and production feedback loops. The shift from "test then deploy" to "deploy then monitor" is fundamental.

Your eval team ran 10,000 test cases. All green. You deployed on Tuesday.

By Friday, engagement tanked 8%.

The evaluation was thorough. But it was static, a snapshot of one moment. Real users hit edge cases you didn't think to test. Data distribution shifted slightly. The model behaves differently when talking to thousands of users than to your test harness.

Pre-deploy testing fails not because it's wrong, but because it's incomplete. You need evaluation that continues after deploy.

The Limitations of Pre-Deploy Testing

Pre-deploy evaluation assumes the world is stable. It's not.

Test data is not prod data. Your 10,000 test cases represent maybe 0.001% of possible user inputs.

Real issues emerge in production:

  • Edge cases you didn't think of: Actual users are more creative at finding weird inputs than your test harness
  • Data distribution shift: User queries change seasonally, by region, by trend. Your model might be trained on 2023 data but it's now 2026
  • Cascading failures: Individual components test fine in isolation. They interact badly in production
  • Slow degradation: Quality doesn't usually crash overnight. It drifts gradually, and you miss it without continuous monitoring
  • Emergent behavior: LLMs sometimes exhibit surprising behavior in specific user contexts that never appeared in evals

The solution: build evaluation infrastructure that's always on, always learning.

Shadow Testing: Safe Validation Before Go-Live

Shadow mode means running a new algorithm or model without showing results to users. You collect metrics silently, compare to production, validate before real traffic.

How to Implement Shadow Testing

Before deploying a new recommendation algorithm, model version, or prompt engineering approach, run it in parallel with production:

async function shadowTestNewAlgorithm(
 userId: string,
 request: Request
): Promise<{
 productionResponse: Response;
 shadowResponse: Response;
 metrics: ShadowMetrics;
}> {
 // Run both algorithms
 const productionTask = runProductionAlgorithm(userId, request);
 const shadowTask = runShadowAlgorithm(userId, request);

 const [productionResponse, shadowResponse] = await Promise.all([
 productionTask,
 shadowTask,
 ]);

 // Serve production to user
 await sendToUser(productionResponse);

 // Collect metrics on shadow in background
 const metrics = await evaluateShadowResponse(
 shadowResponse,
 productionResponse,
 userId
 );

 // Log for later analysis
 await logShadowMetrics({
 userId,
 timestamp: Date.now(),
 productionQuality: metrics.productionScore,
 shadowQuality: metrics.shadowScore,
 deltaQuality: metrics.shadowScore - metrics.productionScore,
 });

 return {
 productionResponse,
 shadowResponse,
 metrics,
 };
}

async function analyzeShadowMetrics(
 window: { startDate: Date; endDate: Date }
): Promise<{
 shadowWins: number;
 shadowLoses: number;
 avgDelta: number;
 statisticallySignificant: boolean;
}> {
 const metricsInWindow = await fetchMetrics(window);

 const wins = metricsInWindow.filter(m => m.deltaQuality > 0).length;
 const loses = metricsInWindow.filter(m => m.deltaQuality < 0).length;
 const avgDelta =
 metricsInWindow.reduce((sum, m) => sum + m.deltaQuality, 0) /
 metricsInWindow.length;

 // Use t-test to check if difference is significant
 const significant = await runTTest(metricsInWindow, 0.05);

 return {
 shadowWins: wins,
 shadowLoses: loses,
 avgDelta,
 statisticallySignificant: significant,
 };
}

Run shadow tests for 1-2 weeks before making a call. The key metric: does the shadow version outperform production? By how much? Is the difference statistically significant or just noise?

When to Graduate From Shadow to Production

Set clear promotion criteria before you start:

  • Shadow must outperform production by at least 3% on your primary metric
  • No regressions on any secondary metric (e.g., no increase in hallucinations)
  • Improvement is statistically significant (p < 0.05)
  • Passes safety checks (no toxic outputs, hallucinations within tolerance)

If shadow doesn't meet criteria, iterate. Don't force it out to save face.

Canary Evaluations: Gradual Rollout with Validation

Once shadow validation passes, don't flip a switch. Roll out gradually to subsets of users, validating at each step.

enum CanaryStage {
 SHADOW = "shadow",
 CANARY_1PCT = "canary_1pct",
 CANARY_10PCT = "canary_10pct",
 CANARY_50PCT = "canary_50pct",
 FULL = "full",
}

interface CanaryConfig {
 stage: CanaryStage;
 trafficPercentage: number;
 holdDurationHours: number;
 rollbackThreshold: number; // e.g., 0.05 (5% quality drop)
 metricsToMonitor: string[];
}

const CANARY_PROGRESSION: Record = {
 [CanaryStage.SHADOW]: {
 stage: CanaryStage.SHADOW,
 trafficPercentage: 0,
 holdDurationHours: 336, // 2 weeks
 rollbackThreshold: 0.1,
 metricsToMonitor: ["quality", "latency", "errors"],
 },
 [CanaryStage.CANARY_1PCT]: {
 stage: CanaryStage.CANARY_1PCT,
 trafficPercentage: 1,
 holdDurationHours: 24,
 rollbackThreshold: 0.05,
 metricsToMonitor: ["quality", "latency", "errors", "user_feedback"],
 },
 [CanaryStage.CANARY_10PCT]: {
 stage: CanaryStage.CANARY_10PCT,
 trafficPercentage: 10,
 holdDurationHours: 48,
 rollbackThreshold: 0.03,
 metricsToMonitor: ["quality", "latency", "errors", "user_feedback"],
 },
 [CanaryStage.CANARY_50PCT]: {
 stage: CanaryStage.CANARY_50PCT,
 trafficPercentage: 50,
 holdDurationHours: 72,
 rollbackThreshold: 0.02,
 metricsToMonitor: ["quality", "latency", "errors", "user_feedback", "churn"],
 },
 [CanaryStage.FULL]: {
 stage: CanaryStage.FULL,
 trafficPercentage: 100,
 holdDurationHours: Infinity,
 rollbackThreshold: 0.015,
 metricsToMonitor: ["quality", "latency", "errors", "user_feedback", "churn"],
 },
};

async function manageCanaryRollout(
 changeId: string,
 currentStage: CanaryStage
): Promise<"promote" | "hold" | "rollback"> {
 const config = CANARY_PROGRESSION[currentStage];
 const metrics = await getMetricsSince(
 changeId,
 Date.now() - config.holdDurationHours * 3600 * 1000
 );

 const baseline = await getBaselineMetrics(changeId);

 // Check if metrics have degraded beyond threshold
 const qualityRegression =
 (baseline.quality - metrics.quality) / baseline.quality;

 if (Math.abs(qualityRegression) > config.rollbackThreshold) {
 console.error(
 `Quality dropped ${(qualityRegression * 100).toFixed(1)}%. Rolling back.`
 );
 return "rollback";
 }

 // Check if we should hold
 if (metrics.hasHighErrorRate) {
 console.warn("Error rate elevated. Holding before next stage.");
 return "hold";
 }

 // Good to promote
 console.log(`${currentStage} passed validation. Promoting.`);
 return "promote";
}

async function rolloutCanary(
 changeId: string,
 stages: CanaryStage[] = [
 CanaryStage.CANARY_1PCT,
 CanaryStage.CANARY_10PCT,
 CanaryStage.CANARY_50PCT,
 CanaryStage.FULL,
 ]
): Promise {
 let currentStage = CanaryStage.SHADOW;

 for (const nextStage of stages) {
 console.log(
 `Promoting ${changeId} from ${currentStage} to ${nextStage}`
 );
 await setCanaryTraffic(changeId, nextStage);

 const decision = await manageCanaryRollout(changeId, nextStage);

 if (decision === "rollback") {
 console.error(`Rolling back ${changeId}`);
 await revertCanary(changeId);
 return;
 }

 if (decision === "hold") {
 console.warn(`Holding ${changeId} at ${nextStage}`);
 // Don't promote yet; check again later
 return;
 }

 currentStage = nextStage;
 console.log(`${changeId} now at ${(CANARY_PROGRESSION[nextStage].trafficPercentage)}% traffic`);
 }

 console.log(`${changeId} is now fully deployed`);
}

Real-Time Quality Monitoring

With production traffic, you need immediate visibility into quality degradation. Set up continuous monitoring that samples and evaluates outputs in real-time:

interface QualityAlert {
 severity: "warning" | "critical";
 metric: string;
 currentValue: number;
 baseline: number;
 percentChange: number;
 timestamp: Date;
}

class ProductionQualityMonitor {
 private sampleRate = 0.01; // Evaluate 1% of requests
 private alertThreshold = 0.05; // 5% drop triggers alert
 private baselineWindow = 24 * 3600 * 1000; // 24 hours

 async evaluateSample(
 request: Request,
 response: Response
 ): Promise {
 // Randomly sample
 if (Math.random() > this.sampleRate) {
 return null;
 }

 // Evaluate this response
 const score = await evaluateQuality(response);

 // Get baseline
 const baseline = await this.getBaseline();

 // Check for degradation
 const percentChange = (baseline - score) / baseline;

 if (percentChange > this.alertThreshold) {
 const alert: QualityAlert = {
 severity: percentChange > 0.1 ? "critical" : "warning",
 metric: "quality_score",
 currentValue: score,
 baseline: baseline,
 percentChange,
 timestamp: new Date(),
 };

 // Log immediately
 await logAlert(alert);

 return alert;
 }

 return null;
 }

 private async getBaseline(): Promise {
 const startTime = Date.now() - this.baselineWindow;
 const historicalScores = await fetchQualityScores(startTime, Date.now());
 return (
 historicalScores.reduce((a, b) => a + b, 0) / historicalScores.length
 );
 }
}

Production Regression Detection

Beyond point-in-time monitoring, detect slow degradation trends. Quality might drift 1% per week without ever hitting an alert threshold:

async function detectRegressionTrend(
 metricKey: string,
 window: number = 7 * 24 * 3600 * 1000 // 7 days
): Promise<{
 trend: "stable" | "improving" | "degrading";
 regressionProbability: number;
}> {
 const dataPoints = await fetchMetricTimeseries(metricKey, window);

 // Simple linear regression
 let sumX = 0,
 sumY = 0,
 sumXY = 0,
 sumX2 = 0;
 const n = dataPoints.length;

 for (let i = 0; i < n; i++) {
 sumX += i;
 sumY += dataPoints[i].value;
 sumXY += i * dataPoints[i].value;
 sumX2 += i * i;
 }

 const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
 const avgValue = sumY / n;
 const percentChangePerDay = (slope / avgValue) * 100;

 let trend: "stable" | "improving" | "degrading" = "stable";
 if (percentChangePerDay < -0.5) trend = "degrading";
 if (percentChangePerDay > 0.5) trend = "improving";

 // Calculate probability of continued regression
 const regressionProbability = trend === "degrading" ? 0.7 : 0.1;

 return {
 trend,
 regressionProbability,
 };
}

Feedback Loops: Let Production Data Improve Evaluation

Don't just monitor in one direction. Use production feedback to retrain your evaluation models and catch real issues faster:

async function collectProductionFeedback(): Promise {
 // Collect user signals: thumbs up/down, explicit ratings, session metrics
 const recentFeedback = await fetchUserFeedback({
 since: Date.now() - 24 * 3600 * 1000,
 });

 // For responses with thumbs down, log for analysis
 const negativeExamples = recentFeedback.filter(f => !f.helpful);

 // Periodically retrain evaluation models using real feedback
 if (negativeExamples.length > 100) {
 console.log("Retraining evaluation model with recent negative examples...");

 const trainingData = negativeExamples.map(ex => ({
 input: ex.prompt,
 output: ex.response,
 label: 0, // Bad
 }));

 // Also sample positive examples
 const positiveExamples = recentFeedback.filter(f => f.helpful);
 const positiveData = positiveExamples.map(ex => ({
 input: ex.prompt,
 output: ex.response,
 label: 1, // Good
 }));

 await retrainEvaluationModel([...trainingData, ...positiveData]);
 }
}

async function retrainEvaluationModel(
 trainingData: Array<{ input: string; output: string; label: number }>
): Promise {
 const modelPath = "/models/production_evaluator_v2";

 // Fine-tune a model on production data
 const model = await loadBaseModel();
 const finetuned = await model.finetune(trainingData);

 // Validate new model against held-out test set
 const testAccuracy = await validateModel(finetuned);

 if (testAccuracy > 0.92) {
 // Good enough, promote
 await promoteEvaluationModel(modelPath, finetuned);
 console.log(`New evaluator promoted. Accuracy: ${(testAccuracy * 100).toFixed(1)}%`);
 } else {
 console.warn(`New evaluator accuracy too low: ${testAccuracy}`);
 }
}

Building the Continuous Evaluation Dashboard

All of this data is worthless if nobody sees it. Build a real-time dashboard that teams check daily:

Key metrics to surface:

  • Quality scores over time (last 24h, 7d, 30d)
  • Error rates and latency trends
  • Regression detection status
  • Canary deployment progress
  • User feedback aggregates
  • Drift alerts and flagged outputs
  • A/B test results (ongoing and recently completed)

Make it obvious when something needs attention. Use red/yellow/green status indicators. Have a one-click rollback button for critical issues.

The Shift From Gates to Guardrails

Traditional testing is a gate: you test, you either deploy or don't.

Continuous evaluation is a guardrail: you deploy with monitoring, and the system can automatically rollback if things break.

Pre-deploy testing asks "Is this safe to ship?" Post-deploy monitoring asks "Is this still working?" Both matter.

This shift requires cultural change. Engineers need to be comfortable deploying with monitoring. You need automated rollback systems. You need on-call rotations that respond to eval alerts fast.

But the payoff is huge: you can ship faster, catch real issues faster, and iterate based on actual user behavior instead of guesses.

Always-On Quality Monitoring

Building continuous evaluation infrastructure is complex. alt.qa provides production monitoring, shadow testing infrastructure, and regression detection out of the box.

Start monitoring your AI systems
James Kim is a production infrastructure engineer at alt.qa, specializing in monitoring and observability for AI systems. He's built continuous evaluation systems for 15+ AI products and learned the hard way which metrics actually catch problems. When not debugging regressions, he's rock climbing.