Agent Architecture7 min readยท

How to Build an AI Agent That Cites Sources Accurately

Your agent answers a customer question with total confidence, complete with a citation. The citation points to a document that says the opposite, or to nothing at all.

Your agent answers a customer question with total confidence, complete with a citation. The citation points to a document that says the opposite, or to nothing at all. That is the failure mode that erodes trust fastest, because a wrong answer with a fake source is worse than an honest "I don't know." And most teams reach for the wrong fix: a bigger model, a better prompt, a longer system message.

Sanity Context (previously Agent Context) starts from a different premise. Hallucination usually means retrieval returned nothing useful and the model filled the gap, so accurate citation is a retrieval and structured-data problem before it is a model problem. Sanity is the Content Operating System for the AI era, the intelligent backend for companies building AI content operations at scale, and it treats the retrieval layer, not the prompt, as the place citation accuracy is won or lost.

This guide walks the architecture end to end: why vector search alone leaves your agent guessing, how hybrid retrieval fixes it, why tools should return objects instead of prose, and how governance keeps citations trustworthy as your content changes underneath the agent.

Citation accuracy is a retrieval problem, not a model problem

The instinct when an agent cites the wrong source is to blame the model. That instinct is almost always wrong. In production, hallucination usually means retrieval returned nothing useful and the model filled the gap. Empty retrieval is the structural ceiling of your retrieval layer, and it is the single most common failure teams see. When the retrieval step returns the right document, the model quotes it; when the retrieval step comes back empty or wrong, no amount of prompt engineering rescues the answer, because the model is reasoning over an absence.

This reframing matters because it tells you where to spend effort. Each failure tag points to a layer, and the fix is at that layer, not in the model. A citation that points to nothing is a signal that your predicates or your ranking missed the source, not that the model needs more parameters. The discipline is to treat every miscitation as a retrieval diagnostic: what did the agent query, what came back, and why did the correct document not surface?

Sanity Context builds on this premise directly. It gives agents structured, governed access to your content through the Content Lake, so the question stops being "how do we stop the model from making things up" and becomes "how do we guarantee the retrieval step returns the exact source the answer should quote." That is a solvable engineering problem, and it is the problem the rest of this guide is about.

Illustration for How to Build an AI Agent That Cites Sources Accurately
Illustration for How to Build an AI Agent That Cites Sources Accurately

Why vector search alone fails on the details that need citing

Vector search is genuinely good at semantic recall. It is genuinely bad at the constraints that a citable answer depends on. A real query carries a structural component, a product feature, a version number, a category, or "in stock," that pure vector retrieval cannot resolve, because vector similarity does not respect any of those constraints. Ask for the spec on version 4.2 and a vector index will happily hand you version 3.9 because the two documents are semantically almost identical. The answer comes back wrong, and the citation is confidently attached to the wrong record.

The deeper problem is structure that embeddings flatten: counter-intuitive field names, second-order reference chains, empty-array data-quality issues. These are the places retrieval fails more than teams expect, and none of them is a model problem. It is a context problem. When Sanity ran schema exploration against Sonos's catalog, teaching the retrieval step the shape of the data rather than just its types, accuracy landed around 83% on a mix of difficulties (using Sonnet 4.5, about 40 seconds of thinking per hard question). The lift came from understanding the schema, not from a stronger model.

Production data reinforces the point. When you look at how agents actually call the Sanity Context MCP endpoint, the heavy majority of calls are structured GROQ queries and schema lookups, with semantic search a small slice. Embeddings are opt-in and off by default. As the team puts it plainly: vector search and RAG are not the same as good retrieval, and "we have embeddings" is not a retrieval strategy.

Hybrid retrieval: the architecture that actually cites correctly

Accurate citation comes from combining three retrieval layers, not from picking a favorite. Keyword search (BM25) handles literal matches, embeddings handle semantic ranking, and structured predicates handle the filters that have to hold. The evidence that no single layer suffices is quantitative. Anthropic's contextual retrieval research measured that contextual embeddings cut top-20 retrieval failures by 35%, adding contextual BM25 took that to 49%, and adding reranking on top brought it to 67%. The shape of that improvement holds whether you read the paper closely or just notice that none of the three layers alone was enough.

In Sanity, this is not three systems stitched together. It is one GROQ query. Structured predicates filter first, so the version number, the category, and the "in stock" constraint are guaranteed to hold. Then a score() pipeline blends a BM25 match() (with boost() weighting title hits 2x, because a title match matters more) and text::semanticSimilarity() across the document, ordered by _score. The result is a small, ranked list that satisfies both the structural constraints and the semantic vibe the user described, which is exactly the list an agent needs to cite from.

The query reads roughly as: filter on the predicates that must hold, then score(boost([title] match text::query($queryText), 2), text::semanticSimilarity($queryText)) ordered by _score, top ten. Because the filtering and the ranking live in the same query against the Content Lake, the agent never has to reconcile a filtered set from one system with a ranked set from another. That reconciliation gap is where a lot of miscitations hide.

Return structured objects, not prose, so the model can quote not paraphrase

Even with perfect retrieval, you can still lose citation accuracy in the last step: how the tool hands data back to the model. A tool that returns prose forces the model to paraphrase, and paraphrasing is where facts go to die. When agents were built against the Sanity Context MCP endpoint, a clear pattern emerged. The ones that worked returned schema-shaped responses the model could pass straight through. The ones that struggled got a wall of text back and re-narrated it, badly, introducing errors that had nothing to do with retrieval and everything to do with format.

