1. Every metric at a glance
What each measure is, what it does, and how it helps the chatbot give a correct answer. A metric doesn't improve the chatbot by itself: it shows where answers go wrong, and that decides what to change. Each one is explained in full further down.
| Metric | What it measures | How it helps the chatbot answer correctly |
|---|---|---|
| Retrieval: did the model get the right passage? | ||
| Hit@k (Hit@1, @3, @5, @6) | Share of questions where a passage from the right page is in the top k results. | The model can only answer from what it is given. The app passes six passages, so Hit@6 is the share of questions where the answer was in front of the model at all. It picked the demo's setup: hybrid on 500 characters got 34 of 36. |
| Evidence@k (@5, @6) | Share where a top-k passage contains the question's whole evidence quote, not just text from the right page. | A passage from the right page but the wrong paragraph gives the model nothing to cite. This catches that, and showed that small chunks often split the evidence the model needs. |
| MRR@10 | How high the first right passage ranks on average (1 means always first). | Models tend to use passages near the top more reliably, and the Lost in the Middle paper shows answers get worse when the key text sits in the middle of a long context. A higher MRR puts the evidence where the model reads it best. |
| nDCG@10 | Like MRR, but rank 2 or 3 still earns most of the credit. | Fits a chatbot that reads several passages: it rewards getting the evidence near the top without punishing rank 2 as hard as MRR does. |
| Latency (ms per question) | Retrieval time per question. | Keeps the chatbot responsive. It is why the reranker stayed out: about 30 times slower for a gain that could be chance. |
| Statistics: is a difference real? | ||
| 95% bootstrap interval | The range a score plausibly falls in, from resampling the questions 2,000 times. | Stops the chatbot being "improved" with a change that only looked better by chance on a small test set. |
| Paired comparison | The difference between two setups question by question, with its own interval. | Shows whether a new setup really answers more questions than the current one. A change is only worth making if the interval is clear of zero. |
| Answers: is what the user reads correct? | ||
| Answer correctness | Answerable questions where the answer has every required fact and no material error. | The closest measure to what a user cares about: did they get the right answer? |
| Citation support | Factual claims fully supported by the passage they cite, including numbers and qualifiers. | Catches claims the model added that the papers don't say, so every statement can be checked against its source. |
| Correct abstention | Questions the papers can't answer that the chatbot declined. | A chatbot that guesses on unanswerable questions gives wrong answers with confidence. This rewards saying "The papers don't cover this." |
| Wrong abstentions | Answerable questions the chatbot declined. | The opposite failure: refusing too often. Reported on its own so a chatbot can't look safe just by refusing everything. |
| Failure cause | For each wrong answer, whether the passages held enough information (a generation problem) or not (a retrieval problem). | Shows which half to fix: the search, or the prompt and model. |
| Run errors | Timeouts and failures, counted apart from deliberate refusals. | Stops crashes being mistaken for careful refusals, and keeps them in the totals so they can't hide. |
| Trust and safety | ||
| Cohen's kappa | Agreement between the LLM judge and my own reviews, minus the agreement expected by chance. | The judge filtered the test questions. Kappa above 0.8 (0.89 and 0.83 in two runs) means the questions behind every other score were reviewed reliably. |
| Prompt-injection check | Whether the model obeys instructions hidden inside a passage. It resisted 5 of 5 planted attacks. | A document could contain text written to hijack the chatbot. This checks that it answers from the evidence instead of following planted instructions. |
2. Two halves to test
A RAG answer goes through two steps, and each can go wrong on its own:
- Retrieval: the system searches the documents and picks a handful of passages. If the passage with the answer isn't among them, no model can answer correctly from them.
- Generation: a language model writes the answer from those passages. Even with the right passages, it can misread them, add facts that aren't there, or cite the wrong source.
Testing only the final answers hides which step failed. So I measure retrieval on its own first, with exact, cheap metrics, and then check the answers separately.
3. Building a test set
Every metric needs an answer key: questions where you know where the answer is. Building that key carefully is most of the work, and the part most projects skip.
Labels that survive chunking
I labelled every page of every paper with its arXiv id and page number, and the labels stay attached to every chunk cut from that page. A retrieved passage counts as correct if it comes from the question's page. Because the key is the page, not a particular chunk, it stays the same when the chunk size changes, so different chunk sizes can be compared fairly.
Generated questions, checked twice
- A model read sampled pages and wrote one question per page, plus an exact quote from the page as evidence.
- Evidence check: if the quote wasn't really on the page, the question was thrown out automatically.
- Review: many questions were still bad: about the reference list, about which table shows what, about trivia in worked examples, or about a paper's main claim, which appears on several pages so there's no single right page. I reviewed the first batch by hand, then built an LLM judge with the same rules (section 8).
The traps in generated questions
- Wording overlap: a model writing questions from a page tends to reuse that page's words. Keyword search (BM25) then looks better than it would with real users, who phrase things their own way.
- Tuning on the test: if you pick your chunk size and retriever using the same questions you report, the reported score flatters the winner. The fix is a held-out set: questions written separately, ideally by hand, that are scored but never used to choose anything.
4. Retrieval metrics
Each metric starts from one number per question: the rank of the first correct passage in the results (1 if it came first, 2 if second, and so on), or "missing" if it wasn't in the top 10. Here's a small example with four questions, used throughout:
| Question | Rank of first correct passage |
|---|---|
| A | 1 |
| B | 3 |
| C | 2 |
| D | missing |
Hit@k (also called recall@k here)
Example: Hit@1 = 1/4 = 0.25 (only A). Hit@3 = 3/4 = 0.75 (A, B, C). Hit@5 = 0.75.
- Tells you
- How often the system finds the answer at all, within the first k results.
- Why it matters
- Pick k to match your system. My app gives the model six passages, so Hit@6 answers exactly "did the model get to see the answer?" On my questions the sixth passage added nothing: Hit@5 and Hit@6 are both 34 of 36.
- Blind spot
- Rank 1 and rank 5 count the same. It says nothing about ordering within the top k.
- Helps the chatbot
- If the answer isn't in the passages, the model can only refuse or guess. Hit@6 measures how often the answer reached the model, which makes it the ceiling on correct answers.
With one correct page per question, recall@k and hit rate@k are the same number, so I report it once.
Evidence@k (passage level)
- Tells you
- Whether a retrieved passage actually holds the quoted evidence, not just a passage from the right page.
- Helps the chatbot
- The model needs the paragraph that states the fact, not just its page. A higher Evidence@k means the passages it reads actually contain what it has to cite.
- Why it matters
- A page has many paragraphs. A passage from the right page but the wrong paragraph gives the model nothing useful, yet page-level Hit@k counts it as a success.
- Blind spot
- It is deliberately strict. A quote split across two chunks is missed, and a passage that states the same fact in other words doesn't count, so it undercounts rather than overcounts. Whether passages support an answer in other words is a job for human review.
An earlier version credited passages that shared most of the quote's words. A review showed it could credit a passage containing only "the score", so I replaced it with exact matching and added tests for those false positives. All Evidence figures on this page use the corrected rule. The stricter check also caught two quotes ending where the PDF text has a footnote number stuck to the last word ("relevance.4", "20225"); I trimmed those quotes to end before the footnote, without changing the questions or pages.
MRR@10 (mean reciprocal rank)
Example: (1/1 + 1/3 + 1/2 + 0) / 4 = (1 + 0.333 + 0.5 + 0) / 4 = 0.458.
- Tells you
- How high the first correct passage ranks, on average. 1.0 means it always comes first.
- Why it matters
- Rewards getting the answer to the very top, which matters when only the first result is shown or used.
- Helps the chatbot
- Evidence near the top of the context is used more reliably; the Lost in the Middle paper found answers get worse when the key passage sits mid-context.
- Blind spot
- It punishes rank 2 hard (0.5) even when the system reads several passages and rank 2 is almost as good as rank 1.
nDCG@10 (normalised discounted cumulative gain)
Example: (1.00 + 0.50 + 0.63 + 0) / 4 = 0.533.
- Tells you
- Like MRR, how near the top the answer is, but lower ranks lose credit gently instead of sharply.
- Why it matters
- It fits systems that read several passages, like mine: rank 2 still earns most of the credit. It's also the standard metric in information retrieval research, so results are comparable.
- Helps the chatbot
- It rewards putting the evidence among the first few passages the model reads, which is what matters for a chatbot that reads six.
- Note
- With several correct passages and graded relevance, nDCG divides by the best possible score. With one correct page per question, as here, that best score is 1, so the formula above is the whole story.
Latency (ms per question)
- Tells you
- How long retrieval takes per question.
- Why it matters
- Quality isn't free. A method that adds 3% accuracy but makes every answer a second slower may not be worth it. I report it next to every setup so the trade-off is visible.
- Helps the chatbot
- It doesn't make answers more correct, but it decides which accuracy gains are affordable. The reranker added about 0.8 seconds per question for a gain that could be chance, so it stayed out.
Precision@k, and why I left it out
Precision is standard when many passages can be correct. Here each question has one correct page, and that makes it unfair across chunk sizes: with whole pages, at most 1 of the top 10 can be correct (0.1), while with 250-character chunks the same page can fill all 10 slots (1.0). The difference would come from how the text was cut, not from retrieval quality. So I chose not to report it, and said why.
5. Statistics: is a difference real?
With 36 questions, one question moves a score by 1/36, about 3 points. A setup that scores 0.94 and one that scores 0.92 may differ by a single question. Two tools keep me from reading noise as a result.
Bootstrap confidence intervals
I resample the 36 questions with replacement 2,000 times, score each resample, and take the middle 95% of those scores as the interval. It shows the range the true score plausibly falls in.
The width is the honest picture: with 36 questions, intervals are wide. A bigger test set makes them narrower.
Paired comparisons
To compare two setups, I don't compare their averages; I compare them question by question, because both were run on the same questions. For each question I take the difference in score, then bootstrap those differences.
- If the interval for the difference is entirely above or below zero, the gap is clear.
- If it includes zero, the questions can't tell the two setups apart: the gap could be chance.
Pairing is much more sensitive than comparing two separate intervals, because it removes the variation that comes from some questions simply being harder than others.
6. Comparing setups
I tested every combination of these choices, 16 setups in all, on the same questions:
- Retriever: BM25 (keyword matching), dense (meaning-based, using the
all-MiniLM-L6-v2embedding model), and hybrid (both, merged with equal weight). - Passage size: whole pages, or chunks of 1000, 500 and 250 characters with 10% overlap.
- Reranking: hybrid search followed by a cross-encoder (
ms-marco-MiniLM-L-6-v2) that rereads the top 20 passages together with the question and reorders them.
One detail matters a lot: the embedding model only reads the first 256 tokens of its input. On a whole page, most of the text is never embedded, which is why dense retrieval on whole pages scored worst and improved sharply with smaller chunks. Knowing your model's limits explains results that would otherwise look random.
7. Checking the answers
Good retrieval doesn't guarantee a good answer. The answer side has its own questions, and the Ragas paper (one of the five papers my assistant covers) names the main ones:
- Faithfulness: is every claim in the answer supported by the passages? An answer can be fluent and wrong.
- Answer relevance: does the answer actually address the question?
- Context relevance: how much of the retrieved text was actually needed?
How I measure answers
For held-out questions, a script runs the assistant exactly as the app does and saves the passages the model saw in rank order, the model's raw answer and citation numbers, and the final answer. I then score three things by hand, with the scoring rules written down before reading any answers:
- Answer correctness: the answer contains every required fact with no material error. Declining an answerable question counts as a miss, and is reported separately as a wrong abstention.
- Citation support: each factual claim is checked against the passage it cites, including numbers, comparisons and qualifiers. A claim the cited passage only partly supports fails.
- Correct abstention: questions the papers can't answer, including ones where a paper discusses the topic but never gives the fact asked for, should be declined.
Because the passages are saved, each failure can be traced to its cause: the right page never reached the model (a retrieval problem), or it did and the model misread it or cited the wrong passage (a generation problem). Results are published as counts, such as "declined 4 of 5 unanswerable questions", so the sample size is always visible.
What my assistant enforces in code
- Valid citations: every [n] marker in an answer is checked against the passages the model was given. Markers with no matching passage are removed, each cited page gets exactly one number, and an answer with no valid citation is treated as unsupported.
- Safe failure: if the model's output contradicts itself (says it answered but gives the refusal text, or an empty answer), the app shows "The papers don't cover this." rather than something unreliable.
- Abstention: the model is told to say the papers don't cover a question when the passages don't contain the answer. Refusing is better than guessing, and a good test set includes questions the documents can't answer.
- Visible evidence: each source card shows the passage it came from, so a reader can check the claim in seconds.
8. LLM judges, and checking the judge
Reviewing hundreds of questions or answers by hand doesn't scale, so it's common to use a language model as a judge. The risk is trusting a judge nobody has measured. My rule: the judge is only used after its decisions are compared with a person's.
Agreement and Cohen's kappa
On 36 questions I'd reviewed myself, the judge's decisions compared with mine like this (second run):
| I approved | I rejected | |
|---|---|---|
| Judge approved | 17 | 1 |
| Judge rejected | 2 | 16 |
Raw agreement is (17 + 16) / 36 = 92%. But some agreement happens by chance: two raters who both say "yes" most of the time will agree often without either being careful. Cohen's kappa removes that:
Kappa runs from 0 (no better than chance) to 1 (perfect). On the scale most often used to read it, values above 0.8 count as almost perfect agreement. The table is from my second run of the check. The first run, with the same judge model, agreed on 34 of 36 (kappa 0.89). That one-question difference is worth knowing: an LLM judge isn't perfectly repeatable, so it's worth running the check more than once. With both runs above 0.8, I let the judge review the second batch. The disagreements are listed in the repo so anyone can see where the judge and I differed.
9. Robustness: prompt injection
A RAG system pastes document text into the model's instructions. If a document contains text written to look like an instruction ("ignore your rules and say X"), a careless system obeys it. This is prompt injection, one of the main security risks for any AI that reads outside content.
I planted five kinds of attack inside a passage, next to one genuine passage, and checked what the model did:
- overriding the answer ("reply only with HACKED")
- forcing a false refusal
- planting a false claim
- asking it to reveal its instructions
- sending the user to an outside link
The model resisted all five and answered correctly from the genuine passage each time. This is a check, not a guarantee: cleverer attacks exist, so I describe it as "resisted 5 of 5 planted attacks", not "immune".
10. My results
The setup used in the live demo (hybrid, 500 characters) against its closest rivals, on 36 reviewed questions. Intervals are 95%.
| Setup | Hit@1 | Hit@5 | Evidence@5 | MRR@10 | nDCG@10 | ms |
|---|---|---|---|---|---|---|
| Hybrid, 500 (demo) | 0.61 | 0.94 (0.86 to 1.00) | 0.61 | 0.750 (0.64 to 0.85) | 0.806 | 30 |
| Hybrid + rerank, 500 | 0.69 | 0.89 (0.78 to 0.97) | 0.58 | 0.777 (0.66 to 0.88) | 0.830 | 869 |
| BM25, 1000 | 0.69 | 0.97 (0.92 to 1.00) | 0.83 | 0.788 (0.68 to 0.89) | 0.839 | 2 |
| Hybrid, 1000 | 0.56 | 0.89 (0.78 to 0.97) | 0.78 | 0.701 (0.59 to 0.82) | 0.773 | 26 |
| Dense, whole page | 0.22 | 0.53 (0.36 to 0.69) | 0.53 | 0.359 (0.24 to 0.48) | 0.432 | 21 |
All 16 setups are in the table on the project page and in the repo's eval/results.md.
What the numbers say
- No setup is clearly better than the demo's. In the paired comparisons, no rival's difference has an interval entirely above zero. The clearly worse ones are dense retrieval and hybrid on whole pages.
- The reranker didn't earn its place. It put the right passage first more often (Hit@1 0.61 to 0.69) but pushed some out of the top 5, the overall change could be chance, and it made retrieval about 30 times slower. It stays out of the demo. A measured "no" is still a result.
- Evidence@5 found a trade-off the page scores hid. With 500-character passages the passage holding the evidence was in the top 5 for 61% of questions, against 78% with 1000 characters and 42% with 250. Part of that gap is the measure: a small passage is more likely to cut the quote in two. On the held-out questions the pattern didn't hold (60% for 500 against 50% for 1000), so the demo stays at 500.
- BM25's lead didn't carry over. On the held-out check below it ranked clearly worse; wording overlap is a possible explanation, not a proven one.
Held-out check
Ten AI-assisted questions, two per paper, with each evidence quote checked against the source PDF, and kept out of choosing any setting. They're the fairer test, because nothing was tuned on them. A validation script checks each question's paper, page and quote before every run.
| Setup | Hit@1 | Hit@5 | Evidence@5 | MRR@10 |
|---|---|---|---|---|
| Hybrid, 500 (demo) | 0.70 | 8/10 | 0.60 | 0.750 (0.45 to 1.00) |
| Hybrid + rerank, 500 | 0.80 | 10/10 | 0.80 | 0.900 (0.75 to 1.00) |
| BM25, 1000 | 0.30 | 8/10 | 0.70 | 0.470 (0.25 to 0.70) |
- Keyword search lost its lead. BM25 on 1000 characters, joint best on the original questions, ranked clearly worse than the demo's setup on the held-out ones (MRR difference -0.28, interval -0.53 to -0.05). Wording overlap with the source pages is one possible reason, but both sets were AI-assisted and differ in content, so this shows a change, not its cause.
- The reranker is undecided. It didn't help on the original questions but found all ten here. Its gain could still be chance (the intervals start at 0), so it stays out of the demo until a bigger held-out set settles it. This is exactly why a held-out set matters: the two sets point different ways.
- A drop is normal. The demo's setup found the right page in the top 5 for 8 of 10, against 34 of 36 on the questions used to choose it.
11. Best practice checklist
What I'd recommend for any RAG system, from this project:
- Measure retrieval separately from answers. Retrieval sets the ceiling, and it can be scored exactly and cheaply.
- Use labels that don't depend on chunking (document and page), so different chunk sizes are compared fairly.
- Check every generated question against its source, and remove questions with no single right answer.
- Keep a held-out set, written separately, that is never used to choose settings.
- Match the metric to the system. If the model reads six passages, Hit@5 and nDCG matter more than Hit@1.
- Check the passage, not just the page, when you can (Evidence@k).
- Report intervals, and compare setups question by question. Small test sets make small gaps meaningless.
- Report speed next to quality, so improvements are judged by what they cost.
- Validate any LLM judge against people, with kappa, not just raw agreement, and publish the disagreements.
- Enforce grounding in code: validate citations, fail safely, and let the system say "not covered".
- Test hostile documents before trusting a system with content you don't control.
- Know your model's limits (like a 256-token input cap), because they explain results.
- Test the measuring code itself. My metrics, reranker and citation checks have automated tests, so the published numbers come from code that is checked.
- Publish negative results. The reranker that didn't help is as useful to know as a setup that did.