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
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?
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
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
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.
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
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
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.
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?
Equation 2 sets the head; Charts 8 to 14 solve it p. 10
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
First-rewrite grep agent searches in French
GREP AGENT, WRONG, 6 CALLS
search("B gas") → 0 matches, 3 times
“The documents do not contain information about… B gas.”
FIRST-REWRITE GREP AGENT, CORRECT, 11 CALLS
search("gaz|fourniture|import…") → 16 lines
“La totalité du gaz B est importée des Pays-Bas”
Local run: Qwen3.5-9B, nvfp4, Ollama, earlier 222-page set, question 4. Source: Programmation pluriannuelle de l’énergie, ViDoRe v3, Etalab 2.0. Programmation pluriannuelle de l’énergie, p. 101
FAQ
29
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.
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
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