The rule is concrete. If your agent is supposed to return three products, the tool should return three product objects, each with its fields and its source ID, not a paragraph describing them. Structured objects give the model something to quote verbatim and something to attach a citation to: the document ID travels with the data, so the answer can point back to the exact record it came from. Prose severs that link, because once the model rewrites a fact in its own words, the provenance is gone.

This is why the shape of the retrieval response, not just its accuracy, determines whether citations survive to the final answer. GROQ returns typed, schema-shaped results by construction, so the agent receives objects with stable identifiers rather than a summary it has to trust and reword. Designing tools to preserve that structure end to end is the difference between an agent that quotes a source and one that vaguely gestures at one.

Freshness: why a citation that was right yesterday is wrong today

A citation is only as accurate as the moment the index was last updated. When a product description updates, when a price changes, when an article publishes, when a record is deleted, the index has to know. If it does not, your agent will confidently cite a price that changed last week or a document that was deleted, and it will do so with a source link that used to be correct. Stale retrieval is a slow-burning version of the empty-retrieval problem, and it is harder to catch because the answer looks plausible.

With a separate vector database plus glue code, freshness becomes a permanent line item on your roadmap: incremental indexing, re-embedding on change, deletion handling, and backfill are all code your team writes and maintains forever. Every content model change is a migration in two places. This is the tax that vector-DB stacks and self-built RAG pipelines quietly carry, and it grows with your catalog.

Content Lake handles this for you. Because dataset embeddings are tied to the content itself, updates propagate within minutes and there is no separate vector pipeline to keep in sync. The retrieval path the agent queries and the store your editors publish to are the same store, so a deletion is a deletion and a price change is a price change everywhere at once. Freshness stops being something you maintain and becomes a property of the backend, which is the entire reason to run retrieval inside the content operating system rather than beside it.

Governance: keeping citations trustworthy as content and agents change

Citation accuracy is not a one-time build; it degrades unless it is governed. Two things change underneath a live agent: the content it retrieves, and the instructions that shape how it answers. Both need to be reviewable, stageable, and auditable, or your carefully cited agent drifts. This is where treating agent behavior as content, rather than as buried configuration, pays off.

In Sanity Context, the agent's static instructions and system prompt live as Sanity documents. As Nearform put it, "Storing the system prompt in a Sanity document is genuinely useful. Editors tuned the agent's voice without any code changes." You stage agent behavior with Content Releases the same way you stage your website: drafts, scheduling, history, permission gating, and audit trails, so you can preview a change to how the agent cites before it reaches a customer. Knowledge Bases turn messy sources, Sanity datasets, support databases, websites, and PDFs, into agent-readable documents that share the same retrieval path, so everything the agent can cite is governed the same way.

There is a diagnostic payoff too. Because conversation scores live next to the source content the agent queries, a reviewer's notes can reference the exact failed conversation and the documents the agent should have retrieved. When a citation is wrong, you can trace it to the query, the result set, and the correct source in one place. For enterprise buyers, the governance surface also carries the compliance story: SOC 2 Type II, GDPR, and regional data residency. That closed loop, govern the instructions, stage the behavior, and review against the real source, is how citation accuracy holds up over months rather than a demo.

Retrieval and citation architecture: Sanity Context vs. common stacks

FeatureSanityPinecone (+ glue)Contentful (+ external search)pgvector / Postgres
Hybrid retrieval (keyword + semantic + filters)Native: predicates filter, then score() blends match() with boost() and text::semanticSimilarity() in one GROQ query, ordered by _score.Hybrid via a metadata filter layer, but you can't pure-vector your way out of the empty-result problem; blending logic is yours to build.Structured content plus an external search service; hybrid ranking is assembled outside the content store rather than native to it.Structured-plus-vector search with pgvector and full-text is genuinely possible; you write the blending and ranking SQL yourself.
Index freshness on content changeHandled by Content Lake; dataset embeddings are tied to content, so updates and deletions propagate within minutes with no separate pipeline.Re-embedding on change, deletion handling, and backfill are glue code your team owns; freshness is a permanent roadmap line item.Content changes must be re-synced to the external search index; incremental indexing is an integration you build and maintain.You own incremental indexing, re-embedding, and deletion handling; every content model change is a migration you write.
Retrieval and content in one storeRetrieval path and the store editors publish to are the same Content Lake, so a deletion is a deletion everywhere at once.Vector index sits beside your content backend; two systems to keep in sync and to reconcile at query time.Search lives outside the CMS; you reconcile a filtered set from one system with a ranked set from another.Content and vectors can co-locate in Postgres, but it is not your content backend, so a separate publishing store still syncs in.
Structured objects with source IDs for citationGROQ returns typed, schema-shaped objects with stable document IDs by construction, so the model quotes rather than paraphrases.Returns vectors and metadata; assembling citable objects with authoritative source IDs is application code you write.Returns content entries via API; source identity is present, but shaping tool output for the model is on your integration layer.Rows carry IDs, but shaping them into schema-shaped tool responses the model can pass through is your query and API work.
Agent instructions as governed contentSystem prompt lives as a Sanity document; staged with Content Releases (drafts, history, permission gating, audit trails).No content governance layer; prompts and agent config live in code or a separate tool outside the data store.Can model a prompt as an entry, but staging agent behavior and conversation review are not part of the platform.Prompt and agent governance are entirely application-side; no built-in staging, review, or audit for agent behavior.
Diagnosing a wrong citationConversation scores live next to the source content, so a reviewer can reference the exact failed conversation and the missed documents.Query logs and eval tooling are separate systems; correlating a bad answer to the missed source is a manual join.No native conversation-to-source diagnostic; tracing a miscitation spans the CMS, the search layer, and the agent app.You build logging and eval yourself; linking a failed answer to the document that should have surfaced is bespoke.