← Blog

Blog

One passage, one yes/no question

The previous post ended with a diagnosis: the Oracle of my D&D wiki finds the right material, it just doesn’t always put it first. Its three search paths bring the right source back somewhere among ~27 candidates every single time, but that source only lands in the top 8 that reach the model 83% of the time. A reranker (the same qwen2.5:14b that answers the chat, reading a short annotation of each candidate instead of its text) lifts that to 93%.

That reranker is a generative model doing a classifier’s job: it reads a numbered list and writes numbers back. So I wanted to know whether a model built for that kind of decision would do it better, and tried two:

  • Jev, from TypeSafe, a hosted model of the kind they call “System One”. You give it a state and a set of typed questions, and it returns an answer to each one with a probability attached. I call it through OpenRouter (~typesafe/jev-latest, which resolved to jev-1.13).
  • Laya, from Convai, which plays the same game with open weights: a 322M-parameter encoder, multilingual checkpoint, running on my own GPU.

Both support a yes/no question type called Noul that returns the probability of “yes”. So instead of asking one model to rank 27 things at once, you ask each passage the same question and sort by the answer.

Same candidates for everyone

For the comparison to mean anything, every variant has to choose from exactly the same candidates, and I didn’t want to rewrite retrieval for an experiment. So the harness calls the production rag.search() and, for the duration of that one call, swaps the reranker for a spy that keeps whatever list it receives and hands back the RRF order. That captures what the reranker sees in production: 26.9 candidates per question on average.

Each gate asks every candidate two Noul questions, is_relevant and contains_answer_evidence, scores it with the mean of the two, and sends the 8 best to the model, the same k the reranker uses. I tried each model with two inputs: the full chunk text (up to 1200 words) and the ~38-word annotation the reranker reads. Every call goes to a disk cache the moment it returns, so rerunning is free and a crash halfway through doesn’t lose anything.

The eval bank is last post’s, plus the six “arc” questions added since (the ones whose answer is spread over several sessions), 36 in total. Before looking at any gate I checked the controls. On the 30 original questions, plain RRF gives recall@8 0.833 and MRR 0.707, exactly last time’s numbers. The reranker gives the same 0.933 recall, with a slightly lower MRR (0.737 against 0.757). And the candidates contain the right source for all 36 questions, so every miss from here on is a ranking miss.

Results

Variantrecall@8MRRper passage (p50)per question (p50)cost, 36 questions
RRF, no reranker0.830.73n/an/a$0
Current reranker (qwen2.5:14b)0.940.7761 ms1.6 s$0
Jev · full text1.000.97268 ms7.5 s$0.062
Jev · annotation0.920.78262 ms7.3 s$0.020
Laya zero-shot · full text0.610.3929 ms0.9 s$0
Laya zero-shot · annotation0.720.4928 ms0.8 s$0

recall@8 is the fraction of questions with at least one passage from an expected source among the 8, and MRR is the mean of 1/rank of the first such passage. The reranker makes a single call per question for all the candidates, so its per-passage time is that call divided by the number of candidates.

With the full text, Jev gets a correct passage into the top 8 for every question, nearly always in first place. Paraphrases were the family that hurt most last time (questions written without any of the session’s proper nouns), and they go from 0.58 with RRF and 0.83 with the reranker to 12 out of 12. Arc questions are the one place where it doesn’t come out ahead: of the sessions an arc question expects, Jev’s 8 cover 0.77, against 0.80 for the reranker and 0.81 for plain RRF.

Of the 8 passages that reach the model, how many come from an expected source, per variant RRF, no reranker (reference): 2.7 of 8 passages from an expected source, recall@8 0.83. Current reranker (qwen2.5:14b): 2.6 of 8 passages from an expected source, recall@8 0.94, 61 ms per passage (p50). Jev · full text: 2.8 of 8 passages from an expected source, recall@8 1.00, 268 ms per passage (p50). Jev · annotation: 2.3 of 8 passages from an expected source, recall@8 0.92, 262 ms per passage (p50). Laya zero-shot · full text: 1.5 of 8 passages from an expected source, recall@8 0.61, 29 ms per passage (p50). Laya zero-shot · annotation: 1.8 of 8 passages from an expected source, recall@8 0.72, 28 ms per passage (p50). from an expected source rest of the 8 0 4 8 RRF, no reranker (reference): 2.69 of 8 passages from an expected source (mean of 36 questions). recall@8 0.83 · no selection cost. RRF, no reranker (reference) recall@8 0.83 · no selection cost 2.7 / 8 Current reranker (qwen2.5:14b): 2.64 of 8 passages from an expected source (mean of 36 questions). recall@8 0.94 · 61 ms/passage · p50. Current reranker (qwen2.5:14b) recall@8 0.94 · 61 ms/passage · p50 2.6 / 8 Jev · full text: 2.81 of 8 passages from an expected source (mean of 36 questions). recall@8 1.00 · 268 ms/passage · p50. Jev · full text recall@8 1.00 · 268 ms/passage · p50 2.8 / 8 Jev · annotation: 2.28 of 8 passages from an expected source (mean of 36 questions). recall@8 0.92 · 262 ms/passage · p50. Jev · annotation recall@8 0.92 · 262 ms/passage · p50 2.3 / 8 Laya zero-shot · full text: 1.53 of 8 passages from an expected source (mean of 36 questions). recall@8 0.61 · 29 ms/passage · p50. Laya zero-shot · full text recall@8 0.61 · 29 ms/passage · p50 1.5 / 8 Laya zero-shot · annotation: 1.78 of 8 passages from an expected source (mean of 36 questions). recall@8 0.72 · 28 ms/passage · p50. Laya zero-shot · annotation recall@8 0.72 · 28 ms/passage · p50 1.8 / 8
fig. 1 — Of the 8 passages that reach the model, how many come from an expected session or document (mean of 36 questions). Ground truth is per source, not per passage.

