ealeng

Episode 43 of 56 · Observability

Traces to tests: what feedback misses, and a test too small to tell

What a “not helpful” click can tell you when it's saved with its trace, and what it can't: in a replayed week with simulated learners, a release that stopped citing sources got fewer complaints, not more. Then a test set sampled from the traces, a gate that catches that release, and a fix that failed it by one answer out of 38, a test too small to tell.

Lab 14 · Observability12 min read
Video transcript7:00

0:00Cold open

0:01

A new version of our AI assistant stopped showing its sources.

0:05

So I wrote a fix, and a test to check it.

0:08

The old version passes. The bad version fails, like it should.

0:13

My fix failed too. By one answer.

0:17

Is my fix broken? Or is my test?

0:21Intro

0:22

Hands-on AI engineering: observability, part four.

0:26

Today: what feedback from users can tell you, and how to turn traces into tests.

0:32Meet Nadia

0:34

I’m Nadia, an AI engineer at Quill Academy, a made-up online course site.

0:39

This is our assistant. It answers learners’ questions from seven short articles about AI engineering.

0:46

Every request leaves a trace: a record of each step inside it, like an itemized receipt.

0:53

On Thursday, a new version, v2, went out to half the learners.

0:57

Someone had made it “more concise”. One sentence, and no sources. And by mistake, it sent the whole conversation with every question.

1:0801 · Feedback

1:09

In a test of the real app, I ask: what does LoRA change in a model? The answer comes with its source, as a link I can check.

1:18

Next: how much does Pinecone cost per month? The articles don’t say. So the assistant says it doesn’t know.

1:25

I click “not helpful”. Both answers are replayed from that test.

1:30

That click is saved with the trace id of the answer: the receipt’s number. Like stapling a complaint to its receipt.

1:38

So I open the trace. Search found a passage about Pinecone, but no price. Not a bug in the assistant: a gap in the articles.

1:4802 · A week of feedback

1:49

Now a whole week. It’s a replay: simulated learners, and problems I planted on purpose. But every answer is a real call to a language model.

2:00

The learners’ feedback is simulated too, from rules I wrote down. For example: a wrong answer gets “not helpful” one time in five.

2:09

Only one request in twelve got any feedback. Most learners said nothing.

2:15

Complaints are a “not helpful”, a retry, an edited question, or asking for a person.

2:21

Complaints rose on the bad days. On Wednesday, when search broke. On Friday, when answers were slow. And on Saturday, when the model’s API failed.

2:3303 · What it can’t see

2:34

Now the bad release. From Thursday, v1 cited a source in most answers. v2, in none.

2:41

Did learners complain more about v2?

2:46

No. About six percent of v1’s requests got a complaint. Three percent of v2’s.

2:53

Of course: my simulated learners only react to errors, “I don’t know”, wrong answers and slow ones. And people can’t miss a source they never saw.

3:03

Feedback catches what users notice. For the rest, I need a test.

3:0904 · Traces → tests

3:10

Traces have a second use. Each one holds a question a learner asked. So my test cases come straight from the week’s traces.

3:19

I took ten of each kind of question: what, how, list, and ones the articles don’t answer. Plus ten long conversations.

3:27

Why not just fifty at random? Most would be “how” and “what” questions. Only about six would be questions the articles can’t answer.

3:37

Every case keeps the trace id it came from. So a failing test leads back to what a learner saw.

3:4405 · The gate

3:46

Then a gate: checks every new version must pass before it ships.

3:50

Check one: at least ninety-five percent of answers are right.

3:55

Check two: sources. At least ninety percent of answers cite one. That’s counted on the thirty-eight questions the articles can answer. The other twelve have nothing to cite.

4:06

Check three: long chats. Remember, v2 sends the whole conversation with every question.

4:13

We pay for every token sent to the model: every piece of a word. v1 sends about five hundred on a long chat. The bar is seven hundred.

4:2406 · v2 at the gate

