Better Harness, Better Answers

Document Intelligence Without Bigger Models

Nirant Kasliwal

Nirant Kasliwal

Machine learning and AI engineer. I started Scaled Focus to help companies evaluate, optimize and deploy agents.

Built

FastEmbed, first version

Embedding library on ONNX, used by Qdrant, NVIDIA NeMo Guardrails and 3,000+ more repositories

Contributed to

Ragas

Metrics for evaluating agents

Wrote

NLP in Python: Quickstart Guide

Book for software engineers, over 5,000 copies sold

Maintained

Awesome NLP

Resource list, recommended by Andrew Ng’s CS 230

Earlier talk on harness engineering: The Agent as Compiler. More at nirantk.com

2

One small model stays locked. Only the harness changes.

Model

Qwen3.5-9B, 4-bit

fp4 weights, Darkbloom through OpenRouter. Temperature 0, three runs, thinking measured apart.

+

Document processing

OCR, conversion, chunks

Search

Embedding, BM25, Hybrid, grep

How we use search

One pass, or tools in a loop

Score on

20 + 20 questions, same 531 pages

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

3

Corpus holds real public documents

Twelve source pages from the corpus: EU labour report, contracts, culvert manual, French energy report, Dior annual report, FATF standard, a technical order with drawings, IMF report and WHO guidance

531

pages

98

documents

20 + 20

validation and test questions

Sources: XL-DocBench, ViDoRe v3 and CUAD. Validation and test differ only in their questions. Answer pages sit among the closest matches by embedding and BM25.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

4

Why is this hard?

The answer is not text

1  The page is a scan

A scanned table holds no text for search to match.

2  The value is in a chart

A bar has a height, not a number.

3  The label is in a drawing

A part’s name exists only as ink on a diagram.

The answer is not where search looks

4  The wording differs

The question says B gas. The report says gaz B.

5  The proof is an absence

No retrieved passage can show a clause is missing.

6  The answer spans pages

One page points to the next. None holds it all.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

5

The answer is not text

1 THE PAGE IS A SCAN

What is the next Ship Transport drop height after a non-damaging impact?

Scanned Table 516.8-X with Ship Transport drop height 1.5 m highlighted

PDF text on this page: 0 characters

MIL-STD-810H, p. 40

2 THE VALUE IS IN A CHART

What was the average hourly wage in the EU’s finance and insurance sector in 2021?

Graph 2.16: bar chart of hourly wages with finance and insurance reaching about 50 euros

The bar reaches 50 with no data label

EU wage report, p. 77

3 THE LABEL IS IN A DRAWING

Where is the 25mm gun safety pin stored when not in use?

Line drawing of an aircraft access door with the label Safety Pin Stowage Bracket highlighted

PDF text: only the figure’s caption

TO 00-25-172CL-3, p. 7-12

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

6

LightOnOCR-3 reads the chart

Graph 2.16 from the EU labour report: bars of hourly wages by sector, finance and insurance near 50 euros, with no printed value

LightOnOCR-3 output

| Sector | Real hourly wage 2021 |
| --- | --- |
| Finance & insurance | 50 |
| ICT | 48 |
| Real estates | 33 |
| Construction | 29 |

PDF text: not_found, because the bar has no data label.

Markdown: 50 EUR, the benchmark’s answer.

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Grep agent, three runs: not_found on PDF text, 50 EUR on Markdown. Validation question 3. EU wage report, p. 77. Output shortened.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

8

PyMuPDF restores lines OCR missed

Docling, OCR off

every page

1x

baseline, words

1x

baseline, numbers

pymupdf4llm

text pages

LightOnOCR-3

scan, chart, drawing pages

3.3x

fewer words lost

1.4x

fewer numbers lost

pymupdf4llm

text pages

LightOnOCR-3

scan, chart, drawing pages

PyMuPDF

restores lines OCR missed

15x

fewer words lost

12x

fewer numbers lost

1x is the worst pipeline. The 531 pages, scored against the text PyMuPDF extracts, so OCR on scans goes unscored. Restoring also runs on pymupdf4llm pages, where it adds little. Results

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

9

