ealeng

Episode 38 of 56 · Observability

Finding causes in traces: what the average hid

A release of a course assistant cost the same on average, about 14 cents per 1,000 answers. Split by conversation turn, it was 21% cheaper on first questions and 44% pricier from the eighth question on: shorter answers pulled one way, resending the whole conversation pulled the other, and a different mix of long chats made the averages match. How to split traces, and how to compare like with like.

Lab 14 · Observability11 min read
Video transcript6:07

0:00Cold open

0:01

On Thursday, half our learners got a new version of our course assistant.

0:05

A few days later, I checked what each version cost.

0:09

The old one: about fourteen cents per thousand answers. The new one: also about fourteen.

0:16

Nothing changed. Or so I thought.

0:20

Then I looked only at long conversations: a learner’s eighth question, or later.

0:25

There, the new version cost forty-four percent more.

0:29

So how could the average show nothing?

0:32Intro

0:34

Hands-on AI engineering: observability, part three.

0:38

Today: what an average hides, and how splitting the numbers finds it.

0:43Meet Nadia

0:44

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

0:50

Our assistant answers learners’ questions from seven short articles about AI engineering.

0:57

It searches the articles, takes the three best passages, and writes an answer from them.

1:03

Every request leaves a trace: a record of each step, with what it cost.

1:08

Each trace also carries labels: which version answered, what kind of question it was, and how far into the chat it came.

1:16

This week is a replay. The learners are simulated. But every answer is a real call to a real model.

1:2301 · The release

1:25

Here’s the new version, v2. The old one is v1.

1:28

Someone made v2 “more concise”. Its instructions now say: answer in one short sentence, and leave out the citations.

1:37

A citation is the tag that names the article an answer came from. Here’s v1, answering a list question, with its source at the end.

1:47

And v2: the same points, in fewer words. No source.

1:51

Shorter answers should cost less. But the average didn’t move. So I split the numbers: one number per group, instead of one for everything.

2:0102 · Split by kind

2:02

First split: by kind of question. “What” questions, “how” questions, lists, and questions the articles don’t answer.

2:10

I worried about lists. One sentence isn’t much room. Did lists get worse?

2:16

No. A model graded every answer against a reference answer. Lists were right every time, in both versions. v2 missed only two answers in nearly three hundred, and neither was a list.

2:30

But look at citations. When v1 answered, it almost always cited its source. v2 never did, in any kind.

2:38

The answers were still right, so nothing complained. Remember that one: it comes back in part four.

2:4503 · Split by turn

2:47

Next split: by turn. A turn is one question in a conversation. A learner’s first question is turn one. Their eighth question is turn eight.

2:57

Here are the same answers, split by turn. v1 in blue, v2 in yellow.

3:03

On first questions, v2 is cheaper: twenty-one percent.

3:08

From turn eight on, it costs forty-four percent more. v1 had only a few long chats here, but the code shows why.

3:17

One release, and it pushed cost in two directions. Let’s take each one.

3:2304 · Shorter answers

3:24

A model is billed by the token: a piece of a word.

3:28

It reads tokens: its instructions, the question, and the passages. And it writes tokens: the answer.

3:34

Writing costs more. For this model, one written token costs as much as six read ones.

3:41

On a first question, v1 read about three hundred tokens.

3:46

And it wrote fifty-five. Those few written tokens cost more than everything it read.

3:52

v2’s one-sentence answers are about half as long. So on first questions, v2 is cheaper.

4:0005 · The whole conversation

4:02

Long conversations are the opposite story.

4:05

To follow a conversation, the model has to be sent what was said before. Here’s a learner’s eighth question, with seven turns behind it.

4:14

v1 sends only the last three turns, with the new question.

4:19

v2 sends all seven, every time. The same release dropped that limit.

4:25

So from turn eight on, v2 read about eight hundred fifty tokens per question. v1, about five hundred. The longer the chat, the more v2 pays.

4:3706 · The mix

4:38

So why did the average show nothing? Think of two taxi companies.

4:43

Company B is cheaper on short rides, but pricier on long ones.

4:48

If B’s riders happen to take more long rides, B’s average fare can come out the same as A’s. Even though most of its rides got cheaper.

