skip to content
---
title: "Ask the Model Where, Not What"
date: "2026-08-31"
---

# Ask the Model Where, Not What


More learnings from using LLMs for document extraction. A convincing answer is great. A convincing but wrong financial number… less great 😅 A made-up amount or a missed "amounts in thousands" note can end up in someone's investment report.


This time, I've been exploring asking the model to point at the source and letting code read the value. The model picks a cell; code reads the digits and applies the currency and scale. The model can still pick the wrong cell, so I want the original text and page attached to the result. That gives me something to check when a number looks off.


I've been working on AI document extraction for a while. In 2016 I was writing extractors for travel voucher PDFs. More recently, I wrote about combining language models with text recognition for invoice validation. Now, working on software for private equity, I keep coming back to the same problem. You need one investment value, but the report has pages of tables, commentary and notes. Getting a number out is the easy part. Checking that it's the number you asked for takes more work.


### How £544 becomes £544,000


Take this fictional financial statement. It shows how much an investor put into a fund, what they got back and what their investment is worth at the reporting date. In finance, you'd call it a capital account statement. For this example, all we want is the value at the end of the period.


The model points at the cell containing £544. Code reads the digits. So, £544? There's a note at the top of the page: amounts are in thousands of pounds. Apply that scale and you get 544 × 1,000 = £544,000. That's quite a difference for one small note. I want to keep both the cell and that note attached to the result.


Financial statementOne pageWhat we wantClosing investment valueat the reporting date1 page2 read3 locate4 convert5 check→ start with the document and the value you need⏸ pause
// follow one value through a fictional statement; click a step to pause and inspect it

To make this work, I give the model a form with a place for a source reference, but no place to write an amount. Code checks that the reference exists, then reads the text from that part of the document. If the model invents a reference, the check rejects it. The digits have to come from the source.


### A source you can check


Now imagine reviewing that £544,000. You can open the original page and see £544 and the scale note highlighted. If something looks off, you can check the cell the model picked, the digits the text-recognition tool read and the scale the code applied. I'd want the same trail for decimal separators: 1.234,56 and 1,234.56 should give you the same amount, but the code needs to handle both.


There's still a catch. Code can read the same cell ten times and get the same answer ten times. If the model picked the beginning balance when you asked for the ending balance, you've reproduced the wrong number ten times. So I'd still check this against documents with known answers and keep human review in the process. Being able to trace a mistake helps you fix it; it doesn't stop the model from making one.


Long reports add another problem: finding the right page in the first place. In a study of scanned financial documents, researchers got their largest performance improvement from locating the relevant pages before extracting the values. That matches the decision to split up the job in my system. Their results aren't an accuracy score for mine, though; that needs its own evaluation.


Technical details: the pipeline and tools

In a recent document extraction system I built, I used this approach to keep each value tied to its source.

The pipeline has five stages, with human review where needed:

fits one prompt? skip embed + retrievalparsedocling +TableFormerclassifyrules first,LLM tie-breakembedbge-m3 →pgvectorextract1 LLM call,temp 0verifyre-derive,no LLMreviewhuman +bbox proof→ docling turns the PDF into a block graph: tables, rows, cells, bboxes⏸ pause
// five stages, from the uploaded PDF to a value you can check

1. Parse. First, turn the PDF into pieces you can refer to. For this, I use Docling, an open-source document parser that identifies page layout and uses TableFormer to recover table structure. Keep each piece of text together with its coordinates on the page. You'll need those later to show where a value came from. This is also where text recognition can misread a digit or the parser can put it in the wrong cell, so the source text still needs checking.

Think of the parsed document as a tree. Pages contain headings, paragraphs and tables; tables contain rows and cells. Our £544 is text inside one of those cells. Give each piece a reference and the model can choose from what's there. Code then checks that its choice exists. Docling stores its structured document in a DoclingDocument; the diagrams below show how you can organise the pieces and connect them back to the page.

page 1612 × 792 ptheadingCapital Account Statementparagraph(amounts in thousands of GBP)paragraphFund: … Investor: …table9 rows × 4 columnsparagraph… is unaudited.row 1 · headersQuarter · Year · Inception… 7 more rows …row 9Partner's capital, endingcell · labelcell · £544the model points herecell · £544cell · £544every piece carries its own position on the page; the model can point at any of them and cannot make a new one
// pages, tables and cells: the pieces the model can point to
100 words on this pageeach one with its position"£544" sits at x 281, y 424too small to point at on their own;they give every piece its place.1 words2 layout3 table4 candidates→ the page starts as words, each with its position⏸ pause
// see where each piece sits on the original page; click a level to explore

