Engineering · AI · Product
Flaskcards, Not Flashcards: How We Fix Typos Without Wild Guesses
A small normalizer for the fast path that corrects only what routing needs, abstains on a tie, and never touches what the model reads.
A student types "make flaskcards on chapter 3". (The message is synthetic.) A language model would understand it without blinking. Cereby's fast path did not.
The fast path runs before the model: pattern matchers, heuristics and lightweight gates that route plain requests in microseconds, feeding the same routing as the regex patterns in A Test Became Flashcards: From First-Match-Wins to Parallel Scoring. Those matchers read text literally, so one wrong letter hid the keyword:
"Test me on chapter 3" missed the same way, because the matchers expected "quiz on". Sending every such message to the model costs tokens and latency on requests the fast path could have routed itself. A general spellchecker is worse: "fixing" ordinary words into the wrong intent keywords makes false positives that are quiet and hard to debug.
So we built the message normalizer, a narrow step that cleans a copy of the message for the fast path alone. It corrects only toward the words routing needs, only when one answer wins, and never touches what the model sees.
Two stages, and the model's copy untouched
Phrase rules run first, longest first, so context beats isolated word fixes. Then each word is checked on its own:
The two synthetic messages, traced:
message "make flaskcards on chapter 3"
token "flaskcards"
canonical? no
blocklisted? no
fuzzy step Damerau-Levenshtein against the intent lexicon
best candidate "flashcards" → adopted
matchers see "make flashcards on chapter 3"
the model sees "make flaskcards on chapter 3"
message "test me on chapter 3"
stage 1 "test me on" → "quiz on"
matchers see "quiz on chapter 3"
the model sees "test me on chapter 3"
Edit distance, inside a small lexicon
Damerau-Levenshtein distance counts the fewest edits between two words: insert, delete, substitute, or swap two adjacent letters. The swap sets it apart from plain Levenshtein, and is why it handles "flaskcards" and "fsalchcards" correctly.
It stays cheap for two reasons. When the length gap between a token and a keyword already exceeds the token's allowed distance, the comparison is skipped, and since keywords are short, few ever run. And short tokens get a tighter cap, because a two-letter token one edit from a keyword is more likely a different word than a typo.
A tie means no correction
Distance alone does not decide:
| Situation | Decision |
|---|---|
| Exactly one minimum-distance winner | Adopt the correction |
| Several candidates tie | Deterministic tie-breakers: the longest unique candidate, optionally plural and singular agreement |
| Still ambiguous after tie-breaking | Leave the token unchanged |
| Token is in the blocklist | Leave it unchanged, whatever the distance |
The blocklist covers words that look like typos of intent keywords but mean something else. The tie rule matters more: a skipped correction is invisible to the student, and a wrong one silently routes them to the wrong intent.
What this taught us
Abstention is a product decision. Leaving a token unchanged is the right answer when the evidence does not support a confident correction.
Scope the algorithm. Edit distance is the right tool inside a curated lexicon; on arbitrary English it finds creative, wrong matches. And the length pruning was never an optimization bolted on later: without it, every medium-length token would be compared against the whole lexicon on every message.