4:25

v1, the version before, passes all three.

4:29

v2 gets every answer right. A test that only checked answers would let it through.

4:35

But it cites a source in just three of thirty-eight answers: almost none. And its long chats need nine hundred and forty-four tokens. Fail.

4:4607 · My fix

4:48

Then my fix, v3. Still concise: two sentences at most, each with its source. And the conversation trimmed again.

4:56

Every answer right. Long chats back to their old size.

5:01

Sources: thirty-four of thirty-eight. The bar needs thirty-five. One more cited answer, and it would pass.

5:08

So, is my fix broken?

5:1308 · Free throws

5:15

Think of free throws. Here’s a player who truly makes nine shots in ten. One round: thirty-eight shots.

5:22

Now eight rounds, simulated. The same player, but every round lands somewhere else.

5:28

Thirty-four or fewer happens in about half the rounds. Even for a player who truly makes nine in ten.

5:36

My thirty-eight answers are one round. My test could fail a fix that’s exactly at the bar, about every other time.

5:4509 · The likely range

5:46

Turn it around. If v3 answered thousands of questions, it would likely cite a source in seventy-six to ninety-six percent of them. That’s the blue bar: its likely range.

5:58

The old version’s range overlaps it.

6:02

So this test can’t tell my fix from the old version. The fail might be bad luck, or a small real drop. Thirty-eight answers can’t say which.

6:12

Two ways out. More cases, so the range shrinks. Or fail only when the whole range sits below the bar, like v2’s. v3’s doesn’t.

6:22Back to Nadia

6:24

So here’s what I do now.

6:26

Feedback is saved with its trace, so I can see why.

6:30

But feedback only shows what learners notice. So I don’t rely on it alone.

6:35

Tests come from traces: every kind of question, plus long chats.

6:40

And I read the range, not only the bar. My fix’s fail doesn’t prove it’s broken. My test was too small to tell.

6:49Summary

6:51

Learners tell you what they noticed. Tests built from traces catch the rest.

6:55

And next week’s tests are already in them.Nadia: Thanks!

Key takeaways

  • ✓Save every feedback click with the trace id of the answer it's about. A “not helpful” on “How much does Pinecone cost per month?” led to a trace whose search found a Pinecone paragraph with no price: a gap in the articles, not a bug in the assistant.
  • ✓Feedback is sparse. With simulated learners, 85 of 1,055 requests (1 in 12) got any; 41 got a complaint (a “not helpful”, a retry, an edited question or asking for a person). Complaints rose on the bad days: Wednesday (search broke), Friday (slow answers), Saturday (API errors).
  • ✓Feedback only shows what users notice. From Thursday, v1 cited a source in 224 of 268 requests and v2 in none of 335, but about 6% of v1's requests got a complaint and 3% of v2's. People can't miss a source they never saw. (In this replay that's by construction: the simulated learners' rules never mention sources.)
  • ✓Traces are test data. 50 cases sampled from the week's clean traces: 10 each of what, how, list and unanswerable first questions, plus 10 long conversations (turn 8+, with 7–17 earlier turns rebuilt from their traces). Stratify: fifty at random would be mostly how and what, with about 6 unanswerable. Each case keeps its trace id.
  • ✓Gate on what production showed matters: right ≥ 95%, cites a source on ≥ 90% of the 38 answerable questions, long chats ≤ 700 input tokens. All three versions got 50 of 50 right, so an answer-only test passes v2. v2 cited 3 of 38 and sent 944 tokens on long chats: fail. v1: 36 of 38, 502 tokens: pass.
  • ✓The fix, v3, cited 34 of 38 (89.5%) with 516 tokens: fail, one answer short. Its likely range is 76–96%, overlapping v1's 83–99%, and a fix exactly at 90% scores 34 or fewer about half the time (53.5%). Read the range, not only the bar: add cases, or fail only when the whole range sits below the bar.

