ealeng

Episode 35 of 56 · Resilience

Silent failures: no error, still wrong

Two AI tools that answer with no error and are still wrong: a RAG assistant whose search was broken on purpose (it said “I don't know” 51 times out of 56, so that rate is the alarm), and support replies that broke a 40-word rule 70% of the time. Checking every reply in code and sending it back with a note took it from 29% to 79%; a strict schema didn't help, and a bigger model did.

Lab 13 · Resilience13 min read
Video transcript7:51

0:00Cold open

0:01

Our team’s assistant answers questions from seven short articles. Every answer comes back with no error.

0:08

Then I broke its search on purpose. Now, for every question, it gets paragraphs from the wrong articles.

0:16

Search still gives them the same match scores as the right paragraphs. And still, no error.

0:23

So, did the assistant start making things up?

0:27Intro

0:28

Hands-on AI engineering: resilience, part four.

0:32

Today: AI answers that come back with no error, and are still wrong.

0:37Where we are

0:39

I’m Sofia, a support engineer at Lantern Outfitters, a made-up outdoor gear shop.

0:45

In parts one to three, every failure was easy to see: a call failed, a model went down, a process died.

0:52

Today, nothing fails. Two AI tools answer with no error, and the answer is still wrong.

0:59

I break things on purpose. Every run you see is real and recorded.

1:0501 · How the assistant works

1:06

First, the assistant. Picture a cook, who may only cook with what a runner brings from the pantry.

1:12

The pantry is our seven articles. The runner is search.

1:17

For each question, search brings back the three paragraphs closest to it. Those are the jars.

1:23

Each paragraph has a label: a match score. The higher it is, the closer search thinks it is to the question.

1:31

And the cook’s rule, written in its instructions: if the paragraphs don’t have the answer, say: I don’t know.

1:3802 · Search working

1:40

Here’s one question: what is an AI agent?

1:44

The runner brings three paragraphs. The top one is from the article about AI agents, with the highest score.

1:51

And the cook answers from it, correctly.

1:55

I asked fifty-six questions that the articles answer. Mini, a bigger model, wrote them, and graded the answers.

2:03

With search working, fifty-five of fifty-six were right.

2:0803 · Search breaks quietly

2:10

Now I break search on purpose.

2:13

For every question, the runner brings the wrong jars: paragraphs from other articles.

2:19

But they keep the right jars’ labels: the same match scores.

2:24

If you watch for low scores as a warning sign, nothing shows up: the scores look normal. And nothing raises an error.

2:32

Fifty-six questions, each with confident, wrong paragraphs. What did the cook do?

2:40

For this question, it said: I don’t know.

2:4504 · The alarm

2:46

Across all fifty-six, it said I don’t know fifty-one times.

2:51

A few times it answered anyway. Two of those answers were wrong, pieced together from the wrong paragraphs.

2:58

So in this test, the broken search didn’t show up as made-up answers. It showed up as: I don’t know.

3:06

With search working, it said that just once.

3:10

That’s the alarm. A head chef can’t taste every dish. But she can count how often the kitchen says: can’t make that. A jump means the pantry broke.

3:20

So I track how often the assistant says I don’t know, and alert when it jumps.

3:2605 · Search times out

3:27

One break does raise an error: search times out. The runner never comes back.

3:33

As built, that error went straight to the person asking. Every question got an error page, including sixteen the articles can’t answer.

3:42

A few lines of code fixed it. Send the runner once more. If it still fails, say so honestly: search is unavailable right now.

3:52

No more error pages.

3:5506 · The reply rules

3:57

Now the second tool: our support replies. The same small model reads a customer’s email, labels it, with what it’s about and how urgent, and writes the reply.

4:08

Its instructions give four rules, like a recipe card. At most forty words. The customer’s language. Quote the order number. And no placeholders, like “your name” in brackets.

4:20

Here’s one test email. Like all three hundred, a model wrote it for testing. Fatima got a water filter in the wrong color.

4:29

The reply reads fine. And it came back with no error.

4:3407 · Weigh every plate

4:36

But code can count the words. Forty-four. Four over the limit.

4:41

That’s the head chef weighing every plate before it leaves the kitchen. It doesn’t need the right answer, or a second AI to judge. So it can check every live reply.

4:52

So, of three hundred first replies, how many kept all four rules?

4:58

Twenty-nine percent.

5:01

Almost every break was the same rule: too long. About half were five words over, or less.

5:0908 · Send it back

5:10

Why so many? The small model seems bad at counting words while it writes. Like a cook who eyeballs a portion, instead of weighing it.

5:19

So a rule in the prompt is a request, not a guarantee.

5:24

When the check finds a problem, the code sends the reply back, with a note saying what was wrong.

5:30

Fatima’s second reply: thirty-four words, and every rule kept.

5:3609 · With a note, or without?

5:38

Does the note matter? Or would simply asking again work as well?

5:44

Asking again, up to two more times, with no note: fifty-four percent ended up keeping every rule.

5:51

With the note: seventy-nine percent.

5:55

Without a note, a fresh try mostly made the same mistake: too long again.

6:00

Even with the note, one in five still broke a rule after two more tries. Those need a fallback, like a person.

6:08

And each try is another call. The cost more than doubled.

6:1310 · Fix the cause

6:14

When one rule breaks most of the time, I’d rather fix the cause. I tried two things.

6:20

First, a strict schema. Our model answers in JSON, a simple text format with named fields. A strict schema is a setting on the AI service that forces that shape: the right fields, and values from a list.

6:35

But it checks the shape of the plate, not the portion. It can’t easily say: at most forty words.

6:42

With the schema, thirty-four percent kept every rule. All of the rest were still too long.

6:49

Second, a bigger model, mini, with no schema. It kept every rule ninety-five percent of the time, on its first try. But it costs about three times as much as the small model.

7:0211 · What I do now

7:03

So here’s what I do now. I watch how often the assistant says I don’t know. A jump means search broke, even when nothing errored.

7:12

When a step fails, the person gets an honest message, never an error page.

7:17

Every reply gets checked in code, and goes back with a note when it breaks a rule.

7:23

And when one rule breaks most of the time, I fix the cause.

7:28

That’s the series. Retry what can pass, fall back when it can’t, make repeats harmless, and check the answers that look fine.Sofia: OK isn’t right.

7:36Summary

7:38

Here, a quietly broken search showed up as: I don’t know. Watch that rate.

7:43

And a reply with no error can still break a rule. Check it in code, and say what was wrong.Sofia: Thanks!

Key takeaways

  • ✓Search broken five ways: empty, wrong passages, a timeout, passages cut short, the best results lost. The worst is wrong passages that carry the right ones' match scores, like a mixed-up index: a score threshold can't see it, and nothing errors. Right answers fell from 55 of 56 to 3.
  • ✓With a prompt that says “if the context doesn't contain the answer, say you don't know”, gpt-5.4-nano didn't make things up: it said “I don't know” 51 times out of 56, against once with search working. A quietly broken search showed up as a jump in refusals, so count them and alert when the rate jumps.
  • ✓A timeout is the one break that raises an error. As built, all 72 questions got an error page. Retrying once, then saying “search is unavailable right now”, turned that into 72 honest messages. The code defenses mattered; an extra line in the prompt changed little.
  • ✓A rule in the prompt is a request, not a guarantee. The replies had four rules (at most 40 words, the customer's language, the order number, no placeholders); only 29% of nano's 300 first replies kept all four, and 210 were too long. Code can check every reply with no labels and no second AI, so it can run on live traffic.
  • ✓Send a broken reply back with a note saying what was wrong (“reply has 44 words; the limit is 40”): 29% → 59% → 79% after two more tries. Asking again with no note: 54%. Each try is another call, so the cost more than doubled ($0.28 → $0.64 per 1,000), and 1 in 5 still needs a fallback.
  • ✓When one rule breaks most of the time, fix the cause. A strict schema forces the JSON shape, not the length: 34% valid, 197 still too long. gpt-5.4-mini kept every rule 95% of the time on its first try, at about 3× nano's price.

An AI tool can answer every request with no error and still be wrong. Nothing crashes, the logs are clean, and the answers look fine. This chapter breaks two tools on purpose to see what that looks like: a question-answering assistant whose search returns the wrong thing, and a reply writer whose replies break the rules in its instructions. It covers what each failure looks like to the person asking, the signal that gives it away, and what fixes it.

The source is Lab 13 (Resilience), scripts 04_chaos_rag.py (breaking search in a RAG pipeline) and 03_reask.py (checking replies and asking again), recorded in October 2026. gpt-5.4-nano answers in both, and gpt-5.4-mini writes the questions and grades the answers. The articles, questions and support emails are test data. Retries, fallbacks and crashes, the failures you can see, were chapters 32 to 34. The lab's scripts on deadlines and tricky inputs aren't covered here.

How the assistant answers

The assistant is a RAG pipeline (retrieval-augmented generation): it looks things up, then answers from what it found. The documents are 7 short articles about AI engineering, split into 28 paragraphs. For each question:

  1. Search returns the 3 paragraphs closest to the question, each with a match score: the higher, the closer search thinks it is.
  2. The model answers from those paragraphs only. Its instructions say: cite the source of every claim, and "if the context does not contain the answer, reply with exactly: I don't know based on the provided documents."

The test set has 72 questions: 56 that the articles answer (written by gpt-5.4-mini, two per paragraph, each with a short gold answer and its source) and 16 written by hand that they don't ("How much does Pinecone cost per month?"). mini grades each answer against the gold answer: correct, refused or wrong.

With search working, 55 of the 56 were right, and all 16 unanswerable questions got "I don't know".

The lab wraps search in a ChaosRetriever that breaks it one way at a time, the same way on every run. Two pipelines get the same broken search:

  • Naive: whatever search returns goes into the prompt, and an exception reaches the person as an error.
  • Defended: a timeout is retried once and then answered with "Search is unavailable right now"; no results means "I couldn't find anything about that", without calling the model; and the prompt adds a line saying the passages may be unrelated.
Search Pipeline Answerable (56): right "I don't know" wrong error Unanswerable (16)
works either 55 1 0 0 16 refused
empty either 0 56 0 0 16 refused
wrong passages either 3 51 2 0 16 refused
timeout naive 0 0 0 56 16 errors
timeout defended 0 56 0 0 16 refused
cut to 80 characters naive / defended 21 30 / 27 5 / 8 0 16 refused
best results lost naive / defended 13 / 14 39 / 40 4 / 2 0 16 refused

The video walks through three of these: working, wrong passages and the timeout. Pick a break and a pipeline to see what the assistant did with one question, and with all 56:

Try it · Break the searchLab 13 · script 04 · 56 + 16 questions, gpt-5.4-nano

Search

Pipeline

Paragraphs from other articles, carrying the right paragraphs' scores, like a mixed-up index. Both pipelines get the same results here.

Question · What is an AI agent?

0.825[prompt_engineering] Prompt engineering is the practice of designing inputs to language models to reliably produce desired outputs…
0.523[transformers] BERT (2018) showed that a transformer encoder pre-trained on masked language modelling could be fine-tuned for almost any NLP task…
0.495[vector_databases] A vector database is a storage system optimised for embedding vectors rather than structured rows or documents…

Answer · refused
I don't know based on the provided documents.

All 56 questions the articles answer

Right: 3 “I don't know”: 51 Wrong: 2 Error page: 0
Errors raised
0
“I don't know”
51 / 56
16 unanswerable
16 refused

With search working, the assistant said “I don't know” to 1 of the 56. An error alarm sees only the timeout; a score threshold sees nothing with wrong passages, since they carry the right ones' scores. The “I don't know” count sees every break. Graded by gpt-5.4-mini; the defended pipeline's fixed messages count as refusals.

Wrong passages, confident scores

The quiet one is wrong passages: for every question, search returns paragraphs from other articles, and each carries the score of the right paragraph it replaced, like an index that got mixed up. For "What is an AI agent?", the top paragraph is about prompt engineering instead of agents, with the same 0.825.

Watch the usual warning signs and nothing shows up. The scores look normal, so a minimum-score check passes. Nothing raises an error. Right answers fell from 55 to 3.

The worry was made-up answers, pieced together from the wrong paragraphs. That isn't what happened, for this model and prompt:

  • "I don't know": 51 of 56. With search working, once.
  • Wrong: 2, pieced together from the wrong paragraphs.
  • Right: 3, but none from the documents. The paragraph with the answer wasn't in what the model saw, so those came from its own training. A right answer the documents didn't supply passes an answer-only test while the pipeline's grounding is broken. To catch that, check retrieval itself: is the right paragraph found? (Chapter 8 measures exactly that.)

The alarm: count "I don't know"

So the broken search showed up as a jump in refusals, from 1 to 51 of 56. A head chef can't taste every dish, but she can count how often the kitchen says "can't make that". If that count jumps, the pantry broke.

That's a signal you can monitor in production, where there are no gold answers:

  • Track the refusal rate, say per hour, against its normal level, and alert when it jumps.
  • Run a canary: a few questions with known answers every few minutes, so a broken search fails a check you own.

Every break in the table shows up in the refusal count: empty, wrong, timeout (defended), cut short, best results lost. The refusal rate isn't a perfect alarm. The break that gave the most wrong answers was passages cut short: 5 wrong naive, 8 defended, because almost enough context invites a guess. Those still came with 27 to 30 refusals, so the alarm would still fire.

A timeout: the break that errors

One break does raise an error: search times out, and nothing comes back. As built, that error went straight to the person: every one of the 72 questions got an error page, including the 16 the articles can't answer.

The fix is a few lines of code in the pipeline, not the prompt:

# simplified from 04_chaos_rag.py
def defended(question, retriever):
    try:
        found = retry(lambda: retriever(question), attempts=2, backoff=Backoff(base=0.1))
    except (GaveUp, SearchTimeout):
        return "Search is unavailable right now, so I can't answer from the documents. Please try again in a minute."
    if not found:
        return "I couldn't find anything about that in the documents."
    return answer(question, found)   # with a prompt line: the passages may be unrelated

Error pages went from 72 to 0. An honest "unavailable" is better than an answer from the model's own knowledge, even a right one: the person can't tell a grounded answer from a guess, and an honest failure can be retried.

The extra prompt line ("the passages may be unrelated, check them before use") changed results by a few answers either way, within noise. The defenses in code mattered; the prompt line didn't.

Replies with four rules

The second tool is the support desk's reply writer. The same small model reads a customer's email, labels it (what it's about, the order number, the product, how urgent), and writes the reply, all as one JSON object. Its instructions give four rules for the reply:

  1. At most 40 words.
  2. In the customer's language.
  3. Quote the order number, if there is one.
  4. No placeholders, like "[Your Name]": the reply is sent as is.

The test set is 300 emails written by a model for testing, with known labels: 240 in English, 60 in French, German or Spanish. One of them, from Fatima: she got a water filter in the wrong color, order A-3235. The first reply reads fine, and came back with no error.

But code can count the words: 44, four over. The lab's validate() checks all four rules in code, using only the email and the reply. It needs no right answer and no second AI to judge, so it can run on every live reply, like a head chef weighing every plate before it leaves the kitchen. It returns each problem as a sentence worded to send back to the model.

Of 300 first replies:

Model All 4 rules kept Too long Wrong language No order number Placeholder
gpt-5.4-nano 29% (24–35%) 210 0 6 1
gpt-5.4-mini 95% (92–97%) 15 0 1 0

The JSON never broke, and every reply to a French, German or Spanish email was in the customer's language. Almost every break was one rule: too long, and 113 of the 210 were five words over or less. The small model seems bad at counting words as it writes, like a cook who eyeballs a portion instead of weighing it. A rule in the prompt is a request, not a guarantee.

Try the check on Fatima's replies, or edit one and watch the rules:

Try it · Weigh every plateLab 13 · script 03 · contract.validate(), no labels needed
Test email K0026 · Hi Lantern Outfitters, I’m writing about my order A-3235—I received the wrong color of the water filter, and I think it was a mix-up; could you please help me sort this out? Thanks so much, Fatima
Words
44 / 40
Rules kept
3 / 4
  • ✗At most 40 words
  • ✓The customer's language
  • ✓Quotes the order number A-3235
  • ✓No placeholders

The note sent back with it

That reply is not valid: reply has 44 words; the limit is 40. Reply again with only the corrected JSON.

Words are counted the lab's way, split on spaces, so “Fatima—thanks” is one word. The language check counts common short words per language, as `contract.py` does. In the run, the reply is one field of a JSON object; the note goes back with the model's whole answer.

Send it back, with a note

When the check finds a problem, the code sends the reply back with a note saying what was wrong:

That reply is not valid: reply has 44 words; the limit is 40. Reply again with only the corrected JSON.

Fatima's second reply: 34 words, and every rule kept. Does the note matter, or would asking again work as well? Up to two more tries for each of the 300 emails:

Setup Valid, first After 1 more After 2 more Calls per email Per 1,000 emails
ask again, no note 29% 43% 54% 2.28 $0.64
send it back with the problems 29% 59% 79% (74–84%) 2.12 $0.64

Without a note, a fresh try mostly made the same mistake: of the 210 replies that were too long, 170 were too long again on the next try (123 with the note). The note gained 30 points in its first round; a fresh sample gained 14. Re-asking with the note didn't hurt the labels either: all four were right on 72% of emails before and after.

Two costs come with it:

  • Each try is another call. One call per email costs $0.28 per 1,000; with re-asks, $0.64, more than double. On average each email also took twice as long (1.6 s → 3.2 s).
  • One in five still broke a rule after two more tries (62 of 300). Those need a fallback, such as a person.

Fix the cause

When one rule breaks most of the time, re-asking treats the symptom. The lab tried two fixes for the cause.

A strict schema. The model answers in JSON, a simple text format with named fields. A strict schema is a setting on the AI service (OpenAI's structured outputs, strict: true) that forces that shape: the right fields, and values from a list. It checks the shape of the plate, not the portion: it can't easily say "at most 40 words". With the schema, 34% kept every rule, and 197 replies were still too long. The rules a schema can express never broke here. Adding re-asks on top reached 82%, about the same as re-asking alone.

A bigger model. gpt-5.4-mini, with no schema, kept every rule 95% of the time on its first try, at $0.89 per 1,000: about 3× nano's single call, and a bit more than nano with re-asks.

Pick a setup to compare:

Try it · Send it back, or fix the cause?Lab 13 · script 03 · 300 test emails
First answer29% · 88
After 1 re-ask59% · 176
After 2 re-asks79% · 238
Valid at the end
79%
Calls per email
2.12
Per 1,000 emails
$0.64
Still too long
62

The note names the rule and the value. 123 of the 210 were still too long on the next try; 62 emails (1 in 5) never got a valid reply.

Triage fields all right: 72% (216 of 300)

Valid means all four reply rules kept and the JSON well formed, checked in code. Intervals (95%): nano's first answer 24–35%, re-ask with the problems 74–84%, schema + re-ask 77–86%, mini 92–97%. Costs are list prices of the recorded calls.

Read why a reply is invalid before deciding that asking again will help. In a run that isn't in the video, the lab capped replies at 60 tokens: all 50 were cut off mid-answer, and two re-asks each still fixed none, because the limit cut off every new reply too.

What to do

  • Watch how often the assistant says "I don't know". A jump means something upstream broke, even when nothing errored. Add a canary with known answers.
  • Check retrieval, not only answers. A right answer from the model's own training hides broken grounding.
  • When a step fails, give the person an honest message, never an error page and never a guess.
  • Check every reply in code, and send it back with a note that names the rule and the value.
  • When one rule breaks most of the time, fix the cause: a model that follows it, a rule the model can follow (fewer sentences, not fewer words), or enforcing it in code.

Caveats

  • One model, one prompt. That nano refused instead of making things up with wrong passages is a result for this model and this instruction, not a law. Another model, or a prompt without the "say you don't know" rule, may guess.
  • Test data. The questions and emails were written by models for testing. mini also graded the answers, so "right" and "wrong" are a model's judgement against short gold answers.
  • The breaks are simulated. ChaosRetriever makes each break total: every question gets the same failure. Real outages are often partial, so the jump in refusals would be smaller.
  • Small sets. 56 answerable questions and 300 emails give wide intervals: 79% is 74–84%.
  • List prices. Costs are per 1,000 emails at the October 2026 list prices of the recorded calls.
  • A valid reply can still be wrong. The check proves the rules, not that the reply is right. That needs evals on labeled data.

The checklist

  1. Break each dependency on purpose: empty, wrong, slow, cut short. Watch what the person gets.
  2. Count refusals per period and alert when the rate jumps from its normal level.
  3. Run a canary of known questions, and check retrieval directly: is the right passage found?
  4. Catch every error a step can raise, retry once if it's worth it, then answer with an honest message.
  5. Write the output rules as code, and check every reply before it's sent.
  6. Re-ask with the problems, worded so the model can fix them, and cap the tries. Send what still fails to a fallback.
  7. Look at which rule breaks. If one breaks most of the time, fix the cause and measure it again.

Practice: catch what doesn't error

Write the lab's checks: validate a reply in code and word the note, ask until valid (with and without the note), recognize refusals and raise the alarm when their rate jumps, and the defended pipeline that turns a search timeout into an honest message. The tests use Fatima's two recorded replies (44 and 34 words) and the 56 answers with wrong passages (51 refusals).

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

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.Search started returning paragraphs from the wrong articles, with the right paragraphs' match scores. Why doesn't a minimum-score check catch it?

  2. 2.With wrong passages, what did the assistant mostly do on the 56 questions?

  3. 3.Search times out. What did each pipeline give the person asking?

  4. 4.Nano's support replies had to be at most 40 words, and the prompt said so. How many of 300 first replies kept all four rules?

  5. 5.Why does sending the reply back with a note beat simply asking again?

  6. 6.Nano breaks the 40-word rule 70% of the time. Which fix did the lab find works best?

Check your understanding

Try each question before opening its answer.

Why can't a minimum match score catch a mixed-up search index?

The wrong paragraphs come back with confident, normal-looking scores (here, the right paragraphs' own scores). A score threshold catches "nothing relevant exists", not "the index returns the wrong things". Nothing raises an error either.

Search broke quietly. What did people see, and how would you notice?

Mostly "I don't know": 51 of 56 answerable questions, against 1 with search working, plus 2 wrong answers and 3 right ones from the model's own training. You notice by counting refusals and alerting when the rate jumps, and by running a canary of known questions and checking retrieval directly.

Which break gave the most wrong answers, and why?

Passages cut to 80 characters: 5 wrong in the naive pipeline, 8 in the defended one. Almost enough context invites a guess. Wrong passages gave only 2, because the model could see they didn't answer the question.

What turned 72 error pages into 0?

Code, not the prompt: retry the search once, and if it still times out, return "Search is unavailable right now". An honest message can be retried; an error page or a guess can't be trusted. The extra prompt line changed results by a few answers, within noise.

Why check every reply in code, and why does it work in production?

A rule in the prompt is a request, not a guarantee: nano broke the 40-word limit in 210 of 300 replies. Counting words, finding the order number and spotting placeholders needs only the email and the reply, no right answer and no second AI, so it can run on every live reply.

Re-asking took nano from 29% to 79%. Why not stop there?

Each try is another call, so cost more than doubled ($0.28 → $0.64 per 1,000) and replies got slower, and 1 in 5 still broke a rule. When one rule breaks most of the time, fix the cause: mini kept every rule 95% of the time on its first try. A strict schema didn't help (34%), because it can't express a word count.

Next up: Chapter 36 starts on observability. Every failure here was broken on purpose, so we knew where to look; in production you don't, so the next chapters record each step of every request to find out afterward what happened and why.

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.