What is LLM Doing Deterministic Work?
LLM Doing Deterministic Work is an AI code smell, a sign of trouble in LLM systems, in the Dispensables group. The model calculates taxes, formats dates, or validates IDs, things a function does without error.
A Brazilian distributor reads electronic invoices with a model. Every company in Brazil has to issue them, so there are thousands a day.
The prompt asks for a lot. Extract the line items. Add them up, apply the discount, and check the result against the invoice total. Compute each item's state tax. Convert the dates. And check that the issuer's tax ID is valid.
Every few hundred invoices, something comes out slightly wrong. The sum is wrong by a few cents. One item's tax is rounded differently. Or an invalid tax ID gets marked as valid.
The team can't find a pattern. Because there isn't one. It's the model doing arithmetic. In this lesson, you'll learn to spot this smell, why it hides so well, and how to split the work so the math is right every single time.
Let's look at what's really happening here. The document arrives.
The model does two different things in one call. Understand the document, and do math.
And the math is generated token by token, as text, with no guarantee of being exact.
Almost always, the result matches. That almost is the whole problem.
Why would a model get simple math wrong? Because it doesn't add. It predicts the next token. A number like one thousand two hundred eighty-four, with cents, becomes a few tokens, and they're not always split in the same places. To add, the model writes digits that look plausible after what came before.
Most of the time, plausible and correct are the same thing. Current models get far more math right than the ones from a few years ago. But far more isn't always. And a tax system needs always.
How do you spot it? The prompt asks to calculate, add, convert, format, or validate something that has an exact rule.
The errors are rare, small, and have no pattern. A cent. A digit. A day.
The same document gives different results on different runs.
And somewhere in the system, there's code that redoes the math to double-check, and sometimes disagrees. If you already wrote the function, why is the model doing it too?
Writing a sentence in the prompt is faster than writing a function with tests.
The model gets it right in testing. With five sample invoices, the math comes out perfect. The error shows up at scale.
The line between understanding and calculating is blurry. Extract the total sounds like calculate the total. It isn't. The first is reading a number that's printed. The second is producing a new one.
And rules nobody wants to maintain. Validating a tax ID or counting business days takes code and tests. Pushing it into the prompt feels like skipping that work.
The cost starts with money. One wrong cent on an invoice creates accounting rework. At scale, it's trouble with the tax authority.
Then trust. When the error is rare and random, nobody trusts any result, and everything gets checked by hand.
Then tokens. Output tokens usually cost several times more than input. You're paying for math that one line of code does for free.
Then flaky tests. The same case passes and fails on different runs, and the team learns to ignore red in the pipeline.
And rules change. Since July twenty twenty-six, new Brazilian company tax IDs can contain letters. A rule like that has to be updated in one place, with a test. In a prompt, you find out about the change when the model starts rejecting valid IDs.
Here's one for you. Your team switched to a bigger model. The invoice sum now fails once every two thousand invoices, instead of once every three hundred. The manager wants to call it solved. What do you say?
That the math has an exact rule, and should leave the model. A better rate isn't a guarantee. A bigger model errs less, but it still produces digits by prediction. A sum function is always right, and costs almost nothing.
And a rare error in a tax amount is worse than a frequent one, in a way. It's more expensive to find, precisely because it's rare. Asking the model to double-check its own sum doesn't help either. The check runs on the same mechanism that made the mistake.
The treatment is a refactoring called replace LLM call with code. And the rule for splitting the work is simple. The model copies what's written in the document. Code produces any new number.
So the model's output becomes structured data with only the values it read. And notice they come back as text, exactly as printed, with the comma Brazil uses for decimals. The tax ID, the date, each item's quantity, unit price and tax rate, the discount, and the printed total.
Then code converts, multiplies, adds and compares.
One hundred twenty boxes at three dollars fifteen is three hundred seventy-eight. Thirty-six rolls of tape at seven ninety is two hundred eighty-four forty.
The sum, minus the discount, is six hundred thirty-seven forty. But the invoice prints six hundred thirty-four forty.
Before, the model might have checked and said it matched. Now the mismatch is a calculated fact. The invoice goes to review, with the reason attached.
And two details matter. For money, use a decimal type, never floating point. And the tax rounding rule is the tax team's decision. Now it's written somewhere you can test.
Now a colleague suggests: let's have the model return each line amount already multiplied, to save some code. Why is that a step back? Because the multiplication goes back to being a number the model produced. The split only works if the model never produces a new number. Even a simple multiplication.
When can you ignore it? Estimates. Roughly how many pages is this report doesn't need to be exact.
Judgment, with no exact rule. Deciding whether a comment is offensive is the model's job. If it disagrees with itself on borderline cases, that's expected, not this smell.
And a prototype. To check that extraction works, letting the model add is fine. Take it out before production.
And watch for the smell hiding inside a conversation. An assistant answers customers about delivery dates. The prompt says: add five business days to the purchase date. Sometimes it counts a holiday. It's a chat with a customer, but the calculation inside it is deterministic. Listing the holidays in the prompt keeps the math in the model, just with more text. A date tool solves it, and the model explains the result.
So here's the idea to take with you. If the operation has an exact rule, it doesn't belong in a prompt. Let the model do what only it can do, read a messy document, and let code produce every new number. Then a mismatch stops being noise, and becomes a useful alarm.
In the next lesson, we'll do that refactoring step by step, with tests that pin the rule down: replace LLM call with code. See you there.
LLM Fazendo Conta
LLM Doing Deterministic Work is an AI code smell, a sign of trouble in LLM systems, in the Dispensables group. The model calculates taxes, formats dates, or validates IDs, things a function does without error.
A Brazilian distributor runs an extractor for NF-e, the electronic invoice every Brazilian company must issue. It asks the model for the invoice line items, the total amount, and whether the sum of the items matches the total. An excerpt from the prompt (anonymized and translated):
Extract the invoice line items with quantity, unit price, and line
amount. Add up the line amounts, apply the discount, and check that the
result matches the invoice total. Also compute each item's ICMS (state
sales tax) with the rate shown. Convert dates to YYYY-MM-DD format.
Validate that the issuer's CNPJ (company tax ID) is valid.
Once every few hundred invoices, the sum is off by cents, one item’s tax is rounded differently, or an invalid tax ID gets marked as valid. The team cannot find a pattern, because there is none: it is the model doing arithmetic.
Signs of an LLM Doing Deterministic Work:
flowchart LR
D(["1. Document"]):::io --> M(["2. Model extracts and calculates"]):::llm
M --> C["3. Sum, tax, validation"]:::tool
C --> R(["4. Result that is almost always right"]):::io
The model does not add. It predicts the next token. A number like 1,284.37 becomes a few tokens, and the tokenizer does not always split it in the same places. To “add”, the model writes digits that look plausible after what came before.
Most of the time, plausible and correct coincide. Current models get far more math right than the ones from a few years ago. But “far more” is not “always”, and a tax system needs always. A 40-item sum with a discount and rounding is exactly the kind of math where one digit slips.
| Refactoring | Choose it when |
|---|---|
| Replace LLM Call with Code | Whenever the operation has an exact rule. The model extracts the raw data; code calculates, formats, and validates. Target: Tool Use, when the model needs the result in the middle of its reasoning (for example, to explain a delivery date to a customer). |
The rule for splitting: the model copies what is written in the document, and code produces any new number. In practice, the model’s output becomes a structured output with only values it read (field names stay in Portuguese, as in the real system):
{
"emitente_cnpj": "12.ABC.345/01DE-35",
"data_emissao": "03/09/2026",
"itens": [
{ "descricao": "Caixa papelão 40x30", "qtd": "120", "valor_unit": "3,15", "aliquota_icms": "18" },
{ "descricao": "Fita adesiva 48mm", "qtd": "36", "valor_unit": "7,90", "aliquota_icms": "18" }
],
"desconto": "25,00",
"total_impresso": "634,40"
}
Notice the values come as text, exactly as printed (Brazil uses a comma as the decimal separator). The code converts, adds, validates, and compares:
import re
from decimal import Decimal, ROUND_HALF_UP
def brl(s: str) -> Decimal:
return Decimal(s.replace(".", "").replace(",", "."))
def dv(base: str) -> str:
soma = sum((ord(ch) - 48) * (2 + i % 8) for i, ch in enumerate(reversed(base)))
resto = soma % 11
return "0" if resto < 2 else str(11 - resto)
def cnpj_valido(cnpj: str) -> bool:
c = re.sub(r"[./-]", "", cnpj.upper())
if not re.fullmatch(r"[0-9A-Z]{12}[0-9]{2}", c) or len(set(c)) == 1:
return False
return dv(c[:12]) == c[12] and dv(c[:13]) == c[13]
def conferir(nota: dict) -> dict:
itens = [brl(i["qtd"]) * brl(i["valor_unit"]) for i in nota["itens"]]
total = sum(itens) - brl(nota["desconto"])
icms = [(v * brl(i["aliquota_icms"]) / 100).quantize(Decimal("0.01"), ROUND_HALF_UP)
for v, i in zip(itens, nota["itens"])]
return {
"total_calculado": total,
"bate_com_impresso": total == brl(nota["total_impresso"]),
"icms_itens": icms,
"cnpj_ok": cnpj_valido(nota["emitente_cnpj"]),
}
On this invoice, 120 times 3.15 is 378.00 and 36 times 7.90 is 284.40. The sum minus the discount is 637.40, not the printed 634.40. Before, the model might have “checked” and said it matched. Now the mismatch is a calculated fact, and the invoice goes to review with the reason attached.
Use Decimal, never float, for money. The ICMS rounding rule (here, half up to two places) is the tax team’s decision, and now it is written in a place you can test.
Answer the 6 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
LLM Doing Deterministic Work is an AI code smell, a sign of trouble in LLM systems, in the Dispensables group. The model calculates taxes, formats dates, or validates IDs, things a function does without error.
With Replace LLM Call with Code. Each refactoring has safe step-by-step mechanics with an eval before and after.
In the real-world scenarios Invoice and contract extraction and Spreadsheet analysis with code execution.