Ga naar inhoud

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 metadata in het Retrieve-antwoord. Multi-chunk-documenten fragmenteren de bron dus niet.
  • includeForEmbedding is per attribuut instelbaar. source_url wordt uitgesloten van de embedding (een URL is retrieval-ruis), title wordt 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.py schrijft naast elke <key>.md een <key>.md.metadata.json met source_url (STRING, includeForEmbedding: false) en title (STRING, includeForEmbedding: true). Het sidecar-bestand self-healt op het unchanged-skip-pad, delete_markdown ruimt het mee op, en iter_live_objects slaat sidecars over.
  • features/atlas/bedrock.py:_kb_retrieve roept de Bedrock Knowledge Base aan via de gedeelde client in common/aws_clients.py (cross-account STS) en leest metadata.source_url en metadata.title per 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: ScrapedPage opvragen per chunk om source_url en title op 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 de display_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_retrieve welke documenten zijn gerefereerd, zonder dat er strings worden geinterpreteerd.