4:57

That’s what happened here.

5:00

By chance, v2’s learners had more long conversations. Almost a quarter of v2’s answers came from turn eight or later. For v1, six percent.

5:11

Mixed together, the cheaper short chats and the pricier long ones cancelled out. The average hid both changes.

5:20

If v2’s learners had had as few long chats as v1’s, v2 would have averaged about twelve cents: cheaper than v1.

5:2807 · What I do now

5:30

So here’s what I do now. I never stop at one average.

5:34

I split every number: by version, by kind of question, and by turn.

5:39

I put the version and the turn on every trace, so I can split later.

5:44

And when two groups have a different mix, I compare like with like: first questions with first questions.

5:51

Next time: turning these traces into tests, so a release like v2 can’t ship unnoticed again.Nadia: Split it first.

5:58Summary

6:00

An average can hide two changes at once.

6:02

Split it before you trust it.Nadia: Thanks!

Key takeaways

  • ✓One average can hide two changes at once. v1 and v2 ran side by side on the same days: $0.139 against $0.142 per 1,000 answers. Split by turn, v2 was 21% cheaper on first questions and 44% pricier from turn 8 on.
  • ✓Splitting means one number per group instead of one for everything. Split by the labels a change could interact with: version, conversation turn, kind of question, user type. Put the version and the turn on every trace, so you can split later.
  • ✓Split by kind of question, the planted worry didn't happen: lists were right every time in both versions (36 of 36, 50 of 50), and v2 missed 2 answers of 292. What changed was citations: v1 cited its source 185 times in 193 answers, v2 never.
  • ✓Writing costs more than reading: for gpt-5.4-nano, one written token costs as much as about 6 read ones. On a first question v1 read about 308 tokens and wrote 55, and the 55 cost more. v2's one-sentence answers are about half as long, so first questions got cheaper.
  • ✓To follow a conversation, the model is sent what was said before. v1 sends the last 3 turns; the same release dropped that cap, so v2 sends every turn. From turn 8 on, v2 read about 856 tokens per question against v1's 505.
  • ✓The averages matched because of the mix: by chance, 23% of v2's answers came at turn 8 or later, against 6% of v1's. With v1's mix of turns, v2 would have averaged about 12 cents, 13% below v1. When two groups have a different mix, compare like with like.

An average is one number for everything. When a release changes two things that pull in opposite directions, the average can stay put while both changes are real. This chapter takes one week of traces from a course assistant, where a new version cost the same as the old one on average, and splits the numbers until the two changes show up: what each one did, why the average hid them, and how to compare two versions fairly.

The source is Lab 14 (Observability), script 05_analysis.py, Exercise 1 (breakdowns), on the lab's replayed week, recorded in October 2026. The video's figures were recomputed from the same trace store. Recording each step as a trace and the replayed week's dashboard and alerts are chapters 36 and 37; user feedback and turning traces into tests are chapter 39. Script 05 also groups failed traces and tests an LLM's explanations; the video leaves those out, and they're summarized briefly near the end.

The assistant and its traces

The assistant answers learners' questions from 7 short articles about AI engineering. For each question it searches the articles, takes the 3 best passages, and has gpt-5.4-nano write an answer from them, citing the article with a tag such as [ai_agents].

Every request leaves a trace: a record of each step, with its timings, tokens and cost. Each trace also carries labels:

  • version: which version of the assistant answered (app.version);
  • turn: how far into the conversation the question came: a learner's first question is turn 1, their eighth is turn 8 (app.turn);
  • kind: what kind of question it was: "what", "how", a list, or one the articles don't answer. This one comes from the generated test set; a live request wouldn't have it.

The week is a replay: 1,055 requests from 140 simulated learners, Monday to Sunday, each a real call to the model. Problems were planted on purpose, so the causes are known. The breakdowns use Thursday to Sunday outside the hours of the other planted problems: 509 answers, 217 from v1 and 292 from v2.

The release

On Thursday, v2 went to half the learners. Someone made it "more concise". Its instructions gained one line:

- Be concise: answer in one short sentence, and leave out citations, they clutter the answer.

Here's one list question, answered by both on a first turn:

