Teams die een assistent over hun eigen documenten willen, stellen meestal dezelfde eerste vraag: trainen we een model op onze documenten, of koppelen we een model eraan? Voor een kennisbank is het standaardantwoord: koppelen, en dat is precies wat retrieval-augmented generation (RAG) doet. Bouw daarnaast een testset van echte vragen voordat je iets anders kiest. Deze notitie legt beide aanpakken in gewone taal uit, geeft een beslistabel en loopt daarna een test van twee weken door die je kunt uitvoeren met één engineer en één persoon die de documenten kent.
RAG en fine-tuning in gewone taal
Een taalmodel weet wat er in zijn trainingsdata zat. Het kent je retourbeleid niet, de prijslijst van vorige maand niet, en ook niet de interne procedure voor het goedkeuren van een leverancier.
RAG lost dat op het moment van de vraag op. Als iemand een vraag stelt, doorzoekt het systeem je documenten, neemt het de handvol passages die het meest relevant lijken, en geeft die aan het model met een instructie: antwoord op basis van deze passages en zeg welke je hebt gebruikt. De documenten blijven waar ze zijn. Pas een beleidsdocument aan, indexeer het opnieuw, en het volgende antwoord bevat de wijziging. De techniek werd beschreven in een onderzoekspaper uit 2020 die een tekstgenerator koppelde aan een doorzoekbare documentindex; het kernidee is sindsdien niet veranderd.
Fine-tuning traint een model verder op voorbeelden van inputs en de outputs die je wilt. Het verandert de gewoonten van het model: toon, structuur, woordkeuze, hoe het één smalle taak afhandelt. Het is een zwakke manier om feiten aan te leren die veranderen. Elke wijziging in de bron vraagt een nieuwe trainingsrun, het model kan niet aanwijzen waar een antwoord vandaan kwam, en alles wat het heeft geleerd, is beschikbaar voor iedereen die het kan gebruiken.
De meeste bedrijven werken al zo. Het rapport van Menlo Ventures over generatieve AI in het bedrijfsleven, van december 2025, vond dat promptontwerp de meest gebruikte manier is waarop bedrijven modellen aanpassen, met RAG op de tweede plaats; fine-tuning en reinforcement learning waren nog niche en werden vooral door frontier-teams gebruikt.
Wat het kost. De test van twee weken hieronder kost de tijd van één engineer en een paar dagen van een inhoudsdeskundige, plus modelgebruik dat op de schaal van een testset klein is. In productie volgen de draaikosten één formule: vragen per maand × tokens per vraag × prijs per token. Ter illustratie: 10.000 vragen per maand, die elk vijf passages van ongeveer 400 tokens plus 500 tokens aan instructies en vraag meesturen en een antwoord van 300 tokens krijgen, is 10.000 × 2.800 = 28 miljoen tokens. Tegen een illustratieve gemengde prijs van $5 per miljoen tokens is dat $140 per maand. Gebruik de actuele prijzen van je provider; het gaat om de formule. Als je het systeem gebouwd wilt hebben in plaats van getest, is onze gepubliceerde bandbreedte voor een interne RAG-copilot $8k–18k over 4–8 weken, en de prijspagina toont de tarieven daarachter.
De beslistabel
| Wat je nodig hebt | RAG | Fine-tuning |
|---|---|---|
| Kennis die wekelijks of dagelijks verandert | Wijzigingen zijn zichtbaar na de volgende herindexering | Elke wijziging vraagt een nieuwe trainingsrun en een nieuwe evaluatie |
| Een bronlink bij elk antwoord | Ingebouwd: het antwoord citeert de gebruikte passages | Geen betrouwbare manier om te zeggen waar een antwoord vandaan kwam |
| Verschillende mensen mogen verschillende documenten zien | Passages worden op de rechten van de gebruiker gefilterd voordat het model ze ziet | Wat erin getraind is, is zichtbaar voor elke gebruiker van het model |
| Een vast outputformat of een huisstijl in toon | Mogelijk met instructies en voorbeelden in de prompt, soms inconsistent | Past goed |
| Lage latency | Voegt een zoekstap en een langere prompt toe | Geen extra stap; een kleiner getuned model kan ook sneller zijn |
| Kosten van een update | De gewijzigde documenten opnieuw indexeren | Voorbeelden voorbereiden, opnieuw trainen, opnieuw evalueren |
| Benodigde data om te beginnen | De documenten en een testset van echte vragen | Voorbeeldinputs met ideale outputs, door mensen gecontroleerd, plus dezelfde testset |
Tel welke rijen voor jouw situatie tellen. Voor een kennisbank geven de eerste drie meestal de doorslag: de inhoud verandert, mensen willen de bron kunnen controleren, en niet iedereen mag alles lezen.
Fine-tuning verdient zijn plek in een kleinere groep gevallen: antwoorden moeten altijd een strikte structuur volgen; een taak met hoog volume, zoals tickets in categorieën indelen, zou op een kleiner, goedkoper model kunnen draaien; of de toon moet aansluiten bij een stijlgids die met prompten niet vol te houden is. Zelfs dan komt het meestal bovenop retrieval in plaats van het te vervangen, als een getuned model dat nog steeds de opgehaalde passages leest.
De standaard is dus eerst RAG plus een evaluatieset. Fine-tune later, voor format, toon of een smalle taak, en alleen als de testset laat zien dat de resterende fouten van die soort zijn.
De test van twee weken
De test vervangt meningen door een getal. Aan het eind heb je een score op je eigen vragen en een lijst met fouten, gesorteerd op oorzaak. Die vertelt je of je moet bouwen, wat je moet repareren, en of fine-tuning überhaupt zou helpen.
Stap 1: verzamel echte vragen
Neem vragen die mensen echt hebben gesteld: helpdesktickets, interne chatthreads, zoeklogs van het intranet, de vragen die de beleidseigenaar per e-mail beantwoordt. Mik op 150 tot 300. Neem 10–15% op die je documenten niet kunnen beantwoorden, want correct weigeren hoort bij het werk.
Waarom zoveel: met 200 vragen en een werkelijke nauwkeurigheid rond 80% is de foutmarge bij 95% ongeveer ±5,5 procentpunt (1,96 × √(0,8 × 0,2 / 200) ≈ 0,055). Een kleinere set kan twee versies van het systeem niet uit elkaar houden.
Voor elke vraag schrijft iemand die de materie kent het verwachte antwoord op en noemt het document waar het uit moet komen. Eén JSON-regel per vraag:
{"id": "q041", "question": "Who approves a new supplier above the purchasing threshold?", "expected": "Procurement lead and finance, both in writing.", "doc": "supplier-onboarding", "groups": ["procurement"], "answerable": true}
Elk antwoord krijgt daarna een van vier labels:
- correct: juist, volledig, en citeert het juiste document;
- partly (deels): juist maar onvolledig, of juist met een foute of ontbrekende bronvermelding;
- wrong (fout): beweert iets onjuists, of beantwoordt een vraag die de documenten niet kunnen beantwoorden;
- declined (geweigerd): zegt dat het geen antwoord kan geven.
Geweigerd is bewust een apart label. Bij een beantwoordbare vraag is het een misser, maar een goedkopere misser dan een fout antwoord, en de business bepaalt hoeveel van elk acceptabel is. Spreek die drempel af vóór de eerste run, op dezelfde manier als een metric wordt afgesproken voordat een model wordt getraind.
Een domeinexpert beoordeelt de eerste 50 antwoorden met de hand. Daarna kan een tweede model de rest beoordelen tegen de verwachte antwoorden, zolang de labels daarvan overeenkomen met die van de expert op die 50. Beide werkwijzen zijn gangbaar: in de enquête van LangChain onder 1.340 practitioners die agents bouwen, gehouden in november en december 2025, gebruikte 59,8% menselijke review, 53,3% een model als beoordelaar, en 52,4% draaide offline evaluaties op testsets.
Stap 2: indexeer passages met hun rechten
Splits documenten langs hun koppen op in passages van een paar honderd woorden. Bewaar bij elke passage het document-id, de titel, de link en de toegangsgroepen. Rechten horen vanaf het begin bij de passage; ze later toevoegen betekent alles opnieuw indexeren.
Stap 3: hybride retrieval met reciprocal rank fusion
Zoeken op trefwoorden (BM25) vindt exacte termen zoals productcodes, formuliernummers en afkortingen. Zoeken met embeddings vindt parafrases: "vrije dagen" als het beleid "jaarlijks verlof" zegt. Elk mist wat de ander vangt, dus gebruik beide en voeg de rangschikkingen samen.
Reciprocal rank fusion (RRF) voegt samen op positie in plaats van op score, zodat de twee scoreschalen nooit gekalibreerd hoeven te worden: elke passage krijgt uit elke lijst 1 / (k + rang), opgeteld. Het paper uit 2009 dat de methode introduceerde, zette k op 60 en vond die waarde bijna optimaal maar niet kritisch.
import numpy as np
from rank_bm25 import BM25Okapi
def tokenize(text):
return text.lower().split()
class HybridIndex:
def __init__(self, chunks, embed):
# chunks: dicts with "text", "doc", "groups" (a set); embed: list[str] -> unit vectors
self.chunks, self.embed = chunks, embed
self.bm25 = BM25Okapi([tokenize(c["text"]) for c in chunks])
self.vectors = np.asarray(embed([c["text"] for c in chunks]))
def search(self, query, user_groups, k=5, depth=50, rrf_k=60):
allowed = [i for i, c in enumerate(self.chunks) if c["groups"] & user_groups]
lexical = self.bm25.get_scores(tokenize(query))
semantic = self.vectors @ np.asarray(self.embed([query]))[0]
fused = {}
for scores in (lexical, semantic):
ranked = sorted(allowed, key=lambda i: scores[i], reverse=True)[:depth]
for rank, i in enumerate(ranked, start=1):
fused[i] = fused.get(i, 0.0) + 1.0 / (rrf_k + rank)
top = sorted(fused, key=fused.get, reverse=True)[:k]
return [{**self.chunks[i], "similarity": float(semantic[i])} for i in top]
embed is elke willekeurige embeddingfunctie, gehost of open-source; wisselen kost één argument. Het rechtenfilter draait vóór de rangschikking, dus een passage die de gebruiker niet mag openen, bereikt het model nooit.
Stap 4: antwoord met bronvermelding, of weiger
Twee checks voorkomen dat het systeem dingen verzint. Vóór de aanroep van het model: als de beste passage niet genoeg op de vraag lijkt, weiger dan zonder iets te genereren. Erna: elk antwoord moet passages citeren, en elk geciteerd nummer moet bestaan. Al het andere telt als geweigerd.
import re
PROMPT = """Answer the question using only the numbered passages below.
After each sentence, cite the passages you used, for example [2].
If the passages do not contain the answer, reply with exactly: NOT_FOUND
{passages}
Question: {question}"""
def answer(question, index, user_groups, llm, min_similarity=0.35):
hits = index.search(question, user_groups)
if not hits or max(h["similarity"] for h in hits) < min_similarity:
return {"status": "declined", "text": None, "docs": []}
passages = "\n\n".join(f"[{n}] ({h['doc']}) {h['text']}" for n, h in enumerate(hits, 1))
text = llm(PROMPT.format(passages=passages, question=question)).strip()
cited = {int(n) for n in re.findall(r"\[(\d+)\]", text)}
if text == "NOT_FOUND" or not cited or not cited <= set(range(1, len(hits) + 1)):
return {"status": "declined", "text": None, "docs": []}
return {"status": "answered", "text": text, "docs": [hits[n - 1]["doc"] for n in sorted(cited)]}
llm is elke functie die een prompt aanneemt en tekst teruggeeft, zodat het model te vervangen is zonder deze code aan te raken. De drempel van 0,35 is een placeholder: similarityschalen verschillen per embeddingmodel. Stel hem in op je testset, zo dat de onbeantwoordbare vragen worden geweigerd zonder te veel beantwoordbare kwijt te raken.
Stap 5: de scoringsloop
import json
from collections import Counter
def run_eval(path, index, llm, judge):
items = [json.loads(line) for line in open(path)]
labels, found = Counter(), 0
for item in items:
groups = set(item["groups"])
if item["answerable"]:
hits = index.search(item["question"], groups)
found += item["doc"] in {h["doc"] for h in hits}
result = answer(item["question"], index, groups, llm)
if not item["answerable"]:
label = "correct" if result["status"] == "declined" else "wrong"
elif result["status"] == "declined":
label = "declined"
else: # judge returns "correct", "partly" or "wrong"
label = judge(item["question"], item["expected"], result["text"])
labels[label] += 1
answerable = sum(1 for item in items if item["answerable"])
print({k: round(v / len(items), 3) for k, v in labels.items()})
print("expected document in top 5:", round(found / answerable, 3))
Het tweede getal is net zo belangrijk als het eerste. Als het juiste document niet in de top vijf stond, had het model nooit een kans, en zit de oplossing in de retrieval of in de documenten, niet in het model. Bewaar de labels van elke run met de datum en de wijziging die is doorgevoerd, zodat elke wijziging tegen dezelfde baseline wordt vergeleken.
Week 1 en week 2
Week 1: de baseline.
- Dag 1–2: verzamel en schrijf de vragen samen met de documenteigenaren; spreek de acceptatiedrempel af.
- Dag 3: indexeer de documenten; draai eerst alleen zoeken op trefwoorden, dan hybride, en noteer voor beide de top-5-hitrate.
- Dag 4–5: eerste volledige run; de expert beoordeelt 50 antwoorden; controleer de beoordelaar daartegen; beoordeel de rest.
Het resultaat van week 1 is een score en een lijst met fouten, verdeeld over vier oorzaken: het document ontbreekt of is verouderd; de retrieval miste de juiste passage; het model had de juiste passage en antwoordde toch slecht; het systeem had moeten weigeren en deed dat niet.
Week 2: repareer op volgorde van grootte.
- Begin bij de grootste oorzaak. Ontbrekende of verouderde documenten zijn een inhoudelijke taak voor hun eigenaren. Retrievalmissers reageren meestal op betere chunking, documenttitels en koppen die aan elke passage worden toegevoegd, of synoniemen voor intern jargon. Slechte antwoorden op basis van goede passages reageren op promptwijzigingen en soms op een ander model.
- Draai na elke wijziging de volledige set opnieuw. Houd een wijziging alleen als de score meer verschuift dan de foutmarge.
- Stel de weigerdrempel als laatste af, op de definitieve retrievalopzet.
Aan het eind van week 2 is de beslissing meestal duidelijk. Ligt het percentage foute antwoorden onder de afgesproken drempel, bouw dan de productieversie en houd de testset aan als releasepoort. Gaan de resterende fouten over format of toon terwijl de feiten kloppen, dan is dat het geval waarin fine-tuning het testen waard is, en de testset bestaat al om te laten zien of het hielp. Zijn de meeste fouten ontbrekende documenten, dan is de kennisbank het probleem, en geen enkele modelkeuze lost dat op.
Dit is de volgorde die we aanhouden bij het bouwen van RAG-copilots: eerst echte vragen, dan retrieval, dan een score die elke release moet verslaan.
Bronnen
- Menlo Ventures, 2025: The State of Generative AI in the Enterprise, december 2025 — https://menlovc.com/perspective/2025-the-state-of-generative-ai-in-the-enterprise/
- LangChain, State of Agent Engineering, december 2025 — https://www.langchain.com/state-of-agent-engineering
- NeurIPS 2020 (arXiv-preprint), Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, mei 2020 — https://arxiv.org/abs/2005.11401
- ACM SIGIR 2009, Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods, juli 2009 — https://doi.org/10.1145/1571941.1572114