OCR wins only when the answer is pixels

0 of 20

test questions need OCR

Every answer is in the PDF text.

1 of 20

validation questions need OCR

EU wage chart: 0 of 3 runs right on PDF text, 3 of 3 on Markdown.

2 of 2

separate questions need OCR

Scanned table and line drawing: not_found on PDF text, correct on Markdown.

Count your scans, charts and drawings before you pay for OCR.

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Needs OCR: the answer exists only in the page image. Chart: grep agent, 3 runs. Scan and drawing: Grep Token Reduction, 1 run each, outside the scored question sets.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

10

Hybrid adds 2 answers for almost no tokens

Strategic alliance agreement clause 1.3 with $19,644,000 highlighted

Embedding top 20

BM25 top 20

Hybrid: RRF

k = 60, keep top 5

BM25 finds the exact string “$19,644,000”, which embedding search misses in every run. Hybrid has a median of 7 of 20 across 3 runs, against 5 on test.

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Three runs on 531 pages. Same chunks, same prompt, same top 5. Tokens: +1% median, +2% total. Exact string: validation question 9. Adaptimmune alliance agreement, p. 2

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

11

Plain RAG answers cybersecurity for biology

Which incident category covers misuse of high-consequence biological material?

One page of the guidance: four incident categories

5.3.1 Incidents directly involving biological agents

“misuse of high-consequence material.”

THE ANSWER. This heading is in no retrieved chunk.

5.3.2 Physical security incidents

5.3.3 Personnel-related biosecurity incidents

5.3.4 Incidents related to information security and cybersecurity

A RETRIEVED CHUNK STARTS HERE.

PLAIN RAG, WRONG IN ALL 3 RUNS

“Incidents related to information security and cybersecurity”

GREP AGENT, READS THE PAGE

“Incidents directly involving biological agents”

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Question shortened. Validation question 18, shown as an example. WHO Laboratory biosecurity guidance, p. 41

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

12

Search runs in one pass or in a loop

Linear pipe aka plain RAG

Question

→

Retrieve top 5

→

Answer

5

Single fan-out

Question

→

Write terms once

→

Grep each term

→

Answer

5

Grep agent

Question

→

Call grep or read

→

Read the result

→

Answer

↺ repeats until it can answer

7

BM25 agent

Question

→

Call BM25 or read

→

Read the result

→

Answer

↺ repeats until it can answer

9

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Correct out of the 20 test questions: median across 3 runs, thinking off. The BM25 agent was added after the test scores were known.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

13

BM25 agent adds 2 answers

2 more answers

Median correct out of 20, across 3 runs

Grep agent 7–9 per run

7

BM25 agent 8–10 per run

9

Hybrid agent 8–10 per run

9

BM25: −19% median, +34% total

Bars: median per question. Total: per run of 20.

Grep agent 2.8M total

155k

BM25 agent 3.7M total

126k

Hybrid agent 4.7M total

238k

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. 20 test questions, 531 pages. Tokens: input and output, medians across 3 runs. Hybrid: embedding + BM25 with RRF, top 5. Both loops were added after the test scores were known.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

14

Our tools hide the clause from the grep agent

Contracts under the law of the State of Israel: our first rewrite named 5 of 6, each with a correct quote.

NICE outsourcing agreement, clause 17.11 Law and Jurisdiction: governed by and construed in accordance with the laws of the state of Israel

The missed contract, in the PDF. The converter merged clauses 17.9 to 17.11 into one line of 4,248 characters.

What the model saw of that one line

Read showed 600 characters. Search showed 400. Neither gave a warning.

Clause 17.11 starts at character 2,197

Fix: 200 characters around each match, whole lines on read. The fixed agent named all 6.

Local run: Qwen3.5-9B, nvfp4, Ollama, earlier 222-page set, question 6, NICE outsourcing agreement, CUAD. Hosted on merged Markdown the long line is gone, and a 6,000-character read limit stops the grep agent instead. Bar drawn to scale. NICE outsourcing agreement, p. 25

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

15

Fresh traces expose a new loop in our own fix

HELD-BACK SET, QUESTION 4

read(line 204) × 9

