Eenvoudige RAG-implementatie: een stapsgewijze tutorial

Deze beginnersvriendelijke tutorial legt uit hoe een eenvoudige RAG-implementatie werkt, behandelt best practices en laat stap voor stap zien hoe je met Python, ChromaDB en de embedding- en taalmodellen van Perplexity een werkend RAG-systeem voor de opdrachtregel bouwt.

Basic RAG implementation process diagram: the interface sends input to the orchestrator, which retrieves data from a vector database, builds the input context for the LLM, and returns the output to the interface.

RAG is een essentieel onderdeel van moderne, op LLM’s gebaseerde infrastructuur. Doordat AI-modellen toegang krijgen tot externe gegevens, helpt RAG problemen zoals hallucinaties en onvolledige trainingsdata aan te pakken. Bovendien kunnen bedrijven zo interne kennisbanken gebruiken die gevoelige of bedrijfseigen informatie bevatten.

Een RAG-implementatie kan zeer complex zijn. Toch is een eenvoudig RAG-systeem verrassend makkelijk te bouwen. Voor beginners kan het opzetten van een eenvoudige architectuur verhelderen hoe RAG in de praktijk werkt, en het biedt een springplank voor verdere experimenten.

In dit artikel brengen we een eenvoudige RAG-implementatie in kaart, bespreken we best practices en bouwen we vervolgens een werkend systeem met Python, ChromaDB en twee Perplexity-modellen voor embedding en het genereren van antwoorden.

Het proces van een eenvoudige RAG-implementatie: een globaal overzicht

Een eenvoudig RAG-systeem bestaat uit drie hoofdonderdelen: een orchestrator, een informatiebron en een LLM. In een typische workflow voert een gebruiker een prompt in, waarna de orchestrator die aanpast om alle gekoppelde informatiebronnen te doorzoeken. De opgehaalde informatie wordt vervolgens aan de oorspronkelijke invoercontext toegevoegd en naar de LLM gestuurd, zodat er een antwoord kan worden gegenereerd.

Bij het bouwen van je RAG-implementatie moet je rekening houden met deze stappen:

  1. Indexering en embedding: In deze fase worden je documenten omgezet in een informatiebron die de orchestrator kan doorzoeken. Een van de meest gebruikte manieren om dit te doen, is het bouwen van een doorzoekbare vectorindex. We gebruiken een speciaal model van Perplexity om je documentchunks om te zetten in embeddings. Vervolgens slaan we de embeddings op met ChromaDB, een populaire opensource-vectordatabase.
  2. Orkestratie: Dit is de applicatie die zich tussen de externe informatiebron (in ons geval een vectordatabase) en de LLM bevindt. We maken een eenvoudig Python-script dat als orchestrator fungeert.
  3. Ophalen en genereren: Wanneer een gebruiker via de chatinterface een vraag stelt, neemt de orchestrator die in ontvangst, voert een vectorzoekopdracht uit en geeft de uitgebreide invoer door aan de LLM. In plaats van een aparte frontend te bouwen, gebruiken we de opdrachtregel om met onze Python-orchestrator te communiceren.

Best practices: wat maakt een RAG-implementatie effectief?

Er bestaan verschillende soorten RAG-architectuur, en elke use case vraagt om een andere aanpak. Toch gelden er een aantal best practices in alle gevallen, die zorgen voor nauwkeurige, kosteneffectieve, schaalbare en onderhoudbare systemen.

