Deploying responsibly
Responsible deployment is four concrete controls — abstain when unsure, limit the blast radius, watch what happens, and be able to switch it off.
- 14 min read
- 3 reading levels
- Updated
Read these first
On this page 8
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
The short answer
Responsible deployment means the system stays safe on the day the model is wrong.
The analogy you have already lived
Think of learning to swim. You did not start in the deep end. You started where you could stand, with somebody watching, near the edge.
Nobody arranged that because they doubted you. They arranged it because the cost of being wrong in deep water is different from the cost of being wrong in shallow water.
A model launch works the same way. Start shallow. Keep somebody watching. Stay near the edge until you have evidence.
The four controls, in order of value
Everything in this lesson reduces to four things a careful team builds. Each one is a few lines of code.
Let it say "I don't know". A model gives a confidence with every answer. When that confidence is low, do not answer — pass the case to a person. The experiment below shows this reaching perfect accuracy on the cases it does answer, by handing over three cases in ten.
Limit what it can touch. Ship to 1% of traffic before 100%. Give it read access before write access. Ask a human to approve anything irreversible.
Watch what actually happens. Not accuracy on your old test set. Live inputs, live outputs, complaint rates, and how many cases get escalated. Failures announce themselves in these numbers first.
Be able to switch it off. One flag, one person, no deploy required. Practise it once before you need it.
The picture
request
│
▼
[ is the model switched on? ] ── no ──► old rule-based path
│ yes
▼
[ within the rate budget? ] ── no ──► old rule-based path
│ yes
▼
[ model answers, with a ]
[ confidence number attached ]
│
├── confident ─────► answer, and log it
└── unsure ─────► send to a human, and log itEvery box except the middle one is ordinary code. That is the point. Most of the safety in a deployed AI system is not in the model.
Why "we tested it and it was accurate" is not enough
Your test set is a photograph of the past. Deployment is the future, and the future differs in ways you did not sample.
New slang appears. A camera model changes. A festival shifts shopping behaviour. A competitor's outage sends you unfamiliar users.
None of that is a bug in your model. All of it changes what your model sees, and therefore how well it works. This is called drift — the world moving away from the data you trained on.
What is honestly hard here
Deciding when to switch it off is harder than building the switch.
Metrics wobble. Every dip looks like the start of a disaster on a small sample. Teams that pull the switch on every wobble stop being trusted, and teams that never pull it discover the problem from a news article.
Write the trigger down before launch: if the escalation rate crosses this line for this long, we roll back. Deciding under pressure, without a rule, goes badly.
Remember this
- Let the model abstain when it is unsure, and route those cases to a person.
- Roll out small first, with the smallest permissions that work.
- Build the off switch on day one, and write down what triggers it.
What to learn next
- Monitoring and model drift — the live signals this lesson depends on.
- Model cards and documentation — recording what you launched and why.
- Why AI safety matters — the start of this section, worth rereading now.
Developer — Code and libraries.
Four controls, one file
Below: selective prediction (abstention), a guarded serving wrapper with a kill switch and a call budget, an audit log, and a canary comparison against a candidate model.
The dataset ships inside scikit-learn, so nothing downloads.
Setup
pip install numpy scikit-learnThe whole deployment shell
import numpy as np
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
digits = load_digits() # bundled with scikit-learn
X, y = digits.data / 16.0, digits.target
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
model = LogisticRegression(max_iter=5000).fit(Xtr, ytr)
proba = model.predict_proba(Xte)
pred = proba.argmax(1)
conf = proba.max(1)
print("SELECTIVE PREDICTION: answer only when confident, send the rest to a human")
print(f"{'threshold':>10}{'coverage':>10}{'accuracy on answered':>22}{'sent to human':>15}")
for t in (0.00, 0.50, 0.80, 0.90, 0.99):
take = conf >= t
acc = (pred[take] == yte[take]).mean() if take.any() else float("nan")
print(f"{t:>10.2f}{take.mean():>10.3f}{acc:>22.4f}{(~take).sum():>15}")
# ---------- a serving wrapper with the four controls every deployment needs ----------
class GuardedModel:
def __init__(self, model, threshold=0.90, budget=3):
self.model, self.threshold, self.budget = model, threshold, budget
self.enabled = True # 1. kill switch
self.log = [] # 2. audit trail
self.calls = 0 # 3. rate limit
def predict(self, x, user):
if not self.enabled:
return {"action": "fallback", "reason": "model disabled"}
if self.calls >= self.budget:
return {"action": "fallback", "reason": "rate limit"}
self.calls += 1
p = self.model.predict_proba(x[None, :])[0]
label, c = int(p.argmax()), float(p.max())
# 4. abstain rather than guess
action = "answer" if c >= self.threshold else "escalate"
self.log.append({"user": user, "label": label, "conf": round(c, 3), "action": action})
return {"action": action, "label": label, "confidence": round(c, 3)}
guard = GuardedModel(model)
easy = int(np.argmax(conf)) # the most confident test image
hard = int(np.argmin(conf)) # the least confident test image
print("\nGUARDED SERVING")
print(" confident input :", guard.predict(Xte[easy], "u-1"))
print(" ambiguous input :", guard.predict(Xte[hard], "u-1"))
print(" third call :", guard.predict(Xte[easy], "u-1"))
print(" fourth call :", guard.predict(Xte[easy], "u-1"))
guard.enabled = False
print(" after kill switch:", guard.predict(Xte[easy], "u-1"))
print(" audit log :", guard.log)
# ---------- canary: compare a candidate against the live model before switching ----------
candidate = LogisticRegression(max_iter=5000, C=0.05).fit(Xtr, ytr)
share = 0.10
rng = np.random.default_rng(0)
on_canary = rng.random(len(yte)) < share
live_acc = (model.predict(Xte[~on_canary]) == yte[~on_canary]).mean()
can_acc = (candidate.predict(Xte[on_canary]) == yte[on_canary]).mean()
print(f"\nCANARY at {share:.0%}: live acc={live_acc:.4f} (n={int((~on_canary).sum())}) "
f"candidate acc={can_acc:.4f} (n={int(on_canary.sum())})")
print(" promote?", "no - not enough evidence" if abs(live_acc - can_acc) < 0.02
else "investigate the gap before promoting")SELECTIVE PREDICTION: answer only when confident, send the rest to a human
threshold coverage accuracy on answered sent to human
0.00 1.000 0.9704 0
0.50 0.969 0.9847 17
0.80 0.830 0.9955 92
0.90 0.711 1.0000 156
0.99 0.165 1.0000 451
GUARDED SERVING
confident input : {'action': 'answer', 'label': 7, 'confidence': 1.0}
ambiguous input : {'action': 'escalate', 'label': 8, 'confidence': 0.307}
third call : {'action': 'answer', 'label': 7, 'confidence': 1.0}
fourth call : {'action': 'fallback', 'reason': 'rate limit'}
after kill switch: {'action': 'fallback', 'reason': 'model disabled'}
audit log : [{'user': 'u-1', 'label': 7, 'conf': 1.0, 'action': 'answer'}, {'user': 'u-1', 'label': 8, 'conf': 0.307, 'action': 'escalate'}, {'user': 'u-1', 'label': 7, 'conf': 1.0, 'action': 'answer'}]
CANARY at 10%: live acc=0.9673 (n=490) candidate acc=0.9800 (n=50)
promote? no - not enough evidenceReading the three blocks
Abstention buys accuracy at a known price. At threshold 0.00 the model answers everything at 0.9704. At 0.90 it answers 71% of cases at 1.0000 and hands 156 cases to a person. You have converted an error rate into a staffing number, and a staffing number can be budgeted.
Pushing further stops paying. At 0.99 coverage collapses to 16.5% for no accuracy gain over 0.90. There is a knee in this curve. Find yours and sit slightly past it.
The canary result is a lesson in restraint. The candidate scored 0.9800 against the live model's 0.9673. That looks like an improvement and is not evidence of one: n=50 gives a confidence interval of roughly plus or minus 4 percentage points. A 1.3-point difference on 50 samples is noise. Decide your required sample size before the canary starts, or you will promote noise.
Line by line
conf = proba.max(1) uses the top class probability as a confidence. It is the cheapest useful signal and it is not calibrated — a neural network reporting 0.9 is right well under 90% of the time. Fit temperature scaling on a validation split before you treat these numbers as probabilities.
self.enabled is a plain attribute here. In production it belongs in a feature flag or a config value that changes without a deploy. A kill switch that needs a release to pull is not a kill switch.
self.log.append({...}) records the confidence and the action, not the input. Log what you need to reconstruct a decision, and no more than your retention policy allows — see AI regulation.
action = "answer" if c >= self.threshold else "escalate" returns three outcomes from one function: answer, escalate, fallback. A serving path with only success and exception forces every uncertain case into a guess.
The monitoring you actually need
Ground truth arrives late or never, so accuracy is not your primary live signal. Watch these instead:
- Escalation rate. A rise means the world moved, before you can prove it any other way.
- Confidence distribution. Compare today's histogram against launch week. Shape changes precede accuracy changes.
- Input drift. Distances between feature distributions now and at training time. Monitoring and drift covers the tests.
- Per-slice volumes and outcomes. Aggregate stability hides a slice collapsing.
- Human overrides and complaints. The most informative signal, and the one nobody instruments.
Common mistakes
Launching to 100% because staging looked good. Staging traffic is the traffic you imagined. Use 1%, then 5%, then 25%, with a hold at each step.
No fallback path. If the model is disabled and there is nothing behind it, the kill switch takes the product down. Keep the old rules or a simple heuristic wired in and tested.
Untested rollback. Practise it in a quiet week. The first attempt always finds something.
Alerting on model accuracy only. You often cannot compute it live. Alert on escalation rate, latency, error rate and input drift, which are available immediately.
Leaving humans no real authority. If the reviewer's override is ignored downstream, or they see 300 cases an hour, you have a rubber stamp. Measure override rate; if it is near zero, oversight is not happening.
Treating launch as the end. Schedule the first re-evaluation before you launch, and put it in the calendar with a name against it.
Try it yourself
Add a slice array marking 15% of the test set as a "new camera" group, and shift those features slightly. Recompute the escalation rate for that slice alone. Watch it climb well before the overall accuracy moves — that is your early-warning signal, and it is why per-slice monitoring is worth the effort.
What to learn next
- Monitoring and model drift — the live signals this lesson depends on.
- Model cards and documentation — recording what you launched and why.
- Why AI safety matters — the start of this section, worth rereading now.
Researcher — Mathematics and papers.
Selective prediction
Abstention is formalised as a predictor $f$ paired with a selection function $g: \mathcal{X} \to {0, 1}$:
$$ (f,g)(x) = \begin{cases} f(x) & g(x) = 1 \ \text{abstain} & g(x) = 0 \end{cases} $$
Two quantities characterise it:
$$ \text{coverage}(g) = \mathbb{E}[g(x)], \qquad \text{risk}(f,g) = \frac{\mathbb{E}\big[\ell(f(x), y)\,g(x)\big]}{\mathbb{E}[g(x)]} $$
The risk-coverage curve is the object to report; a single operating point hides the trade. El-Yaniv and Wiener (2010) establish the framework, and Geifman and El-Yaniv (2017) give SelectiveNet-style bounds for deep models with a guaranteed risk at a target coverage.
Softmax response is a strong baseline. Better selectors: deep ensembles (Lakshminarayanan et al., 2017), MC dropout, and learned selection heads. Ovadia et al. (2019) benchmark uncertainty methods under distribution shift and find that most degrade sharply — the setting where the selector matters most is the setting where it works least well.
Calibration
A model is calibrated if $P(Y = \hat{y} \mid \hat{p} = p) = p$. Guo et al. (2017) show modern networks are systematically over-confident, worsening with depth and width, and that single-parameter temperature scaling — dividing logits by $T$ fitted on a validation set by minimising NLL — removes most of the miscalibration without changing the argmax.
Expected calibration error, binned into $M$ bins:
$$ \text{ECE} = \sum_{m=1}^{M} \frac{|B_m|}{n}\,\big|\,\text{acc}(B_m) - \text{conf}(B_m)\,\big| $$
ECE is sensitive to binning scheme and bin count; report the scheme, or prefer an adaptive-binning or kernel estimator. Calibration on the training distribution does not transfer under shift, which is the finding most often ignored when a threshold is set once and never revisited.
Conformal prediction (Vovk et al.; Angelopoulos and Bates, 2021) offers distribution-free finite-sample coverage guarantees for prediction sets under exchangeability. Exchangeability fails under drift, and that is precisely when guarantees are wanted; split-conformal with periodic recalibration is the practical compromise.
Detecting failure without labels
Labels arrive late, so live monitoring is largely unsupervised.
- Input drift. Per-feature two-sample tests (Kolmogorov-Smirnov, chi-squared) with multiple-testing correction; MMD for multivariate. Rabanser et al. (2019) evaluate shift-detection pipelines and find dimensionality reduction followed by univariate testing competitive with more elaborate methods.
- Output drift. Shifts in the predicted-class distribution or the confidence histogram. Cheap, and sensitive in practice.
- Proxy signals. Escalation rate, override rate, complaint volume, downstream conversion. Usually the earliest indicator.
Distinguish covariate shift ($P(X)$ changes, $P(Y \mid X)$ fixed) from concept drift ($P(Y \mid X)$ changes). Only the first is detectable from inputs alone; the second requires labels or a proxy for them, which is why concept drift is typically discovered through a business metric.
Rollout as sequential testing
A canary is a sequential experiment, and evaluating it repeatedly with a fixed-horizon test inflates the false-positive rate. Two correct approaches: fix the sample size in advance, or use always-valid inference — sequential probability ratio tests, or confidence sequences (Howard et al., 2021) — which permit continuous monitoring at controlled error rates.
Practical sizing: detecting a 1-point difference around 97% accuracy at conventional power needs on the order of thousands of samples per arm, not fifty. Compute the minimum detectable effect before the canary starts, and be prepared for the honest answer that your traffic cannot resolve the difference you care about.
Interference matters for interactive systems. Recommender canaries violate SUTVA through shared item pools and feedback loops; cluster or time-sliced randomisation is often necessary. See A/B testing a recommender.
Feedback loops
A deployed model changes the data it will next be trained on. Predictive policing models allocate patrols, patrols generate arrest records, records train the next model — Ensign et al. (2018) analyse this as a reinforcing process independent of any underlying crime distribution. Recommenders narrow the item distribution users can respond to, so logged feedback is confounded by exposure.
Mitigations: log the propensity of each exposure to permit inverse-propensity-weighted evaluation, retain a randomised holdout stream that never sees model output, and monitor the diversity of the action distribution over time. Sculley et al. (2015) catalogue these and other hidden costs; the paper remains the best short account of why ML systems accumulate maintenance debt faster than ordinary services.
Incident response
Borrow directly from site reliability practice, adapted for the fact that an ML failure is usually silent rather than loud.
- Define an SLO on a user-visible quantity, not on model accuracy — escalation rate, dispute rate, time to resolution.
- Error budgets convert "is it good enough" into a spend, and make the rollback decision a policy rather than an argument.
- Blameless postmortems with a mandatory question: what signal would have caught this earlier, and why was it not instrumented?
- Practise the rollback. An untested recovery path is a hypothesis.
Papers
- Sculley et al., Hidden Technical Debt in Machine Learning Systems, NeurIPS 2015
- Guo et al., On Calibration of Modern Neural Networks, ICML 2017 — arxiv.org/abs/1706.04599
- Geifman and El-Yaniv, Selective Classification for Deep Neural Networks, NeurIPS 2017 — arxiv.org/abs/1705.08500
- Ovadia et al., Can You Trust Your Model's Uncertainty?, NeurIPS 2019 — arxiv.org/abs/1906.02530
- Rabanser, Günnemann and Lipton, Failing Loudly, NeurIPS 2019 — arxiv.org/abs/1810.11953
- Angelopoulos and Bates, A Gentle Introduction to Conformal Prediction, 2021 — arxiv.org/abs/2107.07511
- Ensign et al., Runaway Feedback Loops in Predictive Policing, FAccT 2018 — arxiv.org/abs/1706.09847
What to learn next
- Monitoring and model drift — the live signals this lesson depends on.
- Model cards and documentation — recording what you launched and why.
- Why AI safety matters — the start of this section, worth rereading now.