The AI That Isn’t Allowed to Remember
Building a Qur’an and Hadith AI research tool meant treating the general purpose LLM’s own memory as a liability, not a feature. And three production failures that taught me why every layer of the pipeline around it earns its keep.
Ask a general-purpose chat model to quote a hadith on a specific ruling, and it will usually hand you something that looks right: a plausible book name, a narrator chain, a quote in confident English. Ask it to check its own answer and it will often defend the fabrication just as confidently. That’s not a rare glitch, it’s the model doing exactly what it was trained to do, produce fluent, plausible continuations. The problem is that “plausible” and “real” are unrelated properties, and for a domain where the exact wording of a source is the evidence, a fluent misquote isn’t a stylistic slip. It’s fabricated primary-source material attributed to a real book and a real scholar.
That’s the premise behind Quran AI, a project I’ve spent the past several months building: a research tool over the Qur’an, classical tafsir, and four hadith collections, with one non-negotiable constraint threaded through every layer of it; the model that writes the final answer is never allowed to draw on what it remembers from training. It can only write from what was actually retrieved for the given question, and nothing reaches the reader until that answer has been checked against the stored source text.
It’s important to be clear about what this workaround is addressing, because it only applies to the specific type of model used here. The text‑generation component relies on a general‑purpose LLM trained on a wide, uncurated mix of translations, commentary, and sectarian debate. From the outside, there’s no reliable way to know which part of any confident answer came from which source.
Retrieval and verification are designed to bypass that limitation: they allow the model to produce accurate answers about a defined corpus without relying on its own internal assumptions.
What follows is how the pipeline is actually constructed.
The pipeline, end to end:
- retrieve — Hybrid similarity search across Qur’an, tafsir, and hadith, then expanded along the abrogation and topic graph edges.
- rerank — Two separate, cheap LLM judgment call, one for hadith, one for Qur’an/tafsir, decide which retrieved candidates actually answer the question, independent of the model that will write the answer.
- generate — The answer is composed only from the surviving passages, under explicit citation rules, never from the model’s own training-time recall.
- verify — Every quote and every attribution is checked against the stored source text before the answer is allowed to reach a reader, with up to three rewrite rounds.
This four-stage shape is what I’m calling verified RAG, or VeriRAG, for the rest of this post. Retrieval and reranking do the ordinary job of finding and filtering candidates, but the name is earned by the fourth stage: nothing generated ever reaches a reader until it’s been checked, word for word, against the stored source text. Verification isn’t a quality pass bolted onto a normal RAG pipeline here, it’s the reason the other three stages exist.
Retrieval is a graph walk, not just a similarity score
The retrieval interface in the codebase is described, deliberately, as “vector search + relational filters + graph traversal, behind one call.” Embedding similarity: OpenAI’s text-embedding-3-small over roughly 53,500 rows of Qur’an ayat, tafsir passages, and hadith, indexed with pgvector’s HNSW index in Postgres is the first pass, but it’s not the whole story, because textual similarity and scholarly relevance aren’t the same thing.
Classical scholarship encodes relationships between passages that cosine distance simply can’t see. A verse traditionally read as qualifying or abrogating (naskh) an earlier one won’t necessarily share vocabulary with it. So after the initial similarity search runs, the retriever walks two curated graph edges: one over hand-verified abrogation pairs, one over hand-tagged ayah-to-topic links, pulling in passages that are known to be connected to what was already retrieved, whether or not an embedding would ever have surfaced them on its own.
The reranker that taught me two lessons in one bug
A similarity search doesn’t stop at “relevant”, it stops at “similar,” and those diverge in exactly the cases that matter most. Ask a general question about the ruling on hijab, and a candidate pool of twenty hadith can come back dominated by pilgrimage-specific (Ihram) clothing rules, because they share vocabulary (clothing, covering, modesty) with the general dress question, while actually answering something narrower and different.
The fix is a dedicated relevance judgment, run by its own bare model client rather than the fully-configured one used for answer generation; a cheap, narrow “is this actually on topic” call with no citation formatting or answer-writing instructions anywhere near it. But getting there took two attempts.
A twenty-candidate pool for “what is the ruling on hijab?” was fourteen Ihram-specific clothing hadith and two genuinely general ones, and both a cheap and a stronger model picked the Ihram-specific entries over both clearly on-topic ones.
The first fix asked the model to return an explicit true/false judgment for every single candidate, on the theory that forcing a per-item decision would resist the model anchoring on whichever theme the majority of the pool shared. Reasonable theory but wrong on implementation. That per-index structured-output shape silently failed to deserialize: Spring AI’s structured-output conversion came back with an empty result and no exception, which tripped a null-check fail-open path and let every candidate through completely unfiltered. The bug was invisible until someone checked the debug logs against what actually shipped.
Get Kamran Zafar’s stories in your inbox
Join Medium for free to get updates from this writer.
The shape that’s proven to deserialize reliably through that call is nothing fancier than a plain list of integers, the indices worth keeping. The anti-anchoring idea didn’t get dropped; it moved into the prompt’s wording instead of the response schema:
“Do not let how many excerpts share a theme influence whether any single one of them is relevant; a minority shape can still be the only genuinely relevant excerpt, and a majority shape can be uniformly irrelevant.”
Two separate failure surfaces were tangled up in that one bug: whether the model’s judgment resists anchoring, and whether Spring AI can actually get that judgment back out of the model in the shape you asked for. It’s possible to fix one while quietly breaking the other, and the only way to catch it was to check what actually came back over the wire, not what the code assumed it would.
Verification is where “grounded” actually gets enforced
Retrieval and reranking only guarantee that the right source material reached the model. They don’t guarantee the model quoted it faithfully; paraphrasing under its breath while composing prose is a completely ordinary failure mode for a language model, grounded or not. So before an answer ever reaches a reader, it goes through a verification pass:
- Every citation must carry the exact quoted words beside it. A reference with no real, checkable quote is rejected outright, no exceptions.
- Each quote is checked directly against the stored source text. Paraphrase, dropped clauses, or invented wording get flagged rather than waved through.
- Scholarly commentary is checked against the actual tafsir it’s attributed to, so an interpretation can’t be pinned on Ibn Kathir or al-Tabarsi that they never wrote.
- If verification fails, the answer is rewritten and re-checked, up to three rounds.
The part I’d argue matters most is what happens when all three rounds still can’t produce a verified answer: the app doesn’t ship the best-effort paraphrase anyway. It falls back to the raw, unedited source material. A plain “here’s what we found” instead of a polished paragraph. Making “we can’t verify this” a valid, visible answer (instead of something the pipeline quietly launders into confident prose) is a smaller engineering feat than the retrieval graph, but it’s the decision the whole project’s credibility actually rests on.
Two corpora, kept apart on purpose
Sunni sources (Sahih al-Bukhari, Sahih Muslim, Sunan al-Tirmidhi, and Tafsir Ibn Kathir) and Shia sources (Al-Kafi and Majma al-Bayan) are retrieved and reasoned about separately, then composed into a single answer only at the very end. They’re never pooled into one similarity search and left to embedding scores to decide, by accident, which tradition “wins” by scoring a fraction higher. Ask about one tradition and the answer draws only from it; ask about a point where the traditions differ, and both views are shown side by side, with the system taking no position on which is correct.
One design decision worth calling out: reranking is applied globally across a sect-interleaved candidate pool, not run separately per sect. The pipeline still fetches candidates per sect and interleaves them to guarantee both traditions get a fair share before the reranker sees anything but the actual LLM relevance pass happens once, over the combined list. I considered splitting the reranker itself by sect, but two factors argued against it. First, it simply doubles the LLM calls in the retrieval path, adding cost and latency for every query without a proportional gain, relevance judgment is largely topical, not sectarian, so most of that duplicated work would be redundant. Second, and more importantly, the underlying hadith corpus isn’t evenly distributed across topics per sect, some subjects are far better represented in one tradition’s collections than the other’s. Reranking each sect’s slice in isolation would have graded candidates against a per-sect bar that shifts with how sparse or dense that sect’s material happens to be on a given topic, which in practice produced worse, more inconsistent answers than judging the whole interleaved pool against one consistent standard. In testing, the global-pool approach gave noticeably better answer quality than the per-sect split, so I kept sect-balance enforcement structural (fetch quotas plus a post-rerank floor backfill) and left the relevance judgment itself sect-agnostic.
See the corpus statistics for more details.
- 15,647 Sunni hadith — Bukhari, Muslim & Tirmidhi
- 16,251 Shia hadith — Al-Kafi, 8 volumes
- ~53,500 embedded rows across Qur’an, tafsir & hadith
- 3 of 6 canonical Sunni collections covered so far
Close in raw count, arrived at very differently; three separate Sunni works against one Shia one, which is exactly the kind of asymmetry that gets lost the moment two traditions are treated as a single undifferentiated pool of “hadith.”
Even the translations themselves take the harder route on purpose: rather than reproduce Darussalam’s or WOFIS’s existing copyrighted English translations, each collection’s classical Arabic original is first structurally recovered and validated for real book and hadith numbering, then translated fresh by an AI model and checked against a shared similarity-QA gate before anything ships. Majma al-Bayan’s Shia tafsir needed an extra step, before that its clean Arabic source wasn’t a ready dataset but a site that had to be scraped, and it shipped at 90 of 114 surahs rather than guess at ayah-boundary matches that weren’t confident enough to anchor.
Two bugs, in production, resolved!
Incident — the 40-second retrieval
A benchmark that was only true at one memory size
A local performance report had concluded retrieval was “a rounding error” next to LLM generation time. In production, on a 4 GB box, first-time questions were taking roughly 45 seconds while cached ones were fine; a split that pointed straight at whatever sits in the cold path. Swap, CPU, startup ingestion, and OpenAI-side latency were checked and cleared one by one. DEBUG logging on Spring Retry finally caught it directly: a single 40.5-second silent gap between the question-embedding call and the hadith-reranker call — exactly retrieval’s window, from a class with no logging of its own..
Root cause: pgvector’s HNSW index was running against Postgres’s stock memory settings, never sized for the box it actually shipped on. A single isolated ANN query went from
602ms → 10msafter tuning; end-to-end cold latency fell from~40s → under 1s. A performance number is only valid for the resource envelope it was measured on. And "retrieval is free" was true on a laptop and false on a memory-constrained production box, and nothing in the application code was wrong.
Incident — sect neutrality under load
Balance is a retrieval bug until it becomes a hallucination
Sect balance sounds like a policy decision, treat both traditions as first-class, never let one crowd out the other, but in production it kept surfacing as plumbing instead. The per-sect retrieval query concatenated each tradition’s ranked hadith block by block instead of interleaving them; combined with the LLM reranker’s own tendency to favor earlier indices and a stale post-rerank cap, answers ended up citing Sahih al-Bukhari almost exclusively even when Al-Kafi had equally relevant material sitting in the retrieved pool. The identical shape reappeared one level down once a single sect had more than one collection: the moment Sahih Muslim joined Sahih al-Bukhari on the Sunni side, the same block-concatenation bug now ran within a sect instead of across sects, measured live, five of ten test questions lost an entire tradition to ordering before that fix too..
The sharper version of the same instinct showed up in generation, not retrieval. RULING and POLEMICAL; the two intents built specifically to hold Sunni and Shia positions side by side — were passing citation verification on attribution alone: a book name sitting next to a claim, with no actual quoted text and marker to back it, because the prompt’s own worked examples never demonstrated the real quote-and-marker mechanics they were supposed to illustrate. Worse, on a real answer the model copied the prompt’s own illustrative placeholder straight into its output as if it were genuine evidence: Sahih al-Bukhari 700:9001, a hadith number that exists nowhere in the corpus, because the placeholder looked enough like a real citation to get pattern-matched, despite an explicit instruction never to reuse it.
Fix: the retrieval-side bug needed interleaving, hadith retrieval now splits and interleaves each sect’s budget across its own collections, not just across sects. The generation-side one needed the prompt’s own examples rewritten to show real quote-and-marker usage, with an explicit “also wrong” counterexample for attribution without quoting, and the placeholder itself swapped to
0:0, a number that can never collide with a real citation, so it reads unmistakably as a placeholder rather than something a pattern-matcher might mistake for evidence. Sect neutrality isn't a policy you set once; it's a property that has to be re-verified at every layer that touches ordering, and, as this one showed, every layer that touches wording too.
What “deep research” should mean here
None of this reads like a bigger context window or a more agreeable model, it’s VeriRAG: an architecture that treats retrieval, reranking, generation, and verification as genuinely separate stages with genuinely separate failure modes, each of which has now failed in its own specific way in front of me. And it’s a willingness to let the system answer “I can’t verify this” instead of paraphrasing anyway when the checks don’t pass.
That’s a less flashy definition of “AI research assistant” than a slick chat interface wrapped around a large model. But for a domain where a misquoted hadith isn’t a stylistic error, it’s fabricated evidence attributed to a real book and a real scholar. And I think it’s the only honest one.
Quran-AI is live at https://quranai.au. If you work with these texts professionally and want to stress-test where the verification net still has holes, I’d genuinely like to hear about it.





