RAG hallucination detection with a self-healing layer

Abstract 3D neural network nodes illustrating RAG hallucination detection

Retrieval-Augmented Generation gets sold as the fix for factual accuracy. It is not. Without a real strategy for RAG hallucination detection, what you ship is a more confident class of lies. I have watched production systems retrieve exactly the right document and then hallucinate a price or a citation anyway.

So I built a self-healing layer, which was not on my roadmap. Working on an assistant for my own platform, it became clear that “grounding” is a suggestion as far as the model is concerned: it will read “14 days” and write “30 days” without blinking. Anything high-stakes needs a pipeline that catches, scores and repairs those errors before the response leaves your server.

Five patterns in RAG hallucination detection

Five patterns kept turning up in the production failures I looked at. They behave less like bugs and more like structural properties of how attention drifts:

  • Numeric contradictions, where the context says $49 and the answer says $99.
  • Fake citations, invented arXiv IDs or person names that appear nowhere in the source.
  • Negation flips, turning a “does not support” into a “supports.”
  • Answer drift, where consistency degrades quietly over time.
  • Unfaithful confidence, an authoritative tone with no grounding in the data at all.

If any of that looks familiar, my earlier post on RAG conflict resolution covers why retrieval on its own does not get you there.

The detect, score, heal pipeline

The whole thing has to fit inside a normal FastAPI request budget, which ruled out an external LLM judge and any extra API calls. I gave RAG hallucination detection a budget of 50ms. spaCy handles named entity recognition, regex pulls out the numbers, and a small SQLite monitor tracks drift.

def bbioon_faithfulness_scorer(claim: str, context_lower: str) -> bool:
    # A claim is grounded if at least 40% of its keywords appear in the source.
    kw = _extract_key_words(claim)
    if not kw:
        return True
    overlap = sum(1 for w in kw if w in context_lower) / len(kw)
    return overlap >= 0.40

Catching a bluff needs a separate signal. The ConfidenceScorer weighs overconfident language, words like “definitely” or “guaranteed”, against uncertainty markers. High confidence sitting next to low faithfulness raises a critical risk flag.

Building the self-healing layer

Detection on its own does nothing for the user. The healing side tries three deterministic fix strategies, then re-inspects the patched answer before it goes out. If it still fails, the endpoint returns a safe decline instead.

The strategy that earns its keep is the contradiction patch. On a numeric mismatch it swaps the hallucinated value for the verified one from the context, and it normalizes the language around it, so “monthly” becomes “annual” when the price it replaced was an annual rate.

Messier failures, like context that never got retrieved in the first place, are a context engineering problem instead.

The drift monitor I almost cut

I nearly dropped the drift monitor from the first version as overkill. Then a pricing index rebuild in staging made one endpoint return different values for the same SKU. Not one faithfulness check fired, because the retrieval was technically correct, yet the answers no longer matched what the system had been saying for weeks. The history lived in SQLite rather than an in-memory dictionary, so it survived the rolling restarts and the divergence showed up.

# The test that caught a real mistake during staging
def test_persistence_across_instances(self, tmp_path):
    db_file = str(tmp_path / "drift.db")
    mon1 = AnswerDriftMonitor(db_path=db_file)
    for _ in range(5):
        mon1.record(question, stable_answer)

    mon2 = AnswerDriftMonitor(db_path=db_file)  # fresh instance
    detected, delta = mon2.record(question, drifted_answer)
    assert detected  # History is still there

If RAG hallucination detection is eating your dev hours, I can take it on. I have been wrestling with WordPress and AI integrations since the 4.x days.

Where I landed

Models hallucinate and retrieval fails, so the useful question is how to build a system that does not trust its own output. A 50ms inspection layer built on spaCy and local SQLite persistence gets you a verified answer rather than a hopeful one. For the theory behind all of this, the NeurIPS RAG paper is still the reference.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.