Instructions to use lebe1/lettuceprevent-ettin-decoder-68m-en with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use lebe1/lettuceprevent-ettin-decoder-68m-en with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="lebe1/lettuceprevent-ettin-decoder-68m-en")# Load model directly from transformers import AutoTokenizer, EttinTokenClassifier tokenizer = AutoTokenizer.from_pretrained("lebe1/lettuceprevent-ettin-decoder-68m-en") model = EttinTokenClassifier.from_pretrained("lebe1/lettuceprevent-ettin-decoder-68m-en", device_map="auto") - Notebooks
- Google Colab
- Kaggle
LettucePrevent (Ettin-Decoder-68M): Token-Level Hallucination Detection for Generation-Time Prevention
Model Name: lettuceprevent-ettin-decoder-68m-en
Author: Leon Beccard
GitHub: https://github.com/lebe1/LettucePrevent
Thesis: Real-time Prevention of Factual Hallucinations in Retrieval-Augmented Generation
Base decoder: jhu-clsp/ettin-decoder-68m (Ettin paper)
Custom generate method: lebe1/lettuceprevent-generate
Overview
LettucePrevent is a 68M-parameter decoder with a token-classification head, fine-tuned to flag hallucinated tokens in RAG answers while they are being generated, rather than after the fact.
Existing hallucination detection models are encoder-based and bidirectional: they are trained on complete answers but must operate on incomplete prefixes when placed inside a decoding loop. This mismatch motivates a causal architecture. A decoder processes its input left-to-right, so the prediction at position i is exactly the prediction it would emit if invoked on the prefix ending at i-1 during streaming inference.
The model is designed to be called at every decoding step of a host LLM, where its per-token
hallucination probability is used to penalise candidate tokens in the logits distribution
before the next token is sampled. Parameter count is matched to
tinylettuce-ettin-68m-en so
that detection quality and runtime can be compared on equal footing.
Model Details
- Architecture: Ettin decoder (68M) backbone + dropout (p=0.1) + linear token-classification head (2 output units)
- Task: token classification (0 = supported, 1 = hallucinated)
- Input format:
[CLS] context [SEP] query [SEP] answer [SEP] - Max sequence length: 4096
- Recommended confidence threshold: 0.8 (calibrated, see Results)
- Language: English
- License: MIT
Note: the model uses a custom EttinTokenClassifier wrapper (see Usage) and cannot be loaded
directly with AutoModelForTokenClassification.
Training Data
- RAGTruth (English), span-level annotations, via the
wandb/RAGTruth-processedrelease - Fine-tuning uses the training split only (2,514 context/query pairs); the 450-pair test split is held out for all downstream evaluation
Labels are derived by projecting the character-level span mask onto the token grid. To match
inference conditions, answers are first segmented with the Llama-3.1-8B tokenizer, each
chunk is decoded back to text and re-tokenized with the Ettin tokenizer, and the hallucination
label is assigned per chunk under an any-overlap rule. Special tokens, context and query
positions are masked with the ignore index -100, so the loss is computed on answer tokens
only.
Training Procedure
Hallucinated tokens make up under 4 % of answer positions, so a class-weighted cross-entropy
loss with inverse-frequency weights w_c = N / (2 * n_c) keeps the minority class from being
collapsed away by the optimiser. Loss is computed with ignore_index=-100.
- Backbone left unfrozen so causal representations adapt to the streaming-prefix regime
- Classification head initialised from N(0, 0.01), bias 0
- Epochs: 6, early stopping with patience 2, evaluation and checkpointing every epoch
- Best checkpoint selected on
eval/f1_binary_class_1 - Gradient checkpointing enabled,
use_cache=False, gradient clipping at max norm 1.0 - Mixed precision (fp16)
- Seed 42 across Python / NumPy / PyTorch / Trainer
- Hardware: NVIDIA A100
Selected configuration from a 12-run W&B grid sweep over learning rate [1e-6, 5e-6, 1e-5],
batch size [4, 8] and weight decay [0.01, 0.05]:
| Hyperparameter | Value |
|---|---|
| Learning rate | 1e-6 |
| Batch size | 4 |
| Weight decay | 0.01 |
| Warmup ratio | 0.1 |
Results
Detector comparison under streaming inference
Evaluated on the RAGTruth test split with per-model threshold calibration. LettucePrevent attains the highest hallucination-class F1 and recall of the candidate pool, at the lowest cumulative runtime.
| Model | Params | Threshold | F1 (class 1) | Runtime (s) |
|---|---|---|---|---|
| lettuceprevent-ettin-decoder-68m | 68M | 0.8 | 0.3974 | 1247.89 |
| lettucedetect-base-modernbert-en-v1 | 149M | 0.6 | 0.3598 | 1989.76 |
| tinylettuce-ettin-68m-en | 68M | 0.5 | 0.3010 | 1411.75 |
These numbers are not comparable to standard full-answer RAGTruth benchmarks: they are measured on incomplete prefixes, which is a strictly harder setting.
Downstream prevention results
Hallucinated spans per text over 450 evaluation prompts, each generator paired with its tuned skip threshold and this detector, run on NVIDIA A100s.
| Generator | Baseline | LettucePrevent | Relative change |
|---|---|---|---|
| Qwen2.5 14B Instruct | 1,808 | 1,741 | −3.71 % |
| Mistral 7B Instruct v0.2 | 2,232 | 2,269 | +1.66 % |
| Llama-2 7B | 2,837 | 2,082 | −26.61 % |
The reduction is substantial on Llama-2-7B and essentially flat on the other two hosts at their tuned operating points. Broad factual prevention remains bounded by detector quality, tokenizer alignment and host variability.
Intended Use and Limitations
- Intended for token-level, generation-time hallucination detection in RAG pipelines, particularly as the signal source for a logits processor that suppresses unsupported tokens.
- Precision on the hallucination class is low by design. The operating point favours recall, since the prevention mechanism can only suppress what the detector flags.
- Invoking the detector at every decoding step adds significant runtime overhead; a skip threshold on the generator's own confidence can shorten the loop on confident steps.
- Trained on English RAGTruth only. Behaviour on other domains, languages or answer styles is untested. RAGTruth dataset is not optimized for fine-tuning on generation-time hallucination detection than rather for post-hoc detection.
- Label alignment assumes a Llama-style tokenizer on the host model; tokenizer mismatch degrades performance.
- Not suitable as a standalone post-hoc detector — for that, use LettuceDetect, which is trained for the full-answer setting.
Usage
Simple approach
The provided custom_generate method lets you easily play around with the whole framework used for the lettuceprevent model.
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "mistralai/Mistral-7B-Instruct-v0.2"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto",
trust_remote_code=True)
# For instruction-tuned models, always apply the chat template
context = "Revenue was 2400 million in 2021 and 3100 million in 2022."
question = "What is the percentage increase in revenue from 2021 to 2022?"
text = tok.apply_chat_template(
[{"role": "user", "content": f"{context}\n{question}"}],
tokenize=False, add_generation_prompt=True)
inputs = tok(text, return_tensors="pt").to(model.device)
out = model.generate(
**inputs,
custom_generate="lebe1/lettuceprevent-generate", # or a local path
trust_remote_code=True,
tokenizer=tok,
input_text=context, # grounding context for the detector
detector_type="lettuceprevent", # or "number"
skip_threshold=0.9, # important parameter, which skips the HDM check based on top-token probability
max_new_tokens=300,
)
print(tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
Complex approach
The model wraps the Ettin backbone in a custom token-classification head, so it is loaded
through the class below rather than AutoModelForTokenClassification.
import torch
import torch.nn as nn
from transformers import AutoTokenizer, AutoModel, AutoConfig, PreTrainedModel
from transformers.modeling_outputs import TokenClassifierOutput
MODEL_ID = "lebe1/lettuceprevent-ettin-decoder-68m-en"
LLAMA_TOKENIZER = "meta-llama/Llama-3.1-8B"
THRESHOLD = 0.8
class EttinTokenClassifier(PreTrainedModel):
def __init__(self, config, num_labels: int = 2):
super().__init__(config)
self.num_labels = num_labels
self.backbone = AutoModel.from_config(config)
self.dropout = nn.Dropout(p=0.1)
self.classifier = nn.Linear(config.hidden_size, num_labels)
# placeholder so the training-time buffer loads cleanly at inference
self.register_buffer("class_weights", torch.ones(num_labels))
self.post_init()
def forward(self, input_ids=None, attention_mask=None, labels=None, **kwargs):
outputs = self.backbone(input_ids=input_ids, attention_mask=attention_mask)
logits = self.classifier(self.dropout(outputs.last_hidden_state))
return TokenClassifierOutput(logits=logits)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
llama_tokenizer = AutoTokenizer.from_pretrained(LLAMA_TOKENIZER)
config = AutoConfig.from_pretrained(MODEL_ID)
model = EttinTokenClassifier.from_pretrained(MODEL_ID, config=config)
model.eval()
def tokenize_via_llama_chunks(text):
"""Force Ettin token boundaries to respect Llama token boundaries,
reproducing the segmentation seen during streaming inference."""
enc = llama_tokenizer(text, add_special_tokens=False)
ids = []
for tok_id in enc["input_ids"]:
chunk = llama_tokenizer.decode([tok_id])
if not chunk:
continue
ids.extend(tokenizer(chunk, add_special_tokens=False)["input_ids"])
return ids
context = (
"Ibuprofen is an NSAID. The typical adult dose is 400-600mg every 6-8 hours, "
"not exceeding 2400mg daily."
)
query = "What is the maximum daily dose of ibuprofen?"
answer = "The maximum daily dose of ibuprofen for adults is 3200mg."
cls_id, sep_id = tokenizer.cls_token_id, tokenizer.sep_token_id
answer_ids = tokenize_via_llama_chunks(answer)
input_ids = (
[cls_id]
+ tokenize_via_llama_chunks(context)
+ [sep_id]
+ tokenize_via_llama_chunks(query)
+ [sep_id]
+ answer_ids
+ [sep_id]
)[:4096]
batch = {
"input_ids": torch.tensor([input_ids]),
"attention_mask": torch.ones(1, len(input_ids), dtype=torch.long),
}
with torch.no_grad():
logits = model(**batch).logits
probs = torch.softmax(logits, dim=-1)[0, :, 1]
# answer tokens start after [CLS] context [SEP] query [SEP]
answer_start = len(input_ids) - len(answer_ids) - 1
answer_probs = probs[answer_start : answer_start + len(answer_ids)]
for tok_id, p in zip(answer_ids, answer_probs):
if p > THRESHOLD:
print(f"{tokenizer.decode([tok_id])!r} p={p:.3f}")
Expected output
'320' p=0.810
'0' p=0.807
'mg' p=0.877
Inside the prevention pipeline
In practice the model is consumed through the HallucinationLogitsProcessor of the
LettucePrevent repository, which scores the top-k
candidate tokens at every decoding step and penalises those predicted to introduce a
hallucination:
python main.py \
--generator-model meta-llama/Llama-3.3-70B-Instruct \
--detector-type lettuceprevent \
--skip-threshold 0.99 \
--n-per-task 20
Citation
@mastersthesis{Beccard:2026,
title = {Real-time Prevention of Factual Hallucinations in Retrieval-Augmented Generation},
author = {Leon Beccard},
school = {Technische Universität Wien},
year = {2026},
url = {https://repositum.tuwien.at/handle/20.500.12708/229242}
}
This work builds directly on LettuceDetect and TinyLettuce:
@misc{Kovacs:2025,
title = {LettuceDetect: A Hallucination Detection Framework for RAG Applications},
author = {Ádám Kovács and Gábor Recski},
year = {2025},
eprint = {2502.17125},
archivePrefix = {arXiv},
primaryClass = {cs.CL},
url = {https://arxiv.org/abs/2502.17125}
}
- Downloads last month
- 158
Model tree for lebe1/lettuceprevent-ettin-decoder-68m-en
Base model
jhu-clsp/ettin-decoder-68m