Trail content
8 minEnglish narration, Portuguese subtitlesPodcast (RSS)
Chapters
  1. 0:00Opening
  2. 0:57Version one
  3. 1:24What broke
  4. 2:38The smells
  5. 3:04A challenge
  6. 3:41The fixes, in order
  7. 4:23The final architecture
  8. 6:31In production
  9. 7:26Wrap-up
Transcript

Opening

A building materials distributor in Brazil receives a few thousand supplier documents a month. Printed electronic invoices, service invoices from city governments, each with a different layout, and supply contracts with price adjustment clauses.

Three people in finance type all of this into the management system. During month-end close, the queue falls behind, and payments go out with late fees.

The goal is to read each document and fill in the system with no manual typing. Under one clear rule. When in doubt, the document goes to human review. It never enters with wrong data.

This is the first real system of this trail. And almost every smell and refactoring from the trail shows up in it. You'll see the first version break four times, and how the final one turns silent errors into visible ones.

Version one

The first version took a week to build, and it got all ten demo documents right.

The file arrives by email or through a portal, and a library pulls out the text.

One prompt asks for the issuer, the line items, the taxes and the totals. And to check that the sum matches.

Regular expressions pull the values out of the answer text.

And the data goes into the system.

What broke

Week two. The model started writing invoice total amount, instead of total. The regular expression didn't find the field, and one hundred forty documents went in with no total. Nobody noticed for four days.

Week four. An invoice with thirty-eight line items went in with a total off by twelve cents. The model had added the items, and rounded differently. The error only surfaced during bank reconciliation.

Week five. Someone asked the prompt to keep two decimal places, and tested it on three invoices. Accuracy on the discount field dropped. But only on discounted invoices, and none of them were among the three.

Week seven. A tax ID with an invalid check digit was accepted, because the model validated it and said it was fine. The supplier didn't exist.

Now notice what the four incidents share. None of them raised an error. The system moved on with empty or wrong data, and another team found it, days later. In document extraction, a silent failure is the worst kind. Its cost grows every day nobody notices.

The smells

You already know these smells by name. Free text handoff. A prose answer, read by a regular expression. That's week two.

LLM doing deterministic work. Sums, rounding and tax ID validation, done by the model. That's weeks four and seven.

And the untested prompt. A change tested on three examples. Week five.

A challenge

Here's one for you. A colleague proposes keeping the sum check in the prompt. The model would read the line items, and answer ok, or mismatch. Why doesn't the final architecture do that?

Because adding is deterministic work. Code gets it right every time, for free. The model produces text that looks like a check, with no guarantee that it checked anything. Reading the line items stays with the model. The arithmetic leaves it. And comparing the two is what catches a misread.

The fixes, in order

Here's what the team did, in order. First of all, the eval. One hundred eighty real documents, anonymized, with the right value for each field, split by document type and by issuer. Version one was measured against them before anything changed.

Then, structured output. Extraction now returns an object in a schema, with amounts in cents and typed dates. The regular expression was deleted.

Then, the math moved into code. The model only reads printed values. Sums, taxes and tax ID validation became tested functions. And a mismatch between the calculated total and the printed one became an alarm.

The final architecture

Here's where it landed. The whole document goes to the model, which reads both the text and the image.

A short step classifies the type. A printed invoice, a service invoice, or a contract.

Each type has its own prompt and its own schema. Long contracts are read in parts, and the results combined.

Code adds up the items, checks the taxes, and validates the tax ID and the dates.

If everything matches, the data goes into the system, with a reference to the document and the prompt version.

If something doesn't match, the error goes back to the model once, with the exact validation message.

If the repair fails, the document goes to a person, with the problem field highlighted.

And every human correction becomes a new case in the dataset. The eval grows on its own, through use, with no separate labeling project.

And one decision in there deserves attention. Say the validator reports the item sum differs from the printed total. The repair rereads the page, and returns the same values. What now?

Don't accept it because the model confirmed the reading twice. Two identical readings don't prove they're right, and the invoice itself may truly be inconsistent. And don't retry until the sum matches. That teaches the model to change some number so the math closes, and it swaps a visible error for an invisible one. Send it to a person, with the field highlighted. And store the correction in the dataset.

