Agent Architecture6 min readยท

How to Let a Chatbot Cite Sources With URLs and Snippets

A support chatbot tells a customer that a plan includes SSO, links to a documentation page, and the linked page says nothing of the kind. The answer sounded confident, the citation looked official, and the user trusted both.

A support chatbot tells a customer that a plan includes SSO, links to a documentation page, and the linked page says nothing of the kind. The answer sounded confident, the citation looked official, and the user trusted both. That is the failure mode teams discover the hard way: a chatbot that cites sources is only useful if the URL resolves to the passage that actually grounds the claim, and the snippet quotes text that really exists. Fabricated citations are worse than no citations, because they borrow the authority of a link to launder a hallucination.

Sanity Context (previously Agent Context) is the AI Content Operating System for grounding agents in your real content, an intelligent backend that makes source-cited answers verifiable rather than decorative. The difference is architectural. When retrieval, the canonical URL, and the quoted snippet all come from the same governed content store, a citation stops being a plausible-looking afterthought and becomes a pointer you can click, audit, and trust.

This guide covers how citation actually works end to end: retrieving passages with stable identifiers, carrying URLs and snippets through the generation step, keeping quotes faithful to the source, and governing the whole loop so a stale document never gets cited as current.

Why chatbot citations break, and what a good one requires

Most citation failures are not model failures, they are plumbing failures. A language model asked to "include a source" will happily generate a URL that looks correct, follows your domain's pattern, and points nowhere. It will paraphrase a passage into a "quote" that no document contains. The model is doing exactly what it was trained to do: produce fluent text. Citation accuracy is not a property of fluency, it is a property of the retrieval pipeline that feeds the model.

A trustworthy citation has three parts that must stay bound together. First, the claim in the answer. Second, a resolvable URL that a human or another agent can open. Third, a snippet, a short span of the source text, that literally appears at that URL and supports the claim. Break any one of those bindings and the citation degrades into theater. The common anti-pattern is to retrieve chunks for grounding but throw away their provenance before generation, then ask the model to reconstruct citations from memory. The model cannot cite what it was never handed.

The fix is to treat provenance as a first-class field that travels with every retrieved passage: a stable document identifier, the canonical URL, and the exact text span. This is where the shape of your content store matters. If passages are opaque blobs in a vector index divorced from the content that produced them, reattaching accurate URLs and verbatim snippets becomes a reconciliation job you run after the fact. If passages carry their identifiers natively, citation is just reading fields you already retrieved. This section maps to Sanity's first pillar, model your business: get the content model right and citation becomes a query, not a cleanup step.

Retrieve the passage and its provenance in one query

The cleanest citation pipelines never separate a passage from where it came from. In Sanity Context, retrieval runs over the Content Lake, Sanity's queryable content store, using GROQ. Hybrid retrieval is native inside the Content Lake rather than assembled from a separate vector service: a single GROQ query blends semantic and lexical signals with `text::semanticSimilarity()` for meaning and a BM25 `match()` for exact terms, combined with `score()` and `boost()` to rank the results. Because that query runs against your actual documents, the same projection that returns the matching passage also returns its canonical URL, its document identifier, and the exact text span you want to quote.

That colocation is the whole trick. You are not retrieving a vector and then looking up metadata in a second system that might have drifted out of sync. The URL you cite and the snippet you quote are fields on the same document the semantic score came from. There is no reconciliation step where a chunk ID fails to map back to a live page, which is the usual source of dead citation links.

Dataset embeddings reinforce this. Because embeddings are tied to the content rather than maintained in a parallel pipeline, edits propagate within minutes. When an editor corrects a documentation page, the retrievable passage, its embedding, and the snippet a chatbot would quote all update together. Contrast the bolt-on pattern, where content lives in a CMS, embeddings live in a vector database, and the URL mapping lives in a third place: every write is three writes, and citations rot at the seams between them. Knowledge Bases extend the same retrieval path to websites, PDFs, and support databases, so a citation from a PDF resolves through the same provenance fields as one from a structured document.

Illustration for How to Let a Chatbot Cite Sources With URLs and Snippets
Illustration for How to Let a Chatbot Cite Sources With URLs and Snippets

Carry URLs and snippets through the generation step

Retrieval hands you passages with provenance attached. The generation step has to keep that binding intact and expose it in the output. The reliable pattern is to give the model structured context, not a wall of concatenated text. Each passage goes into the prompt as a discrete unit with an identifier, its text, and its URL, and the model is instructed to cite by referencing those identifiers rather than by writing free-form URLs.

This matters because it makes fabrication mechanically harder. If the model can only cite passage IDs that were actually placed in its context, and your rendering layer resolves those IDs back to the canonical URL and the verbatim snippet, the model never gets the chance to invent a link. It selects from a closed set. The URL and snippet in the final answer are looked up from the retrieved records, not generated. Any citation the model tries to emit that does not correspond to a supplied passage is dropped or flagged before it reaches the user.

For snippets specifically, prefer extracting the supporting span from the retrieved passage over letting the model rewrite it. A faithful citation quotes the source; a paraphrase that drifts even slightly stops being a quote and becomes a new claim wearing quotation marks. When your retrieval already returned the exact text, the snippet is a substring you highlight, not a sentence you regenerate. This maps to Sanity's second pillar, automate everything: the pipeline does the provenance bookkeeping so the model is left to do only what it is good at, composing an answer, while URLs and snippets flow through as data the agent never has to imagine.

