The only failure class with no commit to blame
Debugging assumes a change. You look at what shipped, you bisect, you revert. Drift breaks that reflex, because the system that got worse is byte-for-byte the system that was working, and the thing that moved is either outside your repository or outside your company. That is why drift is where teams lose the most time: they spend the first day looking for a deploy that does not exist.
gen_ai.evaluation.result, and an embedding-version gate that refuses a query rather than answering it badly.
Four drifts, four signals, four fixes 7 min live
"Drift" gets used as one word for four unrelated problems, which is why drift conversations go nowhere. Separate them by the signal that reveals each one, because that is what decides what you have to build, and notice that only one of the four has a change you could point at in a changelog.
LiveThe case study: a vendor could not reproduce its own degradation5 min▶
The best available teaching artifact for output-quality drift is a primary-source postmortem in which a model provider documents how hard its own degradation was to detect. Three overlapping issues, reported by users, and the internal benchmarks stayed green.
- Scale, from the postmortem itself: at peak, 16% of Sonnet 4 requests were affected in the worst hour, and approximately 30% of Claude Code users who made requests during that period had at least one message routed to the wrong server type.
- Platform asymmetry: peak impact on Bedrock was 0.18% and on Vertex under 0.0004%. If you multi-home, one platform's telemetry does not represent the others, and the platform you happen to dashboard is the one whose numbers you will believe.
- Why detection failed: the benchmarks "simply didn't capture the degradation users were reporting", partly because the model recovers from isolated errors, and because the bugs produced different symptoms on different platforms at different rates.
Take exactly two lessons. First: user reports are a signal. They are noisy, unstructured and frequently right. Second, and this is the hard one: "our evals are green" is not a rebuttal to a user report. It is a statement about a distribution you constructed, and drift is precisely the case where your constructed distribution has stopped resembling production traffic. When those two disagree, the honest next step is to sample real traces from the complaining segment, not to re-run the suite.
Self-studyDownstream behaviour: the signal that needs no scorer3 min read▶
Quality scorers are expensive to build, need calibration, and drift themselves. Meanwhile your users are already scoring every answer, in a currency you can measure without any judge at all:
- Retry rate. The same conversation id asking a near-identical question within a minute. One person deciding the answer was useless, recorded as a fact rather than an opinion.
- Escalation to human. The strongest signal available in any product with a support path, and usually already instrumented for a different reason.
- Abandonment. A session that ends immediately after an answer, with no follow-up and no action taken on the result.
These are lagging by minutes rather than by a quarter, they cost nothing to compute, and they are honest in the way judge scores are not, because nobody had to define what "good" means. Their weakness is diagnosis: they tell you something got worse and nothing about what. So the sequence that works is: downstream behaviour alerts you, sampled traces explain it, and a scorer gives you the trend line afterwards. Teams that build in the opposite order spend a quarter on a judge before they have anything watching production.
Three ways a provider changes your system without you 7 min live
This is the drift type with the clearest mechanism and the widest blind spot, because most teams assume that not deploying means not changing. Three concrete mechanisms, all documented by the provider, all capable of moving your behaviour or your bill overnight.
gen_ai.request.model is what you asked for. gen_ai.response.model is what answered. Both are Recommended attributes on the inference span, and the pair costs you one string per call. A non-zero mismatch rate is proof that a floating alias moved, that a gateway rewrote your request, or that a fallback path you forgot about is serving live traffic. Almost nobody emits both, and it is the single highest-value line of instrumentation on this page.
Self-studyWhat a pinned snapshot does not protect you from3 min read▶
Pinning is necessary and it is not sufficient, and it is worth being precise about the residue so you do not oversell it internally:
- The clock still runs. A pinned snapshot has a lifecycle: Active, then Legacy, then Deprecated, then Retired. The commitment is at least 60 days' notice, and requests to a model past its retirement date fail. Pinning converts a silent quality change into a dated hard failure, which is a large improvement and not an exemption.
- Serving-side variation persists. Identical kernels are run-to-run deterministic but not batch-invariant, and server batch size varies with other users' load. The other concurrent users are not an input to your system but they are a nondeterministic property of it, which is why 1000 identical prompts at temperature 0 produced 80 distinct completions.
- Your reproducibility levers may be gone. On Opus 4.7 and later,
temperature,top_pandtop_kare deprecated and return 400 on non-default values, so "just set temperature to 0 and replay it" is not available as a strategy.
Which is the whole argument of this course restated: because you cannot reproduce a production trace by replaying it, you must capture enough of it at the time it happens.
Embedding drift is two problems, and only one has a partial fix 6 min live
People say "our embeddings drifted" to mean two things that share nothing except the word. One is a freshness problem with an obvious SLI. The other is a compatibility problem with no signal whatsoever, and it is the purest coverage failure in this course: retrieval degrades towards random while every single span reports OK.
Self-studyEvals entering the telemetry spec: gen_ai.evaluation.result3 min read▶
Until recently, eval scores lived in one system and traces lived in another, and joining them was somebody's weekend project. That is changing: the GenAI semantic conventions now define an evaluation signal, which means a score can ride the same pipeline as the span it describes.
- The event is
gen_ai.evaluation.result, carryinggen_ai.evaluation.name,gen_ai.evaluation.score.value,gen_ai.evaluation.score.labelandgen_ai.evaluation.explanation. - The practical consequence is the one that matters for this session: a drifting score is now a time series sitting next to the trace that produced it, so "quality dropped" and "here are forty traces from the drop" become the same query instead of two systems and a spreadsheet.
- The usual caveat applies with full force. Like everything else in these conventions it is
Developmentstatus with no tagged release, so check the current spelling in the repository before you build a dashboard on it.
OpenInference reaches the same place from the other direction, with EVALUATOR as one of its ten span kinds. Different dialect, same convergence: the evaluator is becoming part of the trace rather than a separate report.
Wire a sampled online scorer on Beacon traffic ★ 11 min · everyone builds
Beacon's Insight Bot now has coverage, cost attribution and alerts. What it does not have is any way to notice that answers got worse this month. Build the smallest thing that would: an asynchronous scorer on a sample of production traces, emitting a score back onto the trace.
Pick the sample rate and write down why. Prevailing vendor guidance is 1-10% of production traces, commonly 5-10%, scored asynchronously and filtered towards high-value traces. Label that in your design doc as [S] prevailing vendor guidance, not an empirical finding: there is no peer-reviewed or survey source establishing what teams actually use or what is sufficient. You are choosing a starting point, not applying a standard.
Make the sample representative or admit that it is not. A sample skewed towards cheap fast traces will miss exactly the twelve-step runs where quality dies. Stratify by the segments you already alert on in b8: model, cache-hit state, step bucket, and tenant tier. An unstratified 5% of Beacon traffic is roughly 5% of the easy questions.
Score off the request path, always. Read sampled traces from storage, score them, and emit gen_ai.evaluation.result with gen_ai.evaluation.name and gen_ai.evaluation.score.value. A judge in the hot path adds latency and a second failure mode to the thing you were trying to measure.
Calibrate the judge against humans before you alert on it. Have two people label 100 sampled traces, compare with the judge, and record the agreement. Skip this and you are not monitoring your system, you are monitoring the judge, and when the judge drifts you will go looking for a deploy in the wrong repository.
Now plot the score next to three things that need no judge at all: retry rate, escalation-to-human rate and tokens per request. When the honest signals and the scorer disagree, believe the honest signals and go read traces.
Two guards: the model pin, and the embedding version gate ★ 11 min · everyone builds
Beacon's retrieval index was built eighteen months ago and nobody remembers which embedding model produced it. That sentence is a production incident waiting for a migration ticket. Build the two guards that turn both silent-change classes into loud, dated failures.
Replace every floating alias in configuration with a pinned snapshot, and put the pin in the same file as the eval results that justified it. A pin without a recorded reason gets removed by the next person who wants a newer model.
Emit both model fields on every inference span and add the b8 rule: alert when gen_ai.response.model differs from gen_ai.request.model on more than zero requests. Then check the deprecation calendar for every pinned snapshot you own and put the retirement dates in a shared calendar, because at least 60 days' notice is only useful to someone who is reading.
Write the embedding model id and version into the index metadata, not into a README. Index metadata travels with the index through backups, restores and migrations. A README does not.
At query time, compare the query-embedding version with the index version and refuse on mismatch. Return an error and set span status, do not degrade quietly. This is the one place in the whole course where refusing to answer is unambiguously the right behaviour, because an answer built on a mismatched index is a confident answer built on noise.
Add index freshness as an SLI: the age of the oldest embedding relative to its document's modified time. Staleness is the drift that has a partial fix, so give it the metric that lets you fix it incrementally.
Try it yourself - this week ◐ 50-60 min total
- Grep your configuration for model identifiers and count how many are floating aliases. Then check whether you log
gen_ai.response.modelanywhere at all. Most teams discover they have been unable to answer "which model actually served last Tuesday" for their whole history. - Find out which embedding model built your production index, using only artifacts that exist today. If the answer involves asking a person, you have found the gap this session is about, and the fix is one metadata write.
- Plot tokens per request for the last ninety days with no other filter. A step change with no deploy behind it is either a traffic shift or a provider-side change, and either way it is a conversation worth having with the number in hand.
- Compute retry rate and escalation-to-human rate for one AI feature, weekly, for the last quarter. You almost certainly already have the data. This is the cheapest quality time series available to you and it needs no judge.
- Take one user complaint your team previously closed as "could not reproduce, evals green" and pull the actual traces from that user's segment for that hour. Whatever you find, the exercise retrains the reflex, and that reflex is the real deliverable of this session.
Official sources covered
Primary sources are marked [P] and secondary [S]. Two claims on this page are deliberately not presented as sourced facts: the sampling cadence, and the re-embedding requirement. Both are flagged below and in the body.
temperature, top_p and top_k deprecated on Opus 4.7 and later, returning 400 on non-default valuesPart 2 · reproducibility is not available as a levergen_ai.request.model and gen_ai.response.model as Recommended attributes on the inference spanPart 2 · the mismatch rule, and Demo 2gen_ai.evaluation.result with .name, .score.value, .score.label and .explanation; OpenInference's EVALUATOR span kindPart 3 · evals entering the telemetry spec. All Development statusThree questions before you go 🎯 ◐ 90 seconds
1 · Users in one segment report that answers got worse this week. Your eval suite is green and nothing shipped. What is the correct next move?
A model provider documented this failure about itself: benchmarks did not capture the degradation users were reporting, partly because the model recovers from isolated errors, and because symptoms differed by platform and rate. User reports are a signal. "Our evals are green" is not a rebuttal to one, it is a statement about a set of examples you chose before the drift happened. Go and read the traces from the complaining segment.
2 · Someone swaps the embedding model for new document ingestion but does not re-embed the existing corpus. What does your telemetry show?
This is the purest coverage failure in the course. Similarity between vectors from two different models is not a measure of anything, but the arithmetic still completes and still returns a ranked list, so there is no error, no latency change and no empty result to alert on. That is why the fix is structural rather than observational: version the embedding model in the index metadata and refuse any query whose embedding version does not match. Refusing loudly is the feature.
3 · You are asked what sampling rate to score production traces at. What is the honest answer?
The range is real and it is worth using as a starting point, but its provenance is vendor documentation, not evidence. Saying so costs you nothing and buys you the right to change the number when your own data justifies it. Two conditions matter far more than the percentage: the sample has to be representative, so stratify it by the segments you already alert on, and the judge has to be calibrated against humans before you put a threshold on its output, or you are monitoring the judge's drift rather than your system's.
Builder session 9 cheat sheet · pin this
gen_ai.response.model next to the requested model.