Skip to content

Evaluating RAG Responses

Compare RAG axioms to human judgments

This walkthrough shows how to apply the RAG axioms to generated responses and how to evaluate the axioms against human judgments, as in our ICTIR 2025 paper [Merker et al., 2025]. We measure how often an axiom expresses a preference (decisiveness) and how often its preferences agree with the human judgments (consistency).

Based on the notebook ictir2025.ipynb.

Setup

Install ir_axioms with the extras used in the paper, and download the spaCy model:

pip install "ir_axioms[pyterrier,keybert,sbert,fasttext]"
python -m spacy download en_core_web_sm

Then, clone the repository to get the data files in the data/ directory, and run the following code from the repository root.

from pathlib import Path

data_dir = Path("data/")
cache_dir = Path("cache/")
crowd_responses_path = data_dir / "crowd" / "responses.jsonl.gz"
crowd_ratings_path = data_dir / "crowd" / "ratings.jsonl.gz"

Load Requests and Responses

We use a corpus of RAG responses written by humans and LLMs in different styles, together with crowdsourced pairwise utility judgments. Each request (query) has one response per kind and style. We convert the data to GenerationRequest and GenerationResponse objects, so that all requests have the same responses in the same order:

from collections.abc import Sequence
from pandas import DataFrame, read_json
from ir_axioms.model import GenerationRequest, GenerationResponse

responses = read_json(crowd_responses_path, lines=True)
ratings = read_json(crowd_ratings_path, lines=True)
responses = responses[responses["topic"].isin(ratings["query_id"])]
responses["name"] = responses["kind"] + "-" + responses["style"]


def load_requests_responses(
    df: DataFrame,
) -> list[tuple[GenerationRequest, list[GenerationResponse]]]:
    names = sorted(df["name"].unique())
    data = []
    for (query_id, query), df_query in df.groupby(["topic", "query"], sort=False):
        context = df_query["references_texts"].iloc[0]
        request = GenerationRequest(
            id=query_id,
            text=query,
            context=list(context) if isinstance(context, Sequence) else None,
        )
        responses_by_name = {
            row["name"]: GenerationResponse(id=row["name"], text=row["raw_text"])
            for _, row in df_query.iterrows()
        }
        data.append((
            request,
            [responses_by_name.get(name, GenerationResponse(id=name, text="")) for name in names],
        ))
    return data


data = load_requests_responses(responses)

Oracle Preferences

The TrecRagCrowdAxiom derives preferences from the crowdsourced judgments for a given utility dimension (e.g., "overall", "coherence", "broad coverage"). It serves as the ground truth ("ORACLE") for evaluating the axioms:

from ir_axioms.axiom import TrecRagCrowdAxiom

oracle = TrecRagCrowdAxiom(
    utility_type="overall",
    responses_path=crowd_responses_path,
    ratings_path=crowd_ratings_path,
)

Nugget-based oracle

To use the nugget assignments of the TREC 2024 RAG track [Pradeep et al., 2024] instead, load the corresponding responses and use TrecRagNuggetAxiom(assignments_path=data_dir / "nugget_assignment.20241218.jsonl", score_type="vital").

Axioms

We compare three groups of axioms: adapted retrieval axioms, generation-specific axioms, and majority votes of all axioms. The margins below are the ones used in the paper; the aspect-based axioms use spaCy entities as aspects. Because some axioms are costly (e.g., grammar checking with LanguageTool), we cache all axioms on disk:

from ir_axioms.axiom import (
    GEN_TFC1, GEN_LNC1, GEN_TF_LNC, GEN_REG, GEN_AND, GEN_DIV, GEN_STMC1, GEN_STMC2,
    GEN_PROX1, GEN_PROX2, GEN_PROX3, GEN_PROX4, GEN_PROX5, GEN_aSL,
    COH1, COH2, COV1, COV2, COV3, CONS1, CONS2, CONS3, CORR1, CLAR1, CLAR2,
    MajorityVoteAxiom,
)
from ir_axioms.tools import SpacyEntitiesAspectExtraction

entities = SpacyEntitiesAspectExtraction()

retrieval_axioms = {
    "GEN-TFC1": GEN_TFC1(), "GEN-LNC1": GEN_LNC1(), "GEN-TF-LNC": GEN_TF_LNC(),
    "GEN-REG": GEN_REG(), "GEN-AND": GEN_AND(), "GEN-DIV": GEN_DIV(),
    "GEN-STMC1": GEN_STMC1(), "GEN-STMC2": GEN_STMC2(),
    "GEN-PROX1": GEN_PROX1(), "GEN-PROX2": GEN_PROX2(), "GEN-PROX3": GEN_PROX3(),
    "GEN-PROX4": GEN_PROX4(), "GEN-PROX5": GEN_PROX5(), "GEN-aSL": GEN_aSL(),
}
generation_axioms = {
    "COH1": COH1(margin_fraction=0.75),
    "COH2": COH2(margin_fraction=0.75),
    "COV1": COV1(aspect_extraction=entities, margin_fraction=0.5),
    "COV2": COV2(aspect_extraction=entities, margin_fraction=0.5),
    "COV3": COV3(aspect_extraction=entities, margin_fraction=0.5),
    "CONS1": CONS1(aspect_extraction=entities, margin_fraction=0.5),
    "CONS2": CONS2(margin_fraction=0.5),
    "CONS3": CONS3(margin_fraction=0.5),
    "CORR1": CORR1(margin_fraction=0.75),
    "CLAR1": CLAR1(margin_fraction=0.5),
    "CLAR2": CLAR2(margin_fraction=0.5),
}
vote_axioms = {
    "VOTE-all": MajorityVoteAxiom(
        axioms=[*retrieval_axioms.values(), *generation_axioms.values()],
        minimum_votes=0.3,
    ),
}