Keep quoted snippets faithful and verify before you ship the answer

Even with disciplined prompting, you should not trust that every citation in a generated answer is valid. Treat citation as something to verify, not assume. A lightweight validation pass, running between generation and the user, closes the remaining gap and turns "probably grounded" into "checked."

Three checks catch the vast majority of bad citations. First, resolution: does every cited URL correspond to a passage that was actually in the model's context for this turn? A citation to a document the model never received is a hallucinated link, drop it. Second, faithfulness: does the quoted snippet appear, as a substring or a close normalized match, in the source passage? If the model paraphrased instead of quoting, replace the snippet with the true span or strip the quote marks. Third, support: does the cited passage plausibly back the specific claim it is attached to, rather than merely sharing keywords? This last check is where an entailment model or a second LLM judge earns its cost on high-stakes answers.

Governance is the part teams skip and later regret. A citation is only as current as the document behind it, so the content store, not the model, must be the arbiter of what is citable. In Sanity, editors govern agent behavior in Studio, and Content Releases let them stage changes to citable content the same way they stage a website launch, so a not-yet-published policy page cannot be cited as live. Roles & Permissions and Audit logs record who changed the source of a citation and when. For regulated buyers this is the difference between a demo and a deployable system: Sanity is SOC 2 Type II compliant, supports GDPR and regional data residency, and publishes its sub-processor list, so the content a chatbot cites sits on an auditable foundation rather than an ungoverned scrape.

Design your content model so every answer is citable by construction

The highest-leverage move happens before any query runs: shaping content so that citation is possible at all. Retrieval can only return provenance that exists. If your knowledge lives as monolithic pages with no stable anchors, the best a citation can do is point at the top of a long document and hope the reader finds the relevant paragraph. Precise citation requires precise addressability.

Model content at the granularity you want to cite. Break documentation and policy content into semantically coherent sections, each with a stable identifier and its own canonical URL or anchor. Then a citation resolves not to a page but to the passage, and the snippet is unambiguous. Portable Text, Sanity's structured representation of rich content, keeps this structure intact rather than flattening formatted content into a string, so headings, links, and blocks retain the addressability that citation depends on. The retrieval path can return the exact block that grounds a claim, and the URL can deep-link to it.

This is also where Sanity's shared-foundation differentiator shows up against the silo pattern. When the same governed content model powers your website, your documentation, and your agent's retrieval, a citation from the chatbot points at the same canonical URL a human would land on from search. There is no divergence between what the site says and what the agent cites, because there is one source, not a content copy for humans and an embedded copy for machines. Legacy stacks that scale by adding a separate pipeline for AI end up maintaining two versions of the truth; the shared foundation scales output instead of headcount by making one well-modeled content set serve every reader, human or agent. Get the model right and citation stops being a feature you bolt on, it becomes a property the system has by default.

How citation provenance holds up across grounding stacks

FeatureSanityPineconeContentfulpgvector / Neon
Passage and provenance in one queryNative: one GROQ query returns the matching passage plus its canonical URL and text span from the same document in the Content Lake.Vectors carry metadata you populate, but the URL and canonical content live in a separate system you keep in sync.Content and URLs are native; vector retrieval is external via the App Framework, so provenance is reattached across systems.Store URL and text as columns beside the embedding; workable, but you own the schema, joins, and sync yourself.
Hybrid semantic + lexical rankingNative: text::semanticSimilarity() and BM25 match() blended with score() and boost() in a single GROQ query.Dense vector search with metadata filters; lexical/BM25 blending is assembled with a separate keyword layer.No native vector or hybrid ranking; relies on an external search or vector service wired in.Vector similarity native; combining with Postgres full-text search is possible but hand-built per query.
Embeddings stay fresh on editDataset embeddings are tied to content, so an editor's fix propagates to retrieval and citable snippets within minutes.Re-embedding and re-upsert on every content change is a pipeline you build and operate.Content edits are instant, but external embeddings need a re-sync job to reflect them.Triggers or app code must re-embed on write; freshness is only as good as the job you maintain.
Deep-linkable citation granularityPortable Text keeps blocks addressable, so citations resolve to the exact passage and anchor, not the top of a page.Granularity depends entirely on how you chunked before upserting; no content structure of its own.Structured fields help, but rich text granularity for anchors depends on modeling and the retrieval glue.Granularity is whatever your chunking writes into rows; no native rich-text structure to anchor to.
Governance over what is citableEditors stage citable content in Studio with Content Releases; Roles & Permissions and Audit logs track source changes.No editorial layer; governance of source content lives wherever that content is authored, not in the index.Strong editorial workflow and roles for content; the vector/citation layer sits outside that governance.Governance is database access control plus whatever app you build; no content-editor workflow.
Unstructured sources on the same pathKnowledge Bases turn PDFs, websites, and support databases into agent-readable docs sharing one retrieval path.Ingest anything you first parse and embed yourself; each source type is your own pipeline.External content requires separate ingestion; not unified with the CMS retrieval model.Any source works once you parse and embed it into rows; unification is your integration work.