Skip to content

How to Check If an AI Answer Is Actually Backed by Your Documents

Updated October 25, 2026Tested on SynapCores 2.3.2

The questionHow do I check if an AI answer is actually backed by my documents?

Short answer

Check each sentence against the passages it came from. Similarity can't do it: in our test, "Refunds arrive within 24 hours" scored 0.779 similarity to a policy saying "5 to 10 business days". Use similarity only to find on-topic passages. Then use an NLI (natural language inference) model to judge whether each passage supports or contradicts the sentence. NLI misses numbers and can flag unrelated pairs, so compare numbers and dates as typed values and send disputed sentences to a person.

The problem: your AI assistant answers a customer from your own documents. The answer sounds right and cites a real policy. But is every sentence actually in that policy? You need a check you can run on every answer, before the customer sees it.

We build SynapCores. All the numbers below come from our own recipes, with the engine version and models stated for each. The Python section shows how to reproduce the lesson without our engine.

Why a retrieved document doesn't prove the answer

Retrieval-augmented generation (RAG) gives the model the right source to copy from. It doesn't check that the model stayed with that source. The model can reverse a rule, change a number or blend two passages, and the retrieved passage still looks like a match. Most "grounded" checks then compare the answer with that passage by similarity, which is the step that fails below.

The same mistake shows up in other places:

  • RAG groundedness checks that say "the answer is grounded if the top passage scores above X".
  • Agent memory that decides a new fact is "the same as" or "replaces" an old one because they are similar.
  • Duplicate and conflict detection across documents, tickets or witness statements.

In all three, a similarity score is quietly used as if it were evidence.

What NLI is, in one paragraph

