Wat is RAG en waarom is het game-changing voor MKB?
RAG staat voor Retrieval Augmented Generation. Het is een techniek waarbij een AI-model niet alleen vertrouwt op zijn eigen getrainde kennis, maar ook actief relevante informatie opzoekt in een database van jouw eigen documenten. Het resultaat: een AI-assistent die vragen beantwoordt op basis van jouw specifieke bedrijfskennis, niet op basis van algemene internetkennis.
Stel je voor: je hebt honderden pagina's aan productdocumentatie, interne procedures, klantcontracten en FAQ-documenten. Met een RAG-applicatie kun je een chatbot bouwen die precies die informatie raadpleegt wanneer een medewerker of klant een vraag stelt. Geen hallucinations over dingen die het model niet weet, maar feitelijke antwoorden uit jouw eigen documenten.
Voor MKB is dit game-changing. Kennisbeheer is een van de grootste uitdagingen: kennis zit in de hoofden van medewerkers, verspreid over Dropbox-mappen, e-mailthreads en Word-documenten. Een RAG-applicatie ontsluit die kennis en maakt het doorzoekbaar via een conversatie-interface. En met LangChain in Python bouw je zo'n applicatie in minder dan een middag.
Stap 1: Omgeving opzetten
We beginnen met een schone Python-omgeving. Zorg dat je Python 3.11 of hoger hebt geinstalleerd. Maak een virtuele omgeving aan en installeer de benodigde packages.
# Maak een projectmap aan
mkdir rag-mkb && cd rag-mkb
# Maak een virtual environment
python3 -m venv venv
source venv/bin/activate # Op Windows: venv\Scripts\activate
# Installeer alle benodigde packages
pip install langchain langchain-openai langchain-community faiss-cpu pypdf python-dotenv gradio fastapi uvicorn
Toelichting op de packages: langchain is het kernframework, langchain-openai bevat de OpenAI-integratie, langchain-community bevat document loaders en vector store integraties. faiss-cpu is de vector database van Meta voor similarity search. pypdf is nodig voor het lezen van PDF-bestanden. python-dotenv laadt je API-sleutels uit een .env-bestand.
Stap 2: API keys instellen
Maak een .env-bestand aan in je projectmap met je API-sleutels. Voeg dit bestand toe aan je .gitignore zodat je sleutels nooit per ongeluk in je repository terechtkomen.
# .env - voeg dit bestand toe aan .gitignore!
OPENAI_API_KEY=sk-proj-jouw-openai-key-hier
ANTHROPIC_API_KEY=sk-ant-jouw-claude-key-hier
# Optioneel: als je Mistral wilt gebruiken
MISTRAL_API_KEY=jouw-mistral-key-hier
Laad de sleutels in Python met python-dotenv. Voeg dit bovenaan elk script toe:
from dotenv import load_dotenv
import os
load_dotenv()
openai_key = os.getenv("OPENAI_API_KEY")
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
Stap 3: Documenten laden
LangChain heeft een uitgebreide bibliotheek van DocumentLoaders voor vrijwel elk bestandsformaat. Hieronder de meest gebruikte voor MKB-toepassingen.
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
DirectoryLoader,
Docx2txtLoader
)
# Een enkel PDF-bestand laden
loader = PyPDFLoader("documenten/productcatalogus.pdf")
documenten = loader.load()
print(f"Geladen: {len(documenten)} pagina's")
# Een tekstbestand laden
txt_loader = TextLoader("documenten/procedures.txt", encoding="utf-8")
tekst_docs = txt_loader.load()
# Een hele map met PDF-bestanden laden
dir_loader = DirectoryLoader(
"./documenten/",
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
alle_docs = dir_loader.load()
print(f"Totaal geladen: {len(alle_docs)} documenten")
Tip: zorg dat je documenten goede bestandsnamen hebben. LangChain slaat de bestandsnaam op als metadata, zodat je later kunt zien uit welk document een antwoord afkomstig is.
Stap 4: Documenten chunken
Documenten zijn te lang om in een keer als context mee te sturen. We verdelen ze in kleinere stukken (chunks). De grootte van die chunks heeft grote invloed op de kwaliteit van je RAG-applicatie.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Configureer de text splitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # Maximaal 1000 tekens per chunk
chunk_overlap=200, # 200 tekens overlap tussen chunks (voor context-continuiteit)
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""] # Prefereer splitsen op alineas
)
# Splits alle geladen documenten
chunks = text_splitter.split_documents(alle_docs)
print(f"Aantal chunks: {len(chunks)}")
print(f"Voorbeeld chunk: {chunks[0].page_content[:200]}")
Over chunk_size en chunk_overlap: kleinere chunks geven preciezere retrieval maar minder context per chunk. Grotere chunks geven meer context maar kunnen ruis bevatten. Begin met chunk_size=1000 en chunk_overlap=200 en experimenteer op basis van de kwaliteit van de antwoorden.
Stap 5: Embeddings genereren
Embeddings zijn numerieke representaties van tekst waarmee je semantische gelijkheid kunt berekenen. We zetten elk chunk om naar een embedding-vector.
from langchain_openai import OpenAIEmbeddings
# Alternatief: gratis HuggingFace embeddings
# from langchain_community.embeddings import HuggingFaceEmbeddings
# OpenAI embeddings (text-embedding-3-small is goedkoop en goed)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Gratis alternatief via HuggingFace (lokaal, geen API-kosten)
# embeddings = HuggingFaceEmbeddings(
# model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
# ) # Ondersteunt ook Nederlands!
Budget-tip: OpenAI's text-embedding-3-small kost vrijwel niets. Voor 1.000 pagina's bedrijfsdocumenten betaal je minder dan 0,50 euro voor de embeddings. De gratis HuggingFace optie is prima als je geen externe API wilt gebruiken.
Stap 6: Vector store aanmaken en opslaan
We slaan de embeddings op in een FAISS vector store. FAISS (van Meta) is razendsnel en draait volledig lokaal. We slaan de index op schijf op zodat we hem niet elke keer opnieuw hoeven te bouwen.
from langchain_community.vectorstores import FAISS
import os
FAISS_INDEX_PAD = "./faiss_index"
# Bouw de vector store (doe dit eenmalig of als documenten wijzigen)
if not os.path.exists(FAISS_INDEX_PAD):
print("Vector store aanmaken...")
vectorstore = FAISS.from_documents(chunks, embeddings)
vectorstore.save_local(FAISS_INDEX_PAD)
print(f"Vector store opgeslagen: {len(chunks)} chunks geindexeerd")
else:
print("Bestaande vector store laden...")
vectorstore = FAISS.load_local(
FAISS_INDEX_PAD,
embeddings,
allow_dangerous_deserialization=True
)
print("Vector store geladen")
De FAISS index bouwen duurt even (afhankelijk van het aantal documenten), maar daarna laad je hem in seconden. Als je documenten toevoegt of wijzigt, verwijder je de index en bouw je hem opnieuw.
Stap 7: Retriever instellen
De retriever zoekt in de vector store naar de meest relevante chunks voor een gegeven vraag. Er zijn twee hoofdstrategieen: similarity search (de meest semantisch gelijkende chunks) en MMR (Maximal Marginal Relevance, die diversiteit toevoegt om herhaling te voorkomen).
# Optie 1: Similarity search (standaard)
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4} # Haal 4 meest relevante chunks op
)
# Optie 2: MMR voor meer diverse resultaten
retriever_mmr = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={
"k": 4,
"fetch_k": 20, # Haal 20 kandidaten op, kies dan 4 diverse
"lambda_mult": 0.5 # 0 = max diversiteit, 1 = max relevantie
}
)
# Test de retriever
vraag = "Wat zijn onze leveringstijden?"
resultaten = retriever.invoke(vraag)
for doc in resultaten:
print(f"Bron: {doc.metadata.get('source', 'onbekend')}")
print(f"Tekst: {doc.page_content[:200]}\n")
Stap 8: RAG-chain bouwen
Nu combineren we de retriever met een LLM om de volledige RAG-chain te bouwen. We voegen een systeemprompt toe die het model instrueert om in het Nederlands te antwoorden en alleen informatie uit de documenten te gebruiken.
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# Kies je LLM
llm = ChatOpenAI(
model="gpt-4o-mini", # Goedkoop en snel voor productie
temperature=0
)
# Alternatief: Claude via Anthropic
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-3-5-haiku-20241022", temperature=0)
# Systeemprompt die aanstuurt op Nederlandse antwoorden
prompt_template = """Je bent een behulpzame assistent voor {bedrijfsnaam}.
Gebruik ALLEEN de onderstaande context om de vraag te beantwoorden.
Als het antwoord niet in de context staat, zeg dat dan eerlijk.
Antwoord altijd in het Nederlands.
Context:
{context}
Vraag: {question}
Antwoord:"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question", "bedrijfsnaam"]
)
# Bouw de RAG chain
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": PROMPT}
)
# Test de chain
resultaat = qa_chain.invoke({"query": "Wat is ons retourbeleid?"})
print("Antwoord:", resultaat["result"])
print("Bronnen:", [doc.metadata.get('source') for doc in resultaat["source_documents"]])
De parameter return_source_documents=True zorgt ervoor dat je ook kunt zien welke documenten zijn gebruikt voor het antwoord. Dit is waardevol voor verificatie en voor het bouwen van vertrouwen bij gebruikers.
Stap 9: Chatbot interface bouwen
Nu de RAG-chain werkt, bouwen we een gebruiksvriendelijke interface. Gradio maakt het mogelijk om in minuten een web-UI te bouwen die je kunt delen met collega's.
import gradio as gr
def beantwoord_vraag(vraag, geschiedenis):
"""Verwerk een vraag via de RAG chain en geef antwoord + bronnen."""
if not vraag.strip():
return "", geschiedenis
resultaat = qa_chain.invoke({"query": vraag})
antwoord = resultaat["result"]
# Voeg bronnen toe aan het antwoord
bronnen = list(set([
doc.metadata.get('source', 'onbekend')
for doc in resultaat["source_documents"]
]))
if bronnen:
antwoord += f"\n\nBronnen: {', '.join(bronnen)}"
geschiedenis.append((vraag, antwoord))
return "", geschiedenis
# Gradio interface
with gr.Blocks(title="Bedrijfs-AI Assistent", theme=gr.themes.Soft()) as demo:
gr.Markdown("# Bedrijfs-AI Assistent\nStel vragen over onze bedrijfsdocumenten.")
chatbot = gr.Chatbot(height=400)
msg = gr.Textbox(placeholder="Stel een vraag over onze producten of procedures...")
wis = gr.Button("Gesprek wissen")
msg.submit(beantwoord_vraag, [msg, chatbot], [msg, chatbot])
wis.click(lambda: None, None, chatbot, queue=False)
if __name__ == "__main__":
demo.launch(share=False, server_port=7860)
Start de Gradio interface met python app.py en open http://localhost:7860 in je browser. Je ziet direct een werkende chatbot die vragen beantwoordt op basis van jouw eigen documenten.
Stap 10: Deployen als FastAPI endpoint
Voor productiegebruik bouwen we een REST API met FastAPI. Zo kan de chatbot worden geintegreerd in bestaande systemen, websites of tools als n8n en Make.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
app = FastAPI(
title="RAG API - Bedrijfsdocumenten",
description="Beantwoord vragen op basis van bedrijfsdocumenten",
version="1.0.0"
)
class Vraag(BaseModel):
question: str
max_sources: Optional[int] = 3
class Antwoord(BaseModel):
answer: str
sources: List[str]
confidence: Optional[float] = None
@app.post("/ask", response_model=Antwoord)
async def stel_vraag(vraag: Vraag):
"""Beantwoord een vraag op basis van bedrijfsdocumenten."""
if not vraag.question.strip():
raise HTTPException(status_code=400, detail="Vraag mag niet leeg zijn")
resultaat = qa_chain.invoke({"query": vraag.question})
bronnen = list(set([
doc.metadata.get('source', 'onbekend')
for doc in resultaat["source_documents"]
]))[:vraag.max_sources]
return Antwoord(
answer=resultaat["result"],
sources=bronnen
)
@app.get("/health")
async def health_check():
return {"status": "ok", "chunks_geladen": len(chunks)}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Start de API met python api.py. De API is beschikbaar op http://localhost:8000. Test met: curl -X POST http://localhost:8000/ask -H 'Content-Type: application/json' -d '{"question": "Wat is het retourbeleid?"}'
Troubleshooting: 5 veelvoorkomende problemen
Probleem 1: Rate limit errors van OpenAI. Oplossing: voeg exponential backoff toe met de tenacity library. Pip install tenacity en decoreer je API-aanroepen met @retry(wait=wait_exponential(min=1, max=60), stop=stop_after_attempt(6)).
Probleem 2: Lege retrieval (relevante documenten worden niet gevonden). Controleer of je documenten correct zijn geladen en gechunkt. Test de retriever direct: resultaten = retriever.invoke(je_vraag). Als de resultaten leeg zijn, is de vector store mogelijk leeg. Verwijder de FAISS-index en bouw hem opnieuw.
Probleem 3: Hallucinations (model verzint informatie die niet in de documenten staat). Verlaag de temperature naar 0 en maak de systeemprompt strenger: voeg toe 'Als het antwoord NIET in de context staat, antwoord dan: Ik heb geen informatie over dit onderwerp in de beschikbare documenten.'
Probleem 4: Encoding errors bij het laden van documenten. Voeg encoding='utf-8' toe aan de TextLoader. Voor PDF-bestanden met speciale tekens gebruik je PyMuPDF als alternatief: pip install pymupdf en from langchain_community.document_loaders import PyMuPDFLoader.
Probleem 5: Trage performance bij grote documentcollecties. Gebruik FAISS met GPU-ondersteuning (pip install faiss-gpu) of overweeg een persistente vector database als ChromaDB of Qdrant voor grote collecties. ChromaDB ondersteunt incrementeel toevoegen van documenten zonder de hele index te herbouwen.
Uitbreidingen: verder bouwen
Claude als alternatief LLM: vervang ChatOpenAI door ChatAnthropic. Claude Haiku is vergelijkbaar qua kosten met GPT-4o-mini maar sterkt in instructievolging en nuance. Ideaal voor klantenservice-toepassingen waarbij toon en nauwkeurigheid belangrijk zijn.
Chroma als alternatieve vector store: ChromaDB is een open-source vector database die persistentie ingebouwd heeft en incrementeel bijwerken ondersteunt. Beter dan FAISS voor situaties waarbij je regelmatig nieuwe documenten toevoegt. Installeer met pip install chromadb en vervang FAISS.from_documents door Chroma.from_documents.
Streaming responses: voor een betere gebruikerservaring voeg je streaming toe zodat het antwoord woord voor woord verschijnt. Gebruik llm = ChatOpenAI(streaming=True) en verwerk de stream in je Gradio of FastAPI endpoint.
Conversatiegeheugen: de huidige implementatie heeft geen geheugen van eerdere vragen in hetzelfde gesprek. Voeg dit toe met ConversationalRetrievalChain in plaats van RetrievalQA, gecombineerd met ConversationBufferMemory. Zo kan de gebruiker doorvragen op eerdere antwoorden.
Met deze handleiding heb je alle bouwstenen voor een productie-klare RAG-applicatie. Begin klein: laad een paar documenten, test de kwaliteit van de antwoorden, en breid daarna stap voor stap uit. De investering in technische setup betaalt zich terug in tijdsbesparing voor medewerkers die dagelijks vragen stellen over bedrijfsinformatie.