Trail content
8 minEnglish narration, Portuguese subtitlesPodcast (RSS)
Chapters
  1. 0:00Opening
  2. 1:00The diagnosis
  3. 2:28Why it happens
  4. 3:11What it costs
  5. 4:07A challenge
  6. 4:59The treatment
  7. 6:43When to ignore
  8. 7:41Wrap-up
Transcript

Opening

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.

The diagnosis

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?

Why it happens

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.

What it costs

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.

A challenge

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

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 to ignore

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.

Wrap-up

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.

SmellDispensables

LLM Doing Deterministic Work

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.

Element
20/37
Level
Beginner
Read
4 min
Updated
On this page 7
  1. 01Signs and symptoms
  2. 02Reasons
  3. 03Cost of ignoring
  4. 04Treatment
  5. 05Payoff
  6. 06When to ignore
  7. 07Is it this smell?
  8. ··Quick answers

Signs and symptoms

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:

  • The prompt asks to calculate, add, convert, format, or validate something that has an exact rule.
  • Errors are rare, small, and patternless: a cent, a digit, a day.
  • The same document gives different results on different runs.
  • There is code in the system that redoes the math “to double-check” and sometimes disagrees.
  • Dates, currencies, and identifiers come out in slightly different formats.
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
  1. The document arrives.
  2. The model does two things in one call: understand the document and do math.
  3. The math is generated token by token, as text, with no guarantee of exactness.
  4. Almost always the result matches. The “almost” is the problem.

Why the model gets simple math wrong

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.

Check +20 XP on first tryYour team switched to a bigger model and the invoice sum now fails once every 2,000 invoices instead of once every 300. The manager wants to call the problem solved. What do you answer?

Reasons

  • 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 right. The error shows up at scale.
  • The line between understanding and calculating is blurry. “Extract the total” looks like “calculate the total”, and it is not. The first is reading the printed number. The second is producing a new one.
  • The model seems able to do everything. It can do a lot, but not with the guarantee of a function.
  • 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.

Cost of ignoring

  • Financial and tax errors. One wrong cent on an invoice creates accounting rework and, at scale, trouble with the tax authority.
  • Distrust. When the error is rare and random, nobody trusts any result and everything gets checked by hand.
  • Cost. Output tokens spent on math that one line of code does for free. Output tokens usually cost several times more than input tokens.
  • Flaky tests. The same case passes and fails on different runs, and the team learns to ignore red in CI.
  • Rules that change without warning. Since July 2026, new Brazilian CNPJs can contain letters in their first 12 positions (Receita Federal Normative Instruction 2,229/2024). A rule like that must be updated in one place, with a test. In the prompt, you find out about the change when the model starts rejecting valid IDs.

Treatment

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 same extractor, split

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.

Check +20 XP on first tryIn the new version, the model returns valor_unit as text read from the invoice and code computes the totals. A colleague suggests asking the model to return each line amount already multiplied, to save code. Why is that a step back?

Payoff

  • Exact math, every time, testable with ordinary unit tests.
  • The model focused on what only it can do: read a messy document.
  • Smaller, cheaper outputs.
  • Extraction errors separated from calculation errors, easy to diagnose.
  • Rule changes (like the alphanumeric CNPJ) in one place, with a test.

When to ignore

  • Estimates. “Roughly how many pages is this report?” does not need exactness.
  • Math that depends on interpretation. Deciding which value in the document is the total is already interpretation; the sum afterwards is math.
  • Prototype. To validate that extraction works, letting the model add is acceptable. Take it out before production.

Is it this smell?

Check +20 XP on first tryAn assistant answers customer questions about delivery times. The prompt says 'calculate the delivery date by adding 5 business days to the purchase date'. Sometimes it counts the holiday. Is this an LLM Doing Deterministic Work?

Check +20 XP on first tryA classifier decides whether a comment is offensive. There is no exact rule, and the model sometimes disagrees with itself on borderline cases. Is this an LLM Doing Deterministic Work?

Check +20 XP on first tryThe model extracts each line amount from an invoice and code adds them up. The code's total sometimes does not match the total printed on the invoice. Where is the problem?

Check +20 XP on first tryAn HR agent asks the model to convert 'March 3rd' into 2026-03-03 before saving it to the database. The format comes out right 99% of the time. What is the simplest treatment?

Quick answers

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.

How do you fix LLM Doing Deterministic Work?

With Replace LLM Call with Code. Each refactoring has safe step-by-step mechanics with an eval before and after.

Where does LLM Doing Deterministic Work show up in practice?

In the real-world scenarios Invoice and contract extraction and Spreadsheet analysis with code execution.

Go further

Relations

Refactorings that treat it 1
Part of the tracks 1
Search