And one more choice that saves a lot. Most Brazilian invoices also exist as a signed, structured file, authorized by the tax authority. When the distributor can get that file, it parses it in code and skips the model completely. There's nothing to interpret. Sending it to the model would only add cost and a chance of error. The model stays for service invoices, contracts, and documents with no structured file.

In production

So which number answers whether the system meets the rule, when in doubt, review? Not the average accuracy. The average hides where the error is, just like week five. And not the share of documents that skipped review. That measures automation, not safety. It rises when the system improves, and also when it starts letting errors in.

The number is how many documents entered without review, with a critical field wrong. That's the error the rule forbids, and it has to stay at zero. The review rate is what you optimize after, without ever letting that first number rise.

And when a vendor offers a cheaper model, you already know what to do. Run the one hundred eighty documents on both versions, with accuracy per field and per type. Then the saving is either worth it, or it isn't.

Wrap-up

So here's what this system teaches. Extraction and arithmetic are different jobs, and the mismatch between them is your best error detector. A loud error beats a silent one. The eval comes first, and pays for itself. Human review is part of the architecture. And every human correction is a test case. The goal was never zero review. It was zero wrong data.

In the next lesson, the last of this trail, we'll let the model write database queries, and see how to keep that safe: natural language to SQL. See you there.

Real-world scenarioData and documentsBrazilian context

Invoice and contract extraction

Extração de NF-e e contratos

Invoice and contract extraction is a real-world case study of an LLM system, in the Data and documents group. Read invoices and contracts in PDF and fill the ERP with no manual typing.

Element
09/25
Level
Intermediate
Read
5 min
Updated
On this page 11
  1. 01The business
  2. 02Version 1
  3. 03What broke
  4. 04Smells found
  5. 05Refactorings applied
  6. 06Final architecture
  7. 07Numbers
  8. 08Patterns used
  9. 09Reference code
  10. 10Lessons
  11. 11Check your understanding
  12. ··Quick answers

The business

A building-materials distributor in Brazil receives a few thousand supplier documents a month: DANFEs in PDF (the printed form of the NF-e, Brazil’s mandatory electronic invoice), service invoices from city governments with different layouts, and supply contracts with price-adjustment clauses. Three people in finance type this data into the ERP. During month-end close the queue falls behind and payments go out with late fees.

The goal is to read each document and fill in the ERP with no manual typing, under one clear rule: when in doubt, the document goes to human review, and never enters with wrong data.

Context for readers outside Brazil. Every sale of goods in Brazil requires an NF-e, an XML document authorized by the tax authority. Suppliers often send only the PDF. The CNPJ is the company tax ID, with check digits that can be validated in code. The pattern applies to any invoice-heavy back office.

What this scenario measures: accuracy per field, documents sent to review, and cost per document.

Version 1

The first version took a week and got all ten demo documents right.

flowchart LR
  P(["1. PDF"]):::io --> T["2. Text extracted from the PDF"]:::tool
  T --> M(["3. Prompt: list the data and compute totals"]):::llm
  M --> R["4. Regex over the answer text"]:::tool
  R --> E[("5. ERP")]:::store
  1. The PDF arrives by email or portal.
  2. A library extracts the text.
  3. One prompt asks for issuer, line items, taxes, and totals, and to check that the sum matches.
  4. Regular expressions pull values out of the text answer.
  5. The data goes into the ERP.

What broke

Week 2. The model started writing “Invoice total amount” instead of “Total”. The regex did not find the field, and 140 documents went in with no total. Nobody noticed for four days.

Week 4. An invoice with 38 line items went in with a total off by R$0.12. The model had added the items and rounded differently. The error only surfaced during bank reconciliation.

Week 5. Someone asked the prompt to “keep two decimal places” and tested it on three invoices. Accuracy on the discount field dropped, but only on discounted invoices, none of which were among the three.

Week 7. A CNPJ with an invalid check digit was accepted because the model “validated” it and said it was fine. The supplier did not exist in the vendor master.

Smells found

Notice what the four incidents share: none of them raised an error. The system moved on with empty or wrong data, and another team found it days later. In document extraction, a silent failure is the worst kind, because the cost grows every day nobody notices.

Check +20 XP on first tryIn week 2, 140 documents went in with no total and nobody noticed for four days. Which change attacks the cause?

