RAG
RAG voor bouwbesluit: een citation-gated agent-playbook
Een architectenbureau in Deventer krijgt 1.240 bouwbesluit-vragen per week. Dit is het RAG-playbook dat ze beantwoordt met op elke regel een bronvermelding.

Het is 9:43 op een dinsdag in Deventer. Mariëlle is senior projectarchitect op een schoolrenovatie in Apeldoorn, en de plantoetser van de gemeente vraagt zojuist of de nieuwe gymzaal nog voldoet aan de eisen voor daglichttoetreding onder Bbl-artikel 4.96. De bron die ze nodig heeft, ligt op drie plekken: een NEN 2580-meetrapport uit 2018, een Bouwbesluit-memo uit 2023 die al half herschreven was toen de Omgevingswet in januari 2024 inging, en een BIMcloud-commentaarthread uit de oorspronkelijke ontwerpfase van 2014. Ze heeft veertien minuten tot het telefoongesprek.
Dat is het moment waarop de RAG-agent moet landen. Geen chatbot-demo, geen “vat deze PDF eens samen”-speeltje. Een systeem dat een gereguleerde vraag iedere keer beantwoordt met de juiste bronvermelding, of netjes weigert.
Dit is het playbook dat we schreven voor een architectenbureau van 26 mensen in Deventer dat ongeveer 1.240 van zulke vragen per week behandelt, verdeeld over 38.200 NEN 2580-clausules, 14.600 Bbl-artikelen en een twaalf jaar oud Archicad BIMcloud-archief. Het lastige zit niet in de retrieval. Het lastige is ervoor zorgen dat er geen token in een vergunningsaanvraag-draft terechtkomt zonder een bronvermelding die een Wkb-kwaliteitsborger met droge ogen kan tekenen.
Chunken op de clausule, niet op de token
Het eerste instinct bij elk RAG-project is chunken op tekenaantal. Bij bouwregelgeving is dat een kunstfout.
Een Bbl-artikel heeft een vaste structuur: artikel.lid.onderdeel.sub. Een clausule die midden in een zin eindigt, verliest haar scope. Een clausule die samengevoegd is met haar buur, verliest haar uitzondering. We chunken op de kleinste juridisch adresseerbare eenheid — meestal het lid — en geven het volledige adres mee aan de chunk.
@dataclass
class Chunk:
text: str
clause_id: str # "bbl:4.96.2.a"
source_doc: str # "bbl" | "nen-2580" | "bimcloud"
version_date: date # 2024-01-01
parent_path: list[str] # ["Hfd. 4", "Afd. 4.4", "Par. 4.4.2"]
superseded_by: str | None
legal_basis: str # "Omgevingswet art. 4.3"
Het veld superseded_by is wat het systeem een wetswijziging laat overleven. Toen het Bouwbesluit 2012 in 2024 het Bbl werd, kreeg elke chunk die naar het oude artikel verwees een forward-link. De retriever volgt die. De generator krijgt in de system prompt mee dat elke bronvermelding ouder dan de peildatum van het project naar huidig recht herschreven moet worden voordat hij de agent verlaat.
Hybride retrieval met een clausule-graaf
Architecten vragen niet in één register. Binnen één gesprek wisselen ze tussen drie modi:
- “Wat staat er in 4.96 lid 2?” — exacte lookup, BM25 wint altijd.
- “Mag een binnenwand bij een schoolgebouw uit gipsblokken?” — semantisch, dense vectors winnen.
- “Welke artikelen verwijzen naar NEN 6068?” — graph-query, geen van beide volstaat.
We draaien alle drie en re-ranken daarna. BM25 op de clausuletekst en het adres. Dense embeddings (we gebruiken bge-m3 — dat doet Nederlands en de juridische taal redelijk goed) op een concatenatie van clausuletekst plus de bovenliggende paragraafkoppen. En een clausule-graaf in Neo4j die elke kruisverwijzing, elke tenzij-uitzondering en de koppeling tussen een NEN-norm en het Bbl-artikel dat hem per verwijzing incorporeert vastlegt.
De re-ranker is een kleine cross-encoder die we hebben fine-tuned op zo'n 3.000 vraag/clausule-paren die de senior architecten van het bureau in twee weken labelden. Die labelronde is niet onderhandelbaar. Zonder dat lever je een model dat indrukwekkend scoort op MTEB en magertjes op een echte plantoetser-vraag.
Generatie met bronvermelding eerst
De generator spreekt nooit als eerste. Hij citeert eerst.
We forceren een two-pass-output. Pass één is een JSON-object met {claim, citation}-paren. Pass twee rijgt ze aan elkaar tot Nederlands proza. Een guard tussen de passes weigert het antwoord als een claim geen citation heeft, als de chunk achter een citation de tekst van de claim niet daadwerkelijk onderbouwt (we checken met een kleine NLI-head), of als het adres van de citation niet bestaat in de clausule-graaf.
def validate(pairs: list[ClaimCitation], graph: ClauseGraph) -> Result:
for p in pairs:
if not p.citation:
return Reject("uncited claim", p.claim)
chunk = graph.get(p.citation)
if chunk is None:
return Reject("hallucinated address", p.citation)
if not entails(chunk.text, p.claim, threshold=0.82):
return Reject("citation does not support claim", p)
return Accept()
Geweigerde antwoorden komen niet bij de architect terecht. Ze gaan naar een needs human-queue met het mislukte paar erbij. Na acht weken productieverkeer zat die queue gemiddeld op 31 items per week — ongeveer 2,5% van de vragen — en bijna allemaal bleken ze oprecht dubbelzinnig, geen modelfouten.
Het BIMcloud-archief dat niemand had geïndexeerd
Het Archicad BIMcloud-archief van twaalf jaar is waar elk adviesbureau het werk onderschat. Het is geen database. Het is sediment.
Laag één zijn de IFC-exports — geometrie plus een dunne laag property sets. Laag twee zijn de naar PDF gedrukte plantekeningen, waarvan de helft van vóór 2017 nog van papier gescand. Laag drie is een kluwen van commentaarthreads, change requests en “zie e-mail Jeroen 14/3”-annotaties die alleen logisch zijn als je er destijds bij in de kamer zat.
Wat we bouwden zit in drie jobs:
- Een nightly walker over de BIMcloud REST API die per project snapshots maakt van IFC- en PDF-exports, met fingerprints zodat we alleen herindexeren wat veranderd is.
- OCR over de pre-2017 PDF's met
ocrmypdf(Tesseract-backend, Nederlandse en Engelse taalpacks). Confidence onder de 0,7 wordt gemarkeerd en komt nooit in de index. - Een IFC-naar-clausule-linker die property sets als
Pset_FireSafetyuitleest en een kandidaat Bbl-artikel voorstelt. Een senior architect bevestigt of verwerpt in een UI van 30 seconden die we voor ze hebben gebouwd. Zes weken later keurt de linker zelf goed met een precision van 0,9 en is de menselijke queue op de meeste dagen leeg.
Het archief is read-only voor de agent. Het levende projectwerk gebeurt in de huidige Archicad-bestanden; de agent gebruikt het archief voor “wat hebben we hier in 2019 ook al weer over geschreven”-vragen. Die scheiding telt voor de Wkb-poort.
Wkb als harde poort, geen vinkje
De Wet kwaliteitsborging voor het bouwen maakte van de kwaliteitsborger een benoemde, aansprakelijke partij voor gevolgklasse 1-projecten, en dat dekt inmiddels het grootste deel van wat dit bureau bouwt. Dat heeft gevolgen voor elk agentisch systeem dat je in de buurt van een vergunning zet.
We hebben de agent in twee paden gesplitst:
- Read path. Beantwoordt vragen, citeert clausules, brengt BIMcloud-passages naar boven. Geen output die een vergunningsdocument raakt. Geen structured writes.
- Draft path. Genereert concepttekst voor hoofdstukken van een vergunningsaanvraag. Elke alinea vraagt om een handtekening van de kwaliteitsborger in het dashboard voordat de tekst geëxporteerd kan worden naar het officiële template.
Die splitsing is geen UX-versiering. Hij wordt afgedwongen op de API-gateway. Read-path-antwoorden dragen content-type bbl:answer:v1. Draft-path-antwoorden dragen bbl:draft:v1 en vereisen een HMAC-header die getekend is met het hardware token van de kwaliteitsborger. Geen handtekening, geen draft, einde verhaal.
Als de kwaliteitsborger dezelfde persoon is die de agent in het dagelijks werk gebruikt, heb je geen poort gebouwd. Dan heb je toneel gebouwd. Trek de rol los van de stoel.
Identiteit en de agent die om 3 uur 's nachts draait
Twee nieuwsberichten van de afgelopen twee maanden veranderden hoe we agent-identiteit inrichten voor dit bureau.
Anthropic kondigde aan dat bepaalde API-capaciteiten vanaf 8 juli 2026 identiteitsverificatie vereisen. Voor een bureau dat 's nachts agents draait tegen een gereguleerd corpus is dat geen drempel maar een feature: de identiteit van de kwaliteitsborger hoort bij het API-account, niet bij een stub van een service-account. We hebben de facturatie verplaatst naar een geverifieerde Anthropic Console-workspace die op naam staat van de compliance-lead van het kantoor. De audit trail begint bij de rekening.
Voor ephemere runs — de per-project sandboxes die de agent gebruikt voor eenmalige drafts — namen we het patroon over dat Cloudflare beschreef voor tijdelijke accounts gescoped op AI-agents. Elke nightly job spint een scoped account op met een TTL van 24 uur, project-scoped rechten en een audit log die in dezelfde store eindigt als de sign-offs van de kwaliteitsborger. Als er iets misgaat, is de blast radius één project en één dag.
Wat we wekelijks meten
De senior partner van het bureau vroeg niet om een AI-dashboard. Hij stelde vier vragen, en daar bouwden we het dashboard omheen:
- Hoeveel vragen heeft de agent deze week beantwoord, en hoeveel zijn er geëscaleerd naar een mens?
- Bij die escalaties: hoe vaak was de agent terecht voorzichtig, en hoe vaak faalde de retrieval?
- Wat is de mediane tijd van vraag tot beantwoord-met-bron? Doel: onder de 6 seconden. Huidig: 4,1.
- Is er deze week draft-path-output zonder handtekening van de kwaliteitsborger het systeem uitgegaan? Doel: nul. Werkelijk: nul, bewaakt als een hard alarm.
De tweede metric is degene die de meeste teams overslaan. Een RAG-systeem dat te vaak weigert, is niet veiliger; het is alleen luider nutteloos. We taggen elke weigering met een reden-code en bespreken de top drie elke week met de architecten die ze hebben meegemaakt.
Wat je op dag één kunt overslaan
Drie dingen lijken essentieel voor dit soort probleem en zijn dat niet:
- Een fine-tuned generator. Je gelabelde voorbeelden horen eerst naar de re-ranker. Het basismodel heeft alleen een strakke system prompt en de citation-guard nodig.
- Een vector database met een chique filtertaal. Postgres plus
pgvectorplus eenclause_addressbtree-index handelt dit corpus prima af, en de bestaande IT-mensen van het bureau kunnen het beheren zonder een nieuw vendor-contract. - Een streaming UI. Architecten willen het volledige onderbouwde antwoord of niets. Een half gestreamde citation leest als een half gevormd juridisch advies, en zo zullen ze het ook voor een plantoetser hanteren.
Het werk achter één alinea met bronvermelding
Toen we dit bouwden voor het bureau in Deventer, was het ding waar we steeds tegenaan liepen het gat tussen het model produceerde een citation en de citation onderbouwt de claim ook echt. De NLI-guard tussen de twee generatie-passes was de ene wijziging die de kwaliteitsborger bereid maakte überhaupt zijn handtekening onder het draft-pad te zetten. Als je een soortgelijk systeem aan het scopen bent, dan is dat waar de eerstvolgende twee weken van je engineering-budget heen moeten — ruim voordat je gaat debatteren welk embedding-model je kiest. De architectuur zit in onze AI-agents-praktijk als je hem tegen je eigen corpus wilt doorlopen.
Het kleinste dat je vandaag kunt doen: pak één week aan bouwbesluit-vragen van je bureau, label naar welk artikel ze eigenlijk moeten verwijzen, en draai een eenvoudige BM25-baseline over de Bbl-tekst. Haalt BM25 alleen al boven de 60% top-3-accuracy op clausule-adressen, dan is je retrieval-probleem klein en je prompt-probleem groot. Lukt dat niet, dan is retrieval het project, en is al het andere decoratie.
Kern
Op gereguleerde corpora zit de RAG-winst niet in een groter model. Hij zit in een chunker op clausule-niveau, een citation-guard die niet-onderbouwde claims weigert, en een getekend draft-pad.
FAQ
Waarom chunken op het lid in plaats van een vast tekenvenster?
Een lid is de kleinste eenheid waarop een Bbl-artikel geciteerd kan worden. Midden in een lid splitsen verliest de uitzondering of de scope, en twee leden samenvoegen levert een bronvermelding op die zo niet in de wet voorkomt.
Schrijft de agent de vergunningsaanvraag zelf?
Alleen op het draft-pad, en alleen alinea voor alinea nadat de kwaliteitsborger elke alinea heeft getekend met een hardware token. Antwoorden op het read-pad komen nooit in een vergunningsdocument terecht.
Heb je een fine-tuned LLM nodig voor Nederlandse bouwregelgeving?
Niet op dag één. De gelabelde voorbeelden leveren op de re-ranker meer op dan op de generator. Een strakke system prompt plus een citation-guard dekt het gros.
Hoe ging je om met de Omgevingswet-overgang van 2024 in oude bronvermeldingen?
Elke chunk draagt een superseded_by-link. Bronvermeldingen ouder dan de peildatum van het project worden vóór het renderen doorgezet naar hun huidige Bbl-equivalent.
Welke stack draait de retrieval-laag?
Postgres met pgvector voor de dense en adres-indexen, een externe BM25-service voor de lexicale lookups, en Neo4j voor de clausule-kruisverwijzingsgraaf. Geen nieuwe vendor toegevoegd.