Skip to main content

Citation Verification

LLMs hallucinate citations: they reference papers that do not exist, quote sources unrelated to the claim, or synthesize plausible facts the underlying retrieval never returned. Longer answers compound the failure rate.

AgentOS attaches per-claim verdicts to every generation behind a single flag. Each statement gets decomposed into an atomic claim, embedded against the sources the agent retrieved, scored on cosine similarity, and tagged with one of four verdicts: supported, weak, unverifiable, or contradicted. Optional NLI promotes contradictions where the source disagrees with the claim. Optional web-search fallback rescues unverifiable claims that could have been answered from general knowledge.

The full implementation lives in packages/agentos/src/cognition/rag/citation/. The runtime ships CitationVerifier, the VerifiedResponse and VerificationSource types, and the formatVerifiedResponse helper.

Citation verification flow: a generated answer has its claims extracted; each claim is matched against retrieved chunks and scored by an NLI judge; verdicts (ENTAILED, NEUTRAL, CONTRADICTED) are stamped on the verified answer with citation markers, and contradictions get flagged for revision

The one-flag path

For most use cases you never touch the verifier directly. Configure verifyCitations on the agent and the per-claim verdicts land on result.grounding automatically:

import { agent } from '@framers/agentos';

const docsAgent = agent({
model: 'openai:gpt-4o',
verifyCitations: {
embedFn: (texts) => embeddingManager.embedBatch(texts),
retrieve: (query) => retriever.search(query),
},
});

const result = await docsAgent.generate('How do I configure a guardrail?');

console.log(result.text);
console.log(result.grounding?.overallGrounded); // single boolean — safe to ship?
for (const claim of result.grounding?.claims ?? []) {
if (claim.verdict !== 'supported') console.warn(claim);
}