De volgende best practices gelden voor alle soorten RAG-implementaties:

  • Zorg voor een schone ingestiepipeline: Je brongegevens moeten vóór het indexeren zo schoon mogelijk zijn. Gebruik daarnaast een embeddingmodel dat bij je content past.
  • Gebruik hybride zoeken: Welke ophaalmethoden het beste werken, of het nu gaat om zoeken in een vectordatabase, webzoekopdrachten via een API, het doorlopen van een kennisgraaf, zoeken op trefwoorden of andere methoden, verschilt per use case. De juiste mix kiezen is belangrijk om de kosten zo laag mogelijk te houden.
  • Evalueer je systeem: Na de implementatie moet je je systeem evalueren op drie kernmaatstaven: nauwkeurigheid, relevantie en latentie. Nauwkeurigheid verwijst naar feitelijke juistheid, relevantie naar de mate waarin de opgehaalde informatie aansluit bij de vragen van gebruikers, en latentie naar de snelheid waarmee informatie wordt opgehaald.
  • Ga veilig om met informatie: Als je RAG-systeem gebruikmaakt van vertrouwelijke interne informatie, zorg er dan voor dat die veilig wordt opgeslagen en dat de toegang tot de systeeminterface wordt beheerd. Deel API-sleutels nooit.

Eenvoudige RAG-implementatie: tutorial in 7 stappen

In deze tutorial bouwen we een opdrachtregelapplicatie die vragen over één document beantwoordt. We gebruiken Python, ChromaDB en de Perplexity API.

Het doel is niet om een productieklaar RAG-systeem te bouwen. Het is eerder een beginnersvriendelijke tutorial die laat zien hoe een elementaire RAG-architectuur in de praktijk werkt.

Stap 1: installeer de software

Zorg ervoor dat het volgende op je computer is geïnstalleerd en dat je een Perplexity API-account hebt:

  • Python: Dit is de programmeertaal waarmee we de orchestrator bouwen die de gegevensstroom tussen het embeddingmodel, ChromaDB en Perplexity coördineert.
  • Visual Studio: We gebruiken Visual Studio als code-editor. Open na de installatie het paneel Extensions en download de extensie Python.
  • Perplexity API-account: Perplexity levert zowel het embeddingmodel (dat je gegevens omzet in vector-embeddings) als de LLM die de uitgebreide prompt verwerkt. Je hebt hiervoor geen betaald account nodig, al kost het wel een klein aantal credits.

Stap 2: maak een projectmap aan

Maak eerst een map met de naam rag-project aan en open deze in VS Code via File > Open Folder. Open daarna de geïntegreerde terminal via Terminal > New Terminal.

Voer het volgende in om een virtuele Python-omgeving aan te maken:

bash
py -3.12 -m venv .venv

Nu moet je vier Python-bibliotheken installeren: perplexityai, chromadb, python-dotenv en numpy. Met de Perplexity SDK kun je via de API verbinding maken met de modellen van Perplexity. ChromaDB bouwt de vectordatabase en draait lokaal. Python-dotenv laadt je API-sleutels veilig vanuit een lokaal bestand, en NumPy is nodig tijdens het ingestieproces.

Installeer ze met de volgende opdracht:

bash
.\.venv\Scripts\python.exe -m pip install perplexityai chromadb python-dotenv numpy

Stap 3: stel je bestanden in

Voeg de volgende bestanden toe aan rag-project (de hoofdmap van het project) door in de zijbalk van de Explorer New File… te selecteren:

  • index.py: Dit is de applicatie die je document indexeert. Je kunt deze opnieuw uitvoeren om meer tekst toe te voegen.
  • rag.py: Dit is de Python-applicatie die als orchestrator fungeert.
  • document.txt: Dit bestand bevat de documentatie die je omzet in een vectordatabase.
  • .env: Hierin bewaar je je Perplexity API-sleutel.

Voer in .env het volgende in:

plaintext
PERPLEXITY_API_KEY=your_api_key_here

Vervang your_api_key_here door je eigen sleutel. Ga hiervoor naar het startdashboard van de Perplexity API Console en selecteer Create an API key. Houd deze API-sleutel privé.

We gebruiken twee modellen: pplx-embed-v1-0.6b van Perplexity om onze documentatie te embedden voordat we die uploaden naar ChromaDB, en GPT-5.6-Luna (toegankelijk via de API van Perplexity), dat de opgehaalde informatie verwerkt.