Refactorings applied

  1. Add Eval Before Changing the Prompt. First of all, 180 real anonymized documents, with the right value for each field, split by document type and issuer. Version 1 was measured against them.
  2. Replace Free Text with Structured Output. Extraction now returns an object in a schema, with amounts in cents and typed dates. The regex was deleted.
  3. Replace LLM Call with Code. The model only reads printed values. Sums, taxes, and CNPJ validation became tested functions. A mismatch between the calculated and printed totals became an alarm.
Check +20 XP on first tryA colleague proposes keeping the sum check in the prompt: the model would read the line items and answer ‘ok’ or ‘mismatch’. Why does the final architecture not do that?

Final architecture

flowchart LR
  P(["1. PDF"]):::io --> C(["2. Classify type"]):::llm
  C --> X(["3. Extract per type, in a schema"]):::llm
  X --> V["4. Validate in code"]:::tool
  V --> D{"5. Everything matches?"}
  D -->|yes| E[("6. ERP")]:::store
  D -->|no| RP(["7. Repair with the error"]):::llm
  RP --> V
  RP -->|"failed again"| H((("8. Human review"))):::human
  H --> G[("9. Golden dataset")]:::store
  1. The PDF arrives and the whole document goes to the model, which reads both text and image.
  2. A short step classifies the type: DANFE, service invoice, or contract.
  3. Each type has its own prompt and schema. Long contracts are read in parts and the results combined.
  4. Code adds up items, checks taxes, and validates CNPJ and dates.
  5. If everything matches, the document moves on.
  6. The data enters the ERP with a reference to the document and the prompt version.
  7. If something does not match, the error goes back to the model once, with the exact validation message.
  8. If the repair fails, the document goes to a person, with the problem field highlighted.
  9. Every human correction becomes a new case in the dataset.

When the NF-e’s XML is available (the structured, signed invoice authorized by the Brazilian tax authority), parse it in code and skip the model. The model stays for service invoices, contracts, and PDFs with no XML. If a repair still fails the checks, the document goes to a person; do not keep retrying until the numbers close.

Check +20 XP on first tryThe validator reports that the item sum differs from the printed total. The repair rereads the page and returns the same values. What should the system do?

Numbers

This scenario’s metrics are accuracy per field, documents sent to review, and cost per document, comparing version 1 with the final architecture on the same 180 documents.

The numbers are not published yet. They will come from the runnable reference code, running both versions against the same set, and this section will be updated with the result and the script that produces it. Until then, no numbers appear here.

Patterns used

Pattern Role in the system
Prompt Chaining Classify, extract per type, validate
Structured Output One schema per document type
Map-Reduce Long contracts read in parts
Output Validation and Repair Validation error sent back to the model once
Golden Dataset 180 documents that grow with every human correction

The full list of patterns tied to this scenario is under Relations, below.

Reference code

The complete system, with both versions, the anonymized dataset, and the script that produces this page’s numbers, lives in the ai-guru reference code repository, under scenarios/nfe-contract-extraction/. The repository is being prepared, and the link goes here once it is published.

Lessons

  1. Extraction and arithmetic are different jobs. The model reads; code adds. The mismatch between the two became the system’s best error detector.
  2. A silent error is worse than a loud one. Swapping regex for a schema turned empty fields into visible failures.
  3. The eval came first and paid for itself. Without the 180 documents, none of the later changes would have been safe.
  4. Human review is part of the architecture. The goal was never zero review; it was zero wrong data in the ERP.
  5. Every human correction is a test case. The dataset grew on its own through use, with no separate labeling project.

Check your understanding

Check +20 XP on first tryA model vendor offers a cheaper new version. Before you switch, what do you run?

Check +20 XP on first tryThe distributor finds it can obtain the XML for almost every NF-e issued against its CNPJ. What is the best use of that?

Check +20 XP on first tryLeadership asks whether the system meets the rule ‘when in doubt, human review’. Which number answers that?

Quick answers

What is Invoice and contract extraction?

Invoice and contract extraction is a real-world case study of an LLM system, in the Data and documents group. Read invoices and contracts in PDF and fill the ERP with no manual typing.

Which AI patterns does the Invoice and contract extraction scenario use?

Prompt Chaining, Structured Output, Map-Reduce, Output Validation and Repair, and Golden Dataset.

Go further

Relations

Part of the tracks 1
Search