{"slug": "rag-answers-4-controls-to-store-source-urls-and-add-citations", "title": "RAG Answers: 4 Controls to Store Source URLs and Add Citations", "summary": "A developer outlined a citation architecture for retrieval-augmented generation that treats citations as data lineage rather than prose decoration, storing a stable document_id and chunk_id with page locators alongside every embedded chunk. The design freezes the retrieved evidence set, issues short labels like [S1] to the generator, and uses a deterministic Go path to validate labels and render links only to allowed source URLs, so every rendered citation maps to retrieved text and a canonical document. The approach targets a common failure where a chatbot's citation lands on a publisher's home page instead of the cited page.", "body_md": "A citation is useful only if a reader can reopen the exact evidence behind an answer. For a media archive built from PDFs, that operational constraint changes the design: store a stable document URL and page locator with every embedded chunk, carry that metadata through retrieval, assign citation labels before generation, and reject answer citations that are not in the retrieved set. Do not ask the model to remember or invent URLs.\n\nTL;DR: treat citations as data lineage, not prose decoration. The generator receives numbered evidence records such as `[S1]`, writes only those labels, and a deterministic Go path turns the labels into links after validating them. This gives the answer service a measurable invariant: every rendered citation maps to retrieved text and an allowed source URL.\n\nThe bounded incident to plan for is ordinary: an editor asks a chatbot what a 90-page report says, gets a plausible paragraph, clicks its citation, and lands on the publisher's home page rather than page 47. The words may be grounded, yet the citation has failed its job. Under an answer-grounding SLO, that response should count as defective because the evidence cannot be inspected at the claimed location.\n\nThe invariant is tighter than \"include some links\": every factual claim that needs support must refer to an evidence identifier issued by retrieval, and every identifier must resolve to a canonical document plus a useful locator. No exceptions.\n\nPDF URLs identify files, while answers usually depend on smaller units: a page, paragraph, table, caption, or transcript segment. If the ingestion pipeline copies only `source_url` into vector metadata, several chunks from the same report become indistinguishable in the final answer. If it stores only extracted text, the system loses the route back to the publication. Both designs make citation repair guesswork.\n\nLinks rot.\n\nKeep two identities. `document_id` names the logical publication across re-indexing; `chunk_id` names a particular evidence unit. A document URL is presentation data associated with that identity, not the identity itself, because publishers can change hosts, paths, or query parameters. For a folder of PDFs without public URLs, use an internal document URI or an application route that resolves the stable ID under the reader's authorization. Never manufacture an external URL.\n\nA practical metadata record can stay small:\n\n```\npackage evidence\n\ntype Chunk struct {\n    ChunkID    string            `json:\"chunk_id\"`\n    DocumentID string            `json:\"document_id\"`\n    SourceURL  string            `json:\"source_url\"`\n    Title      string            `json:\"title\"`\n    Page       int               `json:\"page\"`\n    Text       string            `json:\"text\"`\n    Attributes map[string]string `json:\"attributes,omitempty\"`\n}\n```\n\nPage numbering needs an explicit convention. PDF page index 46 may be printed as page 39 after front matter, so silently calling either one \"page 47\" creates a hard-to-debug credibility failure. Store the machine locator used by the viewer, and add the printed label separately when extraction can recover it. Empty is better than false precision.\n\nRetrieval-augmented generation conditions generated text on retrieved documents; the original RAG paper describes that general combination of parametric and non-parametric memory. A production citation contract adds a deterministic layer around it. After retrieval and any reranking, collapse duplicate chunks, freeze the evidence set, and issue short labels. The prompt contains labels, titles, page locators, and text. The model never receives permission to emit arbitrary links.\n\nHere is the preventative path. It deliberately separates retrieval metadata from rendered Markdown:\n\n```\npackage evidence\n\nimport (\n    \"fmt\"\n    \"net/url\"\n    \"sort\"\n    \"strings\"\n)\n\ntype Citation struct {\n    Label, DocumentID, Title, SourceURL, Text string\n    Page int\n}\n\nfunc BuildLedger(chunks []Chunk, allowedHosts map[string]bool) ([]Citation, error) {\n    sort.SliceStable(chunks, func(i, j int) bool {\n        return chunks[i].ChunkID < chunks[j].ChunkID\n    })\n    seen := make(map[string]bool)\n    ledger := make([]Citation, 0, len(chunks))\n    for _, chunk := range chunks {\n        if chunk.ChunkID == \"\" || chunk.DocumentID == \"\" || strings.TrimSpace(chunk.Text) == \"\" {\n            return nil, fmt.Errorf(\"incomplete evidence metadata for chunk %q\", chunk.ChunkID)\n        }\n        if seen[chunk.ChunkID] {\n            continue\n        }\n        u, err := url.ParseRequestURI(chunk.SourceURL)\n        if err != nil || u.Scheme != \"https\" || !allowedHosts[u.Hostname()] {\n            return nil, fmt.Errorf(\"disallowed source URL for chunk %q\", chunk.ChunkID)\n        }\n        seen[chunk.ChunkID] = true\n        ledger = append(ledger, Citation{\n            Label: fmt.Sprintf(\"S%d\", len(ledger)+1), DocumentID: chunk.DocumentID,\n            Title: chunk.Title, SourceURL: u.String(), Page: chunk.Page, Text: chunk.Text,\n        })\n    }\n    return ledger, nil\n}\n```\n\nSorting by `ChunkID` makes labels repeatable for the same retrieved set; if rank order is meaningful to the application, preserve that order and use rank plus chunk ID as the deterministic key. The important property is not the chosen order. It is that the ledger is frozen before the answer is generated and retained with the answer trace.\n\nURL validation belongs at ingestion and again at rendering. The second check protects against stale or corrupted records, but it does not replace outbound-request controls if the application fetches URLs. A citation renderer should link to known publication hosts or to an authenticated internal resolver, and it should escape titles before producing HTML.\n\nThe controls form a chain. Losing any link weakens the result, although the right implementation depends on the archive and on-call budget.\n\n| Control | Failure prevented | Capacity and operations cost | \n|---|---|---|\n| Stable document and chunk IDs | Re-indexing silently changes citation identity | ID registry and migration discipline | \n| URL plus page-level locator | Link opens the right file but not the evidence | More metadata and extraction checks | \n| Frozen post-retrieval ledger | Generator cites material outside the evidence set | Ledger storage per answer | \n| Deterministic validation | Unknown labels or unsafe links reach readers | Validation failures need explicit handling | \n\nFor capacity planning, estimate ledger storage from observed retrieved-chunk metadata size, retrieval count, answer volume, and retention period. Do not bury the ledger inside prompt logs with an undefined retention policy. It is part of the evidence record, so its lifecycle should follow the answer and the media archive's access rules. Text duplication can be bounded by retaining chunk IDs and an immutable content digest, provided the referenced chunk version remains available for the full audit window.\n\nThe buy-versus-build question is narrower than \"which vector database?\" Metadata filters and payload fields are useful, but citation correctness lives across ingestion, retrieval, generation, rendering, and evaluation. No storage engine owns that whole contract.\n\n| Approach | Team benefit | On-call and lock-in trade-off | Best fit | \n|---|---|---|---|\n| Build the ledger in the answer service | Contract stays explicit and portable | Team owns validation, migrations, and tests | Stable platform team with custom archive rules | \n| Use a managed retrieval layer's metadata path | Less retrieval plumbing to operate | Export behavior and metadata limits must be tested | Small team accepting a narrower contract | \n| Resolve citations through a separate evidence service | Central policy and audit boundary | Another dependency enters the answer SLO | Several applications sharing one corpus | \n\nI would start with the smallest explicit ledger in the answer service. That is a design judgment, not a claim that self-hosting is universally cheaper. If multiple applications need identical authorization, retention, and redaction rules, the evidence service becomes easier to justify; until then, its extra failure domain is real work for the on-call rotation.\n\nA syntactically valid citation can still support the wrong sentence. Test three layers instead of collapsing them into one score. First, citation validity: does every emitted label exist in the frozen ledger? Second, citation correctness: does the cited excerpt support the nearby claim? Third, citation completeness: are material claims accompanied by evidence? The latter two require a reviewed evaluation set or careful human assessment; string matching alone cannot establish entailment.\n\nThe rendering gate can be mechanical:\n\n``` js\npackage evidence\n\nimport (\n    \"fmt\"\n    \"regexp\"\n)\n\nvar citationLabel = regexp.MustCompile(`\\[(S[1-9][0-9]*)\\]`)\n\nfunc ValidateLabels(answer string, ledger []Citation) error {\n    known := make(map[string]bool, len(ledger))\n    for _, item := range ledger {\n        known[item.Label] = true\n    }\n    for _, match := range citationLabel.FindAllStringSubmatch(answer, -1) {\n        if !known[match[1]] {\n            return fmt.Errorf(\"answer contains unknown citation %s\", match[1])\n        }\n    }\n    return nil\n}\n```\n\nThis function catches fabricated labels, not uncited claims. Keep that limitation visible. A good release gate includes PDF fixtures with repeated headers, scanned pages with weak text extraction, tables split across pages, duplicated editions, moved documents, and restricted publications. Those cases exercise lineage rather than flattering the retriever with clean prose.\n\nTest the contract.\n\nMeasure at least invalid-label rate, citation-resolution success, and the fraction of evaluated material claims judged supported. Put the first two under service objectives because they are deterministic and observable. Treat support judgments as an evaluation program with sampling and reviewer guidance; pretending they are perfect runtime signals creates false precision.\n\nShort failure paths matter. If the ledger is empty, answer that the archive did not provide sufficient evidence. If a cited document is no longer authorized, withhold its content and link rather than degrading to an untraceable claim. If extraction lacks a reliable page locator, cite the document and label the locator as unavailable. Fail closed.\n\nA ledger is unnecessary for responses that make no corpus-derived factual claims, such as asking the user to clarify a title. It is also insufficient for changing sources when the URL can resolve to different content later; preserve an authorized immutable snapshot or content digest if auditability is required.\n\nSome PDF viewers do not honor page fragments consistently. In that environment, route citations through an application viewer that accepts the stable document ID and page index, or render the page as plain citation text without promising a deep link. The rule remains the same: expose only a locator the system can actually resolve.\n\nFor a media archive, the final decision is operational rather than stylistic. Store provenance at ingestion, preserve it through retrieval, bind the generator to a frozen ledger, and validate before rendering. A blue citation marker is easy. A citation that survives re-indexing, access-control changes, and an editor's click is the feature worth shipping.", "url": "https://wpnews.pro/news/rag-answers-4-controls-to-store-source-urls-and-add-citations", "canonical_source": "https://dev.to/nielschristensen4981/rag-answers-4-controls-to-store-source-urls-and-add-citations-3mif", "published_at": "2026-10-19 15:41:58+00:00", "updated_at": "2026-10-19 15:44:35.734485+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "generative-ai", "ai-tools", "developer-tools"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/rag-answers-4-controls-to-store-source-urls-and-add-citations", "markdown": "https://wpnews.pro/news/rag-answers-4-controls-to-store-source-urls-and-add-citations.md", "text": "https://wpnews.pro/news/rag-answers-4-controls-to-store-source-urls-and-add-citations.txt", "jsonld": "https://wpnews.pro/news/rag-answers-4-controls-to-store-source-urls-and-add-citations.jsonld"}}