Voeg in document.txt de tekst toe die je RAG-pipeline moet ophalen. Heb je geen document bij de hand? Gebruik dan voor deze tutorial Songs of Innocence and of Experience van William Blake uit Project Gutenberg. Het is een korte, goed te begrijpen tekst uit het publieke domein.

Stap 4: schrijf het indexeringsprogramma

Kopieer de onderstaande code en plak deze in index.py. Elke tussenkop geeft een korte uitleg van wat het daaropvolgende codesegment doet. Dit programma zet je bronmateriaal om in vector-embeddings en slaat die op in een ChromaDB-database, met behulp van het embeddingmodel van Perplexity, pplx-embed-v1-0.6b, en de NumPy-bibliotheek.

Dit doet het programma:

  1. Opent document.txt.
  2. Splitst de tekst op in overlappende chunks.
  3. Stuurt die chunks naar het embeddingmodel pplx-embed-v1-0.6b van Perplexity.
  4. Decodeert en normaliseert de geretourneerde vectoren met NumPy, een Python-bibliotheek met rekenfuncties.
  5. Slaat de vectoren en de oorspronkelijke chunks op in een ChromaDB-database (die verschijnt in je projectmap).

Het programma is zo ingesteld dat je document niet elke keer opnieuw wordt geïndexeerd wanneer je het uitvoert. Als je informatie toevoegt aan document.txt, worden er alleen embeddings voor de nieuwe tekst gemaakt.

python
from pathlib import Path from collections import defaultdict, deque from hashlib import sha256 import base64 import numpy as np import chromadb from dotenv import load_dotenv from perplexity import Perplexity # 1. Connect to Perplexity and ChromaDB load_dotenv() ai = Perplexity() db = chromadb.PersistentClient(path="./rag_database_perplexity") collection = db.get_or_create_collection( name="documents", metadata={"hnsw:space": "cosine"} ) # Convert Perplexity's encoded embeddings into numerical vectors def decode_embedding(encoded): vector = np.frombuffer(base64.b64decode(encoded), dtype=np.int8).astype(np.float32) norm = np.linalg.norm(vector) if norm == 0: raise ValueError("Received a zero-length embedding vector.") return (vector / norm).tolist() # 2. Read the document and split it into chunks text = Path("document.txt").read_text(encoding="utf-8") words = text.split() chunk_size = 180 overlap = 30 step = chunk_size - overlap chunks = [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), step)] if not chunks: raise ValueError("document.txt is empty.") # 3. Retrieve the chunks already stored in ChromaDB existing = collection.get(include=["documents"]) stored_chunks = defaultdict(deque) existing_ids = set(existing["ids"]) for chunk_id, document in zip(existing["ids"], existing["documents"]): stored_chunks[document].append(chunk_id) # 4. Identify unchanged and new chunks new_chunks = [] new_ids = [] kept_ids = set() unchanged = 0 for chunk in chunks: if stored_chunks[chunk]: chunk_id = stored_chunks[chunk].popleft() kept_ids.add(chunk_id) unchanged += 1 else: chunk_hash = sha256(chunk.encode("utf-8")).hexdigest() chunk_id = f"chunk-{chunk_hash}" suffix = 1 while chunk_id in existing_ids or chunk_id in kept_ids or chunk_id in new_ids: chunk_id = f"chunk-{chunk_hash}-{suffix}" suffix += 1 new_chunks.append(chunk) new_ids.append(chunk_id) kept_ids.add(chunk_id) # 5. Generate embeddings in batches of up to 100 chunks batch_size = 100 for i in range(0, len(new_chunks), batch_size): batch_chunks = new_chunks[i:i + batch_size] batch_ids = new_ids[i:i + batch_size] embeddings = ai.embeddings.create( model="pplx-embed-v1-0.6b", input=batch_chunks, encoding_format="base64_int8" ) vectors = [decode_embedding(item.embedding) for item in embeddings.data] # 6. Store each batch in ChromaDB collection.upsert( ids=batch_ids, documents=batch_chunks, embeddings=vectors ) print(f"Indexed batch {(i // batch_size) + 1}") # 7. Remove outdated chunks old_ids = [chunk_id for chunk_id in existing["ids"] if chunk_id not in kept_ids] if old_ids: collection.delete(ids=old_ids) # 8. Display indexing results print("Indexing complete!") print("Unchanged chunks:", unchanged) print("New or modified chunks:", len(new_chunks)) print("Removed chunks:", len(old_ids)) print("Total stored chunks:", collection.count())