each repeat refused by the harness
budget gone, 20 of 20 calls
answer: not_found

HELD-BACK SET, QUESTION 7

final_answer(…) × 3, no such tool

read(line 54) × 9, refused
budget gone, 20 of 20 calls
answer: wrong figure

Three changes to the harness

Adds a real final-answer tool, the one the model kept asking for

Ends the search after two turns of refused calls

Stops charging refused calls to the budget

Local run: Qwen3.5-9B, nvfp4, Ollama, held-back set of 10. Grep agent after our first rewrite, run 1: half of the ten questions used all 20 calls. With those changes, 2 used all 20.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

16

Fixes cut tokens; the score stays within noise

Score within noise

Median correct out of 20, across 3 runs

Grep agent 7–9 per run

7

Grep Token Reduction 7–8 per run

8

−37% median, −21% total

Bars: median per question. Total: per run of 20.

Grep agent 2.8M total

155k

Grep Token Reduction 2.2M total

98k

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. 20 test questions, 531 pages. Tokens: input and output, medians across 3 runs. Grep Token Reduction frozen before the test questions.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

17

Same model goes from 5 to 9 on harness alone

How the model uses search

Test, median of 20

Tokens

Total

Turns

Plain RAG

Embedding, top 5, one answer

5 (5)

2.6k

51k

1

Hybrid RAG

Embedding + BM25 with RRF, top 5, one answer

7 (6–7)

2.7k

52k

1

Single fan-out

One batch of grep terms, one answer

5 (4–5)

6.6k

141k

2

Grep agent

Grep and read tools in a loop

7 (7–9)

155k

2.8M

20

Grep Token Reduction

Adds refusals, quote checks, a collection map and multi-page reads

8 (7–8)

98k

2.2M

13

Grep Token Reduction + Thinking

Adds reasoning before each reply

8 (8–11)

94k

2.1M

11

BM25 agent

Swaps grep for the five best BM25 passages

9 (8–10)

126k

3.7M

13

BM25 agent + Thinking

Adds reasoning before each reply

6 (5–10)

304k

5.5M

21

Hybrid agent

Swaps grep for the five best Hybrid passages

9 (8–10)

238k

4.7M

19.5

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Medians across 3 runs; brackets show lowest to highest run. Tokens and turns: per question. Total: per run of 20. Loops with BM25 and Hybrid were added after test scores were known. Results

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

18

Thinking does not lift the agents’ medians

Test 20

Thinking offThinking on, one dot per run

Plain RAG

offon5 → 7

Single fan-out

offon5 → 4

Grep Token Reduction

offon8 → 8

BM25 agent

offon9 → 6
05101520Correct answers out of 20

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. 531 pages. Right column: median across 3 runs, thinking off to on. Reasoning shares the output limit: 16,384 tokens on, 4,096 off.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

19

No fix came from these 20 questions

Plain RAG

5

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Hybrid RAG

7

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Single fan-out

5

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Grep agent

7

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Grep Token Reduction

8

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Grep Token Reduction + Thinking

8

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

BM25 agent

9

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

BM25 agent + Thinking

6

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Hybrid agent

9

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. 20 test questions, 531 pages. Dark: correct in all 3 runs. Mid: in some. Right: median correct of 20 across 3 runs.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

20

Loops cost 36–115x plain RAG’s median tokens

Turns per question

Tokens per question, total per run

Plain RAG

1

2.6k 51k

Single fan-out

2

6.6k 141k

Grep agent

20

155k 2.8M

Grep Token Reduction

13

98k 2.2M

Grep Token Reduction + Thinking

11

94k 2.1M

BM25 agent

13

126k 3.7M

BM25 agent + Thinking

21

304k 5.5M

Hybrid agent

19.5

238k 4.7M

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. $0.08 per million input tokens, $0.13 output. Bars: median per question across 3 runs, test 20. Grey: total tokens per run of 20, median across 3 runs. Each panel has its own scale.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

21

Run this loop before you buy a bigger model

Before the first loop

Convert scans, charts and drawings to Markdown, with page markers

Measure plain RAG and Hybrid RAG on your own questions