2. Classify. Work out what you're looking at before asking for values. A report about the whole fund and a statement for one investor need different fields. I start with rules that check the title, headings and table headers, and ask a model when those leave room for doubt.

3. Find relevant passages. A short statement can fit in one model request, so I send all the parsed pieces in page order when it does. A sixty-page report gives the model a lot more to sort through, including plenty of numbers that look like the one you want. For those reports, I search for likely passages first. I combine keyword search in Postgres with a search for similar meaning, using pgvector and bge-m3 embeddings. I use reciprocal rank fusion to combine the two result lists by rank, so you don't have to make their different scoring systems agree.

If you've worked with RAG, retrieval-augmented generation, this part will look familiar. You find relevant passages and give them to the model. For the meaning-based search, bge-m3 turns each piece of text into a vector: a list of numbers that represents aspects of its meaning. Do the same for the description of the field you want, then search for nearby vectors. With pgvector, you can keep those vectors in Postgres alongside the text and page coordinates. Here, the model uses the results to pick source references. Search can miss the right passage, though, so this step needs testing too.

4. Extract. Ask for the fields together in one model call per document, so the model can use the surrounding figures to tell, for example, a beginning balance from an ending balance. Request structured output: a form with a defined set of fields. Each field holds a reference to the piece containing the value. Code validates that reference, reads the text and applies the currency and scale.

I keep one field list per document type. Each entry describes what you're looking for, its value type, whether it's required and the labels you might see in the document. Those labels can cover English, Dutch, German and French, for example. Then you can reuse the same definition for the search terms, the model's instructions and the checks on the returned value. Fewer places to update when you add a field.

5. Verify. Read the selected cell again with code, without asking the model, and check that the text, currency and scale reproduce the stored value. Reject the field if they don't. This catches a mismatch between the stored result and its source. It still won't catch a model choosing the beginning balance instead of the ending balance. That mistake can pass this check, which is why I wouldn't treat a successful re-read as proof that the answer is right.

To find out how well this works, I'd start with documents where I already know the answers. Fictional statements let you change the layout, currency or scale note and see what breaks. Keep some whole layout families out of tuning, then test on representative real documents you have permission to use. I'd score the cell selection and the value conversion separately, so a conversion bug doesn't get mixed up with a wrong-cell mistake. And I'd count missing answers separately from wrong ones. An empty field asks for attention; a plausible wrong number can slip into a report.

a person types itMANUAL · who · whena file is importedIMPORT · job ida document is readOCR · scan idaudit eventwho · source · correlation idfield changeold → newPosition.navValue544,000 GBPdocumentPDF · scan · photoscanschema key + versionparser output keptblock b53page 1 · bbox"£544"field navValue£544 → 544000 GBPVERIFIEDapplicationfrozen once appliedany stored number opens the page it came from, with these pixels highlighted
// follow a stored number back to its source; extracted values also keep the conversion details

Further reading: the research behind the approach

There's a research history behind asking a model to point. A generative model writes an answer; an extractive question-answering model selects text from the source. The pointer network paper from 2015 describes a neural mechanism that outputs positions in its input. A 2021 paper explores extracting answer spans from a generative model's attention instead of using its generated answer. That constrains the answer to source text, but selecting the right text is still a separate problem. A 2026 document-parsing benchmark also evaluates visual grounding: connecting parsed content to the right region of the page. I'm using explicit source references here, rather than implementing those papers' neural mechanisms. The connection is the ability to point back to the evidence.

The OCBC team's April 2026 paper, A Multistage Extraction Pipeline for Long Scanned Financial Documents, is closer to the financial-report problem. They tested extraction from long, scanned documents in several languages. Their pipeline cleans up the image, reads the text, finds the relevant pages and then extracts the data. In their evaluation, this improved performance by up to 31.9 percentage points over giving the model the whole document at once. Finding the right pages made the biggest difference. My system also separates finding the relevant passages from extracting the values, with the source-reference constraint described above.


I still want the convenience of uploading a report and getting the values back. I also want to click a number and see the cell, the page and the conversion that produced it. If the model picks the wrong balance or the code misses a scale note, I need enough information to find the mistake before that number ends up in someone else's report.


#ai #documentextraction #softwareengineering
NORMAL masterask_the_model_where_not_what_document_extraction.md[+]
michel
--:--