The figure counts how many of the 8 come from an expected source, and on that count Jev, the reranker and plain RRF are almost tied, between 2.6 and 2.8. What Jev adds is that one of those passages is always there and usually first, which is what recall and MRR pick up.

The annotation result flips

I didn’t see this one coming. In the previous post the trick with the reranker was never showing it raw text: with the text, recall dropped to 0.73, below not reranking at all, and with the annotation it went up to 0.93. Jev goes the other way. Full text gives 1.00, the annotation gives 0.92, and paraphrases fall back to 0.83.

My guess, which I haven’t tested, is that it depends on what each model is being asked to do. The reranker compares 27 candidates inside one prompt, so it wants short descriptions it can line up side by side, and 27 full sessions are mostly noise to it. Jev reads one passage at a time with a 32k context, so for Jev the full text is information, and the annotation throws away exactly the detail a paraphrased question depends on.

Laya without fine-tuning is worse than doing nothing

Zero-shot Laya picks worse than the RRF order it’s supposed to improve on: 0.61 recall with full text and 0.72 with annotations, against 0.83 for RRF. On paraphrases it gets 4 out of 12. Its scores do spread out (0.02 at p10, 0.92 at p90), but the high ones land on the wrong passages too often.

That matches Laya’s own documentation, which says the base checkpoints score close to chance zero-shot and that the jump comes from fine-tuning on decisions from your own domain. It’s still tempting at 29 ms per passage, running locally for free, so fine-tuning it is the next experiment. I’ve written the preparation script but haven’t run it. It needs hand-labeled passages first, and the train/test split has to be by question: the ~27 candidates of a question share its query, so splitting them up would leak test questions into training.

What Jev costs

In money, very little. The 36 questions with full text cost $0.062, about 1.5M input tokens at $0.042 per million, and the usage.cost OpenRouter sends back matched that estimate. That works out to roughly $0.0017 per question.

Latency is another story. Jev needs a call per passage, around 270 ms each, and my harness sends them one after another, which adds up to 7.5 s per question against 1.6 s for the reranker. Most of those 270 ms are network, and nothing stops you from firing the 27 calls in parallel, but I haven’t measured that, so I’m not going to guess a number. The reranker, for its part, is only “free” because it reuses the 14B model already loaded for chat, and it still ties up the GPU while it runs.

As a strict gate

Top 8 keeps the comparison with the reranker fair. With a probability per passage, though, you can also set a threshold and let through only what clears it, which the reranker’s ordered list doesn’t give you. With a score of 0.5 or more (capped at 8), Jev on full text lets through 4 passages per question on average and still doesn’t lose a single question: recall stays at 1.00. On annotations it’s much stricter, with 1.3 passages on average and 10 questions left with nothing. Laya doesn’t separate here either, and with full text it lets almost everything through (6.6 of 8).

For the Oracle this is the most useful number in the whole experiment: half the context without losing the right source. Whether the answers get better with 4 passages instead of 8 I can’t say, since the experiment only measures what reaches the model.

The fine print

The ground truth is per source. A passage counts as a hit if its session or document is among the expected ones, even when that particular chunk is about something else. That’s why I’m not leaning on precision: every variant lands between 0.19 and 0.35, and on the literal questions, whose ground truth is “sessions where the name appears”, that number comes out inflated. The harness exports every candidate to a CSV for hand labeling, and precision can be recomputed from it without calling any API again.

36 questions is not that many. One question is almost 3 points of global recall, and 8 points inside a 12-question family, so Jev’s 1.00 means zero misses on a small sample.

The two Noul questions turned out to be nearly the same question. In Jev, is_relevant and contains_answer_evidence correlate at 0.95, so the second one adds little. Dropping it wouldn’t save much, though, because most of each call is the passage text, and a call with a one-line passage already costs about 450 tokens.

Two smaller things. In 3 questions the reranker named fewer than 8 passages and the code filled the rest in RRF order; that also happens in production, so I measured it as is. And this is a single run. Everything is cached and reproducible, but I didn’t repeat it to look at variance.

What I take away from this

Last time, the hard part turned out to be choosing which eight candidates to show the model, and the answer then was an LLM reading short summaries of all of them at once. This time the best result came from a model that writes nothing and reads passages one by one, and it only does this well when it gets the full text. On this bank it beats the reranker on recall and MRR and loses a bit on arc coverage. It’s also much slower, and it costs about a sixth of a cent per question. Next I want to measure Jev with parallel calls to see how much of those 7.5 s is left, and fine-tune Laya to see whether a local 322M model can catch up.