Users tell you some of what goes wrong, and traces hold the rest: every question someone asked, with the answer and each step behind it. This chapter covers both. First, feedback saved with the trace of the answer it's about, and what a week of it can and can't show. Then a test set sampled from those traces, a gate that checks what production showed matters, and how to read a result that misses the bar by one answer.

The source is Lab 14 (Observability), scripts 06_feedback.py (a chat page with feedback linked to traces) and 07_traces_to_evals.py (traces → tests), recorded in October 2026. The assistant is gpt-5.4-nano, and gpt-5.4-mini grades the answers. The course site, Quill Academy, is made up, and the week is a replay: simulated learners and problems planted on purpose, but every answer is a real call to the model. Chapter 40 covered recording the traces, chapter 41 the dashboards and alerts on them, and chapter 42 finding causes in them. This chapter, the last on observability, turns those traces into tests.

The assistant and the bad release

The assistant answers learners' questions from 7 short articles about AI engineering, split into 28 paragraphs: it searches for the 3 closest paragraphs and answers from them, citing each source. Every request leaves a trace: a record of each step inside it (search, the model call, the answer), like an itemized receipt.

On Thursday a new version, v2, went out to half the learners. Someone had made it "more concise": one sentence, no sources. And by mistake it dropped the cap on conversation history, so every question carried the whole conversation with it.

Feedback, saved with its trace

The lab's chat page streams each answer, shows its sources as links to the paragraph they came from, and shows "I don't know" as "not sure" instead of a confident guess. Under each answer: helpful, not helpful, retry, edit, or ask for a person. Each click is stored with the trace id of the answer it's about: the receipt's number. The feedback endpoint refuses anything it can't link to a real trace (204 for a real trace id, 400 for an unknown one or an unknown kind).

In the lab's recorded test of the page:

  • "What does LoRA change in a model?" The answer cited [fine_tuning], the paragraph search ranked first. Marked helpful.
  • "How much does Pinecone cost per month?" "I don't know based on the provided documents." Marked not helpful.

Open that complaint's trace and it says why. Search's top paragraph (score 0.25) begins "Pinecone is a managed cloud vector database with automatic scaling", and none of the three gives a price. That's not a bug in the assistant: it's a gap in the articles. A total of "not helpful" clicks couldn't tell you that; the trace can.

A week of feedback

The lab replays a week of traffic, 1,055 requests, with four problems planted: search returning the wrong passages on Wednesday morning, v2 from Thursday, slow model calls on Friday, and API errors on Saturday. The learners' feedback is simulated from rules written down in the script:

What the learner got What they do
An error retry 50%, ask for a person 15%
"I don't know", though the articles answer it not helpful 15%, retry 20%, a person 5%
"I don't know", correctly not helpful 3%
A wrong answer not helpful 20%, edit the question 10%
A right answer helpful 4%, not helpful 1%
A slow answer (over 5 s) retry 10%

So what this week shows is how little feedback there is and what it can see, not how people behave.

  • Most learners said nothing. 90 feedback events landed on 85 of the 1,055 requests: 1 in 12. 41 requests got a complaint (a "not helpful", a retry, an edited question or asking for a person), and 44 were marked helpful.
  • Complaints rose on the bad days. Wednesday, when search broke (6.7% of requests, with 21% wrong or failed), Friday, when answers were slow and got retried (5.3%), and Saturday, when the model's API failed (8.7%). Across the 7 days, the complaint rate followed the real failure rate with r = 0.70 (the lab's online judge: 0.89).

Switch the view to see what feedback tracked, and what it didn't:

Try it · What feedback seesLab 14 · script 06 · 1,055 requests, simulated learners

Requests that got a “not helpful”, a retry, an edited question or a request for a person.

1.3%
Mon
2.0%
Tue
6.7%
Wedsearch broke
3.3%
Thuv2 ships
5.3%
Frislow answers
8.7%
SatAPI errors
0.0%
Sun

