An AI that reads crash stories: first too noisy to use, then too quiet to trust
Seven rules clear most crash reports in an instant, but 3 of the 14 reportable crashes in my test set showed up only in the written story. My first AI version found all 3 and made a reviewer read 44 stories to do it. A second version cut that to 3 reads and missed one. Neither is ready to ship, and both were tested on the same 60 events.
Problem
Rules clear 49 of 60 crash reports, but 3 of those hide a reportable event. The only way to find them is to read every story, about 41 minutes.
Insight
A miss breaks a federal deadline and a false alarm costs one read. So the goal is the fewest reads with zero misses.
Decision
The AI can only add flags, and every flag needs a named reason plus an exact quote from the story.
Result
First version: 0 missed, 44 reads. With the quote rule: 3 reads, 1 missed. Neither ships yet.
The review workbench
One HTML file, no install. A reviewer reads, filters, and labels events here.

01Who it’s for
02The problem
A crash report arrives as a few fields plus a written story of about 165 words. Seven rules check the fields for the obvious triggers: a fatality, a pedestrian, a hospitalization, an airbag, a tow, a minor injury, speed over 35 mph. They are fast, free, and easy to audit. On my 60 events they flagged 11, and all 11 were right.
They also cleared 49, and 3 of those 49 were reportable: an alleged minor injury, an animal struck at 28 mph, and a car that drove forward under a closing railroad gate. The fields look the same as the 46 harmless ones. Only the story gives them away, so the safe way to find them is to read all 49 stories. At about 167 words each that is roughly 41 minutes of reading before any decision. I haven’t timed a real analyst, so treat that as an estimate.
The usual fix is another rule, and that works until the wording gets slippery. A rule that matches injury words fires on 7 of the 49 stories, and 5 of those say no injuries were reported. Every new rule also needs a person to decide where the line sits, and the three misses are three different kinds of event. This is the grey area. Reading a story takes judgment, and that is the part I tried to hand to a model.
03The insight
A miss breaks a deadline. A false alarm costs one read.
That lopsided cost decides the design. The rules stay as a floor the model can’t lower. The number to move is how many stories a person still has to read, not accuracy.
04What good looks like
What I optimized
Stories a person still reads, out of the 49 the rules cleared, with zero missesReading is the cost an analyst actually feels. My bar is 23 or fewer, under half, which is my guess at where it starts saving time. Only an analyst can set the real number, and I haven’t asked one yet.The obvious metric I rejected
AccuracyRules alone score 95% (57 of 60) and miss 3 reportable events. The first AI version scores 32% and misses none. Accuracy would pick the version that breaks a deadline.05How it works
- Read any datasetOne config file maps a CSV’s columns onto a common shape.YAML config
- Rules set the floorFatality, injury, airbag, struck pedestrian, high speed.auditable
- Model reads the storyA local model writes a plain-language summary of each cleared event.Qwen 2.5 14B
- Flags need evidenceTo escalate, the model names the outcome and quotes the story. No quote found in the text, no flag.enforced in code
- A person decidesThe workbench shows both reasons side by side.you label
06Decisions
- Why
- The first version flagged on what could have happened. Now it must pick one stated outcome (injury alleged, animal struck, rail crossing, and so on) and quote the story. The code rejects any quote that isn’t in the text.
- Instead of
- A better-worded prompt alone. A prompt asks the model to behave. The code check doesn’t.
- Cost
- It’s strict. It missed the railroad gate, where nothing was damaged and no listed outcome fit.
- Why
- A rule a person can reproduce by hand is the floor. The model adds judgment on top. The limit is enforced in code, not in the prompt.
- Instead of
- Letting the model make the call, which is neither reproducible nor auditable.
- Cost
- It will never shrink the pile below what the rules flag.
- Why
- The users with the most sensitive data are the target users. “Your data never leaves the machine” is the promise.
- Instead of
- A cloud model with a local fallback.
- Cost
- A weaker model, which is part of why the first version over-flagged.
- Why
- Regulators trigger on outcomes like injury or airbags, not on who caused it. Fault takes months. Deadlines take days.
- Instead of
- Marking not-at-fault crashes as not reportable, the intuitive way.
- Cost
- The list includes crashes where the car did nothing wrong.
- Why
- In a compliance tool, a silent change in what gets flagged quietly invalidates every past decision. Any change fails the build until it’s explained.
- Instead of
- Regenerating the expected results whenever they drift.
- Cost
- Friction on every rules change, by design.
07What I left out
| Left out | Why |
|---|---|
| AI-only classification | Not auditable, and it can’t be reproduced by hand. |
| A database | One JSON file per run is easy to inspect and compare. |
| A web app | Safety analysts won’t install a stack. The workbench is one HTML file. |
| Dataset-specific code | Two unrelated sources, AV crashes and OSHA injuries, run on identical code. |
08What happened
From 44 reads to 3, and one miss
Of the 49 stories the rules cleared. Lower is better, but a miss is worse than a read.
| Approach | Stories to read, of 49 | Hidden cases missed, of 3 | Reading time, est. |
|---|---|---|---|
| Read everything, no AI | 49 | 0 | ~41 min |
| Rules only | 0 | 3 | 0 |
| Word-match rules I wrote after seeing the misses | 11 | 0 | ~9 min |
| AI, first version | 44 | 0 | ~37 min |
| AI, quote required | 3 | 1 | ~2.5 min |
60 real events, labeled by me: 14 reportable (the rules caught 11) and 46 not. The word-match row is flattering because I wrote those rules knowing the answers.
The first version flagged 44 of the 49, which is 90% of the pile. It cleared only 5, so a reviewer reading it saved almost nothing. The cause was in its own reasons: it judged what could have happened, such as “could have caused more severe damage if…”, not what did.
The quote rule fixed that. It escalated 3 events and 2 were right: the alleged injury and the animal. The third was a car rear-ended while yielding to an animal in the road. It also missed the railroad gate. The story says the car’s roof touched the gate, with no damage and no injuries. The model answered “no outcome” on 3 direct tries, and a full run after I reworded that trigger missed it again. I labeled it reportable for the behavior, not an outcome, which is exactly the grey area.
- Tuned on the same 60 events, so the second result is a first look, not proof.
- Only 3 hidden cases. Finding 2 of 3 says almost nothing yet.
- One labeler, and no analyst has used it or been timed.
09My call
Neither version ships. The first fails the reading bar: 44 reads against a target of 23 or fewer. The second fails the safety bar: it missed a reportable event, and zero misses isn’t negotiable.
What I learned is that a yes/no alert is the wrong product. A ranked queue fits better: the flags that come with a quote go first, the model’s looser flags sit below, and the analyst stops when their time runs out. I haven’t tested that yet.
10What’s next
- Test a ranked queuePut quote-backed flags on top and the first version’s looser flags below. Measure how many reads it takes to reach all 3 hidden cases.first
- Label 140 more eventsDraw them fresh, never reuse the 60, and keep the original 60 as a holdout. Narrative-only cases get over-sampled.then
- Put it in front of a safety analystTime them with and without it. Their tolerance for reads sets the real target, not my guess of 23.then