Attribution and Citation: Sourcing LLM Outputs › Code Implementation
Language AI HandbookAll chapters Attribution and Citation: Sourcing LLM Outputs › Code ImplementationOne section · Sections in this chapter9 / 16 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
In this section, we build a simple attribution evaluation system. We simulate a RAG response with citations and use NLI-based scoring to evaluate whether each cited passage supports the corresponding claim.
We need transformers and torch for the NLI model. Install them if needed:
# subprocess.run(["uv", "pip", "install", "transformers", "torch"], check=False, capture_output=True)
# Run once locally, then comment outSetting Up the NLI Scorer
The core of attribution evaluation is natural language inference: given a premise (the cited passage) and a hypothesis (the generated claim), determine whether the premise entails the hypothesis. We use a pretrained NLI model for this. The cross-encoder architecture is important here: it encodes both the passage and the claim jointly, allowing the model to attend to their interaction directly rather than comparing independently encoded representations.
from transformers import pipeline
# Use a lightweight cross-encoder NLI model
nli_model = pipeline(
"text-classification",
model="cross-encoder/nli-deberta-v3-small",
device=-1, # CPU
)NLI model loaded successfully. Model: cross-encoder/nli-deberta-v3-small
Defining the Attribution Evaluator
We define a function that takes a claim and a list of cited passages, and returns an entailment score for each passage. A high score indicates strong support; a low score indicates the passage does not support the claim. The function returns all three NLI scores (entailment, neutral, contradiction) so callers can apply whatever threshold logic fits their application.
def evaluate_citation(claim: str, passage: str, model) -> dict:
"""
Evaluate whether a passage supports a claim using NLI.
Returns entailment, neutral, and contradiction probabilities.
"""
result = model(f"{passage} [SEP] {claim}", top_k=None)
# result is a list of dicts: [{"label": ..., "score": ...}, ...]
# When batched, it may be wrapped in an extra list; unwrap if needed
if result and isinstance(result[0], list):
result = result[0]
scores = {item["label"].lower(): item["score"] for item in result}
return {
"entailment": scores.get("entailment", 0.0),
"neutral": scores.get("neutral", 0.0),
"contradiction": scores.get("contradiction", 0.0),
"is_supported": scores.get("entailment", 0.0) > 0.5,
}
def evaluate_attributed_response(
claims_with_citations: list, passages: dict, model
) -> list:
"""
Evaluate all claim-citation pairs in a response.
claims_with_citations: list of {"claim": str, "citation_ids": list[int]}
passages: dict mapping passage ID to passage text
"""
results = []
for item in claims_with_citations:
claim = item["claim"]
cited_passages = [
passages[cid] for cid in item["citation_ids"] if cid in passages
]
claim_results = []
for idx, passage in zip(item["citation_ids"], cited_passages):
score = evaluate_citation(claim, passage, model)
claim_results.append(
{
"passage_id": idx,
"passage_preview": passage[:80] + "..."
if len(passage) > 80
else passage,
**score,
}
)
# A claim is considered supported if at least one cited passage entails it
any_supported = any(r["is_supported"] for r in claim_results)
results.append(
{
"claim": claim,
"citation_results": claim_results,
"claim_supported": any_supported,
}
)
return resultsExample Attribution Evaluation
We create a simulated RAG scenario with three passages and a model response containing three claims. The first claim is well-supported. The second contains a date error that the source contradicts. The third cites a completely unrelated passage, simulating hallucinated attribution.
# Simulated retrieved passages (as a RAG system would provide)
passages = {
1: "The transformer architecture was introduced in the paper 'Attention is All You Need' by Vaswani et al. in 2017. It replaced recurrent neural networks with self-attention mechanisms.",
2: "BERT (Bidirectional Encoder Representations from Transformers) was published by Devlin et al. in 2019. It uses masked language modeling for pretraining.",
3: "The Amazon rainforest covers approximately 5.5 million square kilometers and is home to 10% of all species on Earth.",
}
# Simulated attributed response claims (what the model generated with citations)
claims_with_citations = [
{
"claim": "The transformer model was proposed by Vaswani et al. in 2017.",
"citation_ids": [1], # Correctly cited
},
{
"claim": "BERT was first introduced in 2017.", # Wrong date!
"citation_ids": [
2
], # Passage 2 actually says 2019, but claim says 2017
},
{
"claim": "Transformers use self-attention instead of recurrence.",
"citation_ids": [
3
], # Completely wrong citation; passage 3 is about the Amazon
},
]
# Evaluate attribution
evaluation_results = evaluate_attributed_response(
claims_with_citations, passages, nli_model
)Attribution Evaluation Results ============================================================ Claim: "The transformer model was proposed by Vaswani et al. in 2017." Supported: YES [Passage 1] Entailment: 0.995 | Contradiction: 0.002 Preview: The transformer architecture was introduced in the paper 'Attention is All You N... Claim: "BERT was first introduced in 2017." Supported: NO [Passage 2] Entailment: 0.000 | Contradiction: 0.960 Preview: BERT (Bidirectional Encoder Representations from Transformers) was published by ... Claim: "Transformers use self-attention instead of recurrence." Supported: NO [Passage 3] Entailment: 0.000 | Contradiction: 1.000 Preview: The Amazon rainforest covers approximately 5.5 million square kilometers and is ...
The results show the behavior we want from an attribution evaluator. The first claim (transformer proposed in 2017) is well-supported by passage 1 and receives a high entailment score. The second claim (BERT in 2017) gets a lower entailment score because passage 2 says 2019, not 2017. Notice that the passage does not merely fail to confirm the date: it actively contradicts it, so the contradiction score should be higher. The third claim (about transformers and self-attention) receives near-zero entailment from passage 3, which is about the Amazon rainforest. The NLI model correctly recognizes that this passage provides no support for a claim about neural architectures.
Computing Attribution Metrics
We compute citation precision and recall across the evaluation results. These aggregate metrics summarize the quality of the attribution system in terms the precision-recall framework we defined earlier.
def compute_citation_metrics(evaluation_results: list) -> dict:
"""
Compute citation-level precision, recall, and F1.
Precision: fraction of cited passages that support the claim.
Recall: fraction of supported claims that have at least one supporting citation.
"""
total_citations = 0
supported_citations = 0
total_claims = len(evaluation_results)
supported_claims = 0
for res in evaluation_results:
for cr in res["citation_results"]:
total_citations += 1
if cr["is_supported"]:
supported_citations += 1
if res["claim_supported"]:
supported_claims += 1
citation_precision = (
supported_citations / total_citations if total_citations > 0 else 0.0
)
claim_recall = supported_claims / total_claims if total_claims > 0 else 0.0
f1 = (
2
* citation_precision
* claim_recall
/ (citation_precision + claim_recall)
if (citation_precision + claim_recall) > 0
else 0.0
)
return {
"citation_precision": citation_precision,
"claim_recall": claim_recall,
"f1": f1,
"total_citations": total_citations,
"supported_citations": supported_citations,
"total_claims": total_claims,
"supported_claims": supported_claims,
}
metrics = compute_citation_metrics(evaluation_results)Citation Metrics ---------------------------------------- Citation Precision: 0.333 (1/3 citations are actually supportive) Claim Recall: 0.333 (1/3 claims are supported by at least one citation) F1 Score: 0.333
These metrics translate directly to system quality. A system with citation precision 0.33 means two-thirds of the citations it provides do not support the claim they are associated with. A system with claim recall 0.33 means two-thirds of the claims it makes are not supported by any of the provided citations. Both failures erode trust in the attributed response in different ways: low precision makes citations unreliable, while low recall means the response has claims that are essentially assertions made without evidence.
Key Parameters
The key parameters for attribution evaluation are:
- Entailment threshold: The minimum entailment probability required to consider a passage supportive. Higher thresholds produce fewer false positives but increase citation misses. The 0.5 threshold used in our example is a reasonable starting point but should be tuned on validation data for each application.
- NLI model: The quality of attribution evaluation is bounded by the NLI model's accuracy. Larger, better models (e.g., DeBERTa-v3-large) will evaluate attribution more accurately than smaller ones. For high-stakes applications, using a larger NLI model for evaluation is worthwhile even if the generation model itself is fast.
- Evaluation granularity: Evaluating at the sentence level is more precise than the paragraph level, but requires accurate sentence segmentation of the generated text. Sentence segmenters introduce their own errors, particularly on lists, numbered items, and sentences containing quotations or parenthetical remarks.
- Passage truncation: Long passages may need to be truncated before being passed to the NLI model, since most NLI models have a maximum input length. Truncating a passage can remove the very sentence that contains the supporting evidence, causing false negatives. Strategies for handling long passages include sliding-window evaluation (evaluate the claim against multiple overlapping windows of the passage and take the maximum score) or passage chunking.