Stap 5. Schrijf de orchestrator voor het RAG-systeem

Kopieer de onderstaande code en plak deze in rag.py. Het is een complete maar minimale RAG-implementatie die gebruikmaakt van de Perplexity SDK en de lokale vectordatabasefunctionaliteit van ChromaDB.

Dit gebeurt er wanneer je het programma uitvoert:

  1. Maakt verbinding met Perplexity en je nieuwe ChromaDB-database.
  2. Controleert of de database geïndexeerde documenten bevat.
  3. Neemt een vraag van de gebruiker in ontvangst.
  4. Stuurt de vraag naar het embeddingmodel van Perplexity.
  5. Decodeert en normaliseert de resulterende embedding met NumPy.
  6. Doorzoekt ChromaDB op de drie meest vergelijkbare documentchunks.
  7. Stelt een prompt samen met de opgehaalde chunks en de vraag van de gebruiker.
  8. Stuurt de prompt via de Agent API van Perplexity naar GPT-5.6 Luna.
  9. Toont het antwoord en wacht op een volgende vraag.
python
import base64 import numpy as np import chromadb from dotenv import load_dotenv from perplexity import Perplexity # 1. Connect to Perplexity and the existing ChromaDB database load_dotenv() ai = Perplexity() db = chromadb.PersistentClient(path="./rag_database_perplexity") collection = db.get_collection("documents") # Convert Perplexity's encoded embeddings into numerical vectors def decode_embedding(encoded): vector = np.frombuffer(base64.b64decode(encoded), dtype=np.int8).astype(np.float32) norm = np.linalg.norm(vector) if norm == 0: raise ValueError("Received a zero-length embedding vector.") return (vector / norm).tolist() # 2. Check that the database contains indexed documents count = collection.count() if count == 0: raise ValueError("The database is empty. Run index.py first.") print("Connected to existing ChromaDB database.") print("Stored chunks:", count) # 3. Accept questions from the user while True: question = input("\nAsk a question (or type exit): ") if question.lower().strip() == "exit": break if not question.strip(): continue # 4. Convert the question into a Perplexity embedding question_response = ai.embeddings.create( model="pplx-embed-v1-0.6b", input=[question], encoding_format="base64_int8" ) question_vector = decode_embedding(question_response.data[0].embedding) # 5. Retrieve the most similar document chunks from ChromaDB results = collection.query( query_embeddings=[question_vector], n_results=min(3, count) ) context = "\n\n".join(results["documents"][0]) # 6. Construct a prompt and send it to the fixed LLM response = ai.responses.create( model="openai/gpt-5.6-luna", instructions=( "Answer using only the supplied document excerpts. " "Do not use external information. " "If the answer is not present, say you don't know." ), input=f"Document excerpts:\n{context}\n\nQuestion: {question}" ) # 7. Display the answer print("\nAnswer:\n", response.output_text)

Stap 6: voer de applicaties uit

Voer in de PowerShell-terminal van VS Code het volgende in om de inhoud van document.txt te embedden:

bash
.\.venv\Scripts\python.exe index.py

Als je document goed is geïndexeerd, zie je uitvoer die er ongeveer zo uitziet:

plaintext
Indexing complete! Unchanged chunks: 0 New or modified chunks: 58 Removed chunks: 0 Total stored chunks: 58

