{"componentChunkName":"component---src-templates-blog-post-js","path":"/blog/2026-09-15-grounded-citations-rag/","result":{"data":{"site":{"siteMetadata":{"title":"M.Hassan Ahmed","author":"Hassan11196"}},"markdownRemark":{"id":"f92a4916-d3a7-5658-bae1-babd493fb8d8","excerpt":"A retrieval-augmented answer that sounds right is worth very little if the person reading it can’t check where it came from. On Archi, the RAG copilot I worked…","html":"<p>A retrieval-augmented answer that sounds right is worth very little if the person reading it can’t check where it came from. On <a href=\"/project/archi/\">Archi</a>, the RAG copilot I worked on for CMS computing operations, the questions look like “what do I do when this workflow keeps failing at the merge step?” The answer might be correct. But an operator on shift at 3am will not act on a confident paragraph from a model unless they can click through to the runbook, JIRA ticket, or logbook entry it came from. No source, no trust, no action.</p>\n<p>That is the real job of citations in a RAG system. They let a human verify the machine, and they are the difference between a demo and something people rely on. This post is for engineers who already have retrieval working and now need the generated answers to point back at real sources, with citations that are true rather than merely plausible.</p>\n<p>It covers why “just tell the model to cite its sources” fails, how to force citations by ID, and the verification step that catches the citations the model invents. There is code, and a section on the ways this breaks in practice, because it breaks in specific and repeatable ways.</p>\n<h2>Why asking nicely doesn’t work</h2>\n<p>The naive version is a one-line prompt addition: “cite your sources.” Try it and you get citations. They look great, and some of them are even correct. The problem is the ones that aren’t, and you can’t tell which is which by looking.</p>\n<p>Language models generate citations the same way they generate everything else: by predicting plausible tokens. A model that has seen thousands of documents with <code class=\"language-text\">[3]</code> after a factual sentence will happily produce <code class=\"language-text\">[3]</code> after its own factual sentence, whether or not source 3 says anything of the kind. The citation is a stylistic pattern the model reproduces, not a lookup it performs. This is the same failure behind fabricated legal citations and invented DOIs: the shape is right, but the referent is fiction.</p>\n<p>Two distinct things go wrong, and it helps to name them separately:</p>\n<ul>\n<li><strong>Fabrication.</strong> The model cites a source ID that supports nothing it said, or a source that doesn’t exist in the retrieved set at all.</li>\n<li><strong>Misattribution.</strong> The model says something true, drawn from chunk 2, and cites chunk 5. The claim is fine; the pointer is wrong. A reader who follows it lands in the wrong place and loses faith in every other citation on the page.</li>\n</ul>\n<p>Both are common enough that you cannot ship citations you generated but never checked. The academic benchmark work makes this concrete. The <a href=\"https://arxiv.org/abs/2305.14627\">ALCE evaluation</a> measures citation <em>precision</em> and <em>recall</em> separately from answer correctness, precisely because models routinely get one right and the other wrong.</p>\n<p>So the design has two halves: make the model cite in a form you can machine-check, then check it. The diagram below shows the whole pipeline.</p>\n<p><img src=\"/a6599ee24bd5b0b9b1bb1ebd2ceef2ab/citation-pipeline.svg\" alt=\"A horizontal pipeline. A retriever produces top-k chunks, which become chunks with stable IDs like runbook#p4, JIRA-8123, logbook#59. Those feed an LLM that returns an answer with inline bracket-n markers. The answer flows into a verifier, and a dashed feedback path shows the verifier re-reading the cited chunk text rather than trusting the model&#x27;s claim. Verified claims flow to a final answer where each claim links to a real source. A caption notes the dashed verification path is what catches an invented citation before the user sees it.\"></p>\n<h2>Step one: cite by ID, not by name</h2>\n<p>The first fix is to stop letting the model write free-form citations and give it a closed set of IDs to choose from. You control the context you send it, so you control the identifiers.</p>\n<p>When you assemble the prompt, tag every chunk with a short stable ID and print that ID right next to its text. The model’s job becomes “pick from these labels,” which is a far easier and more checkable task than “recall a reference.”</p>\n<p>In the code below, <code class=\"language-text\">format_context</code> numbers each chunk and records its <code class=\"language-text\">source_id</code>, and the system prompt tells the model to cite those numbers after every factual sentence:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">def</span> <span class=\"token function\">format_context</span><span class=\"token punctuation\">(</span>chunks<span class=\"token punctuation\">:</span> <span class=\"token builtin\">list</span><span class=\"token punctuation\">[</span>Chunk<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">-</span><span class=\"token operator\">></span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">:</span>\n    <span class=\"token triple-quoted-string string\">\"\"\"Render retrieved chunks with the exact IDs the model must cite.\"\"\"</span>\n    blocks <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\n    <span class=\"token keyword\">for</span> i<span class=\"token punctuation\">,</span> chunk <span class=\"token keyword\">in</span> <span class=\"token builtin\">enumerate</span><span class=\"token punctuation\">(</span>chunks<span class=\"token punctuation\">,</span> start<span class=\"token operator\">=</span><span class=\"token number\">1</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n        <span class=\"token comment\"># [1], [2], ... are what the model cites; source_id is how we</span>\n        <span class=\"token comment\"># resolve a citation back to a real URL after generation.</span>\n        blocks<span class=\"token punctuation\">.</span>append<span class=\"token punctuation\">(</span><span class=\"token string-interpolation\"><span class=\"token string\">f\"[</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>i<span class=\"token punctuation\">}</span></span><span class=\"token string\">] (source_id=</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>chunk<span class=\"token punctuation\">.</span>source_id<span class=\"token punctuation\">}</span></span><span class=\"token string\">)\\n</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>chunk<span class=\"token punctuation\">.</span>text<span class=\"token punctuation\">}</span></span><span class=\"token string\">\"</span></span><span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">return</span> <span class=\"token string\">\"\\n\\n\"</span><span class=\"token punctuation\">.</span>join<span class=\"token punctuation\">(</span>blocks<span class=\"token punctuation\">)</span>\n\n\nSYSTEM <span class=\"token operator\">=</span> <span class=\"token triple-quoted-string string\">\"\"\"You answer questions for CMS computing operators using only the \\\nsources provided. After every sentence that states a fact, cite the source it \\\ncame from using its bracket number, like [1] or [2][3]. If the sources do not \\\ncontain the answer, say so. Do not use any knowledge that is not in the sources.\"\"\"</span></code></pre></div>\n<p>Two details matter here.</p>\n<p><strong>The labels are short and local.</strong> The <code class=\"language-text\">[1]</code>, <code class=\"language-text\">[2]</code> labels are sequential and exist only for this one request. That keeps them short and keeps the model from confusing them with numbers that appear inside the text.</p>\n<p><strong>The real IDs stay on your side.</strong> Each label maps to a <code class=\"language-text\">source_id</code> that you keep, so after the model answers, you can turn <code class=\"language-text\">[2]</code> back into a link to <code class=\"language-text\">JIRA-8123</code> or <code class=\"language-text\">runbook#p4</code>. Never ask the model to emit the real URL or ticket number directly. It will get characters wrong, and then you have a broken link that looks authoritative.</p>\n<p>The instruction to refuse when the sources don’t cover the question is doing real work too. Without it, a model that retrieved nothing useful will still answer from its parametric memory (what it learned in training) and cite whatever chunk is closest. That is misattribution by construction. “I don’t have a source for that” is a feature.</p>\n<h2>Step two: parse the citations out</h2>\n<p>Inline <code class=\"language-text\">[n]</code> markers are easy to extract, and it is easy to check that they point at chunks that exist. The function below splits the cited numbers into valid ones and “phantom” ones, meaning numbers outside the range of chunks you actually sent:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">import</span> re\n\nCITE <span class=\"token operator\">=</span> re<span class=\"token punctuation\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\">(</span><span class=\"token string\">r\"\\[(\\d+)\\]\"</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token keyword\">def</span> <span class=\"token function\">extract_citations</span><span class=\"token punctuation\">(</span>answer<span class=\"token punctuation\">:</span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">,</span> n_chunks<span class=\"token punctuation\">:</span> <span class=\"token builtin\">int</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">-</span><span class=\"token operator\">></span> <span class=\"token builtin\">tuple</span><span class=\"token punctuation\">[</span><span class=\"token builtin\">set</span><span class=\"token punctuation\">[</span><span class=\"token builtin\">int</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span> <span class=\"token builtin\">set</span><span class=\"token punctuation\">[</span><span class=\"token builtin\">int</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">:</span>\n    <span class=\"token triple-quoted-string string\">\"\"\"Return (valid_ids, phantom_ids) referenced in the answer.\"\"\"</span>\n    cited <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span><span class=\"token builtin\">int</span><span class=\"token punctuation\">(</span>m<span class=\"token punctuation\">)</span> <span class=\"token keyword\">for</span> m <span class=\"token keyword\">in</span> CITE<span class=\"token punctuation\">.</span>findall<span class=\"token punctuation\">(</span>answer<span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span>\n    valid <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>i <span class=\"token keyword\">for</span> i <span class=\"token keyword\">in</span> cited <span class=\"token keyword\">if</span> <span class=\"token number\">1</span> <span class=\"token operator\">&lt;=</span> i <span class=\"token operator\">&lt;=</span> n_chunks<span class=\"token punctuation\">}</span>\n    phantom <span class=\"token operator\">=</span> cited <span class=\"token operator\">-</span> valid          <span class=\"token comment\"># e.g. [7] when only 5 chunks were sent</span>\n    <span class=\"token keyword\">return</span> valid<span class=\"token punctuation\">,</span> phantom</code></pre></div>\n<p>A non-empty <code class=\"language-text\">phantom</code> set is an immediate, cheap signal that the model is fabricating: it cited a number you never gave it. In practice, I treat any phantom citation as grounds to regenerate the answer. A model confident enough to invent <code class=\"language-text\">[7]</code> is not one I trust on the citations that happen to land in range.</p>\n<p>If you want something sturdier than regex over prose, ask for structured output instead: a JSON array of <code class=\"language-text\">{claim, source_ids}</code> objects. That moves you onto the ground I covered in <a href=\"/blog/2026-07-14-reliable-json-from-llms/\">getting reliable JSON out of LLMs</a>. It also makes the next step, verification, cleaner, because each claim already arrives paired with its sources.</p>\n<p>Some providers now expose this natively. <a href=\"https://docs.anthropic.com/en/docs/build-with-claude/citations\">Anthropic’s Citations feature</a> returns cited spans with character offsets into the documents you passed, which removes the parsing problem entirely for that API. When it’s available, use it. The verification logic below still applies either way.</p>\n<h2>Step three: verify, because the model will lie to you</h2>\n<p>Extracting a citation only tells you the pointer is in range. It does not tell you the cited chunk actually supports the claim. That is the misattribution case, and catching it is the part people skip and then regret.</p>\n<p>The check is a per-claim gate:</p>\n<ol>\n<li>Split the answer into claims.</li>\n<li>Take each claim’s cited chunk.</li>\n<li>Ask a direct question: does this chunk’s text support this sentence?</li>\n</ol>\n<p>Read the source, not the model’s summary of it. The diagram below shows the three possible outcomes.</p>\n<p><img src=\"/6860fb3a276dc34ebf4af55b9e912829/citation-verify.svg\" alt=\"A decision diagram. A box holds a claim and its citation, for example restart the agent on ECAL errors, cited as bracket 2. An arrow labeled overlap, NLI, or LLM judge leads into a diamond asking whether chunk 2 entails the claim. Three outcomes branch out: supported means keep the claim and link the citation to its source; wrong source means re-cite by searching the other chunks for a real match; unsupported means drop or flag the claim and never show it as fact. A caption notes the verifier reads the source text directly, so the model saying bracket 2 is never enough on its own.\"></p>\n<p>There are three ways to implement this entailment check (deciding whether one text supports another), in increasing order of cost and accuracy.</p>\n<p><strong>String overlap</strong> is the cheap baseline. Does the claim share enough content with the cited chunk, by token overlap or a fuzzy match, to be plausibly grounded? It is fast, needs no model call, and catches gross fabrication, such as a chunk about disk quotas cited for a claim about network timeouts. It misses paraphrase, so treat it as a floor, not a verdict.</p>\n<p><strong>Natural language inference (NLI)</strong> is the middle ground. A small NLI model classifies the (chunk, claim) pair as entailment, neutral, or contradiction. It handles paraphrase, runs locally, and is cheap enough to call on every claim. This is what the faithfulness metrics in evaluation frameworks lean on. <a href=\"https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/faithfulness/\">RAGAS computes faithfulness</a> by checking whether each claim in an answer can be inferred from the retrieved context, which is exactly this gate applied as an offline metric.</p>\n<p><strong>An LLM judge</strong> is the most capable and the most expensive. You hand a model the claim and the cited text and ask for a supported/unsupported ruling with a reason. This is the <a href=\"/blog/2026-07-20-llm-as-a-judge-evaluating-outputs/\">LLM-as-a-judge pattern</a> pointed at citations specifically, and the same caveats from that post apply: keep the judge’s job narrow, give it a rubric, and don’t ask it to be creative.</p>\n<p>Here is the verification loop. The check is a pluggable <code class=\"language-text\">supports</code> function, so you can start with overlap and swap in something stronger without touching the control flow. Each claim comes out as supported, misattributed (it has valid citations, but none back it), or unsupported (no valid citation at all):</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">def</span> <span class=\"token function\">verify_answer</span><span class=\"token punctuation\">(</span>claims<span class=\"token punctuation\">:</span> <span class=\"token builtin\">list</span><span class=\"token punctuation\">[</span>Claim<span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span> chunks<span class=\"token punctuation\">:</span> <span class=\"token builtin\">dict</span><span class=\"token punctuation\">[</span><span class=\"token builtin\">int</span><span class=\"token punctuation\">,</span> Chunk<span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n                  supports<span class=\"token punctuation\">)</span> <span class=\"token operator\">-</span><span class=\"token operator\">></span> <span class=\"token builtin\">list</span><span class=\"token punctuation\">[</span>VerifiedClaim<span class=\"token punctuation\">]</span><span class=\"token punctuation\">:</span>\n    results <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span>\n    <span class=\"token keyword\">for</span> claim <span class=\"token keyword\">in</span> claims<span class=\"token punctuation\">:</span>\n        cited <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span>c <span class=\"token keyword\">for</span> c <span class=\"token keyword\">in</span> claim<span class=\"token punctuation\">.</span>source_ids <span class=\"token keyword\">if</span> c <span class=\"token keyword\">in</span> chunks<span class=\"token punctuation\">]</span>\n        <span class=\"token comment\"># A claim survives only if at least one cited chunk backs it.</span>\n        grounded <span class=\"token operator\">=</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">(</span>supports<span class=\"token punctuation\">(</span>claim<span class=\"token punctuation\">.</span>text<span class=\"token punctuation\">,</span> chunks<span class=\"token punctuation\">[</span>c<span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span>text<span class=\"token punctuation\">)</span> <span class=\"token keyword\">for</span> c <span class=\"token keyword\">in</span> cited<span class=\"token punctuation\">)</span>\n        <span class=\"token keyword\">if</span> grounded<span class=\"token punctuation\">:</span>\n            results<span class=\"token punctuation\">.</span>append<span class=\"token punctuation\">(</span>VerifiedClaim<span class=\"token punctuation\">(</span>claim<span class=\"token punctuation\">,</span> status<span class=\"token operator\">=</span><span class=\"token string\">\"supported\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n        <span class=\"token keyword\">elif</span> cited<span class=\"token punctuation\">:</span>\n            results<span class=\"token punctuation\">.</span>append<span class=\"token punctuation\">(</span>VerifiedClaim<span class=\"token punctuation\">(</span>claim<span class=\"token punctuation\">,</span> status<span class=\"token operator\">=</span><span class=\"token string\">\"misattributed\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n        <span class=\"token keyword\">else</span><span class=\"token punctuation\">:</span>\n            results<span class=\"token punctuation\">.</span>append<span class=\"token punctuation\">(</span>VerifiedClaim<span class=\"token punctuation\">(</span>claim<span class=\"token punctuation\">,</span> status<span class=\"token operator\">=</span><span class=\"token string\">\"unsupported\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">return</span> results</code></pre></div>\n<p>What you do with a failed claim is a product decision, not a technical one:</p>\n<ul>\n<li><strong>Drop it silently.</strong> This keeps the output clean but can gut an answer.</li>\n<li><strong>Flag it</strong> (“this part could not be sourced”). This is more honest and, for an operations tool where a wrong instruction has consequences, usually the right call.</li>\n<li><strong>Repoint a misattributed citation.</strong> The cheapest fix is to re-run the overlap check against <em>all</em> the retrieved chunks, not just the cited one, and repoint the citation to the chunk that genuinely matches. Often the model had the right fact and simply grabbed the wrong label.</li>\n</ul>\n<h2>Where it breaks</h2>\n<p>The pipeline is straightforward. The failures are where the time goes.</p>\n<p><strong>Lost in the middle.</strong> Models attend unevenly across a long context, favoring the beginning and end and skimming the middle, a bias documented in the <a href=\"https://arxiv.org/abs/2307.03172\">Lost in the Middle</a> study. For citations, this means a claim’s real support might sit in chunk 6 of 10 while the model cites chunk 1, because that’s where it was paying attention. Verification catches this as misattribution, which is one more reason not to skip it, and it argues for retrieving fewer, better chunks rather than stuffing the context.</p>\n<p><strong>ID drift after reranking.</strong> If you assign the <code class=\"language-text\">[n]</code> labels and a reranker then reorders the chunks, the numbers the model cites no longer line up with the sources you thought you sent. Assign IDs <em>after</em> every reordering step, immediately before you build the prompt, and freeze them. This bites hardest when you add a <a href=\"/blog/2026-07-08-cross-encoder-reranking-rag/\">cross-encoder reranker</a> to an existing pipeline and forget that it changed the ordering the labels were built on.</p>\n<p><strong>Over-citation.</strong> Some models cite every chunk after every sentence, <code class=\"language-text\">[1][2][3][4]</code>, which is technically defensible and practically useless: it points at everything and therefore nothing. Verification helps here too. If a claim genuinely follows only from <code class=\"language-text\">[2]</code>, drop the other three citations even though the model offered them, because a citation the reader can’t act on is noise.</p>\n<p><strong>The refusal that should have happened.</strong> The worst output is a fully cited answer to a question the sources don’t actually address. Every citation passes a loose overlap check because the topic words match, but the specific claim isn’t there. Tighten the entailment check and lean on the retrieval side: if <a href=\"/blog/2026-07-04-measuring-rag-retrieval-quality/\">retrieval quality</a> is poor, no citation layer will save you, because the model is choosing the least-wrong of several wrong chunks.</p>\n<h2>Tradeoffs, and what I’d do differently</h2>\n<p>Verification costs latency and, if you use a judge, money. On Archi, I don’t verify every claim with an LLM on the hot path. The checks are tiered instead:</p>\n<ul>\n<li><strong>String overlap</strong> runs on everything as a fast fabrication tripwire.</li>\n<li><strong>NLI</strong> is the per-claim gate on live answers.</li>\n<li><strong>The expensive LLM judge</strong> is reserved for offline evaluation runs, where I’m measuring citation precision across a test set rather than gating a single live answer.</li>\n</ul>\n<p>That tiered setup keeps the interactive path responsive while still catching the failures that matter most.</p>\n<p>If I were starting fresh, I’d reach for a provider’s native citation API before building the parse-and-verify machinery by hand. Character-offset citations remove the misattribution class almost entirely, because the model isn’t picking a label; it’s pointing at a span. The hand-rolled version in this post is what you build when you’re running open models, mixing providers, or need the verification to be inspectable rather than a black box. That describes a lot of production systems, including the ones I work on.</p>\n<p>The through-line back to <a href=\"/project/archi/\">Archi</a> and the wider <a href=\"/project/cms-workflow-operations/\">CMS operations tooling</a> is trust. Operators don’t need the copilot to be right every time; they need to be able to check it every time. A grounded, verified citation is how a RAG answer earns the right to be acted on instead of double-checked from scratch, which is the entire reason the tool exists.</p>\n<hr>\n<p><em>Diagrams by M. Hassan Ahmed, released under CC0. No external image was used for this post; the figures are original work by the author.</em></p>","frontmatter":{"title":"RAG Citations: Make the LLM Cite Its Sources","date":"2026-09-15T00:00:00.000Z","description":"How to make a RAG system cite its sources: force the model to reference chunks by ID, then verify each citation against the source text before trusting it.","thumbnail":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAAB6UlEQVQozzWR7ZKaMBSG+edKwAABCSQBFMKHCooiorW62467s1tvodPW+7+IHth25pl3Ts5n5hzFFMeBDrOWhCdZ3+P1R7z5CJevsv7xz9jewQ85Bj+A/i85KggzdeJjEhGaOH6Wrbq8OmZllxR7sBfrL3LRruqveXWCKKFStwIVMzSgIDNQDYHMcGLPdTJTjUDtPQGywt74xBCATiIAmQIZHA0lChE7Grd0tsVuMXFzUMAAvIVBC0jSzAiRRLUAqdmZ7uQ9dgohZRpuuTyweE/nOzfaQhc625Fka7LK9FYq9mEXov4dFd95+mx4G0w3oIa/Q9ZMoWEd5h2Xe5F2LNkH6YGlBzvdWryE4Sr2INvP30Vx4/nNky+AHZ0N3iEzUtywFukhyKC+FbL15UkeHzZvLN6YvG+PWRfVv5L2AcT7R9z88cufdnLvv+0EayZbL268eQMaxE2YtAYrTR8mL+EQOi3dxYWVV391ZeWzm59JcjDDFpaqiLgpNy+L8rKsrovqKmXLeIW9leVXUDzWXN0v3eNNnN7Y8ZWf3tzdN7K+WPlZnQjFnKaEFsTNba8gNNccqU9TPM2wm02mcqy7KoarzFUMV+wBG1kx6Finygg5I83pdeBJm/agAe3zCX77CdAGRfZoAEJ/AcmbURXYoUZ7AAAAAElFTkSuQmCC","aspectRatio":1.899441340782123,"src":"/static/dea034eb79a4b0ee218da87ab715cee7/40a76/hero.png","srcSet":"/static/dea034eb79a4b0ee218da87ab715cee7/c972b/hero.png 340w,\n/static/dea034eb79a4b0ee218da87ab715cee7/27625/hero.png 680w,\n/static/dea034eb79a4b0ee218da87ab715cee7/40a76/hero.png 1360w,\n/static/dea034eb79a4b0ee218da87ab715cee7/ed396/hero.png 2000w","sizes":"(max-width: 1360px) 100vw, 1360px"}}}}}},"pageContext":{"slug":"/2026-09-15-grounded-citations-rag/","previous":"blog/2026-09-16-fastapi-pydantic-v2-request-validation/","next":"blog/2026-09-14-opensearch-vector-store-rag/"}},"staticQueryHashes":["32046230"]}