An NLI model reads two pieces of text, a premise (a passage from your documents) and a claim (one sentence of the AI answer). It returns three scores that add up to 1: entailment (the passage supports the claim), contradiction (the passage says the opposite) and neutral (the passage doesn't settle it). It is a small model, it runs on a CPU, and it gives the same scores every time. It knows nothing about the world. It only judges the passage you hand it, so it answers "is this backed by my documents?", not "is this true?".

What each signal actually measures

text
                 "Is it on topic?"      "Does the source support it?"    "Is it true?"
                 ─────────────────      ─────────────────────────────    ─────────────
 tool            vector similarity      NLI (entailment/contradiction)   source quality
                                                                         + exact checks
                                                                         + people
 good at         finding candidates     judging a claim vs a passage     —
 blind to        negation, numbers,     topic (unrelated pairs),          —
                 who/when/scope         numbers, scope

The data

Everything below comes from three of our recipes, run with the default 384-dim embedding model and the nli-default NLI model (DeBERTa-v3-small, English):

  • Recipe 037 (RAG hallucination gate, observed on 2.3.1, re-run on 2.3.2-ce): five retrieved refund-policy passages and a six-sentence LLM-style answer about annual-plan refunds.
  • Recipe 036 (agent memory contradiction check, observed on 2.3.1, re-run on 2.3.2-ce): four stored memories about a customer and six incoming facts.
  • Recipe 034 (statement contradiction mining, observed on v2.1.2-ce, re-run on 2.3.2-ce): four witness statements.

Every score quoted below was re-run on 2.3.2-ce (build 563) and came out the same as on the earlier version, except the reworded-passage score in Failure 3.

The passages from recipe 037:

id passage
1 Annual plans can be refunded in full within 30 days of purchase.
2 After 30 days, annual plans are refunded pro rata for the unused full months.
3 Monthly plans are not refundable.
4 Refunds are issued to the original payment method within 5 to 10 business days.
5 The support office is closed on public holidays.

Failure 1: similarity scores a wrong answer as close

Here are four sentences of the generated answer, each with the passage it is most similar to (recipe 037, Step 3, 2.3.1):

sentence best passage similarity correct?
1. "You can get a full refund on an annual plan within 30 days of buying it." 1 0.903 ✔ yes
2. "After that, annual plans cannot be refunded at all." 3 0.736 ✘ no: passage 2 says pro-rata refunds
3. "The refund goes back to your original payment method." 4 0.717 ✔ yes
4. "Refunds arrive within 24 hours." 4 0.779 ✘ no: passage 4 says 5 to 10 business days

A "grounded if similarity > 0.7" rule passes all four, including both wrong sentences. Sentence 4 even outscores sentence 3, which is correct.

Look closer at sentence 2. The passage that contradicts it (passage 2) is its least similar on-topic passage, at 0.561. Passages 3 and 1 score higher (0.736 and 0.727), because they share the words and the topic. Similarity ranks the evidence that would catch the error below evidence that is merely related.

Why: an embedding places texts by topic and vocabulary. "Can be refunded" and "cannot be refunded" are about the same thing, so they land close together. Negation, numbers and scope barely move the vector.

Failure 2: the NLI model calls unrelated sentences contradictions

So use a model that judges support instead. An NLI model takes a premise and a claim and returns scores for entailment, contradiction and neutral. It catches sentence 2 straight away: contradiction 0.999 against passage 2 (recipe 037, Step 3).

But run it on pairs that were never comparable and it still answers confidently. From recipe 036, Step 3 (2.3.1), the incoming fact "The customer is learning to play the cello." against four stored memories:

stored memory similarity contradiction
The customer manages procurement for a mid-size logistics company. 0.228 1.000
The customer prefers to be contacted by email, never by phone. 0.169 0.999
The customer's office is in Denver. 0.235 0.997
The customer is on the Aurora plan, which includes a 14-day return window. 0.132 0.996

The cello "contradicts" everything. Of the nine incoming/memory pairs scored above 0.6 contradiction in that recipe, only three were real conflicts. Recipe 034 shows the same thing with witness statements: "The north parking garage was closed for repairs all week." scored ≈ 0.999 contradiction against two unrelated statements, above both real contradictions (≈ 0.995 and ≈ 0.990) (observed on v2.1.2-ce).

Why: a pairwise NLI model is trained on sentence pairs that were meant to be compared. It was never asked "are these about the same thing?", so it has no way to say "unrelated".

The fix is the thing that failed in Failure 1. Similarity is a poor judge of truth but a good judge of relevance. In recipe 036, real conflicts sat at similarity 0.50–0.74 and false ones at 0.41 or below, so a floor of 0.45 removed every false pair. With the floor in place, all six incoming facts were sorted correctly: three conflicts, one already known, two new (recipe 036, Step 5).

Failure 3: NLI misreads scope

Sentence 1 of the answer ("full refund … within 30 days") is correct. Passage 1 entails it at 0.998. But passage 2 ("after 30 days … pro rata") scores 0.995 contradiction against it (recipe 037, Step 3). The two rules don't conflict: one covers the first 30 days, the other the time after. The model doesn't see that. Rewording passage 2 as "Annual plans cancelled after the first 30 days receive a pro-rata refund." still scored 0.994 (0.990 with "for the unused full months" added; recipe 037, Step 6, re-run on 2.3.2-ce).

If you block every answer with any contradiction, you block correct answers. Recipe 037 handles this by labelling a sentence with both strong entailment and strong contradiction as DISPUTED and sending it to review instead of blocking it.

Failure 4: NLI doesn't compare numbers

Back to sentence 4, "Refunds arrive within 24 hours." The NLI model scores it as entailed by "within 5 to 10 business days" at 0.930 (recipe 037, Step 3). Similarity missed it, and NLI missed it too. It only ended up flagged because passage 1 happened to contradict it (0.977), for an unrelated reason.

Neither tool compares quantities. Extract numbers and dates and compare them as typed values.

What holds up: a layered check

Layer Tool Its job here
1 SQL filters Only passages the user may see, from the right product and version
2 Similarity floor Pair each claim only with on-topic evidence (removes the cello and garage pairs)
3 NLI Supported / contradicted / disputed / unproven, per sentence
4 Exact checks Numbers, dates, amounts compared as values (catches "24 hours")
5 People Review DISPUTED and UNPROVEN; decide which source wins

The thresholds are a trade-off. In our internal tests on agent memory, stricter settings (a higher similarity floor, a higher contradiction threshold, and replacing only the single closest memory) avoided wrongly overwriting good facts, at the cost of missing some real updates. Tune them on your own data, and send the cases you're unsure about to review rather than overwriting.

When this doesn't matter

Similarity on its own is fine when a person reads the results: search boxes, "related articles", recommendations, finding near-duplicate tickets for an agent to merge. It's also fine when the claims are structured: if both sides are rows with typed columns, compare the columns and skip NLI entirely. And if you already run an LLM judge with human review and the cost works at your volume, you may not need an NLI layer.

A fair comparison of checks

Check Catches reversed policy (sentence 2) Catches "24 hours" (sentence 4) Ignores unrelated pairs Same result every run Cost per check
Similarity threshold ✘ ✘ ✔ (that's what it's good at) ✔ Very low
NLI alone ✔ ✘ (0.930 entailment) ✘ ✔ Low; small local model
Similarity floor + NLI ✔ Only by luck here ✔ ✔ Low
LLM judge Usually Often Usually ✘ One LLM call
Exact typed check Only if encoded as a rule ✔ ✔ ✔ Very low; needs extraction
Human review ✔ ✔ ✔ Mostly Highest

The LLM-judge row is a general expectation, not a measurement. We haven't benchmarked LLM judges on this data.

Reproduce it without our database

You can see the same effect with open-source models in Python. The embedding model below is the common 384-dim MiniLM; the NLI model is the cross-encoder our engine bundles as nli-default.

pythonPython 3.9 · sentence-transformers 5.1.2
# pip install "sentence-transformers==5.1.2"
from sentence_transformers import SentenceTransformer, CrossEncoder, util

emb = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
nli = CrossEncoder("cross-encoder/nli-deberta-v3-small")
LABELS = ["contradiction", "entailment", "neutral"]

pairs = [
    ("Refunds are issued to the original payment method within 5 to 10 business days.",
     "Refunds arrive within 24 hours."),
    ("After 30 days, annual plans are refunded pro rata for the unused full months.",
     "After that, annual plans cannot be refunded at all."),
    ("The customer's office is in Denver.",
     "The customer is learning to play the cello."),
]

for premise, claim in pairs:
    sim = util.cos_sim(emb.encode(premise), emb.encode(claim)).item()
    probs = nli.predict([(premise, claim)], apply_softmax=True)[0]
    scores = {l: round(float(p), 3) for l, p in zip(LABELS, probs)}
    print(f"{sim:.3f}  {scores}  | {claim}")

Output (sentence-transformers 5.1.2, torch 2.8.0, transformers 4.57.6, Python 3.9, CPU). The model's label order is contradiction / entailment / neutral (id2label in its config), and the scores match the engine's to three decimals:

textPython 3.9 · sentence-transformers 5.1.2
0.779  {'contradiction': 0.059, 'entailment': 0.93, 'neutral': 0.012}  | Refunds arrive within 24 hours.
0.561  {'contradiction': 0.999, 'entailment': 0.0, 'neutral': 0.001}  | After that, annual plans cannot be refunded at all.
0.235  {'contradiction': 0.997, 'entailment': 0.0, 'neutral': 0.003}  | The customer is learning to play the cello.

The same check in one SQL statement (SynapCores)

On SynapCores the embeddings, the NLI model and the data are in one engine, so the layered check is one query over a whole batch. This is the gated memory check from recipe 036, Step 5: each incoming fact is compared only with memories above the 0.45 similarity floor, NLI_VERIFY judges it against all of them at once, and a LEFT JOIN keeps facts with no related memory as NEW. Table setup is in recipe 036, Steps 1–2.

sqlEngine version 2.3.2
SELECT i.id, i.source,
       CASE g.verdict
         WHEN 'LIE'   THEN 'CONFLICT'
         WHEN 'TRUTH' THEN 'ALREADY_KNOWN'
         ELSE 'NEW'
       END AS status,
       g.conflicts_with
FROM recipe_memcheck_incoming i
LEFT JOIN (
  SELECT c.id AS cand_id,
         JSON_UNQUOTE(JSON_EXTRACT(NLI_VERIFY('nli-default', c.fact, NLI_ANCHORS(m.fact)),
              '$.verdict')) AS verdict,
         JSON_UNQUOTE(JSON_EXTRACT(NLI_VERIFY('nli-default', c.fact, NLI_ANCHORS(m.fact)),
              '$.violated_anchor')) AS conflicts_with
  FROM recipe_memcheck_incoming c
  CROSS JOIN recipe_memcheck_memories m
  WHERE COSINE_SIMILARITY(c.embedding, m.embedding) > 0.45
  GROUP BY c.id, c.fact
) g ON g.cand_id = i.id
ORDER BY i.id;

Output on 2.3.2-ce (build 563), the same as recipe 036 recorded on 2.3.1:

id source status conflicts_with
101 chat CONFLICT The customer prefers to be contacted by email, never by phone.
102 crm-sync ALREADY_KNOWN
103 webinar-tool NEW
104 scraper CONFLICT The customer's office is in Denver.
105 agent-b CONFLICT The customer is on the Aurora plan, which includes a 14-day return window.
106 chat NEW

Without the WHERE COSINE_SIMILARITY(...) > 0.45 line, the same query rejected five of six facts, including the cello and the webinar (recipe 036, Step 4). The numbers check (Failure 4) is not in this query; add it as a typed comparison.

Where to go next

Run it yourself: recipe 037, How to Detect Hallucinations in RAG: a Per-Sentence NLI Gate in SQL, builds layers 2 and 3 end to end on the refund-policy data above, and turns the result into a ship / review / block decision. Recipe 038 checks that each cited passage supports the sentence that cites it.

Frequently asked questions

How do I fact-check an AI answer?
Split the answer into sentences. For each one, find the on-topic passages in your own documents, then ask an NLI model whether any passage supports or contradicts it. Sentences with no support are unproven, not false. Numbers and dates need an exact comparison, because NLI doesn't compare quantities.
How do I stop AI hallucinations?
You can't stop a model from ever writing an unsupported sentence. You can stop it reaching the user: check every sentence against your sources before you show the answer, and ship, send to review or block based on the result.
Why does RAG still hallucinate when it retrieves the right document?
It reduces them by giving the model the right source to copy from. It doesn't check that the model actually stayed with that source. The model can still reverse a rule, change a number or blend two passages, and the retrieved passage will still look like a match.
What similarity threshold means an answer is grounded?
None does. In the example on this page, a 0.7 threshold would pass two wrong sentences out of four. Use similarity only to decide which passages are on topic, then check support separately.
Why does an NLI model call two unrelated sentences a contradiction?
It was trained on sentence pairs that were meant to be compared. It never learned to say "these are about different things". Given "the office is in Denver" and "the customer is learning the cello", it returned 0.997 contradiction. A similarity floor in front of it removes those pairs.
Can NLI check numbers and dates?
Not reliably. The model scored "within 24 hours" as entailed by "within 5 to 10 business days" (0.930). Extract quantities and dates and compare them as typed values in SQL or code.
Do I need a special database to do this?
No. The Python example on this page reproduces the lesson with open-source models. A database that runs embeddings and NLI in SQL lets you apply the check to every row in one query, next to the data, but the idea works anywhere.