Skip to the solution
HardAI Engineering18 min read

How do you monitor LLM quality drift in production?

348 views

The problem

Question presented to candidate: "Our RAG support assistant scored well at launch and nobody has touched the prompt since, but support leads say answers feel worse this month. How would you set up monitoring so that a slow decline in quality is detected by us, with evidence, before customers tell us β€” and how would you work out what caused it?"

What a strong answer should cover:

  • Quality can change with zero deploys, and why: input drift (the question mix changes), upstream data drift (documents in the index go stale or change format), provider-side change (alias moves, serving or infrastructure bugs), and slow accumulation of small config changes
  • Two kinds of signal: direct quality measured on a daily sample of live traffic (LLM judge or human labels, with a calibrated judge) and cheap proxies on all traffic (thumbs-down, rephrase and retry rate, escalations, refusals, guardrail triggers, output length, parse failures)
  • Statistics that fit: a rolling baseline instead of fixed thresholds, the noise of a daily sample (about plus or minus 4 points at 200 samples and 91%), a change detector such as CUSUM or EWMA for small sustained drops, and a distribution distance such as PSI for the input mix
  • Slicing by topic, customer, language and model version, because drift is usually concentrated in one slice and the slice points to the cause
  • The response loop: confirm with a full eval run, separate input drift from a real regression, fix (data, prompt, routing, model pin) and add the new failure pattern to the golden set

Clarifying questions expected:

  • How much traffic per day, and how much judging budget, so we know how many conversations we can score?
  • Is the model pinned to a dated snapshot, and do we get notice of provider changes?
  • Which outcome matters most β€” correct answers, resolved tickets, or safety β€” so the primary metric is clear?

Code / implementation expected: Optional β€” a monitor that compares daily metrics with a baseline (CUSUM, EWMA or PSI), or a sampling and judging pipeline sketch.

  • drift
  • monitoring
  • production
  • quality

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

18 min read Β· 13 sections

Read the code

21-day drift monitor: fixed threshold vs daily z vs CUSUM, PSI and slices
// Daily drift monitor: PSI on the input topic mix, and a one-sided CUSUM on the
// sampled LLM-judge pass rate, compared with a fixed threshold and a daily z alarm.
// 21 simulated days, 200 judged conversations per day. No deploy happens.
const N = 200;
const TOPICS = ["billing", "bug", "account", "other", "promo"];
const BASE_MIX = [80, 60, 40, 20, 0];          // the mix the golden set was built from
const PROMO_MIX = [72, 54, 36, 18, 20];        // a marketing campaign from day 10
const WIGGLE = [0, 3, -2, 1, -4, 2, -1, 3, 0, -3, 1, 2, -2, 0, 2, -1, 0, 3, -2, 1, -1];

function topicCounts(day) {
  const mix = (day >= 10 ? PROMO_MIX : BASE_MIX).slice();
  mix[0] += WIGGLE[day - 1];                    // small day-to-day noise
  mix[1] -= WIGGLE[day - 1];
  return mix;
}
// Judge passes per day: about 90% until day 15, about 87% after (an upstream document went stale).
function passes(day) {
  return (day >= 15 ? 174 : 180) + WIGGLE[(day * 7) % 21];
}

function psi(expected, actual) {
  const eT = expected.reduce((a, b) => a + b, 0), aT = actual.reduce((a, b) => a + b, 0);
  let s = 0;
  for (let i = 0; i < expected.length; i++) {
    const e = Math.max(expected[i] / eT, 0.001), a = Math.max(actual[i] / aT, 0.001);
    s += (a - e) * Math.log(a / e);
  }
  return s;
}

// Baseline from the first 7 days (a rolling window in a real system).
let base = 0;
for (let d = 1; d <= 7; d++) base += passes(d);
const p0 = base / (7 * N);
const sigma = Math.sqrt((p0 * (1 - p0)) / N);
const K = 0.5, H = 4;                          // CUSUM slack and alarm level, in sigmas
console.log(`baseline pass rate ${(100 * p0).toFixed(1)}%, daily sigma ${(100 * sigma).toFixed(2)} pts, ` +
  `95% interval for one day +/-${(196 * sigma).toFixed(1)} pts`);
console.log("day  pass%     z  cusum   psi  alarms");
let S = 0;
const first = {};
for (let day = 1; day <= 21; day++) {
  const p = passes(day) / N;
  const z = (p - p0) / sigma;
  S = Math.max(0, S + (p0 - p) / sigma - K);
  const drift = psi(BASE_MIX, topicCounts(day));
  const alarms = [];
  if (p < 0.8) alarms.push("FIXED<80%");
  if (z < -3) alarms.push("Z<-3");
  if (S > H) alarms.push("CUSUM");
  if (drift > 0.25) alarms.push("PSI>0.25");
  else if (drift > 0.1) alarms.push("PSI>0.1");
  for (const a of alarms) if (!(a in first)) first[a] = day;
  console.log(`${String(day).padStart(3)}  ${(100 * p).toFixed(1).padStart(5)} ${z.toFixed(2).padStart(5)}` +
    ` ${S.toFixed(2).padStart(6)} ${drift.toFixed(3)}${alarms.length ? "  " + alarms.join(" ") : ""}`);
}
console.log("first alarm day per detector:");
for (const a of ["FIXED<80%", "Z<-3", "CUSUM", "PSI>0.25"]) console.log(`  ${a.padEnd(9)} ${a in first ? "day " + first[a] : "never"}`);

// Slice view on day 18: the drop is concentrated, which points at the cause.
const slices = { billing: [69, 75], bug: [47, 51], account: [34, 36], other: [15, 18], promo: [9, 20] };
const slicesBefore = { billing: [66, 72], bug: [50, 54], account: [34, 36], other: [15, 18], promo: [17, 20] };
console.log("day 18 by topic (day 14 -> day 18):");
for (const t of TOPICS) {
  const [b, bn] = slicesBefore[t], [a, an] = slices[t];
  console.log(`  ${t.padEnd(8)} ${(100 * b / bn).toFixed(1)}% -> ${(100 * a / an).toFixed(1)}%`);
}
// How many judged samples a day to see a 3-point drop in one day (z=1.96, 80% power z=0.84)?
const n3 = Math.ceil(((1.96 + 0.84) ** 2 * p0 * (1 - p0)) / 0.03 ** 2);
console.log(`samples per day to detect a 3-point drop in one day: ${n3}`);

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 79 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 nextHard

What techniques reduce LLM latency at each stage of a request?

Continue