Freeze validation and test questions

Observe

What happened?

Log every tool call and every tool result

Example: read(line 204) nine times, then not_found

Orient

Which layer failed?

Place each failure in documents, search or use of search

Example: a silent 400-character cut hid the Israel clause

Decide

What do we change?

Fix a class of failure, never one question

Example: silent cuts, so tools now label cuts and empty results

Act

Did it work?

Change one part, then retest on fresh questions in three runs

Example: one run scored 13 of 20; three more scored 11, 8 and 8

Same
model

Next failure

OODA loop: John Boyd. Examples come from the traces in this talk.

1 The Challenge

2 Document processing

3 Search

4 How we use search

5 Fixes cut tokens

6 Results and playbook

22

Change search and the loop before the model

Read the trace to learn why an answer failed.

Questions, pages and traces: nirantk/document-harness-evaluation
Code: github.com/NirantK/better-harness-better-answers

Documents: CUAD (The Atticus Project, CC BY 4.0), ViDoRe v3 (arXiv 2601.08620), XL-DocBench (arXiv 2608.00036).

23

Questions the details answer

What if BM25 gets the token fixes?

25

Can it connect facts across pages?

26

What is in the harness?

27

How does it choose the file?

28

Does it handle other languages?

29

How did the excerpt fix work?

30

Who decides what the model sees?

31

Where does the empty-result hint go?

32

Why does the agent read the wrong lines?

33

What does refusing a call mean?

34

What else changed in the harness?

35

What runs in each component?

36

Which embedding and index run search?

37

One full agent run, every message with notes: Inside One Agent Run

FAQ

24

Token fixes save BM25 a third of its total

Score within noise

Median correct out of 20, across 3 runs

BM25 agent 8–10 per run

9

BM25 Token Reduction 7–9 per run

8

With model-chosen count 6–9 per run

7

−22% median, −34% total

Bars: median per question. Total: per run of 20.

BM25 agent 3.7M total

126k

BM25 Token Reduction 2.5M total

97k

With model-chosen count 2.8M total

111k

Hosted runs: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. 20 test questions, 531 pages. Tokens: input and output, medians across 3 runs. Tuned on validation questions and added after the test scores were known.

FAQ

25

Multi-hop question spans three pages

For outlet-control headwater of a flowing-full concrete box culvert, which equations and chart are used?

Federal Highway Administration page: Equation 2 can be solved for H by the use of nomographs, Charts 8 through 14

Equation 2 sets the head; Charts 8 to 14 solve it p. 10

Page text: for tailwater at or above the top of the culvert, find headwater by Equation 3

High tailwater needs Equation 3 as well p. 17

Chart list with Chart 8, Head for Concrete Box Culverts Flowing Full, highlighted

Of those charts, only Chart 8 is concrete box p. 21

Answer: Equation 2 and Equation 3; Chart 8

Plain RAG on PDF text: 2 of 3

Equation 3 and Chart 8

Hybrid RAG: 1 of 3

Chart 8

Trace Fixes: 3 of 3

All three, across 11 calls

Local runs: Qwen3.5-9B, nvfp4, Ollama, earlier corpus. Validation question 16. HEC-5, Hydraulic Charts for the Selection of Highway Culverts, Federal Highway Administration.

FAQ

26

The harness is a prompt, tools and checks

<role>

Every fact must come from a line you read with a tool.

<catalogue>

path | title | language | pages, for every file

<collection>

Which sources exist and how each one words things.

<file_format>

A printed contents page number is not a line number.

<procedure>

Search terms in the document’s language. Read around every hit. Check every candidate.

<final_answer>

JSON with a quote for each part of the answer.

Tool output

“0 matches in all 98 files. This does not yet show absence. Try one distinctive word, a synonym, or the document’s language.”

Loop checks

Refuses repeated calls, checks each quote against the cited file, and sends early not_found answers back.

Final answer

A real tool: the model kept calling one we had not defined.

Prompt sections use XML tags, as Qwen’s guidance suggests. Full prompt: structured_agent.py

FAQ

27

Two maps show collection and contents

<catalogue> in the prompt, 98 files

