Evaluation

When RAG fails, separate missed retrieval from retrieved-then-invented. This chapter uses classroom checks: hit@k, a faithfulness heuristic, and “answer only from context.” These are not paper metrics and they do not replace spot-checking.

Write 20–50 Q&A pairs that depend on made-up facts in your corpus (order KH-8842, a “7 day” deadline). Trivia the web already knows cannot test retrieval.


Retrieval: hit@k

Label each question with the chunk id(s) that must appear. After retrieval, see whether the gold id is in the top k.

def hit_at_k(retrieved: list[str], gold: str | set[str], k: int) -> float:
    targets = {gold} if isinstance(gold, str) else set(gold)
    return 1.0 if targets & set(retrieved[:k]) else 0.0


cases = [
    {"q": "How do I refund KH-8842?", "gold": "refund-0"},
    {"q": "When is support online?", "gold": "hours-0"},
]

# retrieved_ids = collection.query(...)["ids"][0]
retrieved_ids = ["refund-0", "hours-0", "other"]
print(hit_at_k(retrieved_ids, "refund-0", k=4))

score = sum(hit_at_k(["refund-0"], c["gold"], 4) for c in cases) / len(cases)
print("hit@4", score)

Low hit@k: fix chunking, the embedding model, k, and filters before swapping a larger chat model. High hit@k but angry users: the generation side.

Recall@k (multiple gold chunks) uses the same lists; hit@k is enough in class.


Faithfulness

Faithfulness asks: can each claim in the answer be supported by the context? It is not “does this sound helpful?” Two classroom layers:

1. Number / code scan (no model)

Every number and order id in the answer must appear in context. Invented 8 days or KH-0001 fail.

import re

TOKEN = re.compile(r"KH-\d+|\d+")


def unsupported_tokens(answer: str, context: str) -> list[str]:
    ctx = set(TOKEN.findall(context))
    return sorted({t for t in TOKEN.findall(answer) if t not in ctx})


ctx = "Refunds must be requested within 7 days using order KH-8842."
print(unsupported_tokens("Please apply within 8 days with KH-0001.", ctx))
# ['8', 'KH-0001']
print(unsupported_tokens("Please apply within 7 days with KH-8842.", ctx))
# []

This will not catch “7 days” paraphrased as “a week,” but it catches the usual hallucinated digits.

2. Short judge prompt (optional, any LLM)

You are an auditor. List UNSUPPORTED claims only; if none, output NONE.
CONTEXT:
{context}

ANSWER:
{answer}

Do not score “was this useful.” Useful-but-unfaithful is still an incident for support bots.


“Answer only from context”

This is both a prompt rule and a test suite:

Question typeExpectation
Context has a hard numberQuote it; do not rewrite policy
Context is silentDo not know / insufficient evidence—not a common-sense essay
Context is partialState only the supported part; mark the rest unknown
REFUSAL_MARKERS = ("do not know", "don't know", "not in the context", "insufficient")


def answers_only_from_context(answer: str, context: str, expect_unknown: bool) -> bool:
    leaked = unsupported_tokens(answer, context)
    if leaked:
        return False
    if expect_unknown:
        lower = answer.lower()
        return any(m in lower for m in REFUSAL_MARKERS)
    return True


assert answers_only_from_context(
    "The context does not mention the stock price.",
    "Support is online 09:00–18:00.",
    expect_unknown=True,
)

Put expect_unknown=True items in the regression set. Models love to “help” there.


Suggested loop

  1. Freeze chunking and the embedding model; measure hit@4.
  2. Read 10 full answers by hand (including should-refuse items).
  3. Run the token scan on every answer.
  4. Then touch rerank or hybrid search—one knob per change.

LangSmith, LlamaIndex evaluators, and Dify retrieval tests productize the same idea: LangChain RAG, LlamaIndex, Dify Knowledge Base. This course does not bind you to any of them.


Next steps

评论