Nu is het tijd om de hoofdapplicatie, de orchestrator, uit te voeren. Voer deze opdracht in:

bash
.\.venv\Scripts\python.exe rag.py

Als alles goed werkt, zie je het volgende:

plaintext
Connected to existing ChromaDB database. Stored chunks: 58 Ask a question (or type exit):

Je kunt nu vragen stellen die putten uit de vectordatabase die je hebt gemaakt, zoals:

plaintext
Ask a question (or type exit): What animals does William Blake talk about? Answer: The excerpts mention sheep, lambs, birds, beasts, wolves, tigers, and a lion.

Stap 7: begrijp wat je hebt gebouwd

Het systeem dat je hebt gebouwd, bevat de twee kernpipelines die in elke RAG-architectuur voorkomen: een indexerings- of “ingestie”-pipeline en een pipeline voor ophalen en genereren.

Je eenvoudige RAG-implementatie doet nu het volgende:

  • Vectoropslag: Embeddings worden op basis van je kennisbestand opgeslagen in je ChromaDB-database in je lokale omgeving.
  • Incrementele indexering: Voor ongewijzigde documentchunks worden niet opnieuw embeddings gemaakt, en je kunt het document bijwerken wanneer je maar wilt.
  • Semantisch ophalen: ChromaDB haalt chunks op aan de hand van vectorgelijkenis (in gang gezet door de orchestrator).
  • Gefundeerde antwoordgeneratie: De LLM ontvangt opgehaalde passages als context en verwerkt die in een antwoord.
  • Onafhankelijk indexeren en bevragen: Je kunt vragen stellen zonder alle tekst in je kennisbestand opnieuw te verwerken.
  • Modelverbinding via API: Alle antwoorden worden gegenereerd met GPT-5.6 Luna via Perplexity, in plaats van met een model dat op je eigen computer draait.

Volgende stappen: zo ontwikkel je deze implementatie verder

Het doel van deze tutorial is een overzichtelijke, eenvoudige implementatie te bouwen. Ze behandelt enkele van de belangrijkste onderdelen van RAG-architectuur die in moderne systemen voorkomen. Wil je verder gaan, dan zijn er verschillende manieren om erop voort te bouwen.

Hier zijn enkele ideeën om de reikwijdte van je implementatie uit te breiden:

  • Experimenteer met LangChain: LangChain is een van de populairste opensource-orkestratieframeworks. Het bevat kant-en-klare componenten voor het bouwen van apps die complexe RAG-pipelines kunnen uitvoeren.
  • Breid de zoekfunctionaliteit uit: Combineer dense vectorzoekopdrachten met lexicaal zoeken (op trefwoorden), zoals BM25. Door webzoekopdrachten toe te voegen via de Perplexity Search API kun je antwoorden bovendien baseren op actuele gegevens.
  • Voeg een rerankingmodel toe: Integreer een rerankingmodel dat opgehaalde passages rangschikt ten opzichte van de oorspronkelijke prompt en daaruit de meest relevante selecteert voor opname in de invoercontext die naar de LLM gaat.
  • Bouw een adaptief RAG-systeem: Adaptieve RAG gebruikt een LLM of een classifier (een component binnen de orchestrator) om te bepalen welke ophaalmethode in een modulair RAG-systeem wordt gebruikt. Zo’n systeem bouwen is een goede manier om geavanceerdere RAG-implementaties te verkennen.

Ook al klinkt dit misschien allemaal complex, bedenk wel dat gangbare AI-platforms zoals Perplexity geavanceerde hulp bij het programmeren kunnen bieden. Ze zijn uitstekend geschikt voor persoonlijke projecten en experimenten, en de kosten kunnen relatief laag blijven.

Opmerking: deze tutorial gebruikt GPT-5.6 Luna. De beschikbaarheid van modellen kan veranderen, dus mogelijk moet je de API-aanroep bijwerken.