Don't let the LLM do the math
By Flavio Copes
Keep pricing and numbers deterministic in AI products. Let the LLM write prose only. A hybrid architecture from a real deployment advisor.
When I built StackPlan, I made one rule early: the LLM never picks a price.
It never ranks a stack. It never computes monthly cost at 10k DAU.
It writes the explanation. That’s it.
LLMs are bad at arithmetic
Ask a model what Railway charges for 50GB egress. You’ll get a confident number. Often wrong. Out of date by six months.
Ask it to multiply overage rates across three services and sum the total. It will drift. Round wrong. Forget a tier boundary.
This isn’t a prompting problem. Models optimize for plausible text, not ledger math.
Pricing pages change. Free tiers get resized. Your product cannot depend on whatever the model memorized last training run.
The hybrid architecture
StackPlan’s recommendation engine has three layers:
-
Curated data in SQLite — providers, services, plan tiers with
effective_fromdates. An admin can update pricing without redeploying code. -
Deterministic scoring in TypeScript — filter by capability fit, assemble candidate stacks, run the cost model. Integer cents end to end. No floating-point surprises.
-
LLM as presentation — given the pre-computed candidates, write strengths, tradeoffs, and gotchas. Prose only.
The flow looks like this:
Questionnaire → rules engine → cost model → ranked stacks → LLM rationale → report page
The LLM sees exact numbers in the prompt. The prompt says: do NOT invent or alter any cost figures.
return `You are writing rationale prose for a stack recommendation tool.
The rules engine already computed candidate stacks with exact costs.
Your job is ONLY to explain — never invent or alter any numbers.`
If the model hallucinates a price anyway, it doesn’t matter. The UI renders costs from the JSON the engine produced. The rationale is decoration.
The cost model stays pure
costModel.ts has no network calls. No fetch. No env vars.
It takes tier definitions and a usage profile. It returns monthly cost in cents, or null when a hard limit is exceeded.
function computeTierCostCents(tier, usage) {
let total = tier.basePriceCents
for (const unit of ALL_USAGE_UNITS) {
total += overageCostCents(tier, unit, usage)
}
return total
}
Same function runs on the server when building a report. Same bundled copy runs in the browser when you drag a tier knob on the canvas.
One source of truth. Testable with unit tests. No “the AI said $47” ambiguity.
Graceful degradation
getAdapter() returns null when no API key is configured.
No LLM? Reports still generate. Stacks still rank. Costs still compute. The rationale section simply doesn’t render.
Prefill from a free-text description also degrades. The form works manually.
This is a product decision, not an afterthought. The engine must stand alone. The LLM is a nice layer on top.
I run Cursor’s SDK locally through a dev bridge. Production uses a plain fetch adapter to an OpenAI-compatible endpoint. Both implement the same LlmAdapter interface. Swap providers without touching the engine.
Testability is the real win
When the LLM owned pricing, every test would be flaky. Different run, different number.
Now I test the engine with fixtures. Given this questionnaire and this KB snapshot, expect these three candidates in this order at these cent values.
The LLM output gets zod-validated JSON parsing. Malformed response? Retry once, then fail silently. The report is still useful without the prose block.
You can cache rationale by input hash in KV. Same questionnaire + same candidates = same text. Cheaper and consistent.
What this means for your AI product
If your product outputs numbers users will act on — prices, scores, rankings, compliance flags — compute them in code.
Use the LLM for:
- summarizing structured data
- filling a form from free text
- writing user-facing explanations
- generating labels and copy
Don’t use it for:
- arithmetic
- looking up live prices
- deciding who wins a comparison
- anything you’d put in a contract or invoice
The split feels like more work upfront. You maintain a data layer. You write a cost function.
You ship something users can trust. And when the model is down, your product still works.
That’s the bar I want for anything that touches money.
Related posts about essay: