Skip to the solution
MediumAI Engineering9 min read

How do you catch regressions when changing a prompt or upgrading a model?

142 views

The problem

Question presented to candidate: "Someone on the team rewrote the support assistant's system prompt, and separately we want to move to a newer model version. Both look better when we try a few examples. How would you make sure neither change quietly breaks something that works today?"

What a strong answer should cover:

  • A fixed, versioned golden set run on the baseline and the candidate with everything else pinned (model snapshot, judge, temperature, retrieval index), compared case by case as a paired comparison, not as two separate averages
  • Measuring the noise floor first: run the baseline against itself, run each case several times, and only call a difference a regression when it is larger than that noise (a paired sign or McNemar test, or confidence intervals on the difference)
  • Per-slice results and flipped-case diffs, because an unchanged average can hide one slice that collapsed; plus must-pass cases that block on any failure
  • Model upgrades are bigger than prompt edits: pin dated snapshots, re-run the full suite, re-check the LLM judge, compare format adherence, refusals, length, tool calls, latency and cost, and expect to re-tune the prompt
  • Offline evals are necessary but not sufficient: shadow or canary traffic with live quality signals before full rollout, and every regression found becomes a permanent case

Clarifying questions expected:

  • How large is the golden set, and is it sliced by the categories that matter (language, product area, customer tier)?
  • Is the current model pinned to a dated snapshot or an alias that can move?
  • What is the cost of a regression compared with delaying the change?

Code / implementation expected: Optional β€” a regression gate that compares baseline and candidate per slice with a noise check and a significance test.

  • regression-testing
  • evals
  • model-upgrade
  • ci

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 12 sections

Read the code

Regression gate: A/A noise floor, per-slice paired deltas and an exact sign test
// Regression gate for a prompt or model change. Each golden case is graded 3
// times per version (outputs are stochastic); a seeded generator stands in for
// the model + grader, so the run is reproducible. Steps: A/A noise floor,
// baseline vs candidate overall and per slice, flipped cases, exact sign test, gate.
let state = 42;
const rand = () => { state = (1664525 * state + 1013904223) % 4294967296; return state / 4294967296; };

const SLICES = ["billing", "shipping", "multilingual"];
// True per-slice pass probability for each version (unknown to the harness).
const P = { base: { billing: 0.88, shipping: 0.86, multilingual: 0.86 }, cand: { billing: 0.97, shipping: 0.96, multilingual: 0.7 } };
const CASES = [];
for (const s of SLICES) for (let i = 0; i < 25; i++) CASES.push({ id: `${s.slice(0, 2)}${String(i + 1).padStart(2, "0")}`, slice: s, critical: i === 0 });
const RUNS = 3;

function evaluate(version) {
  // Returns, per case, how many of RUNS attempts passed.
  return CASES.map((c) => { let k = 0; for (let r = 0; r < RUNS; r++) if (rand() < P[version][c.slice]) k++; return k; });
}
const rate = (res, idx) => idx.reduce((s, i) => s + res[i], 0) / (idx.length * RUNS);
const passed = (k) => k * 2 > RUNS; // majority of runs
const C = (n, k) => { let r = 1; for (let i = 0; i < k; i++) r = (r * (n - i)) / (i + 1); return r; };
function signTest(reg, imp) { // exact two-sided McNemar / sign test on flipped cases
  const n = reg + imp; if (n === 0) return 1;
  let tail = 0; for (let i = 0; i <= Math.min(reg, imp); i++) tail += C(n, i) * Math.pow(0.5, n);
  return Math.min(1, 2 * tail);
}
function flips(a, b, idx) {
  let reg = 0, imp = 0; const ids = [];
  for (const i of idx) { if (b[i] < a[i]) { reg++; ids.push(CASES[i].id); } else if (b[i] > a[i]) imp++; }
  return { reg, imp, ids };
}
const pct = (x) => (x * 100).toFixed(1);
const sgn = (x) => (x >= 0 ? "+" : "") + pct(x);
const ALL = CASES.map((_, i) => i);

const base = evaluate("base"), rerun = evaluate("base"), cand = evaluate("cand");
const aa = flips(base, rerun, ALL);
console.log(`A/A noise floor (baseline vs baseline rerun): ${pct(rate(base, ALL))}% vs ${pct(rate(rerun, ALL))}%, delta ${sgn(rate(rerun, ALL) - rate(base, ALL))} pts, flips ${aa.reg} down / ${aa.imp} up, p=${signTest(aa.reg, aa.imp).toFixed(3)}`);
const d = rate(cand, ALL) - rate(base, ALL), all = flips(base, cand, ALL);
console.log(`overall: baseline ${pct(rate(base, ALL))}% -> candidate ${pct(rate(cand, ALL))}% (${sgn(d)} pts), flips ${all.reg} down / ${all.imp} up, p=${signTest(all.reg, all.imp).toFixed(3)}`);

const failures = [];
if (d < -0.02) failures.push("overall drop > 2 pts");
for (const s of SLICES) {
  const idx = ALL.filter((i) => CASES[i].slice === s);
  const ds = rate(cand, idx) - rate(base, idx), fl = flips(base, cand, idx), p = signTest(fl.reg, fl.imp);
  console.log(`  ${s.padEnd(13)} ${pct(rate(base, idx)).padStart(5)}% -> ${pct(rate(cand, idx)).padStart(5)}%  ${sgn(ds).padStart(6)} pts  flips ${fl.reg} down / ${fl.imp} up  p=${p.toFixed(3)}${fl.ids.length ? "  regressed: " + fl.ids.join(",") : ""}`);
  if (ds < -0.05 && p < 0.05) failures.push(`${s} slice dropped ${pct(-ds)} pts (p=${p.toFixed(3)})`);
}
for (const i of ALL) if (CASES[i].critical && passed(base[i]) && !passed(cand[i])) failures.push(`critical case ${CASES[i].id} regressed`);
console.log(failures.length ? `GATE: BLOCK -> ${failures.join("; ")}` : "GATE: PASS");

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 56 of 83 in AI engineering

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

How does function calling (tool use) work under the hood?

Continue