The agent retrieves sources for each user input, generates the response, and runs CitationVerifier over the text+sources pair on the way out. Same flag works on QueryRouter (verifyCitations: true there reuses the router's existing retrieval and embedding paths).

Reach for CitationVerifier directly only when you already own both sides — generated text in one place and source chunks in another — and want to run scoring without an agent in the loop.

Architecture

Agent generates response with sources
→ ClaimExtractor: split into atomic claims (grounding-guard ext)
→ Batch embed: claims[] + sources[] (one API call) (your embedFn)
→ Cosine similarity matrix: claims × sources (CitationVerifier)
→ Per-claim verdict: supported / weak / unverifiable / contradicted
→ Optional: NLI contradiction check (grounding-guard nliFn)
→ Optional: web search fallback (verify_citations) (citation-verifier ext)
→ VerifiedResponse with per-claim verdicts (citation/types.ts)

Each step maps to a real source surface:

Core API

CitationVerifier

Source: packages/agentos/src/cognition/rag/citation/CitationVerifier.ts.

import { CitationVerifier } from '@framers/agentos';

const verifier = new CitationVerifier({
// Required: batch embedding function
embedFn: async (texts: string[]) => embeddingManager.embedBatch(texts),

// Optional: similarity thresholds (defaults shown)
supportThreshold: 0.6, // >= this = "supported"
unverifiableThreshold: 0.3, // < this = "unverifiable"

// Optional: NLI for contradiction detection (from grounding-guard)
nliFn: async (premise, hypothesis) => nliModel.predict(premise, hypothesis),

// Optional: custom claim extractor (from grounding-guard)
extractClaims: async (text) => claimExtractor.extract(text),
});

Verify Claims

verify() accepts the input in two shapes — pick whichever matches what you already have on the calling side.

Pattern A — pass raw LLM text and let the verifier decompose:

// Use this when the input is one block of LLM-generated prose and you
// want the verifier to handle decomposition (LLM-driven if you've
// configured `extractClaims`, otherwise built-in sentence splitting).
const result = await verifier.verify(
"Tokyo is the capital of Japan. " +
"Tokyo proper has roughly 14 million residents. " +
"Tokyo hosted the 2020 Summer Olympics in 1457.",
[
{ content: "Tokyo is the capital and seat of government of Japan.", url: "https://example.com/japan" },
{ content: "The population of Tokyo proper is approximately 14 million.", url: "https://example.com/tokyo" },
]
);

Pattern B — pass a pre-decomposed claim array directly:

// Use this when you've already extracted the claims yourself (your own
// parser, an NER step, a user-edited list, etc.) and want each item
// scored as-is without any further decomposition. The verifier preserves
// caller-provided order in result.claims.
const claims = [
"Tokyo is the capital of Japan.",
"Tokyo proper has roughly 14 million residents.",
"Tokyo hosted the 2020 Summer Olympics in 1457.",
];

const result = await verifier.verify(claims, [
{ content: "Tokyo is the capital and seat of government of Japan.", url: "https://example.com/japan" },
{ content: "The population of Tokyo proper is approximately 14 million.", url: "https://example.com/tokyo" },
]);

Or inspect the extracted claims before verifying:

// extractClaims() exposes the same decomposition Pattern A uses
// internally, so you can filter or edit the claim list before scoring.
const claims = await verifier.extractClaims(llmGeneratedText);
const filtered = claims.filter((c) => c.length > 20); // skip stubs
const result = await verifier.verify(filtered, sources);

VerifiedResponse

Source: packages/agentos/src/cognition/rag/citation/types.ts.

{
claims: [
{
text: "Tokyo is the capital of Japan.",
verdict: "supported", // matches source 0
confidence: 0.87, // cosine similarity
sourceIndex: 0,
sourceSnippet: "Tokyo is the capital and seat of government of Japan.",
sourceRef: "https://example.com/japan",
},
{
text: "Tokyo proper has roughly 14 million residents.",
verdict: "supported", // matches source 1
confidence: 0.83,
sourceIndex: 1,
sourceSnippet: "The population of Tokyo proper is approximately 14 million.",
sourceRef: "https://example.com/tokyo",
},
{
text: "Tokyo hosted the 2020 Summer Olympics in 1457.",
verdict: "unverifiable", // no source covers this claim
confidence: 0.12,
},
],
overallGrounded: true, // no contradictions
supportedRatio: 0.67, // 2 of 3 claims fully supported
totalClaims: 3,
supportedCount: 2,
weakCount: 0,
unverifiableCount: 1,
contradictedCount: 0,
}

For a one-line human summary ("2/3 claims verified (67%)"), import the formatVerifiedResponse helper:

import { formatVerifiedResponse } from '@framers/agentos';
console.log(formatVerifiedResponse(result));

Verdicts

VerdictCosine SimilarityMeaning
supported>= 0.6Claim semantically matches a source
weak0.3 - 0.6Partial match, lower confidence
unverifiable< 0.3No source matches this claim
contradictedN/A (NLI)NLI model detects contradiction with source

When Verification Runs

QueryRouter Integration

The default QueryRouter runtime can now trigger CitationVerifier automatically when verifyCitations: true is set.

Verification runs only when both of these conditions are true:

  • route() retrieved source chunks to verify against
  • the router has an active embedding path available for similarity scoring

When verification runs successfully, the result is attached to QueryResult.grounding. If either prerequisite is missing, QueryRouter skips verification gracefully.

Outside QueryRouter, you can still use the verifier directly in host-managed flows:

Deep Research Synthesis

Deep Research emits a synthesized report with inline citations to every source it discovered during search and extraction. The verifier closes the loop on that report by decomposing it into atomic claims and scoring each against the retrieved sources — turning "the model cited it" into "the source actually says it."

Two integration paths:

  • Automatic via QueryRouter — set verifyCitations: true and the router attaches per-claim verdicts to QueryResult.grounding whenever the route retrieves source chunks.
  • Host-managed — call CitationVerifier.verify(report, sources) directly after deep_research resolves, mapping the engine's extracted source content into the VerificationSource shape.

On-Demand (via Tool)

Agents can call verify_citations explicitly:

verify_citations({
text: "The speed of light is 300,000 km/s in a vacuum.",
sources: [
{ content: "Light travels at 299,792 km/s in vacuum.", title: "Physics Reference" }
],
webFallback: true // search web if sources don't match
})

Via Skill

The fact-grounding skill instructs the agent to:

  1. Verify key factual claims before presenting to the user
  2. Mark unverified claims with "[unverified]"
  3. Cite sources inline: "According to [Source Title]..."
  4. Flag contradictions with both sides presented

Web Fallback

When webFallback: true, unverifiable claims are checked via the fact_check tool (web search):

Claim: "Mars has two moons"
→ No matching source in provided documents
→ Web search: "Mars has two moons fact check"
→ Verdict: TRUE (multiple sources confirm)
→ Updated to "supported" with webVerified: true

Requires a search API key (SERPER_API_KEY, TAVILY_API_KEY, or BRAVE_API_KEY).

Claim Extraction

Claims are extracted using sentence splitting by default:

  1. Strip code blocks
  2. Split on sentence boundaries (. , ! , ? )
  3. Filter: remove questions, hedging ("I think..."), meta-text ("Let me know...")
  4. Keep claims longer than 15 characters

For higher quality extraction, install @framers/agentos-ext-grounding-guard — its ClaimExtractor provides LLM-based claim decomposition that handles complex sentences with multiple assertions.

Performance

  • Embedding cost: One batch call per verification (claims + sources)
  • Compute: Cosine similarity matrix is O(claims × sources) — sub-millisecond for typical sizes
  • Total overhead: ~50ms for 5 claims × 5 sources (excluding embedding latency)
  • No LLM calls unless NLI or web fallback is triggered

Source Files

SymbolRepoPath
CitationVerifierframersai/agentossrc/cognition/rag/citation/CitationVerifier.ts
VerifiedResponse, VerificationSource, ClaimVerdict (types)framersai/agentossrc/cognition/rag/citation/types.ts
formatVerifiedResponseframersai/agentossrc/cognition/rag/citation/format.ts
Citation tree (re-exports)framersai/agentossrc/cognition/rag/citation/
QueryRouterframersai/agentossrc/orchestration/pipeline/query/QueryRouter.ts
ClaimExtractorframersai/agentos-ext-grounding-guardsrc/ClaimExtractor.ts
Grounding Guard package rootframersai/agentos-ext-grounding-guard(root)
verify_citations tool (VerifyCitationsTool)framersai/agentos-extensionsregistry/curated/research/citation-verifier/src/VerifyCitationsTool.ts
fact_check toolframersai/agentos-extensionsregistry/curated/research/web-search/src/tools/factCheck.ts
fact-grounding skill (SKILL.md)framersai/agentos-skillsregistry/curated/fact-grounding/SKILL.md