Skip to content
Cerosh Jacob
Back to Knowledge Hub
ceroshjacob.comOct 13, 20265 min read

Let Code Own the Facts

  • ai-engineering
Let Code Own the Facts

I built a spelling tutor for a six-year-old using a local LLM. It was kind, patient and confidently wrong about letters.

It invented spelling rules, miscounted syllables, misread handwritten words and claimed to change settings it had never touched. The surprising part? It sometimes answered the very same questions correctly when I asked them one at a time.

The fix wasn't a better prompt. It was a better division of responsibility: code owns the facts; the model owns the explaining. I moved spelling checks, error detection, scoring and settings changes into ordinary code, leaving the model to do what it does best — explain mistakes in language a child can understand.

Here are six examples of what went wrong, why they happened, and what changed when I stopped asking the model to be the source of truth.

A kind, confident, wrong tutor

Across a few sessions, it told a young learner:

  • "necessary comes from nectar." It's from Latin necessarius; nothing to do with nectar.
  • "necessary has two syllables, nec-es-sa-ry." Four; its own split shows four.
  • "necessary ends with two s." The double s is in the middle.
  • "In library, the y sounds like a long i." The y says "ee"; the long i is the i.
  • "Remember that y comes before the final r." Said twice. In library, r comes first.
  • "There's a 'liar' hidden inside library." l-i-b-r-a-r-y doesn't contain "liar".

And two more, while I was testing:

  • "I have set the difficulty to hard." No tool was called; nothing changed.
  • Reading a photo of a handwritten "necesary". The page said necesary; it reported necessary, the word it expected.

Every answer was kind, plain and short, exactly as the prompt asked. An adult might notice there's no "liar" in library. A six-year-old hears a confident teacher and learns the rule.

Why the model gets it wrong

Letters are exactly the kind of fact an LLM is worst placed to know, and an action is easy for it to describe instead of doing. Five reasons:

  1. It doesn't see letters. It reads tokens (whole words or chunks), so "does r come before y?" asks about something it never looks at. It recalls the spelling rather than reading it.
  2. It's trained to be plausible, not correct. "Nectar" sounds like a source for necessary, and nothing inside checks fluent against true.
  3. It can't tell when it's guessing. Wrong answers come with the same confidence as right ones, so "say if you're unsure" in the prompt couldn't stop them.
  4. A persona shapes style, not knowledge. A prompt can make the model warm and brief; it can't add a fact.
  5. Calling a tool is optional; talking is the default. A tool call is just another reply the model may or may not choose. Under a long, chatty persona, saying "I have set it" was the likelier reply than calling the tool, and nothing inside the model checks that its words match what happened.

The surprise: it isn't always wrong

Later I asked the same facts directly, one question at a time:

  • Which comes first in library, the r or the y? — r, correct.
  • How many syllables does necessary have? — four, correct.

Both were right: the same facts it had got wrong mid-conversation.

The problem isn't that the model never knows. It's that you can't tell when it knows, and nothing in the reply signals which kind you got.

The split: code owns the facts, the model owns the words

The design rule became simple: if a wrong answer would be taught, code owns it. The model gets only the jobs that genuinely need language.

Code owns:

  • Is the spelling right? A string comparison. Must be right every time; the model is never asked.
  • Streaks and history. Code plus SQLite. Rules and records, not language.
  • What exactly was the mistake? A character diff (Python's difflib). Trivial for code, unreliable for the model.
  • The word, letter by letter. "-".join(word). One line, and the model can now read the order instead of recalling it.
  • Changing the difficulty. The model requests; code validates and applies. The model had claimed changes it never made.
  • Judging a photo of handwriting. The model transcribes, strictly; code judges. Asked loosely, it "corrected" the child's spelling.

The model owns:

  • Choosing a word, its meaning and an example sentence. Language content, and the result is checked by code before use.
  • Explaining the mistake kindly to a child. Tone, simplicity and patience are what it's good at.

Before and after

Now, when a round ends, Python computes the facts and appends them to the tutor's prompt:

Facts about the last round:
Word: Business
Learner typed: bsiness
Letters: b-u-s-i-n-e-s-s
Differences: missing 'u' after 'b'
Result: False
Use these facts exactly; do not invent other letters or rules.

With the model as the source of facts, the tutor said: "Remember that y comes before the final r." (about library) — invented, confidently.

With code supplying the facts, it said: "You were missing the letter u after the letter b. The word is spelled b-u-s-i-n-e-s-s." — exactly the computed difference.

The model's job shrank from knowing to explaining, and explaining is what it does well.

Where grounding stops

The facts it was given came out right, but it still added its own. Despite "do not invent other letters or rules", it offered a memory trick both times: "govern and ment joined together" and "a business has a bus in it". Both happen to be true; nothing guaranteed that.

It can also notice missing information but not wrong information. Asked "why is it spelled that way?" with no word given, it asked which word I meant. It never flags an error in its own memory.

Splitting the jobs fixed the rest: a short-prompt call runs the tools, the persona writes the reply, and the app shows its real state, not the model's account.

The next step keeps code in charge: retrieval, where tricks and rules come from a checked source instead of memory.

The rule I'm building by

When you add an LLM to an app, sort every job into two piles before writing a prompt:

  • Must be right, every time — checking, scoring, counting, letters, records, changes to settings, anything a user will learn or act on. Code owns it.
  • Needs language — explaining, encouraging, rephrasing, understanding a messy question. The model owns it, working from facts code hands it.

A better prompt can change how a model sounds, and even whether it acts. Only moving the facts and the actions out of the model changes whether it's right.

Setup: a 12B local model (gemma4, 4-bit) through Ollama on a 16 GB laptop; a handful of sessions in October 2026. These are observations from one project, not measured error rates.


Let code own the facts. Let the model own the words.