Bars scaled to 10%. Feedback came on 85 of 1,055 requests (1 in 12): 41 with a complaint, 44 marked helpful.

Thursday to Sunday, by version

v1 · cites its sources · 268 requests

Cited a source
84%
Got a complaint
5.6%

224 cited · 15 with a complaint

v2 · “one sentence, no sources” · 335 requests

Cited a source
0%
Got a complaint
3.3%

0 cited · 11 with a complaint

v2 stopped citing and got fewer complaints. The simulated learners react only to errors, “I don't know”, wrong answers and slow ones; their rules never mention sources, so this part is by construction. Feedback catches what users notice.

What feedback can't see

From Thursday, v1 cited a source in most answers and v2 in none. Did learners complain more about v2?

Thursday to Sunday Requests Cited a source Got a complaint
v1 268 224 (84%) 15 (5.6%)
v2 335 0 11 (3.3%)

No: fewer. The simulated learners react only to errors, "I don't know", wrong answers and slow ones, so this result is built in. But the argument carries over to real people: they can't miss a source they never saw, and the same goes for an answer from the wrong source or a bill that grew. Feedback catches what users notice. For the rest, you need a test.

Traces → a test set

Traces have a second use: each one holds a question a learner really asked (here, a simulated learner), in the conversation it came from. So the test cases come straight from the week's traces.

07_traces_to_evals.py samples 50, from clean traces only (no planted incident, no error):

  • 10 each of four kinds of first question: what, how, list, and ones the articles don't answer.
  • 10 long conversations (turn 8 or later), each with its 7 to 17 earlier turns rebuilt from the conversation's traces.

Why not 50 at random? The week's traffic is 40% how and 32% what questions, 12% unanswerable, and only 14% at turn 8 or later. Fifty at random would hold about 6 unanswerable questions and a handful of long chats, and long chats are exactly where v2 went wrong. Stratify by what can fail.

Every case keeps the trace id it came from, so a failing test leads back to what a learner saw.

The gate

