Engineering · AI · Product

Flaskcards, Not Flashcards: How We Fix Typos Without Wild Guesses

4 min read

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:

SituationDecision
Exactly one minimum-distance winnerAdopt the correction
Several candidates tieDeterministic tie-breakers: the longest unique candidate, optionally plural and singular agreement
Still ambiguous after tie-breakingLeave the token unchanged
Token is in the blocklistLeave 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.

All engineering posts