A 2B model that localizes and types hallucinated spans across code, tool output, and prose in one pass:
Unified test set (10,698): span-F1 0.689, example-F1 0.921, IoU 0.758.
Code-agent answers: span-F1 0.602 / example-F1 0.835 — where it beats every alternative we tested: our own 8B sibling (LFM-8B, 0.507), a self-hosted 120B judge (0.21) and a 550B judge (0.22), and off-the-shelf detectors (HHEM / Lynx-8B / Granite-Guardian / MiniCheck, all ≈ chance). Large general judges over-flag generated code; this model doesn't.
Established prose benchmarks: RAGTruth example-F1 0.818 (> LettuceDetect-large 0.792); PsiloQA strong across 14 languages.
One small, fast model — no separate judge, no giant LLM — for span-level hallucination detection from RAG to agentic coding.
Overview
lettucedect-v2-qwen-2b is a generative hallucination detector for Retrieval-Augmented
Generation (RAG) and coding-agent settings. Given a user request, the supporting
context, and an answer, it emits the exact spans of the answer that are not
supported by the context, each tagged with a hallucination category and
subcategory. Unlike the encoder models in the LettuceDetect family (token
classifiers), this is an instruction-tuned generative model that returns structured
JSON, so it localizes and types hallucinations in a single pass — across prose
and code.
It is the first LettuceDetect model trained on a unified benchmark spanning code
(SWE-bench-derived coding-agent traces) and prose (RAGTruth, PsiloQA, and synthetic
ACL / README / tool-output / Wikipedia sources), in 14 languages.
Model Details
Base model: Qwen3.5-2B (hybrid Gated DeltaNet + attention)
Training: LoRA supervised fine-tuning (full bf16), merged to a standalone model
Context: long-context (handles the full request + retrieved context + answer)
Languages: English plus 13 more (via the multilingual PsiloQA portion)
How It Works
The model is prompted with a fixed, data-agnostic system prompt that defines a
hallucination ("a substring of the answer not supported by the context") and
enumerates the taxonomy, followed by the user request, the context, and the answer
to verify. It replies with a JSON object listing each hallucinated span verbatim
with its category and subcategory; a fully supported answer returns an empty list.
Span offsets are recovered by matching each returned substring back into the answer.
Usage
The model is a generative span detector: serve it with vLLM (OpenAI-compatible) and
send the LettuceDetect detection prompt, which defines a hallucination and enumerates
the taxonomy. It replies with JSON; match each returned text back into the answer to
get character offsets.
The model expects the exact detection prompt it was trained on (below). Send the
user request + context as the user turn, ending with the answer to verify.
python
1import json
2from openai import OpenAI
34SYSTEM ="""You are an expert annotator who identifies hallucinated spans in a generated answer with respect to a given context (the only trusted evidence). A hallucinated span is a substring of the answer that is not supported by the context. Spans consistent with the context are not hallucinations.
56Quote each hallucinated span verbatim from the answer and classify it into exactly one category and one subcategory.
78Categories (the kinds of unsupported span):
9- contradiction: conflicts with the context (a wrong value, number, date, name, or relationship)
10- fabricated_reference: an entity, name, identifier, or section that is absent from the context
11- unsupported_addition: a claim, detail, or behavior the context never states
1213Subcategories:
14- entity: a wrong or invented name, entity, or object
15- temporal: an incorrect date, time, duration, or ordering
16- numerical: an incorrect number, quantity, or amount
17- value: a wrong value, setting, or attribute value
18- relational: an incorrect relationship or association between things
19- identifier: an invented identifier or name not found in the context
20- section: a reference to a section, part, or location that does not exist
21- attribute: an invented or incorrect attribute or property
22- claim: an added factual claim the context does not support
23- behavior: an added or changed action or behavior the context never states
24- elaboration: extra detail or elaboration beyond what the context supports
25- subjective: an unsupported subjective or evaluative statement
26- unspecified: unsupported, with no more specific subtype
2728Reply with ONLY a JSON object (no markdown, no code fences): {"hallucinated_spans": [{"text": "...", "category": "...", "subcategory": "..."}]}. If nothing is unsupported, reply {"hallucinated_spans": []}."""2930client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")31context ="France is a country in Europe. Its capital is Paris."32question ="What is the capital of France? What is its population?"33answer ="The capital of France is Paris. Its population is 2 million."34user =f"User request: {question}\n\n{context}\n\nAnswer to verify:\n{answer}"3536resp = client.chat.completions.create(37 model="lettucedect-v2-qwen-2b", temperature=0.0,38 messages=[{"role":"system","content": SYSTEM},{"role":"user","content": user}],39)40spans = json.loads(resp.choices[0].message.content)["hallucinated_spans"]41for s in spans:# recover character offsets in the answer42 s["start"]= answer.find(s["text"])43 s["end"]= s["start"]+len(s["text"])44# [{"text": "Its population is 2 million.", "category": "unsupported_addition",45# "subcategory": "numerical", "start": 32, "end": 60}]
Or via the LettuceDetect package: serve the model with vLLM, point OPENAI_API_BASE at
it, and call
It auto-routes this model to its native training prompt and hallucinated_spans output
(typed category/subcategory). Pass native=True if served under a different name, and
include_reasoning=True to request per-span explanations.
With explanations (pure prompts)
To get a short per-span explanation without the LettuceDetect package, use the
explanation variant of the system prompt. It differs from the base prompt in exactly
two places — the instruction sentence gains one clause, and the output contract gains
an "explanation" field:
python
1SYSTEM_EXPL ="""You are an expert annotator who identifies hallucinated spans in a generated answer with respect to a given context (the only trusted evidence). A hallucinated span is a substring of the answer that is not supported by the context. Spans consistent with the context are not hallucinations.
23Quote each hallucinated span verbatim from the answer and classify it into exactly one category and one subcategory, and give a short explanation of why it is unsupported.
45Categories (the kinds of unsupported span):
6- contradiction: conflicts with the context (a wrong value, number, date, name, or relationship)
7- fabricated_reference: an entity, name, identifier, or section that is absent from the context
8- unsupported_addition: a claim, detail, or behavior the context never states
910Subcategories:
11- entity: a wrong or invented name, entity, or object
12- temporal: an incorrect date, time, duration, or ordering
13- numerical: an incorrect number, quantity, or amount
14- value: a wrong value, setting, or attribute value
15- relational: an incorrect relationship or association between things
16- identifier: an invented identifier or name not found in the context
17- section: a reference to a section, part, or location that does not exist
18- attribute: an invented or incorrect attribute or property
19- claim: an added factual claim the context does not support
20- behavior: an added or changed action or behavior the context never states
21- elaboration: extra detail or elaboration beyond what the context supports
22- subjective: an unsupported subjective or evaluative statement
23- unspecified: unsupported, with no more specific subtype
2425Reply with ONLY a JSON object (no markdown, no code fences): {"hallucinated_spans": [{"text": "...", "category": "...", "subcategory": "...", "explanation": "..."}]}. If nothing is unsupported, reply {"hallucinated_spans": []}."""
The user turn is unchanged. Each returned span then carries an explanation, e.g.
{"text": "Its population is 2 million.", "category": "unsupported_addition", "subcategory": "numerical", "explanation": "The context does not state the population."}.
Production tip: enforce the output schema
At temperature=0 the model reliably emits valid JSON, but when embedding it in a
serving stack (e.g. a router response filter), pass a strict schema so parsing can
never fail — vLLM supports OpenAI structured outputs:
(For the explanation variant, add "explanation": {"type": "string"} to properties
and to required.)
Plain transformers (no vLLM, no lettucedetect)
python
1import json, torch
2from transformers import AutoModelForCausalLM, AutoTokenizer
34model_id ="KRLabsOrg/lettucedect-v2-qwen-2b"5tok = AutoTokenizer.from_pretrained(model_id)6model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="auto", device_map="auto")78SYSTEM ="..."# the detection prompt shown above (verbatim)9context ="France is a country in Europe. Its capital is Paris."10question ="What is the capital of France? What is its population?"11answer ="The capital of France is Paris. Its population is 2 million."12user =f"User request: {question}\n\n{context}\n\nAnswer to verify:\n{answer}"1314inputs = tok.apply_chat_template(15[{"role":"system","content": SYSTEM},{"role":"user","content": user}],16 add_generation_prompt=True, enable_thinking=False, return_tensors="pt", return_dict=True,17).to(model.device)18out = model.generate(**inputs, max_new_tokens=512, do_sample=False)19reply = tok.decode(out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True)2021# raw_decode reads the first JSON object and ignores any trailing text22spans = json.JSONDecoder().raw_decode(reply[reply.index("{"):])[0]["hallucinated_spans"]23for s in spans:# recover character offsets in the answer24 s["start"]= answer.find(s["text"]); s["end"]= s["start"]+len(s["text"])25print(spans)26# -> [{'text': '2 million', 'category': 'unsupported_addition', 'subcategory': 'claim', 'start': 50, 'end': 59}]
Performance
Char-level span-F1 / example-F1 / IoU on the unified test set (10,698 samples). The
model beats the mmBERT-base encoder on every source, span- and example-level.
dataset
n
span-F1
example-F1
IoU
ALL
10698
0.689
0.921
0.758
acl
440
0.749
0.942
0.811
code-agent
2015
0.602
0.835
0.707
readme
641
0.866
0.984
0.894
tool-output
617
0.719
0.907
0.793
wikipedia
1388
0.817
0.974
0.871
psiloqa (14 langs)
2897
0.732
0.966
0.687
ragtruth
2700
0.574
0.818
0.765
Its strength is unified cross-domain coverage — one model handling code, prose, and
14 languages — rather than a single-benchmark record.
Prose benchmarks: RAGTruth & PsiloQA
On the established prose benchmarks the same model is competitive with or better than
specialized methods, so the code/tool sources extend rather than trade off against prose.
RAGTruth (official test set, n=2700) — example-level F1, on the standard leaderboard:
Method
Example-level F1
RAG-HAT (fine-tuned Llama-3-8B)
83.9
lettucedect-v2-qwen-2b (ours)
81.8
LettuceDetect-large (v1)
79.2
Fine-tuned Llama-2-13B (RAGTruth)
78.7
Luna
65.4
GPT-4
63.4
Second only to a fine-tuned 8B, and above the v1 detector, the fine-tuned 13B, Luna, and GPT-4.
(Our span-level on RAGTruth: P 0.601 / R 0.548 / F1 0.574 / IoU 0.765.)
PsiloQA (multilingual) — span IoU. Our generative 2B matches PsiloQA's best fine-tuned
encoder and is far above the strongest LLM judge:
Method (English)
IoU
lettucedect-v2-qwen-2b (ours)
0.724
mmBERT-base (PsiloQA paper, fine-tuned encoder)
0.707
Qwen2.5-32B-it, 3-shot (PsiloQA paper, LLM judge)
0.400
Across all 14 languages our IoU is 0.689. Numbers for the other methods are from the PsiloQA
paper (arXiv 2510.04849); our IoU uses the unified char-overlap scorer, so cross-paper rows
are indicative rather than an identical protocol. Full per-language results:
PsiloQA — per language (n=2897, 14 languages):
lang
n
span-F1
example-F1
IoU
ALL
2897
0.733
0.966
0.689
en
1098
0.785
0.987
0.724
es
100
0.817
0.973
0.685
ca
100
0.721
0.959
0.704
fi
200
0.683
0.963
0.727
it
99
0.680
0.984
0.724
cs
100
0.672
0.974
0.604
fa
100
0.650
0.977
0.789
hi
300
0.649
0.933
0.637
fr
100
0.634
0.944
0.661
zh
300
0.597
0.952
0.650
ar
100
0.594
0.966
0.606
de
100
0.587
0.901
0.650
sv
100
0.512
0.959
0.707
eu
100
0.511
0.890
0.569
Example-F1 stays high across all 14 languages (answer-level detection is reliable);
span-F1 is strongest for higher-resource languages, as expected.
Baselines on code-agent
Code-agent (2,015 samples) is where off-the-shelf detectors and even frontier LLM judges
collapse, and where this model's value is clearest:
The off-the-shelf detectors and the 550B judge over-flag on code-agent (balanced accuracy
near chance) because a generated code patch is not literally present in the context.
Citing
bibtex
1@misc{kovács2026documentgroundingspanlevelhallucination,
2 title={Beyond Document Grounding: Span-Level Hallucination Detection over Code, Tool Output, and Documents},
3 author={Ádám Kovács and Bowei He and Xue Liu and István Boros and Szilveszter Tóth and Gábor Recski},
4 year={2026},
5 eprint={2607.00895},
6 archivePrefix={arXiv},
7 primaryClass={cs.CL},
8 url={https://arxiv.org/abs/2607.00895},
9}