feat(evals): add retrieval-recall metric and the #174 scope regression fixture - #186
Conversation
…n fixture Measurement only, no retrieval tuning, per the plan recorded on #174. The deliverable is a failing baseline a later tuning change can be judged against. Nothing in the harness could express "the expected chunk must be retrieved". citation_accuracy is set precision over what the answer cited; context_recall is an LLM-judged semantic score needing a live judge. The one identity-level predicate, the context_omission label, was joined on IDs alone and was wrong in both modes: offline the retrieved IDs *were* the record's citation list, so the set difference was always empty and the label could never fire; live they are corpus chunk hashes sharing no namespace with dataset citation IDs, so it fired for every record. Repairing that join is the substantive change here. resolve_retrieved_citations picks the join that means something per mode -- exact on ID when overlap with the declared citations proves a shared namespace, else citation text against retrieved context text -- and reports None when neither is possible, which is not evidence of omission and must not be scored as zero. The new retrieval_recall metric and the context_omission label share it, so they cannot disagree. Offline mode could not previously express retrieval failure at all, since its retrieved IDs were defined as every declared citation. offline_retrieved_- citation_ids makes that an explicit, validated declaration; absent, the old all-citations default stands, so existing fixtures keep their meaning (localrag-core scores retrieval_recall 1.000, unchanged). The localrag-scope fixture is a synthetic authored CC0 corpus about a fictional device, mirroring the acute-vs-chronic shape without redistributing the copyrighted book the failure was observed against. Four records: the motivating failure (0.0), a graded partial (0.5), a boilerplate-outranks-substance case (0.0), and a control that must not be zero (1.0). Offline baseline: retrieval_recall 0.375 against a 0.8 threshold -- FAIL, as intended. The live baseline is still outstanding and needs a machine that can host the stack. Refs #174 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZ7AySD1EFME3xjMiQNp1p
|
Adding the full offline Command: Two things this settles:
|
What this does
Adds the measurable regression target #174 asks for as its suggested first step, plus the metric needed to express it. Measurement only — no retrieval tuning. The deliverable is a failing baseline that a later tuning change can be judged against.
Why a new metric was needed
Nothing in the harness could express "the expected chunk must be retrieved":
citation_accuracyis set precision over what the answer cited, so retrieval returning topically-similar passages instead of the ones that answer the question scores fine.context_recallis an LLM-judged semantic score and needs a live judge.context_omissionfailure label, was joined on IDs alone and was wrong in both modes: offline the retrieved IDs were the record's citation list, so the set difference was always empty and the label could never fire; live they are corpus chunk hashes sharing no namespace with dataset citation IDs, so it fired for every record.Repairing that join is the substantive change here.
resolve_retrieved_citationspicks the join that means something per mode — exact on ID when overlap with the declared citations proves a shared namespace, else citation text against retrieved context text. When neither join can be made it returnsNone, meaning undecidable: that is not evidence of omission and is not scored as zero. The newretrieval_recallmetric and thecontext_omissionlabel share this one join, so they cannot disagree.Offline mode previously could not express retrieval failure at all, since its retrieved IDs were defined as every declared citation.
offline_retrieved_citation_idsmakes that an explicit, validated declaration; absent, the old all-citations default stands, so existing fixtures keep their meaning.The fixture
localrag-scopeis a synthetic, authored CC0 corpus about a fictional device. It mirrors the acute-vs-chronic shape of the reported failure without redistributing the copyrighted book the failure was observed against. Four records: the motivating failure (0.0), a graded partial (0.5), a boilerplate-outranks-substance case (0.0), and a control that must not be zero (1.0).Verification
Measured on this branch, rebased on current
main:retrieval_recalllocalrag-scopelocalrag-core(23 records)uv run pytest -m "not integration"— 496 passed, 1 skippedruff check/ruff format --check/mypy localrag/— all cleanDocs updated in the same change per AGENTS.md:
docs/eval-datasets.mdanddocs/evaluation-metrics.md.Note for reviewers
A full
run_evals --offlineinvocation on a slow local judge can report every metricUNAVAILABLE: the default--case-timeoutof 120s kills each case before the four judge metrics finish, discarding the deterministic values computed alongside them. That is a local-environment artifact, not a defect introduced here — raising--case-timeoutavoids it. Whether the deterministic metrics should survive a judge timeout is worth considering separately.The live baseline is still outstanding and needs a machine that can host the stack.
Closes #174