axioms = {
    name: axiom.cached(cache_dir / "axioms" / f"{name}.cache")
    for name, axiom in {**retrieval_axioms, **generation_axioms, **vote_axioms}.items()
}

Compute Preferences

For each request, we compute the preference matrices of the oracle and of all axioms, and normalize the preferences to −1, 0, or 1:

from numpy import sign, stack
from tqdm.auto import tqdm

oracle_preferences = sign(stack([
    oracle.preferences(input=request, outputs=outputs)
    for request, outputs in tqdm(data, desc="ORACLE")
]))
axiom_preferences = {
    name: sign(stack([
        axiom.preferences(input=request, outputs=outputs)
        for request, outputs in data
    ]))
    for name, axiom in tqdm(axioms.items(), desc="Axioms")
}

Decisiveness and Consistency

We evaluate each axiom by two measures, ignoring the trivial comparisons of each response with itself:

  • Decisiveness: the proportion of response pairs for which the axiom expresses a preference.
  • Consistency: the proportion of the axiom's preferences that do not contradict the oracle's preference.

The harmonic mean of both measures summarizes the axiom's effectiveness:

from numpy import eye

num_outputs = oracle_preferences.shape[-1]
off_diagonal = ~eye(num_outputs, dtype=bool)

results = []
for name, preferences in axiom_preferences.items():
    decisive = (preferences != 0) & off_diagonal
    decisiveness = decisive.sum() / (off_diagonal.sum() * len(preferences))
    consistent = decisive & (abs(preferences - oracle_preferences) <= 1)
    consistency = consistent.sum() / decisive.sum() if decisive.sum() > 0 else 0
    effectiveness = (
        2 * decisiveness * consistency / (decisiveness + consistency)
        if decisiveness + consistency > 0 else 0
    )
    results.append({
        "axiom": name,
        "decisiveness": decisiveness,
        "consistency": consistency,
        "effectiveness": effectiveness,
    })

DataFrame(results).sort_values("effectiveness", ascending=False).round(2)

The consistency above is relaxed: an axiom preference does not contradict a zero oracle preference. For a strict consistency, count only exact matches, i.e., preferences == oracle_preferences.

Learn More

  • The ICTIR 2025 notebook also evaluates on the TREC 2024 RAG nugget assignments, compares to LLM-based ORIG axioms (TrecRagLlmOrigAxiom), and contains the code for all figures and tables of the paper.
  • The RAG axioms page maps the axioms to the utility dimensions of related evaluation frameworks.

References

Jan Heinrich Merker, Maik Fröbe, Benno Stein, Martin Potthast, and Matthias Hagen. Axioms for Retrieval-Augmented Generation. In Proceedings of the 2025 International ACM SIGIR Conference on Innovative Concepts and Theories in Information Retrieval, ICTIR 2025, Padua, Italy, July 18, 2025, pages 67–77, 2025. ACM. [] [doi]
@inproceedings{merker2025axioms,
  author       = {Jan Heinrich Merker and
                  Maik Fr{\"{o}}be and
                  Benno Stein and
                  Martin Potthast and
                  Matthias Hagen},
  title        = {Axioms for Retrieval-Augmented Generation},
  booktitle    = {Proceedings of the 2025 International {ACM} {SIGIR} Conference on
                  Innovative Concepts and Theories in Information Retrieval, {ICTIR}
                  2025, Padua, Italy, July 18, 2025},
  pages        = {67--77},
  publisher    = {{ACM}},
  year         = {2025},
  url          = {https://doi.org/10.1145/3731120.3744601},
  doi          = {10.1145/3731120.3744601}
}
Ronak Pradeep, Nandan Thakur, Shivani Upadhyay, Daniel Campos, Nick Craswell, and Jimmy Lin. Initial Nugget Evaluation Results for the TREC 2024 RAG Track with the AutoNuggetizer Framework. CoRR, abs/2411.09607, 2024. [] [doi]
@article{pradeep2024initial,
  author       = {Ronak Pradeep and
                  Nandan Thakur and
                  Shivani Upadhyay and
                  Daniel Campos and
                  Nick Craswell and
                  Jimmy Lin},
  title        = {Initial Nugget Evaluation Results for the {TREC} 2024 {RAG} Track
                  with the AutoNuggetizer Framework},
  journal      = {CoRR},
  volume       = {abs/2411.09607},
  year         = {2024},
  url          = {https://doi.org/10.48550/arXiv.2411.09607},
  doi          = {10.48550/ARXIV.2411.09607},
  eprinttype   = {arXiv},
  eprint       = {2411.09607}
}