fix(bench): separate retrieval quality from reader-dependent scores, and say what cannot be measured (#1160) - #1229
Conversation
There was a problem hiding this comment.
Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR-size soft capThis PR is over the advisory size threshold:
Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the |
Reviewer's GuideSeparates reader-dependent correctness metrics from reader-independent retrieval-quality metrics in the benchmarking harness, adds rank-based retrieval metrics, introduces a not-applicable sentinel for structurally uncomputable metrics, updates adapters and tolerance logic to use these, and verifies behavior with new tests and docs/changelog updates for #1160. Sequence diagram for benchmark adapter retrieval vs reader-dependent metricssequenceDiagram
participant Adapter as locomo_adapter
participant Store as MemoryStore
participant Retriever as retrieve_v2
participant RankMetrics as retrieval_metrics
participant QAScorer as score_qa
Adapter->>Store: _retrieve_beliefs(question, budget)
Store->>Retriever: retrieve_v2(query, budget)
Retriever-->>Store: beliefs
Store-->>Adapter: list[str] beliefs
Adapter->>RankMetrics: retrieval_metrics(beliefs, [qa.answer])
RankMetrics-->>Adapter: per_query_rank_scores
Adapter->>QAScorer: score_qa(prediction, qa.answer, qa.category)
QAScorer-->>Adapter: f1
alt qa.category in UNSCORABLE_CATEGORIES
Adapter->>Adapter: f1_display = NOT_APPLICABLE
else qa.category scorable
Adapter->>Adapter: update total_f1, category_scores
end
Adapter->>Adapter: append per_question_retrieval and per_question
Adapter->>Adapter: retrieval_quality() = mean_metrics(per_question_retrieval)
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
[claim:review:Setr:2026-07-31T03:14:52Z] |
Review: request changes — one blocker in the new headline metricEverything structural checks out; the blocker is a data-dependent zero inside Verified
Blocker — LoCoMo category 5 contributes a structural 0.0 to
|
|
[release:review:Setr:2026-07-31T03:21:33Z] |
|
[claim:review:Toug:2026-07-31T03:23:04Z] |
|
Unblocked. The blocker is fixed in The blockerConfirmed independently before touching anything: Fixed by gating the append on scorability in the loop that already knows, as rank_scores = retrieval_metrics(beliefs, [qa.answer])
if qa.category not in UNSCORABLE_CATEGORIES:
result.per_question_retrieval.append(rank_scores)
The open question — yes, and the suggested guard would not have caught itI could check locally; the cleaned dataset is in the HF cache. Measured on
The golds are refusal sentences, e.g. "You did not mention this information. I did not add the generic empty-gold guard, on the strength of that Both new tests are mutation-checked — reverting the LoCoMo gate fails one, NitAddressed. Also confirmed from your reviewI re-walked the two claims most likely to be load-bearing later and agree: Full suite on the branch head: 6576 passed, 69 skipped, 71 xfailed. One caveat worth stating plainly: this changes the value of |
|
[release:review:Toug:2026-07-31T03:33:14Z] |
|
[claim:review:Setr:2026-07-31T05:17:13Z] |
Second pass — blocker cleared, held on two packaging itemsVerified at
One test each, nothing incidental. The inversion is done right: asserting My suggested guard was wrong, and the measurement is what shows itI proposed a defensive guard in The measurement settles it — 30 of 500 questions carry Worth noting the Two packaging items, neither substantive
Rebase, resolve the thread, add Everything from the first pass still standsThe canonical coupling (five leaves to Substance approved. Flagging |
|
[release:review:Setr:2026-07-31T05:19:26Z] |
|
[claim:review:Setr:2026-07-31T05:59:20Z] |
4474674 to
b3208f1
Compare
Took the two packaging items rather than leave this parked — pushed as
|
|
[release:review:Setr:2026-07-31T06:08:08Z] |
|
merge-train: blocked branch is not fast-forward on The |
…a sentinel The canonical dispatcher runs no reader, so every adapter scores the joined retrieval context as if it were a model answer. Token-F1 between ~2000 tokens of context and a three-token gold has precision ~3/2000: halving the budget roughly doubles the reported F1 while retrieving strictly less. retrieval_metrics reads the ordered retrieved list instead. Because retrieval fills the budget in rank order, a smaller budget truncates the tail, so recall@k and reciprocal rank are monotone non-decreasing in the budget — moving them requires ranking a relevant belief higher. A test pins that property. metric_status carries the sentinel for the other half: metrics that are not zero but uncomputable. Neither module is wired into an adapter yet. Refs #1160
…ession A metric written as the not-applicable sentinel is a non-numeric leaf, so check_report would have taken the 'observed leaf is not numeric' branch and reported FAIL — turning an adapter's honest 'this cannot be computed' into a fabricated regression, which is worse than the 0.0 it replaces. Verdict.NOT_APPLICABLE is separate from SKIP because the two point at different work: SKIP means fix the runner, n/a means the metric needs a reader or needs deleting. It rolls up like SKIP — never raises the verdict, never counts as evidence anything was measured — so an all-n/a run is NO_DATA rather than PASS. Also classifies the rank metrics as higher-is-better, so a ranking win does not fail the nightly it exists to show up in. Refs #1160
…zero Category 5 scores a point only when the prediction contains a refusal. The prediction is the retrieved context and no reader runs, so the category returned a hard 0.0 on every run ever recorded — and the heuristic meant to rescue it (promote thin retrieval to a refusal at under ten words) is unreachable at any realistic budget. That zero sat inside overall_f1 and inside a tolerance band, where a genuine fix would have registered as an excursion. It is now reported as n/a and excluded from overall_f1, which becomes the mean over the categories that were scored. total_qa still counts every question — it is a corpus invariant the band-check watches — so scored_qa is reported beside it rather than redefining either. Alongside it the adapter reports retrieval_quality: MRR and recall@k over the ordered retrieved list, covering every question including the unscorable ones, since whether the gold answer was retrieved at all is measurable without a reader. Those stay out of per_question, which doubles as the --retrieve-only reader payload and must not carry gold-derived fields. score_qa keeps its category-5 branch: it mirrors LoCoMo's evaluation.py and is correct for a reader's answer. Wire a reader in and it is live again unchanged. Refs #1160
Exact match asks whether the prediction equals the gold string after normalisation. In aelf bench all the prediction is the joined retrieval context — hundreds of tokens — so it never equals a short gold answer, at any retrieval quality, for any corpus. MAB recorded 0.0 for all four splits; LongMemEval and AMA-Bench the same. Read as a score, that says retrieval failed completely. It says nothing of the kind. All three now report the sentinel and carry the reason in the artifact, and all three gain retrieval_quality: MRR and recall@k over the ordered retrieved list, which is where a ranking change can show up without the token budget dominating it. amabench's per-group breakdown needed the same treatment: an n/a metric contributes nothing to the group sum, so dividing by the row count would have silently reconstructed the zero the sentinel replaced. substring_exact_match and f1 stay numeric. They are reader-dependent too and the docstrings now say so, but unlike exact_match they do move with retrieval, so they remain measurements rather than placeholders. Refs #1160
… costs BENCHMARKS.md described the numbers as the metrics each external benchmark defines, naming a GPT-4o judge on LongMemEval and LLM-judge accuracy on StructMemEval and AMA-Bench. Those come from the two-pass protocol further down the same page, run by hand. aelf bench all has no generation pass at all, so the canonical JSON is not what the paragraph described. Splits the reported metrics into reader-dependent and reader-independent, says which way the token budget pushes each, and tabulates the metrics that report n/a with the reason. Also records that the shipped canonical file predates the change and stays green against it until it is recut. Refs #1160
Review blocker: the metric built to remove placeholder-as-measurement
reproduced it. LoCoMo category 5 leaves `answer` empty (its gold lives in
`adversarial_answer`), and `is_relevant` correctly refuses an empty gold,
so every adversarial question appended a structural 0.0. On LoCoMo-10
that is 446 of 1986 questions, capping mrr and every recall_at_k at 0.775
regardless of ranking, and making the number move with the category mix
that `--conversations` / `--subset` select.
LongMemEval carries the same shape, and the empty-gold guard suggested in
review would NOT have caught it: the `_abs` golds are long refusal
sentences ("You did not mention this information..."), not empty strings.
Measured on longmemeval_s_cleaned.json: 30 of 500 questions (6.0%) are
`_abs`, and 0 of 30 have a gold `is_relevant` can find anywhere in their
own haystack, against 55 of 60 for a non-`_abs` control. The unscorable
set therefore has to be declared per adapter, which is what this does.
Scoring these 0.0 would also conflate "correctly found nothing to find"
with "failed to find what was there".
`test_retrieval_quality_covers_the_unscorable_questions_too` pinned the
symptom and is inverted — and asserts mrr == 1.0 strictly, because the
old `<= 0.5` bound would pass against the behaviour being fixed.
Also documents that `mean_metrics`' empty-input 0.0 is a shape
placeholder rather than a score.
Refs #1160
CodeQL "Unnecessary lambda" on `default_factory=lambda: dict[str, float]()`. `default_factory=dict` is equivalent at runtime — the subscript is erased — and the field annotation already carries the type for the checker. Spelled differently from the module's other typed-empty factories on purpose: the rest are pre-existing and unflagged, and sweeping all 20 across the four adapters is a wider change than this thread needs. Refs #1160
b3208f1 to
fdc47f2
Compare
|
Rebased again and re-labelled. The train bounced this at 06:10 for a reason that was my doing: #1234 merged into Now on |
|
merge-train: merged fdc47f2 → |
Closes the last open acceptance criterion on #1160: "Separate retrieval-quality metrics (recall@k, MRR over the retrieved set) from reader-dependent metrics; report
exact_matchand LoCoMo cat-5 as n/a rather than 0.0." The other four ACs landed in #1213, #1195/#1194, #1209/#1200 and #1197.The problem
aelf bench allruns no reader — there is no generation pass inbenchmarks/run.py. Every adapter joins the retrieved beliefs into one string and hands it to a scorer written for a model's answer. Two things follow.Token-F1 measures the token budget. Precision between ~2000 tokens of context and a three-token gold is about 3/2000, so halving the budget roughly doubles the reported F1 while retrieving strictly less. There was no metric in the canonical cut that moved the other way.
Some metrics were never measurements at all.
exact_matchasks for whole-normalised-string equality against a short gold; a multi-hundred-token context never satisfies it, at any retrieval quality, for any corpus. LoCoMo category 5 scores a refusal, and nothing in this path can refuse — the< 10 words ⇒ refusalheuristic meant to rescue it is unreachable at any realistic budget. Both reported0.0, which is the worst possible score, so the canonical file read as a total retrieval failure. Worse, a tolerance band around0.0turned any genuine fix into a band excursion.What this does
benchmarks/retrieval_metrics.py— MRR and recall@k over the ordered retrieved list, before it is joined. Retrieval fills the budget in rank order, so a smaller budget truncates the tail: these are monotone non-decreasing in the budget, and moving them requires ranking a relevant belief higher. That property is pinned by a test rather than asserted in prose. Multi-answer gold is treated as alternative surfaces of one answer, matchingqa_scoring.score_multi_answer, and gold that normalises to empty (an article, a bare.) is guarded —"" in anythingwould otherwise award a free hit.benchmarks/metric_status.py— then/asentinel and the_not_applicablereason block. Underscore-prefixed, so the reason rides in the artifact without landing on the band-check walk.Adapters —
locomo,mab,longmemevalandamabencheach report aretrieval_qualityblock beside the existing scores, and mark the uncomputable metricsn/a. LoCoMo additionally drops category 5 fromoverall_f1, which becomes the mean over the categories that were scored;total_qais untouched (it is a corpus invariant the band-check watches for drift) andscored_qais reported beside it.benchmarks/tolerance.py—Verdict.NOT_APPLICABLE. Without it,n/alands on the existing "observed leaf is not numeric" branch and reports FAIL, converting an adapter's honest this cannot be computed into a fabricated regression. It is separate fromSKIPbecause the two demand different responses:SKIPmeans fix the runner,n/ameans the metric needs a reader or needs deleting. It rolls up likeSKIPand, critically, does not count as evidence anything was measured — so an all-n/arun isNO_DATA, notPASS.Effect on the nightly
benchmarks/results/v2.0.0.jsonpredates this and still records0.0for the affected leaves. It cannot be recut from here — that needs the real datasets — so the coupling is exercised against the shipped file directly intest_new_adapter_shape_never_fails_the_shipped_canonical:NOT_APPLICABLE(four MAB splits + LoCoMo category 5). LongMemEval's and AMA-Bench'sexact_matchalso gon/a, but their canonical cut predates the metric, so those leaves are not in the tree being walked.locomo.overall_f1can only rise. It is higher-is-better, so leaving the band is a WARN on the improving side (fix(bench): enforce tolerance bands on the regression side only (#1160) #1209), not a FAIL. Swept across category-5 shares from 0 to ~70% of the corpus: worst case is WARN, andbench-canonical.ymlexits 0 on WARN.Recutting the canonical baseline is not in this PR. It is operator-run and, per the standing note that every
v3.0.1*file is schema 1.0 so the nightly cannot simply be repointed, a recut is its own piece of work.Deliberately out of scope
f1→context_token_overlap. The finding's prose suggests it; the AC does not. A rename orphans every canonical leaf path and forces the recut above.substring_exact_matchandf1stay numeric — they are reader-dependent, and the docstrings now say so, but unlikeexact_matchthey do move with retrieval, so they remain measurements.mab_adapter's forked scorer (the separate low-severity finding on the umbrella, including its missing empty-gold guard). Untouched here.score_qa's category-5 branch is kept, not deleted: it mirrors LoCoMo's ownevaluation.pyand is correct for a reader's answer. Wire a reader in and it is live again unchanged.Verification
test_retrieval_metrics.py,test_bench_metric_separation.pyand additions totest_bench_tolerance.py.test_na_leaf_is_not_applicable_not_fail(reads FAIL without the sentinel branch),test_overall_f1_excludes_the_unscorable_categories(asserts the old 0.4 divisor alongside the new 0.5), andtest_amabench_per_group_breakdown_does_not_rebuild_the_zero(an n/a contributes nothing to the group sum, so dividing by the row count silently reconstructs the zero).scripts/check_changelog_dupes.pyclean; CHANGELOG edit is insert-only.Refs #1160
Summary by Sourcery
Separate reader-dependent correctness metrics from reader-independent retrieval quality across benchmark adapters, and mark structurally uncomputable metrics as not applicable rather than failed, while adjusting nightly tolerance handling accordingly.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: