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.
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:
ClaimExtractorships in the@framers/agentos-ext-grounding-guardpackage.CitationVerifierowns the similarity scoring and aggregation.- The
verify_citationstool and itsfact_checkcompanion live in the extensions registry.
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
| Verdict | Cosine Similarity | Meaning |
|---|---|---|
supported | >= 0.6 | Claim semantically matches a source |
weak | 0.3 - 0.6 | Partial match, lower confidence |
unverifiable | < 0.3 | No source matches this claim |
contradicted | N/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:
- call
CitationVerifierdirectly after generation - call the
verify_citationstool from an agent workflow - wire it into your own host/runtime around
deep_researchsynthesis (see below)
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: trueand the router attaches per-claim verdicts toQueryResult.groundingwhenever the route retrieves source chunks. - Host-managed — call
CitationVerifier.verify(report, sources)directly afterdeep_researchresolves, mapping the engine's extracted source content into theVerificationSourceshape.
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:
- Verify key factual claims before presenting to the user
- Mark unverified claims with "[unverified]"
- Cite sources inline: "According to [Source Title]..."
- 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:
- Strip code blocks
- Split on sentence boundaries (
.,!,?) - Filter: remove questions, hedging ("I think..."), meta-text ("Let me know...")
- 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
| Symbol | Repo | Path |
|---|---|---|
CitationVerifier | framersai/agentos | src/cognition/rag/citation/CitationVerifier.ts |
VerifiedResponse, VerificationSource, ClaimVerdict (types) | framersai/agentos | src/cognition/rag/citation/types.ts |
formatVerifiedResponse | framersai/agentos | src/cognition/rag/citation/format.ts |
| Citation tree (re-exports) | framersai/agentos | src/cognition/rag/citation/ |
QueryRouter | framersai/agentos | src/orchestration/pipeline/query/QueryRouter.ts |
ClaimExtractor | framersai/agentos-ext-grounding-guard | src/ClaimExtractor.ts |
| Grounding Guard package root | framersai/agentos-ext-grounding-guard | (root) |
verify_citations tool (VerifyCitationsTool) | framersai/agentos-extensions | registry/curated/research/citation-verifier/src/VerifyCitationsTool.ts |
fact_check tool | framersai/agentos-extensions | registry/curated/research/web-search/src/tools/factCheck.ts |
fact-grounding skill (SKILL.md) | framersai/agentos-skills | registry/curated/fact-grounding/SKILL.md |
Related Features
- Grounding Guard — real-time NLI streaming verification (guardrail)
- Reranker Chain — multi-stage result ranking before citation
- Deep Research — multi-source research pipeline whose synthesized report feeds directly into the verifier
- Content Policy Rewriter — content filtering guardrail