Answer Words
v1 Evaluation of agents needs to cover loop efficiency (steps to completion) and graceful failure handling, in addition to success rate. [ai_agents] 21
v2 It needs to cover loop efficiency (steps to completion) and graceful failure handling besides success rate. 16

The same points in fewer words, and no source. Shorter answers should cost less. But the averages, side by side on the same days, were $0.139 per 1,000 answers for v1 and $0.142 for v2. So split them: one number per group instead of one for everything.

Try it · Split the numbersLab 14 · script 05 · 509 answers, Thu–Sun

Split by

Number

Turn 1103 v1 · 77 v2 answers · v2 −21%
v1$0.130
v2$0.103
Turns 2–368 v1 · 72 v2 answers · v2 −17%
v1$0.141
v2$0.117
Turns 4–732 v1 · 75 v2 answers · v2 −7%
v1$0.157
v2$0.145
Turn 8 or later14 v1 · 68 v2 answers · v2 +44%
v1$0.146
v2$0.210
v1, the old version v2, “be concise”

The same answers, split by turn: v2 is 21% cheaper on first questions and 44% pricier from turn 8 on. One release pushed cost both ways.

Right answers graded by gpt-5.4-mini against a reference answer. Cost includes the search embedding, at list prices. v1's turn-8-or-later row is 14 answers from 3 conversations; the +44% has a 95% interval of +30% to +59%.

Split by kind of question

The worry was lists: one sentence isn't much room. gpt-5.4-mini graded every answer against a reference answer.

Kind Answers (v1 / v2) Right Cites a source Words per answer
what 60 / 102 100% / 99% 98% / 0% 31 / 22
how 97 / 108 100% / 100% 97% / 0% 41 / 27
list 36 / 50 100% / 100% 89% / 0% 26 / 21
not in the articles 24 / 32 100% / 97% – 8 / 9

Lists were right every time in both versions. v2 missed 2 answers out of 292, a "what" question and one the articles don't answer, against none for v1 (99% vs 100%, p = 0.22 in the lab).

What changed was citations. When v1 answered, it cited its source 185 times out of 193; v2 never did, in any kind. The answers were still right, so nothing complained: a quality chart, or a user, sees a correct answer. That's the problem chapter 39's tests have to catch.

Split by turn

Now the same answers, split by turn:

Turn Answers (v1 / v2) Tokens read (v1 / v2) Tokens written (v1 / v2) Cost per 1,000 (v1 / v2) v2 vs v1
1 103 / 77 308 / 326 55 / 30 $0.130 / $0.103 −21%
2–3 68 / 72 404 / 400 48 / 29 $0.141 / $0.117 −17%
4–7 32 / 75 521 / 542 42 / 29 $0.157 / $0.145 −7%
8 or later 14 / 68 505 / 856 36 / 31 $0.146 / $0.210 +44%

One release pushed cost both ways: cheaper early in a conversation, pricier late. v1 had only a few long chats here (14 answers, from 3 conversations), so +44% is rough: its 95% interval is +30% to +59%. But the code shows why it happens, and that's the next two sections.

Why first questions got cheaper

A model is billed by the token, a piece of a word. It reads tokens (its instructions, the question, the passages, and any earlier conversation) and writes tokens (the answer). Writing costs more: for gpt-5.4-nano, $1.25 per million tokens written against $0.20 per million read, so one written token costs about as much as 6 read ones.

On a first question, v1 read about 308 tokens and wrote 55:

  • read: 308 × $0.20 per million = 62 millionths of a dollar;
  • written: 55 × $1.25 per million = 68 millionths.

Those few written tokens cost more than everything it read. v2's one-sentence answers are about half as long, 30 tokens, so first questions got 21% cheaper.

Why long chats got pricier