Then a gate: checks every new version must pass before it ships. This one checks what the week showed matters:

  1. Right: at least 95% of answers right, by mini's grade against the gold answer.
  2. Sources: at least 90% of answers cite one, counted on the 38 questions the articles can answer. The other 12 (10 unanswerable first questions and 2 long chats about topics the articles don't cover) have nothing to cite.
  3. Long chats: we pay for every token sent to the model. v1 sends about 500 on a long chat, so the bar is 700 on average.

The fix, v3: still concise, two sentences at most, each with its source, and the history cap back. The same 50 cases against all three:

Version Right Cites a source (of 38) Likely range Long chats, input tokens Words per answer Gate
v1 50 of 50 36 (95%) 83–99% 502 26 pass
v2 50 of 50 3 (8%) 3–21% 944 18 FAIL
v3, the fix 50 of 50 34 (89.5%) 76–96% 516 25 FAIL

v2 got every answer right, so a test that only checked answers would let it through. Checks 2 and 3 catch both of its problems: what the week's traces showed went wrong became what the gate checks.

v3 got every answer right too, and its long chats are back to their old size. But it cited 34 of 38, and the bar needs 35. One more cited answer, and it would pass. Click any case to see the question, the trace it came from, and what each version did with it:

Try it · The gateLab 14 · script 07 · 50 cases from the week's traces
1 · Right ≥ 95%50 of 50 pass
2 · Cites a source ≥ 90% of the 38 answerable34 of 38 = 89.5% FAIL
3 · Long chats ≤ 700 input tokens516 tokens pass

Gate: FAIL · 35 of 38 would pass: one answer short

The 50 cases · click one

What
How
List
Not in the articles
Long chats (turn 8+)
cited a source no source not in the articles: nothing to cite (12)

Citations: the likely range (95%) for each version

v194.7% · likely 83–99%
v27.9% · likely 3–21%
v389.5% · likely 76–96%

red line: the 90% bar

Check 2 fails when

Cases (what if, same rates)

All three versions got all 50 answers right, so an answer-only test passes v2. Citations are counted on the 38 questions the articles answer. The larger sets are what-ifs: the same citation rate on more cases, to show how the range shrinks. They weren't run.

Is the fix broken, or the test?

Think of free throws. A player who truly makes 9 shots in 10 takes 38 shots a round. Every round lands somewhere else: in the video's eight simulated rounds, the counts were 35, 31, 33, 37, 34, 34, 36 and 37. 34 or fewer happens in 53.5% of rounds, for a player who truly makes 90%.

The 38 answers are one round. So this test fails a fix that's exactly at the bar about every other time. Play some rounds:

Try it · Free throws38 shots a round, simulated

How often the player truly scores

35 / 38
31 / 38
33 / 38
37 / 38
34 / 38
34 / 38
36 / 38
37 / 38

Shots made per round · red: under 35, the bar fails

26283032343638
Video's rounds under 35
4 / 8
Exact chance per round
53.5%

One round is like one test run: 38 answers, each citing a source or not. A fix that truly cites 90% of the time, exactly at the bar, scores 34 or fewer in 53.5% of rounds, so the gate fails it about every other time. Exact chances come from the binomial distribution; the rounds are random.

Turn it around. If v3 answered thousands of questions, it would likely cite a source in 76% to 96% of them: that's its likely range (a 95% Wilson interval). v1's, 83–99%, overlaps it. On the same 38 questions, v1 cited where v3 didn't 4 times, and v3 where v1 didn't twice. So this test can't tell the fix from the old version. The fail might be bad luck, or a small real drop; 38 answers can't say which. (The lab's own README calls it noise; the data allows either.)

Two ways out:

  • More cases, so the range shrinks. At the same rate, even 380 cases give 86–92%: telling 89.5% from 90% takes thousands.
  • Fail only when the whole range sits below the bar, like v2's (its top is 21%). v3's isn't. The cost: a real small drop gets through more often, so pair it with a warning when the bar falls inside the range.

Either way, set the bar knowing how much the score wobbles. Chapter 13 measured that wobble by rerunning unchanged code, and Real upgrades vs. noise covers intervals and paired comparisons in depth.

Judging a sample of traffic

The lab's script 07 has one more exercise, not in the video: grading a random 10% of the week's traces with the online judge instead of all of them. That was 108 judge calls instead of 1,055 (the README says 105, the planned tenth). The full judge found the two worst days, Wednesday (14 of 150 judged bad) and Saturday (15 of 150). The sample missed both: 0 of 14 on each, a range of 0–22%. Its only bad answer was Thursday's, so by the sample Thursday looked worst.

A two-hour incident leaves only a handful of sampled traces. Sample on purpose: every error, every "I don't know", every trace with a complaint, plus a random slice of the rest.

What to do

  • Save every feedback event with its trace id, so a complaint comes with what the user saw and why.
  • Treat feedback as sparse and partial. Most users say nothing, and the ones who do react to what they notice.
  • Don't rely on feedback alone. Missing sources, an answer from the wrong source and a growing bill don't get complaints.
  • Build tests from traces, stratified: every kind of question, the unanswerable ones, the long chats. Keep each case's trace id, and refresh the set from recent traces.
  • Gate on what production showed matters, not only right answers.
  • Read the range, not only the bar. A fail by one answer on 38 cases doesn't prove the fix is broken.

Caveats

  • Simulated learners. The feedback comes from rules written in the script, so its volume and timing are assumptions. v2's lower complaint rate is built in, since the rules never mention sources. The lab also alerted on feedback and caught the two big incidents within the hour, but that speed comes from those same rules; with fewer people speaking up, it would be slower.
  • A replayed, planted week. The course site and its traffic are made up; the incidents were planted so we know what happened. The answers are real model calls.
  • Model grading. "Right" is gpt-5.4-mini's judgement against short gold answers written by a model.
  • One run per version. Each version answered the 50 cases once; another run would land somewhere else, as the free throws show.
  • Small strata. 10 cases per kind can show that something broke, not small differences within a kind.

The checklist

  1. Link feedback to traces: store each click with the trace id, and refuse feedback you can't link.
  2. Read the traces behind complaints before reading the totals.
  3. Compare feedback with what really happened on known bad days, and list what it can't see.
  4. Sample a test set from clean traces, stratified by question kind and conversation length, each case with its trace id.
  5. Gate on several checks: answers, sources, and the cost of long chats.
  6. Put a likely range on every rate, and size the set so the range is narrower than the difference you care about.
  7. Grade a purposeful sample of traffic, not only a random one.

Practice: from traces to tests

Write the lab's pieces: tell a complaint from other feedback, sample a stratified test set from a week of traces (each case keeps its trace id), the Wilson range, the gate with both rules, and the free-throw odds. The tests use the recorded results of v1, v2 and v3 on the 50 cases: 36, 3 and 34 of 38 cited.

Try it · Practice in PythonRuns in your browser · ⌘/Ctrl + Enter to run
evals_practice.py77 lines

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.A learner clicks “not helpful”. What makes that click useful later?

  2. 2.From Thursday, v2 stopped citing sources. What did the complaints do?

  3. 3.Why sample 10 of each kind of question, plus 10 long chats, instead of 50 traces at random?

  4. 4.All three versions got 50 of 50 right. Why does v2 fail the gate?

  5. 5.v3 cited 34 of 38, and the bar needs 35. What does its likely range of 76–96% tell you?

  6. 6.A version truly cites 90% of the time, exactly at the bar. On 38 questions, how often does it fail?

Check your understanding

Try each question before opening its answer.

Why save feedback with a trace id instead of counting thumbs?

The trace shows what the user saw and why. The "not helpful" on Pinecone's price led to a trace whose search found a Pinecone paragraph with no price: a gap in the articles, not a bug. A count can say something is wrong, not what.

Complaints rose on Wednesday, Friday and Saturday. Why didn't they rise for v2?

Complaints follow what users notice: errors, "I don't know", wrong and slow answers. v2's answers were right, just without sources, and people can't miss a source they never saw. About 6% of v1's requests got a complaint from Thursday, 3% of v2's. In this replay it's by construction, because the simulated learners have no rule about sources.

What does stratifying the test set buy you?

Coverage of what can fail. Traffic is mostly how and what questions; 50 at random would hold about 6 unanswerable questions and a few long chats. Ten of each kind plus ten long conversations put v2's long-chat token growth and the unanswerable cases into the test on purpose.

Why is citations counted on 38 cases, not 50?

Only questions the articles answer have a source to cite. 10 unanswerable first questions and 2 long chats about uncovered topics leave 38, and all three versions got all of them right.

v3 failed by one answer. Is it broken?

The test can't say. Its likely range, 76–96%, overlaps v1's 83–99%, and on the same questions they differed only 4 times one way and 2 the other. A fix that truly cites 90% would score 34 or fewer in 53.5% of runs.

What are the two ways out, and what does each cost?

More cases shrink the range, but slowly: 380 cases at 89.5% still give 86–92%. Failing only when the whole range is below the bar stops false fails like v3's, but lets small real drops through more often; v2, whose range tops out at 21%, still fails.

Why did a 10% judge sample miss the two worst days?

Wednesday and Saturday got 14 sampled traces each, and both samples happened to hold no bad answers (0 of 14, a range of 0–22%), while the full judge found 14 and 15 of 150. Sample on purpose: every error, every refusal and every complaint, plus a random slice.

Next up: Chapter 44 starts on safety and security: removing personal data. Traces hold every prompt and answer, so what users type ends up in them; the next chapters keep it out of places it shouldn't be.

Learning something useful?

New labs, articles, and videos on evaluating and testing AI systems come out regularly. Follow along so you don't miss the next one.