Skip to content

SynapCores / Recipe

RAG Citations: Does the Cited Source Actually Say That?

Check every (sentence, cited source) pair in a RAG answer with the built-in NLI model, and in the same SQL query use EMBED to find the source that should have been cited. Flags misattributed, contradicted and unsupported citations

All recipes· ml· 15 minutesintermediateen
Instance: localhost:8080

Opens your running SynapCores (RAG Citations: Does the Cited Source Actually Say That? will be staged for a preview — nothing runs until you click Run). No instance yet? Install free in ~30s.

Share

RAG Citations: Does the Cited Source Actually Say That?

Objective

A RAG answer with [1] [2] markers looks trustworthy. Most citation checkers only confirm that each [n] points to a document that exists. Few check whether that document says what the sentence claims. In practice the common failures are:

  • Misattributed: the claim is true and one of the sources supports it, but the marker points to a different one.
  • Contradicted: the cited source says the opposite.
  • Unsupported: no retrieved source backs the sentence, but it still carries a citation that makes it look sourced.

This recipe checks every (sentence, cited source) pair with the NLI model built into SynapCores. When the cited source doesn't support a sentence, the same SQL query uses EMBED similarity to find a source that does. The output is one row per citation: SUPPORTED, MISATTRIBUTED (with the correct source), CONTRADICTED or UNSUPPORTED. It runs as one query in the engine that holds the sources, with no second service.

Generating the citations in the first place is covered in RAG with Citations. Citation Integrity applies the same check to research papers.

Before you start: NLI is off by default

Without an enabled model, NLI calls fail with Inference error: NLI is disabled; install a model and enable NLI.

synapcores --config /etc/synapcores/gateway.toml pull nli-default
synapcores --config /etc/synapcores/gateway.toml models enable-nli nli-default
# then restart the engine

On Docker, set AIDB_NLI_ENABLED=1 and AIDB_NLI_MODEL=nli-default. GET /v1/ai/nli/models should report ready.

Step 1: The sources the answer may cite

These are the retrieved chunks, numbered the way the answer cites them:

CREATE TABLE IF NOT EXISTS recipe_citesupport_sources (
    id INT PRIMARY KEY,
    title TEXT,
    body TEXT,
    embedding VECTOR(384)
);
INSERT INTO recipe_citesupport_sources (id, title, body, embedding) VALUES
  (1, 'Backup policy',      'Backups run every night at 02:00 UTC and are kept for 35 days.',
      EMBED('Backups run every night at 02:00 UTC and are kept for 35 days.')),
  (2, 'Plans and features', 'Point-in-time recovery is available on the Business plan and above.',
      EMBED('Point-in-time recovery is available on the Business plan and above.')),
  (3, 'Security overview',  'All customer data is encrypted at rest with AES-256.',
      EMBED('All customer data is encrypted at rest with AES-256.')),
  (4, 'Data residency',     'Customer data is stored in the EU (Frankfurt) region by default.',
      EMBED('Customer data is stored in the EU (Frankfurt) region by default.'));

Step 2: The answer, one row per cited sentence

Parse the generated answer into (sentence, [n]) rows. If a sentence cites two sources, it becomes two rows.

CREATE TABLE IF NOT EXISTS recipe_citesupport_claims (
    id INT PRIMARY KEY,
    sentence TEXT,
    cited_source INT,
    embedding VECTOR(384)
);
INSERT INTO recipe_citesupport_claims (id, sentence, cited_source, embedding) VALUES
  (1, 'Backups are kept for 35 days.',                             1,
      EMBED('Backups are kept for 35 days.')),
  (2, 'Your data is encrypted at rest.',                           2,
      EMBED('Your data is encrypted at rest.')),
  (3, 'Point-in-time recovery is included on every plan.',         2,
      EMBED('Point-in-time recovery is included on every plan.')),
  (4, 'By default, customer data is stored in the United States.', 4,
      EMBED('By default, customer data is stored in the United States.')),
  (5, 'Backups are replicated to a second region.',                1,
      EMBED('Backups are replicated to a second region.'));

Claim 1 is correct. Claim 2 is true but cites the wrong source; it should be [3]. Claim 3 overstates source 2. Claim 4 reverses source 4. Claim 5 isn't in any source.

Step 3: Check the cited source only, and see what it misses

The obvious check scores each sentence against the source it cites:

SELECT c.id, c.cited_source, c.sentence,
       ROUND(COSINE_SIMILARITY(c.embedding, s.embedding), 3) AS cited_similarity,
       ROUND(CAST(JSON_EXTRACT(NLI_CLASSIFY('nli-default', s.body, c.sentence),
             '$.scores.entailment') AS DOUBLE), 3) AS cited_entailment,
       ROUND(CAST(JSON_EXTRACT(NLI_CLASSIFY('nli-default', s.body, c.sentence),
             '$.scores.contradiction') AS DOUBLE), 3) AS cited_contradiction
FROM recipe_citesupport_claims c
JOIN recipe_citesupport_sources s ON s.id = c.cited_source
ORDER BY c.id;

Observed on 2.3.1 with nli-default:

id cited sentence similarity entailment contradiction
1 1 Backups are kept for 35 days. 0.870 0.995 0.000
2 2 Your data is encrypted at rest. 0.106 0.000 0.997
3 2 Point-in-time recovery is included on every plan. 0.836 0.004 0.003
4 4 By default, customer data is stored in the United States. 0.808 0.000 1.000
5 1 Backups are replicated to a second region. 0.546 0.001 0.001

