How to Build a Returns/Refunds Agent Without Hallucinated Policies
A customer asks your support agent whether they can return a final-sale item bought 40 days ago, and the agent confidently invents a 60-day window that your policy never offered. Nobody typed that rule.
A customer asks your support agent whether they can return a final-sale item bought 40 days ago, and the agent confidently invents a 60-day window that your policy never offered. Nobody typed that rule. The model filled a gap because retrieval came back empty, and now you have a promise on the record that Compliance never approved. That is the failure mode that turns a returns/refunds agent from a cost saver into a liability.
Hallucinated policies are almost never a model problem. They are a retrieval problem. When a query carries a structural component, a policy condition, a purchase date, an "in stock" flag, that pure vector similarity cannot resolve, the query comes back wrong and the model either hallucinates or hedges. Sanity Context, the AI Content Operating System surface for grounding agents in structured content, treats this as the intelligent backend problem it actually is: get the retrieval and governance layers right, and the model stops guessing.
This guide walks the harness layer by layer, hybrid retrieval, structured tool responses, and governed prompts, so your refund agent quotes the policy you actually wrote.

Why refund agents hallucinate policies in the first place
The failure almost always has the same shape. A customer question carries a real structural component: a purchase date, a product category, a "final sale" flag, an order status. Pure vector retrieval cannot resolve any of those, because semantic similarity does not respect constraints. "Can I return this?" is semantically close to a dozen policy paragraphs, most of which do not apply to this order. The query comes back empty or wrong, and depending on how the prompt is written, the model either hedges or fabricates a plausible-sounding rule to fill the gap.
This is why swapping in a bigger model rarely helps. The information the agent needed was never retrieved, so a smarter model just hallucinates more fluently. Sanity's own diagnostic map makes the distinction concrete: hallucination usually means retrieval returned nothing useful and the model filled the gap, which is a retrieval problem; a scope violation means the agent answered something the never-say list should have caught, which is a prompt problem; and empty retrieval is the structural ceiling of your retrieval layer, the most common cause of the whole class. Each tag points to a layer, and the fix lives at that layer, not in the model.
Refund policies are unusually dense with structural conditions, eligibility windows, exclusions, category carve-outs, so they are exactly the content where pure similarity search fails hardest. Treating hallucination as a prompting quirk instead of a retrieval gap sends teams down months of prompt-tuning that never closes the hole.
Good retrieval is hybrid, not just embeddings
The fix for empty retrieval is not one better technique, it is three working together. Keyword search (BM25) catches literal matches like a SKU or the exact phrase "final sale." Embeddings handle semantic ranking, so a customer who describes a problem in their own words still surfaces the right policy. Structured predicates enforce the filters that simply have to hold: this order, this date, this category. Drop any one layer and a class of queries starts failing.
Anthropic's contextual retrieval research measured this directly rather than leaving it to intuition. Contextual embeddings alone cut top-20 retrieval failures by 35 percent. Adding contextual BM25 took that to 49 percent. Layering reranking on top brought it to 67 percent. The shape of the result holds whether you read the paper closely or just notice that none of the three layers alone was ever enough.
No single retrieval layer is enough
What hybrid retrieval looks like inside the Content Lake
On Sanity Context, hybrid retrieval is not a stack you assemble. It is a single GROQ query against the Content Lake. The structured predicates do the filtering that has to hold, then a score pipeline blends keyword and semantic signals in one pass. Using the text search operators from the Sanity docs, a product lookup reads:
*[ _type == "product" && category == $category && price < $maxPrice && stockLocation == $warehouse ] | score( boost([title] match text::query($queryText), 2), text::semanticSimilarity($queryText) ) | order(_score desc) [0...10] {...}
The predicates guarantee the results actually satisfy the constraints. The score() pipeline blends a BM25 keyword match on the title, weighted 2x with boost() because title hits matter more, against a text::semanticSimilarity() score across the document, ordered by _score. The result is a small, ranked list that matches both the structural constraints and the vibe of the question. For a refunds agent, that same pattern filters to the eligible policy documents for this order type, then ranks by how well they answer the phrasing.
The quieter win is freshness. Content Lake keeps the search index fresh for you, and because dataset embeddings are tied to the content, edits propagate within minutes. Building incremental indexing, re-embedding on change, deletion handling, eventual-consistency reasoning, and backfill for schema changes yourself is a real project and a whole class of bugs. When retrieval is wired into the content backend, freshness stops being something you maintain. With a separate vector database plus glue code, freshness becomes a permanent line item on your roadmap, right where a stale index quietly starts serving last quarter's return window.
Structured retrieval dominates, and "we have embeddings" is not a strategy
There is a strong instinct to reach for a vector database the moment someone says "RAG." Sanity's own production data argues against leading with it. When you look at how agents actually call the Context MCP endpoint, the heavy majority of calls are structured: GROQ queries and schema lookups, with compressed initial context behind them. Semantic search is a small slice of real traffic.
Embeddings adoption tells the same story. On Sanity Context, embeddings are opt-in and off by default, and most projects shipping on Context MCP never turn them on. That is not because semantic search is useless. It is because the structural side of the query is where agents fail first, and it is where a refunds agent fails hardest. The customer's real question is almost always gated by a fact, this order, this date, this exclusion, and getting that fact wrong is what produces a hallucinated policy. Vector search and "RAG" are not the same thing as good retrieval, and "we have embeddings" is not a retrieval strategy.
Getting the structural side right is harder than it looks, and it is not a model problem. Sanity's schema-exploration test against Sonos's catalog landed around 83 percent accuracy on a mix of difficulties, using Sonnet 4.5 with roughly 40 seconds of thinking per hard question. Reaching that meant teaching retrieval counter-intuitive field names, second-order reference chains, and data-quality issues the schema alone would never reveal. The model needed the shape of the data, not just its types. That figure comes from a product catalog, not a refund policy, but the lesson transfers exactly: the difficulty lives in the structure of your content, so that is where the investment belongs.
Return the policy object, not a paragraph about it
Even with retrieval fixed, there is a second place facts go to die: the handoff back to the model. A tool that returns prose forces the model to paraphrase, and paraphrasing is where facts go to die. When Sanity watched agents get built against Context MCP, 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, drifting a "30-day" window into "about a month" and a hard exclusion into a soft suggestion.
For a refunds agent this is the whole ballgame. If your tool returns three eligible policies, it should return three policy objects, not a paragraph describing them. A policy object carries the window as a number, the exclusion list as a discrete array, the escalation flag as a boolean. The model presents those fields; it does not re-derive them. When the agent says "final-sale items are not eligible for return," that sentence should be reading a field, not summarizing a document it half-remembers.
This is the difference between an agent that quotes your policy and one that approximates it. Structured, schema-shaped responses keep the authoritative values intact from the Content Lake all the way to the customer, so the number the customer hears is the number Compliance approved. Prose responses invite exactly the drift that turns a correct retrieval into an incorrect answer, which is a maddening way to lose after doing the hard retrieval work correctly.
Govern the prompt like customer-facing behavior, because it is
The application system prompt, the never-say list, the escalation policy, the refund-eligibility language, is not configuration buried in a repository. It is customer-facing behavior, and it should be authored like content and gated like code. On Sanity Context, that prompt lives as a document in the Studio, split into fields, and splitting it into fields is not cosmetic. It is access control.
Compliance owns the mustNotSay never-say list. Support owns the escalation policy. Brand owns voice. Product owns how the agent uses user context. None of them files a pull request, none waits for a deploy, and each edits the field they are responsible for. Because it is content in the Studio, you get real-time collaboration, version history, scheduled publishing, and rollback for free. The gate is evals: a prompt change runs the eval bench in CI before it can ship, so a well-meaning wording tweak cannot quietly widen your refund liability. The release that ships a homepage change ships a prompt change, staged with Content Releases the same way you stage the website.
This closes the second failure mode from the diagnostic map. Scope violations, the agent answering something the never-say list should have caught, are prompt problems, and you fix prompt problems by governing the prompt. Storing refund-eligibility language as governed, versioned, eval-gated content means the rule the agent enforces is the rule the business actually approved, with an audit trail showing who changed it and when.
Author it like content, gate it like code
Grounding a refunds agent: native retrieval and governance vs assembled stacks
| Feature | Sanity | Pinecone | pgvector / Postgres | Decagon / Sierra AI |
|---|---|---|---|---|
| Hybrid retrieval (keyword + semantic + filters) | Native: predicates plus score(boost([title] match text::query(), 2), text::semanticSimilarity()) blended in one GROQ query against the Content Lake. | Vector-first with a metadata filter layer; structured-plus-relevance is possible but the keyword and predicate blend is assembled around the index, not native. | Capable via pgvector plus Postgres full-text search, but you write and tune the hybrid ranking SQL yourself and maintain it over time. | Hosted retrieval over crawled or indexed site content; the blend is a black box you configure rather than a query you own. |
| Index freshness on policy changes | Content Lake keeps the index fresh and dataset embeddings are tied to content, so edits propagate within minutes with no separate pipeline. | You own incremental indexing, re-embedding on change, deletion handling, and backfill; freshness is a permanent roadmap line item. | Re-embedding and reindex jobs on content change are yours to build, schedule, and monitor for drift. | Re-crawl or re-sync cadence is vendor-controlled; stale policy content between syncs is a known risk. |
| Structured, schema-shaped tool responses | Returns policy objects (window as number, exclusions as array) the model passes through; no paraphrasing step where facts drift. | Returns matched chunks with metadata; assembling a clean policy object for the model is application code you write. | Returns rows you shape in SQL, so schema-shaped output is achievable but hand-built per query. | Typically returns generated prose answers, which reintroduces the paraphrase drift a refunds agent must avoid. |
| Governed, eval-gated prompt authoring | Never-say list, escalation, and eligibility live as Studio fields owned by Compliance, Support, and Brand, staged with Content Releases and gated by evals in CI. | No prompt governance layer; prompt and never-say rules live in your application code and deploy pipeline. | No native prompt governance; you build versioning, ownership split, and eval gating yourself. | Configuration is in the vendor console; field-level ownership across Compliance, Support, and Brand is limited. |
| Access to governed structured and logged-in data | Agents query governed structured content and references directly through the Sanity Context MCP endpoint. | Whatever you ingest into the vector index; joining live structured or logged-in data is your integration work. | Full SQL access to your own tables, but wiring it into agent retrieval and governance is self-built. | Customers report difficulty getting logged-in user data and governed structured content into the hosted agent. |