eolas / eval /runbook.md
rahulraj1406's picture
v0.4.0-alpha: cross-encoder reranking + eval harness with CI gate
310c2f8
|
Raw
History Blame Contribute Delete
3.97 kB
# Eval Runbook
How to read, run, and triage the eval harness when a metric drops.
---
## What runs where
| Where | What | When |
| --- | --- | --- |
| Local dev | `python -m eval.smoke_eval` (10 questions) | Iterating on retrieval changes |
| Local dev | `python -m eval.run_full --out eval/baselines/<name>.json` | Before opening a PR |
| Local dev | `python -m eval.run_full --with-llm --judge --out ...` | Pre-release; uses LLM quota |
| GitHub Actions | `.github/workflows/eval.yml` | Every PR touching `app/`, `ingest/`, `eval/` |
CI runs retrieval metrics only — no LLM calls, no rate limits, fully deterministic.
Generation faithfulness is graded offline before each release.
---
## CI gate
The job fails if either:
- `recall_at_5 < 0.85` (answerable questions where any top-5 chunk is relevant)
- `ooc_refusal_rate < 0.90` (out-of-corpus questions where the gate fired)
Set in `eval/run_full.py` via the `EVAL_CI_GATE=1` env var, so the same script
runs locally without gating.
---
## Triage: a metric just dropped. What now?
1. **Pull the artifact** from the failed PR's "eval-results" upload, or rerun
locally: `python -m eval.run_full --out eval/baselines/diag.json`.
2. **Diff against the last good baseline** (`eval/baselines/v0.4.0-rerank-full.json`).
Look at `results[*].verdict_pass` flips and `results[*].retrieved_top5_urls`
shifts.
3. **Categorise the regression** by `category`:
| Category that dropped | First suspects |
| --- | --- |
| `vocab-mismatch` | Reranker config, query expansion synonyms |
| `entity-lookup` | Embedder change, index rebuild gap, chunk boundaries |
| `fact-lookup` | Chunking strategy, embed model, top-k |
| `multi-hop` | Top-k too small, chunk granularity, relevance threshold |
| `ooc-negative` | `relevance_threshold` lowered too far |
4. **Reproduce in isolation**: pick one failing question and run
`python -m app.ask --no-llm "<question>"` to see what dense retrieval gave.
If `gate=PASS` but the right chunk isn't in top-K, the issue is rank order
(reranker / query expansion). If `gate=FAIL`, retrieval missed entirely.
5. **Knobs in order of safety to twist** (safest first):
- Add a synonym to `_EXPANSIONS` in `app/providers/reranker.py`
- Bump `RERANK_TOP_N` (currently 20)
- Re-tune `RELEVANCE_THRESHOLD` against `eval/questions_v2.jsonl`
- Re-chunk and re-index (last resort; rebuilds 12k vectors)
6. **Never** silently lower the CI thresholds to make the gate pass. If a
threshold is wrong, change it in a dedicated PR with an explanation, not
in the same PR that introduced the regression.
---
## Updating the eval set
`eval/questions_v2.jsonl` is the source of truth for retrieval metrics.
When you add a question:
- Pick a `category` from the five existing ones; don't invent new ones unless
you also update this runbook.
- `relevant_text_any` should be the smallest substrings that uniquely identify
the right answer in the corpus (e.g. `"Désirée Finnegan"`, not `"is the"`).
- `answer_must_contain_any` is checked only in `--with-llm` mode; it's looser
than the retrieval needle (lets the LLM paraphrase).
Re-run the baseline and commit the new `eval/baselines/*.json` so PR reviewers
can see whether your question changed the aggregate metrics.
---
## Faithfulness scoring
Faithfulness is judged by an LLM (NVIDIA Llama 70B by default) against the
retrieved chunks the answerer was actually shown. It's NOT part of the CI gate
because:
1. It's non-deterministic (judge LLM sampling).
2. It costs quota — running it on every PR would exhaust free-tier limits.
3. A faithfulness miss is a generator bug, not a retrieval bug — different
triage path.
Run it before tagging a release:
```bash
python -m eval.run_full --with-llm --judge \
--out eval/baselines/v$(date +%Y%m%d)-prerelease.json
```
If `faithfulness_avg < 0.85`, investigate the system prompt or grounding
discipline before shipping.