Join the conversation

Join the community of Machine Learners and AI enthusiasts.

Sign Up
adaamkoΒ 
posted an update about 5 hours ago
Post
27
πŸ₯¬ LettuceDetect v2 β€” span-level hallucination detection for code, tool output, and structured documents.

Hallucination detectors are trained on document QA, but agents ground their answers in source code, tool output and markdown. On code-agent answers, existing detectors reach 0.17 span-F1 and even 550B zero-shot judges at most 0.22.

We built a unified span-level benchmark β€” 74,285 newly constructed examples (145K+ with RAGTruth and 14-language PsiloQA folded in), every span typed and character-labeled β€” and trained two detectors on it:

πŸ€– KRLabsOrg/lettucedect-v2-qwen-2b β€” generative, typed spans + explanations in one pass, 32K context, **0.689 span-F1** (0.60 on code-agent)
⚑ KRLabsOrg/lettucedect-v2-mmbert-base β€” 307M multilingual encoder for high-throughput setups
🏷️ KRLabsOrg/lettucedect-v2-taxonomy-head β€” types the spans of any binary detector

It also reaches the best reported English PsiloQA IoU (0.724) and 81.8 RAGTruth example-F1, so specializing on code didn't cost general RAG performance.

πŸ“š Dataset: KRLabsOrg/lettucedetect-code-hallucination
πŸ“„ Paper: https://arxiv.org/abs/2607.00895

The models are now integrated natively into vLLM Semantic Router β€” joint blog post on how it works: https://vllm-sr.ai/blog/lettucedetect-v2-generative-hallucination-detection

Span-F1 0.60 on code-agent answers is a real number, and the cheapest way to show that is the floor nobody publishes.

I pulled your verified code-agent test split through the viewer (2,015 rows) and scored the most degenerate detector there is: mark every character of every answer as hallucinated.

It gets char-level span-F1 0.113. Labelled spans cover 5.99% of 3.02M answer characters, so precision is pinned at 0.06 and recall is free. Your 0.60 sits far above that, and above the 0.17 you attribute to existing detectors. The span task is hard and the metric is carrying signal.

The example-level metric is a different animal. Your split is 1,014 hallucinated against 1,001 clean, so "always say hallucinated" scores example-F1 0.669 and the majority class gets 50.3% accuracy. Any example-F1 on this benchmark has to be read against 0.669, not against 0. You led with span-F1, which is the right call.

Two things I hit on the way.

The Usage snippet returns an empty dataset. The card says:

code = ds["train"].filter(lambda r: r["dataset"] == "lettucedetect-code")

Zero rows carry that tag, in all three splits. The live value is lettucedetect-code-agent: 16,319 train, 190 validation, 2,015 test, which is exactly the 18,524 your own Sources table lists. One word.

And that dead tag is not arbitrary. The test.jsonl sitting in the repo root is tagged lettucedetect-code, has 1,670 rows, and is 37.3% hallucinated. The parquet has 2,015 at 50.3%, which is precisely what annotations/REPORT.md describes. So the repo ships two generations of the code test set side by side, load_dataset serves the newer one, and the snippet still speaks the older one's vocabulary. Anyone who downloads the JSONL evaluates on a 13-point different class balance, where the example-F1 floor is 0.543 rather than 0.669.

The verification report is the best thing in this release, for what it is worth. Rubric, per-sample verdicts, and the contested cases with their resolutions is rarer than the model.

Is the root JSONL meant to stay, or is the parquet the one you want people scoring against?

Β·

the parquet (what load_dataset serves) is canonical β€” 2,015 rows at 50.3%, the verified split that annotations/REPORT.md describes. The root JSONLs are an earlier, pre-verification generation; they're now moved to legacy/ to avoid confustion. The usage snippet tag is fixed too. Thanks for noticing :)