connected an LLM as interface to a genomics database, then spent most of the project working out how to tell whether it was right.
Kayla Queenazima · Github: kaylaque · chatbgc.matinnu.org/
One experiment, and how I think about benchmarking an LLM system. Early results — I have more questions now than when I started.
Not a finished study, and not a set of best practices.
Say the honest version out loud: most of the work was measurement, not model choice. Set the expectation that this is open-ended.
Next: who I am.

Twenty seconds. The third line is the one that matters — say it and move on.
Next: the actual problem.
65 tables with names that don't self-explain. Nothing in pre-training tells it how they connect.
Which path between two tables is the right one is a domain judgement, not a syntax question.
No error. No warning. Just a plausible answer that a non-SQL user cannot check.
Corridor story first. Then the three cards quickly. Spend your time on the two hallucination types — the second one is the reason the rest of the talk exists.
Next: why is the database shaped like this?
regions and bgc_types have no direct key — you go
through the junction table or you get nothing.
Walk the loop once: bacterium, genome, cluster, compounds, antiSMASH, database. Then the chips — that is what people actually want to ask about. The "nobody designed the mess" line usually lands.
Next: so what did I build?
Unpublished genomes, so everything runs locally on one GPU.
A lab, not a service. Optimise one request, not throughput.
Past question → SQL pairs are stored and reused, so the system gets better from use.
Different steps can use different models, to spend the local GPU where it counts.
Trace one question across the pipeline. Then the four cards fast. The callout is the hinge of the whole talk: benchmarking exists to stop me over-engineering.
Next: what does correct even mean here?
| Execution accuracy | rows match a known-good query | 1 / 0 |
| Negative cases | correctly says "nothing found" | 1 / 0 |
| Domain rules | schema prefix, junction path, read-only | 0–1 |
| Answer faithfulness | sentence matches the rows fetched | 0–1 |
| No-error | kept off the accuracy axis | 1 / 0 |
Seven layers in total — the full set is in the appendix.
This is the conceptual centre. Walk the four checkpoints, then say the red line out loud. Deterministic wherever possible; the judge only for phrasing.
Next: what I varied.
From one-shot SQL up to a second model critiquing the first.
A 27B generalist, a 7B SQL specialist, and a hosted free model.
Simple lookups, joins, aggregations — plus 14 where the answer is "nothing found".
One replicate per combination — the known weak spot. One 48 GB GPU.
The multiplication is the visual. Then one line per dimension — say the "what I want to know" line, not the description. Be honest about the single replicate.
Next: early results.
All scored runs, single replicate. Dashed = provisional — those two sweeps lost runs to a server fault and a wiring error, so their position here is a floor, not a score. Matched like-for-like they sit at 0.673 and 0.642 against Qwen's 0.769.
Let the chart sit before talking. Three insight lines, then the model provenance — the quantisation matters for anyone trying to reproduce this. If asked why the two dashed points are low: those are floors, not scores — one sweep lost 200 runs to a server fault, the other ran with its model tiers wired backwards. Matched like-for-like they are 0.673 and 0.642 against Qwen's 0.769. End on the question and pause.
Next: same question, per architecture.
| DEEPSEEK | QWEN 27B | XIYAN 7B | LAGUNAsmall n | ROUTER2 models | |
|---|---|---|---|---|---|
| t0 one shot | .793 | .732 | .159 | .850 | .800 |
| t1 think + retry | .768 | .780 | overflow | .833 | .861 |
| t2 retrieval | .512 | .829 | .749 | .800 | .557 |
| t3 picks own tool | .692 | .647 | .334 | .500 | .502 |
| t4 plan-execute | .561 | .748 | .598 | .667 | .681 |
| t5 critic loop | .700 | .854 | .788 | .696 | .671 |
| h1 retrieval + loop | .718 | .664 | .744 | .567 | .550 |
| h2 draft + revise | .554 | .793 | .761 | .613 | .513 |
| baseline plain pipeline | .512 | .841 | .749 | .727 | .603 |
Execution accuracy, pooled. n = 30–41 per cell, except Laguna at 9–30 — its sweep lost 200 runs and the survivors skew easy, so read that column as provisional.
t0 runs from .159 to
.850 across this row — the same code, a 0.69 swing. Small and weak models
do better with less room to reason; the strong one does better with more.t5, baseline,
t2 — are three of the router's worst four. Splitting work across two models
reordered the architectures. Any leaderboard has to name the model and the route.Do not read the grid. Point at the t0 row and sweep left to right — .159 to .850 on identical code. Then Qwen's best three against the router's worst four. Flag the Laguna column as provisional before anyone asks: 200 of its runs died on a server fault and the survivors skew easy.
Next: which questions are hard?
| DEEPSEEK | QWEN 27B | XIYAN 7B | LAGUNA | ROUTER | |
|---|---|---|---|---|---|
| simple one table | .673 | .827 | .683 | .744 | .685 |
| join across tables | .577 | .543 | .213 | .442 | .446 |
| aggregation counting, grouping | .487 | .486 | .400 | .423 | .405 |
Execution accuracy, pooled. n = 13–294 per cell, single replicate.
Point at the red cell, then sweep the whole aggregation row — five systems inside 0.087. The last callout is the one worth landing — strongest where it matters least.
Next: two problems I could not solve.
The 7B specialist has an 8k context window. Any architecture that pastes the whole schema into the prompt fills that window before the model has written a single line of SQL.
Left column, then right. The point is that a benchmark number can be about your plumbing. Tie it back to the .159 cell they saw two slides ago.
Next: the harder problem — my own evaluation failed.
And no run ever got the right rows while breaking a rule — so the checks never fire falsely. They just miss these.
Every rule I wrote encodes something I already knew to look for. These 36 runs broke something I had not thought of yet.
Read the four ticks, then the cross, then the number. The empty-set bug is the story — tell it as something I got wrong, not as a lesson. End on the question and stop.
Next: close.
Thanks to Matin Nuhamunada and the BEAMS Lab, Faculty of Biology, UGM.
The table is the argument, not a result — both rows are configuration defects, so say "measured my harness" and do not let either number stand as a verdict on the model. If asked: the router had interpret routed to the small tier, which happened to be the SQL specialist; the Laguna losses were 200 connection errors, and its surviving runs were 35% easy questions against a 15% baseline. Neither sweep has been repeated yet.
Then the two questions, slowly. End open; do not tidy it into a conclusion.
Next: one more thing.
| LAYER | CHECK | A SCORE LOOKS LIKE | COST | WHAT IT CATCHES |
|---|---|---|---|---|
| 1 Does it run | EXPLAIN + deny-list | 1 or 0 | FREE | Syntax errors, writes, unknown columns |
| 2 Right rows | gold vs predicted result sets | 1 or 0 | FREE | Wrong joins, wrong filters |
| 3 Right nothing | declared negative cases only | 1 or 0 | FREE | Rows invented for things that don't exist |
| 4 Right method | schema prefix · junction join · SELECT-only | 0.75 = 3 of 4 | FREE | Right rows by luck |
| 5 Answer matches rows | exact numbers + named entities | 0.60 | FREE | Correct query, invented summary |
| 6 Answer is sensible | LLM judge, rubric + reference | 0.83 | 1 CALL | Phrasing the other checks can't score |
| 7 Nothing crashed | no pipeline error | 1 or 0 | FREE | Timeouts and rate limits, kept off accuracy |
| ROLE | USED HERE | WHY THIS ONE |
|---|---|---|
| Serving | vLLM | Continuous batching, prefix-cache support |
| Local model | Qwen3.6-27B-FP8 | Official safetensors; FP8 fits the card with context to spare |
| SQL specialist | XiYanSQL 7B | Community build, AWQ INT4, 8k context |
| Hosted model | DeepSeek Flash v4 | Free tier via the opencode endpoint |
| Fourth model | poolside/Laguna-XS-2.1-INT4 | INT4, 8k context; sweep invalid — see A5 |
| Router | model_router.py per_stage | Keyword → tier; Qwen large, XiYanSQL small |
| Orchestration | LangGraph | Typed state, swappable architectures |
| Retrieval | LlamaIndex + ChromaDB | Keyword and meaning search, fused by rank |
| Reranker | BAAI/bge-reranker-base | Re-scores the shortlist reading query + doc together |
| Embeddings | nomic-embed-text-v1.5 | Runs alongside the 27B on the same card |
| Warehouse | DuckDB | Single file, analytical, no server |
| Tracing / eval | Langfuse · promptfoo + runner | One span per phase, one per model call |
Holds the 27B in FP8, a long context window, the embedding model and the reranker at once. No second machine, no network hop.
SELECT r.*, m.*, bgc.* FROM antismash.regions r JOIN antismash.modules m ON r.region_id = m.region_id JOIN antismash.rel_regions_types rt ON r.region_id = rt.region_id JOIN antismash.bgc_types bgc ON rt.bgc_type_id = bgc.bgc_type_id WHERE bgc.term ILIKE '%PKS%' AND m.trans_at = true;
regions and bgc_types have no direct key. Go through the junction table or get nothing.antismash. and the query fails outright — which is the good case.interpret maps to the small tier, which was the SQL specialist — so answer quality landed at the interpreter's level (0.478) rather than the SQL writer's.model_name per run, no per-span tier, phases[].details empty. Which model served which call is unrecoverable — so "did not behave like Qwen" is as far as the diagnosis goes.