ADR-0008: RAG citation strategy
Datum: 2026-04-25 Herzien: 2026-05-18 (migratie naar Bedrock Knowledge Bases) Status: Geaccepteerd
Context
Atlas Chat citeert publieke kennisbronnen uit het RAG-corpus. Per bron wil de gebruiker een klikbare link naar de originele URL zien (bijvoorbeeld farmacotherapeutischkompas.nl/paracetamol). Het corpus staat in een Amazon Bedrock Knowledge Base met een Aurora Serverless v2 pgvector-cluster als vectoropslag en Amazon Titan Text Embeddings v2 als embedder. De S3-data-source wordt gevuld door de scraper.
De vraag is hoe title en source_url deterministisch per chunk bij het antwoord komen. Bedrock geeft geen grounding-supports-metadata terug waarin de chunk-bron is verwerkt. De applicatie haalt daarom expliciet evidence op en bepaalt zelf de citatie-presentatie.
Empirisch onderzocht
De Bedrock S3-data-source ondersteunt een sibling-sidecar per object: naast <key>.md staat <key>.md.metadata.json met getypeerde metadataAttributes. Bevindingen:
- Attributen in het sidecar-bestand komen op elke chunk van het bijbehorende document terug onder
metadatain hetRetrieve-antwoord. Multi-chunk-documenten fragmenteren de bron dus niet. includeForEmbeddingis per attribuut instelbaar.source_urlwordt uitgesloten van de embedding (een URL is retrieval-ruis),titlewordt wel mee-geembed (een nuttig retrieval-signaal).- Het sidecar-bestand wordt zelf niet als content geembed. Het telt apart als metadata-document in de ingest-statistieken.
Beslissing
Per markdown-document een .metadata.json-sidecar met source_url en title.
Concreet:
features/scraper/utils/artifact_store.pyschrijft naast elke<key>.mdeen<key>.md.metadata.jsonmetsource_url(STRING,includeForEmbedding: false) entitle(STRING,includeForEmbedding: true). Het sidecar-bestand self-healt op het unchanged-skip-pad,delete_markdownruimt het mee op, eniter_live_objectsslaat sidecars over.features/atlas/bedrock.py:_kb_retrieveroept de Bedrock Knowledge Base aan via de gedeelde client incommon/aws_clients.py(cross-account STS) en leestmetadata.source_urlenmetadata.titleper chunk. Het bouwt een genummerd evidence-blok en een ontdubbelde, geordende bronnenlijst.- Het LLM krijgt het genummerde evidence-blok en schrijft
[N]-citatiemarkers in het antwoord. De applicatie beslist vóór de invoke of retrieval nodig is. Triviale vragen (begroeting, herformulering) slaan de retrieve-stap over. - De
_BRON_LINE_RE-regex op een**Bron:**-regel blijft als transitionele fallback bestaan voor documenten die nog niet opnieuw zijn geingest. Die fallback gaat weg zodra de eenmalige volledige re-ingest is bevestigd.
Er is geen services/rag.py en geen |||-encoding in een display_name meer. De Atlas-citatiecode doet geen DB-lookup per chunk.
Alternatieven overwogen
- Manifest-lookup via een document-id:
ScrapedPageopvragen per chunk omsource_urlentitleop te halen. Werkt, maar voegt een DB-roundtrip per citatie toe (ongeveer drie tot vijf per antwoord). De sidecar levert dezelfde data zonder per-query DB-lookup en is daarom gekozen. - Markdown frontmatter (
source_url:als YAML-header bovenin de file): Bedrock ziet dit als chunk-tekst en neemt het mee in retrieval. Dit vervuilt de chunk-content en is verworpen. |||-encoding in een document-naam: een eerder gebruikt encoding-pattern in dedisplay_name. Bedrock heeft een getypeerd metadata-mechanisme, dus die encoding-hack is niet meer nodig.
Gevolgen
- De citatie is deterministisch en fragmenteert niet over chunks, omdat de sidecar-attributen op elke chunk terugkomen.
- De scraper en de Atlas-pipeline zijn ontkoppeld via het sidecar-contract (
source_url,title). Een wijziging in de citatie-presentatie raakt de scraper niet. - De
_BRON_LINE_RE-fallback is expliciet tijdelijk en wordt verwijderd zodra het corpus volledig opnieuw is geingest. - Auditors zien via de audit-log met scope
atlas_retrievewelke documenten zijn gerefereerd, zonder dat er strings worden geinterpreteerd.