Ìtàn: an offline extension officer on a $200 laptop

Inspiration

Sub-Saharan Africa runs roughly one agricultural extension officer per 1,000 to 3,000 farming households. The FAO recommends something closer to 1:400. That gap is not abstract. It shows up as a farmer spraying the wrong pesticide on a misidentified pest, applying urea at the wrong growth stage, or missing a planting window by three weeks because the calendar for Oyo is not the calendar for Kano.

The obvious fix is a phone and a frontier model. It does not work. Rural coverage is intermittent, per-query data is a recurring cost against thin seasonal margins, and the models themselves are weak in Hausa, Yorùbá and agricultural Pidgin. Worse, a general model does not know that the maize window in Oyo differs from Kano, and it answers anyway, fluently and confidently and wrongly.

So we named the project Ìtàn, Yorùbá for story or account, and built the thing that actually fits the constraint: a fully offline advisory agent on a commodity Ubuntu laptop, four languages, a citation on every fact, zero network dependency.

The arithmetic that decided the architecture

ADTC publishes its scoring function in advance. That quietly converts an open-ended build into a constrained optimisation problem, and we chose to take it literally:

$$S_{\text{total}} = 0.50 \cdot S_{\text{acc}} + 0.30 \cdot S_{\text{perf}} + 0.20 \cdot S_{\text{eff}} - P_{\text{thermal}}$$

$$S_{\text{perf}} = 100 \cdot \frac{\text{TPS}{\text{actual}}}{\text{TPS}{\text{ref}}}, \qquad S_{\text{eff}} = 100 \cdot \frac{7\,\text{GB} - \text{Peak}_{\text{RAM}}}{7\,\text{GB}}$$

Read casually, "accuracy is 50%" says use the biggest model that fits in 8 GB. We think that is the single most expensive mistake available in this competition, because model size does not only move accuracy, it dominates the other half of the score.

Metric 7B @ Q4_K_M 1.5B @ Q4_K_M
Peak RAM ≈ 4.9 GB ≈ 1.5 GB
$S_{\text{eff}}$ contribution 6.0 pts 15.7 pts
Throughput ≈ 5 to 7 tok/s ≈ 26 to 34 tok/s
$S_{\text{perf}}$ contribution 5.4 pts 30.0 pts
Non-accuracy total 11.4 / 50 45.7 / 50

Going small is worth roughly 34 points before a single accuracy question is asked. A 7B would need to beat a 1.5B by 68 raw accuracy points just to break even, which is not a thing that happens on a retrieval-grounded, domain-narrow task.

That single table forced every other decision. If the model must be small, it cannot be asked to memorise. So knowledge moved to retrieval, arithmetic moved to Python, and the model was left with the one job it is actually good at: reason, select a tool, read the evidence, answer in the user's language.

How we built it

Qwen2.5-1.5B-Instruct at Q4_K_M on llama.cpp, CPU only, memory-mapped. Retrieval is hybrid: BM25 plus dense vectors from bge-small-en-v1.5 running under ONNX, fused with Reciprocal Rank Fusion. Hybrid rather than pure dense, because agricultural queries are dense with rare proper nouns (cultivars, active ingredients, pest binomials) that small embedders represent badly.

The core idea is a four-tier taxonomy, where the architecture is the classification, and each tier controls exactly how much the model gets to see of any number:

  • Tier A, exact facts. Spacing, NPK rates, pre-harvest intervals. Answered by a SQL query against structured tables, every row carrying a source_id. The model is invoked only to phrase the card in the user's language. It cannot invent the number because it is never asked for one.
  • Tier B, explanation and diagnosis. Hybrid retrieval, top passages, constrained generation with mandatory citation.
  • Tier C, calculated. agri_calc runs four deterministic functions (fertiliser rate, seed rate, spray dilution, gross margin) in Python. The model phrases the result, it does not compute it.
  • Tier D, refusal. Out of scope, or retrieval confidence below threshold, and Ìtàn says so plainly and refers to a human officer.

Scope was cut hard and deliberately: six Nigerian staples (maize, cassava, rice, yam, cowpea, tomato), pest and disease diagnosis plus soil fertility. No livestock, no market prices, no credit. A narrow accurate table beats a wide inaccurate one, and we were willing to throw away earlier corpus work to keep that true.

What we learned

Chunking is not a detail. Fixed-size chunking splits a fertiliser dosage table across two chunks and the system returns half a table. Semantic boundaries and a hard no-split-tables check moved accuracy more than anything else we tried.

Never hardcode an agronomic constant. Every rate, concentration and interval in agri_calc comes out of SQLite with source_id NOT NULL. A constant baked into Python is a claim nobody can audit and nobody can cite.

Decide the rounding policy before writing the test cases. We learned this the annoying way. Python's built-in round() uses banker's rounding, so round(2.5) is 2. Under exact-match scoring that is a silent, reproducible wrong answer. We moved to explicit round-half-up and documented it.

Fluent wrongness is the real enemy. Our sharpest failure was not a refusal or a crash. A pest query that should have returned fall armyworm returned weevil, attached to fabricated source IDs. The wrong answer arrived wearing the costume of a grounded one. That single bug reshaped how we think about citation: a citation the system can invent is worse than no citation at all, because it converts a visible error into an invisible one.

Challenges

Building the evaluation harness first, before any application code, was the best decision we made and the one that hurt most. It kept producing numbers we did not want.

Retrieval Hit Rate is sitting at 70% at $k=5$, against an exit criterion of $\geq 0.80$ at $k=4$. Our chunk count drifted to roughly 16k against a 10k design target, and we believe the excess is diluting the ranking rather than enriching it. Pruning is in progress.

The router collapsed under end-to-end testing, sending Tier A and Tier C queries to Tier D refusals. The safety gate we built to prevent confident hallucination became so conservative it started refusing questions the system could answer perfectly. Calibration cuts both ways.

Observed throughput came in around 8.7 tok/s against a roofline ceiling near 33.7 tok/s for this model and this memory bandwidth:

$$\text{TPS}_{\max} \approx \frac{32\ \text{GB/s}}{0.95\ \text{GB}} \approx 33.7\ \text{tok/s}$$

Being at 26% of ceiling does not mean the design is wrong, it means the build configuration is. Knowing the ceiling is what tells you whether a 20% improvement is progress or noise.

And the least glamorous challenge of all: verifying gold answers. A 200-question gold set is worthless if the answers were never checked against the corpus, and no script can do that for you. Five of our questions asked for chemical treatments against viral diseases where no chemical cure exists. Catching those took a human reading every line.

What is next

Prune the corpus toward the retrieval target, fix the router gate, close the throughput gap against the roofline, finish gold verification, and expand the Hausa and Yorùbá evaluation coverage to match the rest.

Ìtàn puts a competent agricultural extension officer on a refurbished laptop, in the farmer's own language, with no internet and no data cost, because we made the model small enough to be fast and grounded enough to be right.

What it does

How we built it

Challenges we ran into

Accomplishments that we're proud of

What we learned

What's next for itan

Built With

Share this project:

Updates