FarmTwin
A cited agronomic advisor that runs entirely on a 2014 laptop with the network unplugged.
- Period
- 2026 · 7-day sprint
- Role
- Sole developer
- Status
- Submitted
Model selection and benchmarking, knowledge base curation and chunking, retrieval pipeline, backend, frontend and design system, the speech-to-text evaluation, and the full submission artifacts including ADRs.
The problem
Smallholder maize farmers in Nigeria make high-stakes agronomic decisions — planting dates, fertilizer timing and rates, pest response — largely without extension services. Where digital advisory tools exist they assume reliable connectivity, a modern smartphone and a cloud backend. That assumption fails in exactly the places the advice is most needed.
There is also a trust problem specific to AI advisory in agriculture. A chatbot answering how much urea to apply at six weeks with a plausible but ungrounded number is worse than no tool at all, because a wrong dose costs a season.
Why it mattersThe agronomic knowledge is not missing — NAERLS, IITA, CIMMYT and FAO have published extensively. It sits in PDFs and extension bulletins that never reach the farm. Making that corpus queryable in natural language, on a device with no network, is the actual delivery problem. Offline is not a constraint imposed by a brief; it decides whether the system is usable at all.
What I built
A fully offline digital twin of a smallholder maize farm. It models the farm's state and answers agronomic questions in natural language, with every answer grounded in a curated knowledge base and cited back to source. No network calls, no API keys, no cloud inference — the model, the vector index and the data all live on the device.
Built in a seven-day sprint for the Africa Deep Tech Challenge 2026 Laptop LLM Challenge.
How it holds together
Everything inside the boundary runs on the device. Cut the network and nothing changes — that is the entire thesis. Try the switch.
45 curated chunks drawn from NAERLS, IITA, CIMMYT and FAO, embedded and stored in SQLite via sqlite-vec. Using SQLite as the vector store rather than a dedicated vector database keeps the whole system to a single file dependency and avoids running a second server process on a machine already CPU-bound by inference.
Qwen2.5-3B-Instruct at Q4_K_M quantization through llama.cpp. Queries are embedded, top-k chunks retrieved, and context assembled into a prompt that requires the model to cite the chunks it used.
FastAPI and SQLAlchemy over SQLite, exposing a streaming SSE endpoint with a non-streaming fallback preserved. On the client, a stripJsonBlock utility separates the structured citation payload from the prose stream as tokens arrive, so text renders progressively without raw JSON scrolling past.
Decisions
- 01
Model selection turned on grounding, not fluency
Benchmarked Qwen2.5-3B against Llama-3.2-3B on a structured rubric with manual grading against gold prompts. Llama was disqualified primarily because it hallucinated citations — inventing sources that were not in the retrieved context. For a system whose entire value is that an answer comes from IITA and here is the chunk, that is decisive rather than a rough edge. Qwen took the composite 83.5 to 72.9, driven by retrieval grounding.
- 02
I graded my own benchmark down
My first pass scored Qwen around 98.8. That was inflated — I had been generous on partial-credit cases. Re-graded against a stricter reading of the rubric it landed at 87.6. The temptation to let your chosen model score well is real, and a benchmark you have quietly rigged tells you nothing.
- 03
I cut the feature I wanted most, on evidence
I am a native Pidgin speaker, and Pidgin voice input was the feature I most wanted — it is the language a large share of the target users actually speak. I ran a 15-clip evaluation against a 40% WER gate defined before testing. Whisper base failed on Pidgin. The Nigerian-English fine-tune failed on Pidgin. NCAIR1's dataset was behind a gated repo and Orinode was not publicly available inside the sprint window. The feature scoped down to English-only, labelled honestly in the interface and documented as ADR-011.
CostThe most valuable feature for the target user is not in the product. The gate existed so that decision would be forced by data rather than by how much I wanted it.
- 04
The development environment was a variable
On an 8GB 2014 Intel MacBook Pro, the desktop app I was using competed with llama.cpp for CPU cores during inference testing. I moved to the browser client for the sprint and wrote it up as ADR-009. Small, but the kind of thing that silently wrecks benchmark timings if you do not notice it.
Stack
- llama.cpp
- Qwen2.5-3B-Instruct
- Q4_K_M
- SQLite
- sqlite-vec
- semantic chunking
- FastAPI
- SQLAlchemy
- SSE
- Vite
- React
- TypeScript
- Tailwind
- whisper.cpp
- Whisper base
- Nigerian-accent fine-tune
- NAERLS
- IITA
- CIMMYT
- FAO
- Local quantized inference on decade-old consumer hardware
- Retrieval fidelity verified by a benchmark I re-graded against myself
- Citation enforcement as a model selection criterion
- Streaming prose and structured payload separated client-side
- A decision trail in ADRs, so the tradeoffs are auditable rather than folklore