Look at claim 2. On contradiction alone, it looks as bad as claim 4 (0.997). But its similarity to the cited source is 0.106: the two texts are about different topics. The NLI model scores pairs of sentences, and it often calls unrelated sentences a "contradiction". Claim 2 is a wrong pointer, not a false statement. Telling those apart needs the similarity score. It also needs a look at the other sources.

Claim 3 is an overclaim ("every plan" vs "Business and above"). The model reads it as neutral, not contradictory: entailment is missing, but nothing contradicts it. Flag on missing entailment, not only on contradiction.

Step 4: One query: classify each citation and suggest the correct source

SELECT c.id AS claim, c.sentence, c.cited_source,
       CASE
         WHEN chk.cited_ent > 0.6 THEN 'SUPPORTED'
         WHEN chk.cited_sim > 0.45 AND chk.cited_con > 0.6 THEN 'CONTRADICTED'
         WHEN alt.better_source IS NOT NULL THEN 'MISATTRIBUTED'
         ELSE 'UNSUPPORTED'
       END AS status,
       alt.better_source
FROM recipe_citesupport_claims c
JOIN (
  SELECT c2.id AS claim_id,
         COSINE_SIMILARITY(c2.embedding, s.embedding) AS cited_sim,
         CAST(JSON_EXTRACT(NLI_CLASSIFY('nli-default', s.body, c2.sentence),
              '$.scores.entailment') AS DOUBLE) AS cited_ent,
         CAST(JSON_EXTRACT(NLI_CLASSIFY('nli-default', s.body, c2.sentence),
              '$.scores.contradiction') AS DOUBLE) AS cited_con
  FROM recipe_citesupport_claims c2
  JOIN recipe_citesupport_sources s ON s.id = c2.cited_source
) chk ON chk.claim_id = c.id
LEFT JOIN (
  SELECT c3.id AS alt_claim_id, MIN(o.id) AS better_source
  FROM recipe_citesupport_claims c3
  CROSS JOIN recipe_citesupport_sources o
  WHERE o.id <> c3.cited_source
    AND COSINE_SIMILARITY(c3.embedding, o.embedding) > 0.45
    AND CAST(JSON_EXTRACT(NLI_CLASSIFY('nli-default', o.body, c3.sentence),
             '$.scores.entailment') AS DOUBLE) > 0.6
  GROUP BY c3.id
) alt ON alt.alt_claim_id = c.id
ORDER BY c.id;

Observed:

claim sentence cited_source status better_source
1 Backups are kept for 35 days. 1 SUPPORTED
2 Your data is encrypted at rest. 2 MISATTRIBUTED 3
3 Point-in-time recovery is included on every plan. 2 UNSUPPORTED
4 By default, customer data is stored in the United States. 4 CONTRADICTED
5 Backups are replicated to a second region. 1 UNSUPPORTED

All five are classified correctly. Claim 2 comes with its fix: source 3 entails it (similarity 0.629, entailment 0.930), so [2] should be [3]. CONTRADICTED is only assigned when the cited source is on topic (similarity above 0.45), so an off-topic citation can't be mislabelled as a contradiction.

Two notes for 2.3.1:

  • Give each derived table's join key a unique name (claim_id and alt_claim_id above). With the same alias in both, 2.3.1 silently joined the LEFT JOIN on the wrong key, and every row showed better_source = 3.
  • Put similarity filters in WHERE over a CROSS JOIN. In JOIN … ON they returned no rows on that build.

Step 5: Turn the result into an action

status What to do
SUPPORTED Keep the citation
MISATTRIBUTED Rewrite [n] to better_source automatically, or show both
CONTRADICTED Remove the sentence or regenerate it; never show it with the citation
UNSUPPORTED Remove the citation marker and caveat the sentence, or drop it

The thresholds come from the observed scores. Supported pairs entailed at 0.930 or higher, and the unsupported ones at 0.004 or lower. The off-topic citation had similarity 0.106, while the on-topic ones were 0.546 or higher. Re-run Step 3 on a sample of your own answers before fixing the numbers.

Cleanup (Optional)

DROP TABLE IF EXISTS recipe_citesupport_sources;
DROP TABLE IF EXISTS recipe_citesupport_claims;

Honest boundaries

  • Support, not truth. SUPPORTED means the cited text entails the sentence. If the source itself is wrong, the check passes.
  • Chunk-level. The check reads the retrieved chunk, not the whole document. A claim supported three paragraphs away will look UNSUPPORTED, so cite at the chunk level you retrieve at.
  • One source per row. A sentence that combines facts from two sources ("kept 35 days and encrypted with AES-256") won't be entailed by either source alone. Split compound sentences before checking.
  • Overclaims look neutral, not contradictory, and numbers aren't compared as numbers. Check quantities and dates exactly.
  • English model. NLI is off by default, so enable it as described above.

Key Concepts Learned

  • Checking that a citation exists isn't enough. The real question is whether the cited text entails the sentence (premise = source, hypothesis = sentence).
  • A high contradiction score with low similarity means a wrong pointer, not a false claim. Using similarity and NLI together tells you which.
  • One query checks the cited source and searches the other sources for a better one. That needs the embeddings, the NLI model and the join in the same engine.

Tags

nliragrag-citationscitation-checkcitation-hallucinationnli-classifyembedvector-searchgroundingsql

Run this on your own machine

Install SynapCores Community Edition free, paste the SQL or Cypher above into the bundled web UI, and watch it run.

Download Free CE