vidore-finance_fr/christian_dior_2023.md | christian_dior_2023 | french | 8 pages

vidore-industrial/TO_00-25-172CL-5.md | TO_00-25-172CL-5 | english | 3 pages

xl-docbench/doc_000143.md | HYDRAULIC CHARTS FOR THE SELECTION OF HIGHWAY CULVERTS | english | 11 pages

<collection> in the prompt

cuad/ Commercial contracts. Governing law reads “governed by” or “laws of”.

vidore-*/ Reports on energy, finance, employment and industry. French reports need French terms.

xl-docbench/ Long reports, standards and books. A question often names the document by its title.

<file_format> in the prompt

A page number printed in a table of contents is not a line number.

outline(TO_00-25-172CL-5.md)

1: page 0

2: # TO 00-25-172CL-5

33: # 4 RELATED PUBLICATIONS.

45: page 1

73: page 2

76: # 6 HARDNESS CRITICAL ITEMS (HCI).

84: # 7 ELECTROSTATIC DISCHARGE SENSITIVE (ESDS) ITEMS.

92: # 8 IMPROVEMENT REPORTS.

read(pages=[2])

88: All ESDS parts shall be handled in accordance with the ESDS device handling procedures in TO 00-25-234.

Hosted run: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Validation question 5, first run, Grep Token Reduction. Outline shown as the tool returns it; this run reached page 2 by search. TO 00-25-172CL-5, p. 11. Excerpts; markup removed.

FAQ

28

Excerpts expose the clause on the same line

BEFORE: FIRST-REWRITE GREP AGENT

search("governed|law of|Governing Law")

13 [page 1]: - 17.9. DISPUTE RESOLUTION. Any dispute arising out of or relating to this Agreement or the breach, termination or validity thereof shall be settled in accordance with one of the following procedures. Contractor and NICE shall use the procedures in the following order of priority. 43 17.9.1. In the event of any dispute, controversy or claim of any kind or nature arising under or in connection with

answer: 5 of 6, NICE missed

AFTER: TRACE FIXES

search("governed by")

13 [page 1]: provided herein, and except that the provisions of Section 8.2. and all its subsections will apply with regard to details included in APPENDIX C. 17.11. LAW AND JURISDICTION. This Agreement shall be governed by and construed in accordance with the laws of the state of Israel, without giving effect to choice of law rules. 44 17.12. SCHEDULES AND APPENDICES. Each Schedule and Appendix hereto is incorporated h [excerpts of a 4248-character line]

answer: 6 of 6, NICE named

Tool

Before

After

Search

First 400 characters, no warning

±200 characters around each match, at most 3, then a length marker

Read

First 600 characters, no warning

Whole line; past 6,000 characters a marker says how many are left

Local run: Qwen3.5-9B, nvfp4, Ollama, earlier 222-page set, question 6, first run of each. The model wrote each search itself, so the patterns differ.

FAQ

30

Search cuts a long line around each match

One line of the NICE contract, 4,248 characters. Blue goes to the model. Orange marks each match.

search("governed by")

1 match, 1 excerpt, 411 characters

search("court")

2 matches, 2 excerpts, 810 characters

search("agreement")

15 matches, 3 excerpts, 3,685 characters

Sends short lines whole

A line of 400 characters or fewer goes through uncut.

Finds the model’s pattern

The search the model wrote decides where the excerpts sit.

Keeps 200 characters each side

Excerpts that overlap merge into one.

Stops at 3 excerpts

Later matches drop out. A marker gives the line’s length.

No model call: matching_text in structured_tools.py run on line 13 of the earlier Markdown. Bars drawn to scale.

FAQ

31

The hint lives in the tool response

Tool response, role=tool

0 matches for 'ESDS.*technical order|TO.*ESDS' in file TO_00-25-172CL-5.md.

This does not yet show absence. Try one distinctive word, a synonym, or the wording of the document’s language.

No single line holds all of these terms. Lines holding each term alone:

ESDS: 4 matching lines in 1 files.

technical order|TO: 21 matching lines in 1 files.

Search one term at a time, then read the page that holds it.

Pseudocode

