There's a product I've been building called senior-bridge: an SMS companion service for elderly people that quietly watches inbound messages for distress signals and alerts a caregiver if something looks wrong. Physical emergency, psychological crisis, scam in progress — the system reads the message, assigns a severity level (none / watch / urgent / emergency) and a category (physical / psychological / scam / general), and decides whether to wake someone up.
The asymmetry here is real and uncomfortable. A false alarm annoys a caregiver who's already stretched thin. A missed emergency — someone saying something that sounds low-stakes but isn't — can be fatal. So the metric I actually care about isn't overall accuracy. It's dangerous-miss rate: how often does a truly URGENT or EMERGENCY message get quietly downgraded to NONE or WATCH, so no alert fires and nobody checks in? That's the number that can hurt someone.
With that framing in mind: here's the story of how I made a very human mistake, almost shipped it, and then had the discipline (and dumb luck) to find out before it mattered.
The gut-punch. My original first-pass classifier was Google Gemini 2.5 Flash — lightweight, fast, cheap, good enough out of the box for most categories. It was doing fine until I tested it against a synthetic IRS gift-card scam message. A senior relaying what a "government agent" just told them on the phone: that they owed back taxes, needed to pay in gift cards immediately, and shouldn't tell their family because it was under investigation.
Textbook scam. Category: scam. Severity: urgent — active financial exploitation in progress, a caregiver should know now.
Gemini 2.5 Flash classified it: NONE. "Factual report of a phone call."
That's a gut-punch. And it stuck in my head — not just as a bug, but as a category of failure. I thought: this is what happens when you use a non-reasoning model for something that requires reading between the lines. You need a model that can think about what's actually going on, not just what's literally written. I tested the same message with xAI Grok 4 Fast. It flagged it immediately as URGENT / scam. Correct.
So I did what any rational engineer would do after one compelling data point: I rewrote the pipeline.
One vivid failure is a hypothesis, not a verdict. I wish I'd told myself that before I shipped.
The overcorrection. The new pipeline was Grok 4 Fast as first-pass classifier, with Claude Sonnet 4.6 as an escalation reviewer for borderline cases. My reasoning felt solid: reasoning models catch subtle intent, scams require reading subtext, Grok got the answer Gemini missed — therefore Grok-first is safer.
It felt right. The logic was coherent. And I had a vivid story to tell about why. What I didn't have was data.
The discipline (the boring part that saved me). Before leaning on the new pipeline, I built a benchmark. Not a vibe-check — an actual evaluation corpus: 1,000 synthetic senior SMS messages, hand-labeled with gold-standard severity and category, stratified into three difficulty tiers:
• Easy — overt distress, clear language ("I fell and I can't get up").
• Oblique — indirect or euphemistic distress ("not really feeling like myself lately, don't worry").
• Adversarial — false-positive bait designed to trigger over-alerting (cheerful messages mentioning illness, hyperbole, and so on).
I ran five models and several candidate escalation pipelines through it. After cleaning up five rows that turned out to be API error artifacts accidentally saved as messages (more on that in a moment), I had 995 scored examples to work with, including 470 truly-dangerous messages — 137 of which were scam scenarios. Here's what came back:
| Pipeline | Severity Acc | Dangerous-Miss | Scams Missed |
|---|---|---|---|
| Gemini 3.5 Flash → Sonnet | 87.6% | 0.6% (3) | 0 / 137 |
| Gemini 2.5 Flash → Sonnet | 86.3% | 0.9% (4) | 0 / 137 |
| Claude Sonnet solo | 87.3% | 2.1% (10) | 1 / 137 |
| Grok 4 Fast → Sonnet | 86.0% | 2.6% (12) | 0 / 137 |
| Grok 4 Fast solo | 75.0% | 4.0% (19) | 2 / 137 |
| GPT-5.4 nano | 72.5% | 15.1% | disqualified |
The Grok-first pipeline I'd been so confident about was the worst of the three escalation blends I tested — and second-worst overall, beaten toward the bottom only by the solo models. It had more than four times the dangerous-miss rate of the winner. GPT-5.4 nano was disqualified entirely — a ~15% dangerous-miss rate on a senior safety product is not a tuning problem, it's a no.
And the kicker: both the winner and my anecdote-driven pick caught zero out of 137 dangerous scams. Grok's whole claim to fame in my head was catching scams that Gemini missed. At scale, Gemini 3.5 Flash was equally perfect on scams. The IRS gift-card failure was a Gemini 2.5 problem, not a "non-reasoning models can't catch scams" problem. One model version, one example — and I'd drawn a sweeping architectural conclusion from it.
That's embarrassing. I'm publishing it anyway because it's also extremely normal, and maybe useful.
The corpus was also lying. Sidebar, because this one is too good not to share. While doing QA on the benchmark results, I noticed five rows where every single model scored identically wrong. Not close — completely off. Turned out those rows weren't senior SMS messages at all. They were API error and refusal fragments that had gotten saved into the corpus during an earlier generation run — the synthetic-data model occasionally choked on the spicier prompts and returned JSON like this instead of a message:
{"success": false}
{"safety_filter_triggered": true}
Complete with gold-standard severity labels still attached. No classifier in the world could score those correctly because they're not messages — they're JSON blobs that happened to end up in a CSV. And they were silently inflating everyone's miss rate by a small but annoying amount.
Lesson: audit your eval data before you trust your eval. Garbage in, garbage out applies to your test set just as much as your training set. Your benchmark can lie to you too. After removing those five rows, everything recalculated cleanly.
The resolution. The new pipeline: Gemini 3.5 Flash as first-pass, Grok 4 Fast and Gemini 2.5 Flash as fallbacks, Claude Sonnet 4.6 as escalation reviewer on borderlines. Roughly 4× fewer dangerous misses than the anecdote-driven pick, higher overall accuracy, equal scam-catching, and cheaper to run.
But I didn't stop there, because there were still three misses in the winning pipeline. Three messages scored WATCH instead of URGENT. I looked at all three. They were the same pattern: a senior expressing low mood while explicitly asking for contact. Something like: "Not doing so great today. Could really use a check-in."
Every model scored these as WATCH. Which is technically defensible — the person is sad but functional, no immediate danger signal, monitoring makes sense. But think about what that message is actually saying. Someone is asking for help. They're naming it. The text isn't ambiguous; the threshold was wrong.
This is the other thing I learned: when every model agrees and they're all "wrong," the problem is usually your labels or your rules, not the model. No amount of model-swapping was going to fix this because the models weren't confused — my definition of URGENT was incomplete. The fix was a small prompt rule: if a message contains distress and an explicit request for contact, score it URGENT regardless of severity language. Verified it doesn't trigger on cheerful messages that mention contact. Applied across all pipelines. Model-independent. No retraining. No benchmark regression.
What I'd tell past-me:
1. One vivid failure is a hypothesis, not a verdict. It tells you there's something worth investigating. It doesn't tell you the root cause, and it really doesn't tell you whether your fix generalizes.
2. Pick the metric that matches your asymmetry. Overall accuracy felt like the right thing to optimize until I thought about what it means to be wrong in each direction. Dangerous-miss rate is the number that can hurt someone. Everything else is secondary.
3. Audit your eval data. It lies. Maybe not maliciously — maybe just because someone ran a script at 2 AM and didn't notice the error rows were landing in the CSV. Check anyway.
4. When all the models agree and they're all wrong, look at your labels. This saved me from a rabbit hole of prompt engineering on a problem that wasn't actually there.
5. Escalation pipelines are underrated. Cheap first-pass model catches the obvious cases, strong reviewer handles the hard ones. You get near-frontier safety at a fraction of the cost. Worth designing for from the start.
There's a version of this post where I skip the overcorrection and just lead with the benchmark results. Cleaner narrative, better look for the author. But the overcorrection is the actual story. Because every engineer I know has done this — seen one dramatic failure, pattern-matched it to a theory, and shipped the theory before testing it. The benchmark just happened to catch me before the theory shipped to people who matter.
senior-bridge reads messages from people in their 80s who sometimes can't call their kids because they don't want to be a burden. A lot of what the classifier is really doing is pattern-matching for the moment someone quietly isn't okay — the message that sounds fine until you read it twice. Getting that wrong, in either direction, has stakes that a false-positive on some image-classification toy problem does not.
So yeah. I built the benchmark. I was wrong. I'm glad.
Woof,
Harvey