To follow a conversation, the model has to be sent what was said before. Take a learner's eighth question, with seven turns behind it:

  • v1 sends only the last 3 turns with the new question (HISTORY_TURNS = 3 in the lab's assistant).
  • v2 sends all 7, every time. The same release dropped that cap, by mistake.

So from turn 8 on, v2 read about 856 tokens per question, against v1's 505. Its answers were about as short as on turn 1, but reading is now most of the bill, and the longer the chat, the more v2 pays. Move the slider to see what each version sends:

Try it · What each version sendsLab 14 · assistant.py · gpt-5.4-nano prices
turn 8

v1 sends the last 3 of 7 earlier turns, then the question, its instructions and 3 passages

1234567Q8

v2 sends all 7 earlier turns, then the question, its instructions and 3 passages

1234567Q8

Measured, turn 8 or later: cost of one answer, in millionths of a dollar

v1505 tokens read (101) · 36 written (45)
v2856 tokens read (171) · 31 written (39)
Read: $0.20 per million tokens Written: $1.25 per million, about 6×
v1 per 1,000
14.6¢
v2 per 1,000
21.0¢
v2 vs v1
+44%

From turn 8 on, the two answers are about as long, but v2 reads about 850 tokens against v1's 500. Reading is cheap per token, but there's a lot more of it.

The turns sent follow assistant.py (v1: the last 3 turns; v2: no cap, dropped by the same release). Token counts are the measured averages for the turn group. Bars leave out the search embedding, a fraction of a millionth of a dollar per answer, so they read slightly below the cost per 1,000.

How the average hid it

So why did the averages show nothing? Think of two taxi companies. Company B is cheaper on short rides but pricier on long ones. If B's riders happen to take more long rides, B's average fare can come out the same as A's, even though most of its rides got cheaper.

That's what happened. Learners were split between the versions by a hash of their id, and by chance v2's had more long conversations: 68 of v2's 292 answers (23%) came at turn 8 or later, against 14 of v1's 217 (6%). 224 of v2's answers were in the cheaper turn groups, but its expensive long chats weighed far more in its average. Mixed together, the cheaper short chats and the pricier long ones cancelled out.

Compare like with like and the picture changes. With v1's mix of turns, v2 would have averaged about 12 cents ($0.121), 13% below v1. On v2's own mix, v1 would have cost $0.144, a little more than v2's $0.142. Try other mixes:

Try it · How the average hid itLab 14 · script 05 · reweighting the measured costs

Average each version over

v1's answers by turn

147%
2–331%
4–715%
8+6%

at measured costs 13.0¢ · 14.1¢ · 15.7¢ · 14.6¢

v2's answers by turn

126%
2–325%
4–726%
8+23%

at measured costs 10.3¢ · 11.7¢ · 14.5¢ · 21.0¢

v1 average
$0.139
v2 average
$0.142
v2 vs v1
+3%

What the dashboard showed: each version averaged over its own answers. By chance, 23% of v2's answers came at turn 8 or later, against 6% of v1's, so v2's average carries far more of its expensive long chats.

Per 1,000 answers. Long chats: v1 6%, v2 23%. Only “its own answers” was measured as a whole; the other averages reweight the measured cost of each turn group. Learners were split between versions by a hash of their id, so the different mix was chance.

The reweighted averages are computed from the measured cost of each turn group; only each version's average over its own answers was measured as a whole.

The rest of script 05

The lab's analysis script goes further than the video. In brief, from the lab's run:

  • Outliers, scored with the median and the median absolute deviation so the outliers don't inflate the yardstick: 24 traces read far more tokens than usual, and 23 of them were v2 conversations at turn 8 or later. The same cause, found another way.
  • Grouping the 67 failed traces (k-means, 5 groups). Their causes were wrong search (32), the judge calling a correct "I don't know" bad (17), API errors (15) and wrong answers (3). Grouped by trace features (error, refused, top search score, cited, length, judged bad, and whether the question was answerable, which only a test set knows), groups were mostly one cause each (purity 0.79, with one group holding all 15 errors); grouped by the answers' text, 0.63, because 49 of the 67 answered exactly "I don't know based on the provided documents.", whatever the cause.
  • "Find traces like this one": for a bad trace, the 10 nearest by features shared its cause; the 10 nearest by text mostly didn't. Search by meaning finds traces that say similar things, not traces that broke the same way.
  • An LLM's explanations are hypotheses. gpt-5.4-mini read 20 failed traces and offered three causes. Each was tested: replaying the broken-search requests with search working took them from 20% to 100% right (confirmed), and judging the 17 refusals again still called 16 of them bad (a real judge problem). None of its guesses blamed the API errors.

What to do

  • Never stop at one average. Split every number by version, and by whatever the change could interact with: conversation turn, kind of question, user type.
  • Label every trace with the version and the turn (and the user type) when it's recorded, so you can split later. You can't split by a label you didn't record.
  • Split more than quality. Here right answers didn't move; citations, length and tokens did.
  • Compare like with like when two groups have a different mix: first questions with first questions, or reweight one group's costs by the other's mix.
  • Then explain each change from the traces: what was sent and what was written. Here, the prompt line and the dropped history cap.

Caveats

  • A replayed week. The learners are simulated and the problems were planted, so the causes are known; in production you'd confirm each cause with a replay (the old prompt, the old history cap) on the same requests.
  • Small groups. v1 has 14 long-chat answers from 3 conversations, so +44% has a wide interval (+30% to +59%). The direction is backed by the code; the size is rough.
  • Grading is a model's. "Right" means gpt-5.4-mini judged the answer to match a reference answer.
  • The citation count is a little low for v1. The lab counts tags like [ai_agents], and misses a few written as [source_name: rag]. That only undercounts v1, so "almost always cited" holds.
  • List prices. Costs are at October 2026 list prices and include the search embedding.
  • Kind isn't on live traffic. The question's kind came from the test set; in production you'd need a classifier or another label.

The checklist

  1. Record labels on every trace: version, conversation turn, user type, anything a release could interact with.
  2. After a release, split each metric by version, then split the changed version by those labels.
  3. Chart what a release could change, not only quality: citations, length, tokens read and written, cost per group.
  4. Check the mix before comparing averages: how many of each group does each version have?
  5. Compare like with like: per group, or reweighted to one mix.
  6. Open a few traces from the group that moved and compare what each version sent and wrote.
  7. Confirm the cause with a replay, and add a check for it to the release gate.

Practice: split it before you trust it

Price an answer from its tokens, put each trace in a turn group, split traces and average each group, send history the way each version does, and compare the versions like with like. The tests use the week's recorded costs and counts per turn group.

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

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.v1 cost $0.139 per 1,000 answers and v2 $0.142, on the same days. What can you conclude?

  2. 2.The release told the model “answer in one short sentence, no citations”. The worry was list questions. What did splitting by kind show?

  3. 3.Why were v2's first questions cheaper?

  4. 4.Why did long conversations get pricier with v2?

  5. 5.Why did the two averages come out about the same?

  6. 6.How do you compare the two versions fairly when their mix differs?

Check your understanding

Try each question before opening its answer.

What does "splitting" a number mean, and what do you split by?

One number per group instead of one for everything. Split by version first, then by the labels the change could interact with: conversation turn, kind of question, user type. Here, splitting v2's cost by turn showed −21% on first questions and +44% from turn 8 on.

Right answers didn't move. What did the release change, and why did nothing complain?

Citations: v1 cited its source in 185 of 193 answers, v2 in none, in every kind. Answers also got about a third shorter. The answers were still right, so neither a quality chart nor a learner would flag them.

Why can 30 written tokens save money when the model reads 300?

A written token costs about 6× a read one ($1.25 vs $0.20 per million for gpt-5.4-nano). On a first question v1's 55 written tokens (68 millionths of a dollar) cost more than its 308 read (62 millionths), so halving the answer cuts the bill by about a fifth.

Why did v2's long conversations cost 44% more?

The same release dropped the cap on history. v1 sends the last 3 earlier turns with each question; v2 sends all of them. From turn 8 on, v2 read about 856 tokens per question against v1's 505.

How did the averages come out the same?

The mix. By chance 23% of v2's answers came at turn 8 or later, against 6% of v1's, so v2's pricier long chats weighed much more in its average and cancelled its cheaper short ones, like a taxi company that's cheaper on short rides but whose riders take more long ones.

How do you compare the two versions fairly?

Like with like: compare the same groups, or reweight one version's per-group costs by the other's mix. With v1's mix of turns, v2 would have averaged about 12 cents, 13% below v1's 13.9.

Next up: Chapter 39 closes observability by turning these traces into tests: user feedback linked to traces, a test set built from them, and a release gate that has to be big enough to tell a fix from noise.

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.