system = rules # fixed, sent every turn

result = search(pattern, file)

if result is empty:

    result += HINT

    if pattern joins terms with .*:

        result += count_each_term()

messages.append(role=tool, result)

Next call in this run

search("ESDS")

88 [page 2]: All ESDS parts shall be handled…

Hosted run: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Validation question 5, third run, Grep Token Reduction. Output shortened.

FAQ

32

Grep agent reads a page number as a line

FATF Recommendations contents page with the entry Table of Acronyms, page 140, highlighted

read(1) sees “Table of Acronyms|140”

read(140)

read(240) holds the evidence, unused
read(120) read(143) read(230)

search("Table of Acronyms") finds line 15
read(138) read(135) read(130)

read(138) × 7, until the budget ends

answer: not_found

Local run: Qwen3.5-9B, nvfp4, Ollama, earlier 222-page set, question 17, The FATF Recommendations, grep agent, run 2, 20 of 20 calls. Printed page 140 is not line 140 of a 264-line file. FATF Recommendations, p. 3

FAQ

33

Harness refuses a repeated call

Question: which official in the 2019 OCC Annual Report oversees “compliance and community affairs”?

read(lines 225-254)
line 230 names Morris R. Morgan
read(lines 165-194)
draft answer: Morris R. Morgan, then a review turn

read(lines 228-237)
Lines 228-237 are already above. Read other lines, search again, or answer.
read(lines 228-237)
You made this exact call at step 5; the result is unchanged. Change the terms or the lines.

harness, tools removed:
Stop searching. Give the final JSON now.
answer: Morris R. Morgan, correct

Skips the tool

The reply says why and what to try instead.

Spares the budget

A refused call uses none of the 20-call budget.

Stops after two turns

After two turns of refused calls, the tools go away and the model must answer.

Hosted run: Qwen3.5-9B, 4-bit (fp4), Darkbloom through OpenRouter. Validation question 13, first run, Trace Fixes. Question and output shortened.

FAQ

34

Every fix answers a failure in the traces

Grep agent failure in the trace

Change to the harness

Misses a title on letter case

Ignores case in search

Guesses file paths from titles

Lists paths and titles in the prompt

Reads a contents page number as a line

Adds an outline tool and a page number on every hit

Repeats a read until the budget ends

Refuses repeated calls and reads

Gives up after one or two phrasings

Returns early not_found answers to the model

Answers without a line to back it

Checks quotes against the cited file

Stops at a tool call Ollama cannot parse

Sends the model a correction, not 20 retries

Loses the end of long lines with no warning

Returns excerpts on search, whole lines on read

Prompt sections use XML tags. Tool calls use the native tools API, which renders Qwen’s own format. Sampling settings are unchanged.

FAQ

35

We measure three components, one at a time

1

Document processing

OCR: LightOnOCR-3 0.8b, for scans, charts and drawings

Other pages: pymupdf4llm

Merge: PyMuPDF restores lines OCR missed

Chunks: 512 tokens, 50 overlap

2

Search

Embedding: qwen3-embedding:0.6b

Vector search: FAISS IndexFlatIP

Keyword: BM25, by bm25s

Hybrid: RRF, k = 60, top 5

Grep: ripgrep over every file

3

How we use search

Linear pipe aka plain RAG: retrieve once, answer once

Single fan-out: one batch of terms, one answer

Grep agent: grep and read tools in a loop

BM25 agent: BM25 search and read tools in a loop

FAQ

36

Search stack has five exact parts

Embedding

qwen3-embedding:0.6b in Ollama

Qwen/Qwen3-Embedding-0.6B, 1024 dimensions

Vector search

FAISS IndexFlatIP

Exact inner product, no approximation, top 5

Keyword

BM25, by bm25s

Over the same 512-token chunks

Hybrid

Reciprocal rank fusion (RRF)

k = 60, top 20 of each, keep 5

Grep

ripgrep

Every file, line by line, no index

SentenceSplitter builds 1,219 chunks. Voyager HNSW: 0.10 ms, 97% recall. Exact FAISS: 0.13 ms, 100%. Comparison

FAQ

37