Writing/·7 min read

Proving a model swap is safe before it reaches users

Learn how to build a golden set, run shadow comparisons, and use robust statistics to validate new AI models in production before they impact your users.

Share
model-swap-cover

The new model promised a [NUMBER]% cost cut and a [NUMBER]% latency drop. On the vendor's benchmark, it even looked slightly more accurate. But after seeing one too many 'model upgrades' break production features, I learned a hard truth: their benchmarks are not your benchmarks. If you run a production LLM feature, a vendor's numbers tell you almost nothing about how a new model will perform on your specific users, prompts, or data.

I've shipped Python and AI systems for five years, building everything from voice AI recruitment screeners to AI agencies. The only way to truly prove a model swap is safe for your feature is to build your own robust evaluation process, right in your own stack. Anything less is just hoping for the best.

70%of recruiter time goes to screening
3 weeksaverage time to hire

Build a golden set from your traffic

The first step is to build a golden set, which is a collection of inputs and their expected outputs. Do not, under any circumstances, just hand-write a few examples or reuse the ones already in your prompt. That measures memorization, not real-world performance. You need to sample logged production traffic.

When you sample, stratify it: grab a good chunk of your head cases, the high-volume queries that make up most of your usage. Then, deliberately pull in the tail cases: the ones that generate support tickets, known past failures, and any adversarial or malformed inputs you've encountered. This mix ensures you test the model where it counts the most, and where it's most likely to fail.

A working range for a solid golden set is 200 to 500 labelled items. Once you have it, freeze it. Version control it right alongside your prompt. Treat any change to this golden set like a breaking change to your API; it should be reviewed and justified carefully. This baseline is how you measure progress.

Sample size that tells the truth

When comparing models, a common mistake is to just look at overall accuracy. If your incumbent model scores 78.0% and the candidate hits 81.4% on 500 items, that looks like a clear win. But what really matters is where they disagree. If they agree on 430 of those 500 items, your decision actually rests on just 70 items.

For binary outcomes, like 'correct' or 'incorrect', you should use McNemar's test. It focuses specifically on the discordant pairs: where one model was right and the other was wrong. This gives you a more honest picture of whether any observed difference is statistically significant, or just noise.

Here's an illustrative example with McNemar's test from statsmodels:

import numpy as np
from statsmodels.stats.contingency_tables import mcnemar
 
# Illustrative data: counts of discordant pairs
# Row 0, Col 1: incumbent correct, candidate incorrect
# Row 1, Col 0: incumbent incorrect, candidate correct
 
# Scenario 1: 45 discordant pairs, split 31/14
# This often indicates a real difference.
contingency_table_1 = np.array([[0, 31], [14, 0]])
result_1 = mcnemar(contingency_table_1, exact=True)
print(f"Scenario 1 p-value: {result_1.pvalue:.3f}") # Will be near 0.017
 
# Scenario 2: 39 discordant pairs, split 22/17
# This split might just be noise.
contingency_table_2 = np.array([[0, 22], [17, 0]])
result_2 = mcnemar(contingency_table_2, exact=True)
print(f"Scenario 2 p-value: {result_2.pvalue:.3f}") # Will be near 0.52

For non-binary scores, you can use bootstrapping to compare the difference of means. SciPy's scipy.stats.bootstrap can help here, giving you confidence intervals around the observed differences.

Shadow mode, the honest test

Running a model in shadow mode means mirroring a percentage of your production traffic to the candidate model without actually returning its output to users. This is the closest you can get to real-world testing without any risk.

You have two main ways to do this:

  1. Infrastructure layer: Tools like Istio offer traffic mirroring. You can configure a mirrorPercentage to send a fraction of requests to a shadow service. This is great for broad load testing.
  2. Application layer: This is usually better for LLMs. Make an async, fire-and-forget call to the candidate model behind a feature flag. This approach means you get paired outputs for each request, which is crucial for detailed comparison. Log both the incumbent's and candidate's outputs, keyed by the request ID.

Budget this carefully. Shadowing all traffic doubles your inference spend. Start with a small percentage and run it long enough to gather a few hundred paired samples. Specify exactly what percentage you're running and why, perhaps targeting a specific number of unique user interactions.

Calibration, the skipped step

If your feature emits a confidence score or routes requests based on a threshold, you cannot skip calibration. A new model might show higher accuracy while completely destroying the meaning of its confidence scores. Downstream thresholds will then silently misfire.

Plot a reliability diagram for both models on your golden set. Compute the Brier score using sklearn.metrics.brier_score_loss and the expected calibration error. If the candidate has better accuracy but a worse Brier score, it means it's less reliable in its probability predictions. You might need to re-fit your threshold, or simply reject the model if the calibration is too far off.

Scikit-learn's documentation on probability calibration is an excellent resource here.

The decision table

Before you start, define your accept thresholds. What is 'good enough' for each metric? Then, fill out a decision table. This forces a structured, objective comparison.

MetricIncumbentCandidateAccept ThresholdResult
Task Accuracy[NUMBER]%[NUMBER]%>= [NUMBER]%[Pass/Fail]
Brier Score[NUMBER][NUMBER]<= [NUMBER][Pass/Fail]
p50 Latency[NUMBER]ms[NUMBER]ms<= [NUMBER]ms[Pass/Fail]
p99 Latency[NUMBER]ms[NUMBER]ms<= [NUMBER]ms[Pass/Fail]
Cost per 1k requests$[NUMBER]$[NUMBER]<= $[NUMBER][Pass/Fail]
Output Format Violation Rate[NUMBER]%[NUMBER]%<= [NUMBER]%[Pass/Fail]
Over-Refusal Rate[NUMBER]%[NUMBER]%<= [NUMBER]%[Pass/Fail]
Tool-Call Correctness[NUMBER]%[NUMBER]%>= [NUMBER]%[Pass/Fail]

Output format violation rate earns its own row because it is one of the commonest reasons a model swap that passed accuracy still breaks production. If the model starts returning JSON with a missing bracket or an extra comma, your downstream parsers will fail, regardless of how 'accurate' the content is.

Rollout and rollback

If your decision table gives you the green light, move to a controlled rollout. This is where you actually expose the new model to users, but cautiously.

  1. Feature FlagWrap the new model in a feature flag. This is your immediate kill switch if anything goes wrong.
  2. Canary DeploymentUse a canary strategy. Start by routing a tiny percentage of live traffic, say 1%, to the new model. Tools like Argo Rollouts are built for this.
  3. Define Rollback MetricsBefore you even start, define your automatic rollback metrics and their thresholds. What constitutes a failure that triggers an instant revert to the incumbent? Latency spikes, increased error rates, or a drop in key business metrics are good candidates.
  4. Keep Both Models CallableDuring the entire bake period, keep both the new candidate and the incumbent model callable. This makes instant rollback possible.
  5. Pin Incumbent's PromptKeep the incumbent's prompt pinned. The trap here is re-tuning the prompt for the candidate model during the comparison phase. If you re-tune, you invalidate your paired comparison; you must re-run the entire evaluation from the start.

Shipping AI features is a continuous process of iteration. But blindly trusting vendor benchmarks or superficial comparisons will only lead to late-night PagerDuty calls. By building your own golden sets, running shadow comparisons, and focusing on concrete metrics like calibration and format adherence, you can swap models with confidence. It's not about avoiding problems; it's about catching them before your users ever do.

Share
Get the next one by email

Essays on shipping software and building products. No schedule, no filler.