Einfache RAG-Implementierung: Ein Schritt-für-Schritt-Tutorial

Dieses einsteigerfreundliche Tutorial erklärt, wie eine einfache RAG-Implementierung funktioniert, stellt Best Practices vor und führt Schritt für Schritt durch den Aufbau eines funktionierenden RAG-Systems für die Befehlszeile mit Python, ChromaDB sowie den Embedding- und Sprachmodellen von Perplexity.

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 ist ein wesentlicher Bestandteil moderner LLM-basierter Infrastruktur. Da KI-Modelle damit auf externe Daten zugreifen können, hilft RAG, Probleme wie Halluzinationen und unvollständige Trainingsdaten zu beheben. Außerdem können Unternehmen so interne Wissensdatenbanken nutzen, die sensible oder proprietäre Informationen enthalten.

Die Implementierung von RAG kann sehr komplex sein. Ein einfaches RAG-System lässt sich jedoch überraschend leicht aufbauen. Für Einsteiger kann eine einfache Architektur veranschaulichen, wie RAG in der Praxis funktioniert, und ein Sprungbrett für weitere Experimente bieten.

In diesem Artikel skizzieren wir eine einfache RAG-Implementierung, gehen auf Best Practices ein und bauen dann mit Python, ChromaDB und zwei Perplexity-Modellen für Embedding und Antwortgenerierung ein funktionierendes System.

Ablauf einer einfachen RAG-Implementierung: Ein Überblick

Ein einfaches RAG-System besteht aus drei Hauptkomponenten: einem Orchestrator, einer Informationsquelle und einem LLM. In einem typischen Workflow gibt ein Nutzer einen Prompt ein, und der Orchestrator passt ihn an, um alle angebundenen Informationsquellen zu durchsuchen. Die abgerufenen Informationen werden dann dem ursprünglichen Eingabekontext hinzugefügt und an das LLM gesendet, damit eine Antwort generiert werden kann.

Diese Schritte müssen Sie beim Aufbau Ihrer RAG-Implementierung berücksichtigen:

  1. Indexierung und Embedding: In dieser Phase werden Ihre Dokumente in eine Informationsquelle umgewandelt, die der Orchestrator durchsuchen kann. Eine der gängigsten Methoden dafür ist der Aufbau eines durchsuchbaren Vektorindex. Wir verwenden ein spezielles Modell von Perplexity, um Ihre Dokument-Chunks in Embeddings umzuwandeln. Anschließend speichern wir die Embeddings mit ChromaDB, einer beliebten Open-Source-Vektordatenbank.
  2. Orchestrierung: Dies ist die Anwendung, die zwischen der externen Informationsquelle (in unserem Fall einer Vektordatenbank) und dem LLM sitzt. Wir erstellen ein einfaches Python-Skript, das als Orchestrator dient.
  3. Abruf und Generierung: Wenn ein Nutzer über die Chat-Oberfläche eine Frage stellt, nimmt der Orchestrator sie entgegen, führt eine Vektorsuche durch und übergibt die erweiterte Eingabe an das LLM. Statt ein eigenes Frontend zu entwickeln, nutzen wir die Befehlszeile, um mit unserem Python-Orchestrator zu kommunizieren.

Best Practices: Was zeichnet eine effektive RAG-Implementierung aus?

Es gibt verschiedene Arten von RAG-Architekturen, und unterschiedliche Anwendungsfälle erfordern unterschiedliche Ansätze. Dennoch gelten einige Best Practices in allen Fällen und sorgen für präzise, kosteneffiziente, skalierbare und wartbare Systeme.

Die folgenden Best Practices gelten für alle Arten von RAG-Implementierungen:

  • Sorgen Sie für eine saubere Ingestion-Pipeline: Ihre Quelldaten sollten vor der Indexierung so sauber wie möglich sein. Verwenden Sie außerdem ein Embedding-Modell, das zu Ihren Inhalten passt.
  • Nutzen Sie eine hybride Suche: Welche Abrufmethoden am besten geeignet sind – ob Vektordatenbanksuche, API-basierte Websuche, Traversierung von Wissensgraphen, Stichwortsuche oder andere Methoden –, hängt vom Anwendungsfall ab. Die richtige Kombination zu wählen, ist wichtig, um die Kosten gering zu halten.
  • Evaluieren Sie Ihr System: Nach der Implementierung sollten Sie es anhand von drei zentralen Kriterien bewerten: Genauigkeit, Relevanz und Latenz. Genauigkeit bezieht sich auf die sachliche Richtigkeit, Relevanz darauf, wie gut die abgerufenen Informationen zu den Nutzeranfragen passen, und Latenz auf die Geschwindigkeit, mit der Informationen abgerufen werden.
  • Gehen Sie sicher mit Informationen um: Wenn Ihr RAG-System auf private interne Informationen zugreift, stellen Sie sicher, dass diese sicher gespeichert werden und der Zugriff auf die Systemoberfläche kontrolliert wird. API-Schlüssel sollten niemals weitergegeben werden.

Einfache RAG-Implementierung: Tutorial in 7 Schritten

In diesem Tutorial erstellen wir eine Befehlszeilenanwendung, die Fragen zu einem einzelnen Dokument beantwortet. Wir verwenden Python, ChromaDB und die Perplexity API.

Ziel ist es nicht, ein produktionsreifes RAG-System zu entwickeln. Vielmehr handelt es sich um ein einsteigerfreundliches Tutorial, das veranschaulichen soll, wie eine grundlegende RAG-Architektur in der Praxis funktioniert.

Schritt 1: Software installieren

Stellen Sie sicher, dass Folgendes auf Ihrem Computer installiert ist und Sie ein Perplexity-API-Konto haben:

  • Python: Mit dieser Programmiersprache erstellen wir den Orchestrator, der den Datenfluss zwischen dem Embedding-Modell, ChromaDB und Perplexity koordiniert.
  • Visual Studio: Wir verwenden Visual Studio als Code-Editor. Öffnen Sie nach der Installation das Panel Extensions und laden Sie die Erweiterung Python herunter.
  • Perplexity-API-Konto: Perplexity stellt sowohl das Embedding-Modell (das Ihre Daten in Vektor-Embeddings umwandelt) als auch das LLM bereit, das den erweiterten Prompt verarbeitet. Dafür benötigen Sie kein kostenpflichtiges Konto, allerdings werden in geringem Umfang Credits verbraucht.

Schritt 2: Projektordner erstellen

Erstellen Sie zunächst einen Ordner namens rag-project und öffnen Sie ihn in VS Code über File > Open Folder. Öffnen Sie dann das integrierte Terminal über Terminal > New Terminal.

Geben Sie Folgendes ein, um eine virtuelle Python-Umgebung zu erstellen:

bash
py -3.12 -m venv .venv

Nun müssen Sie vier Python-Bibliotheken installieren: perplexityai, chromadb, python-dotenv und numpy. Mit dem Perplexity SDK können Sie sich per API mit den Modellen von Perplexity verbinden. ChromaDB erstellt die Vektordatenbank und läuft lokal. Python-dotenv lädt Ihre API-Schlüssel sicher aus einer lokalen Datei, und NumPy wird während des Ingestion-Prozesses benötigt.

Installieren Sie diese mit dem folgenden Befehl:

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

Schritt 3: Dateien einrichten

Fügen Sie dem Ordner rag-project (Projektstammverzeichnis) die folgenden Dateien hinzu, indem Sie in der Explorer-Seitenleiste New File… auswählen:

  • index.py: Diese Anwendung indexiert Ihr Dokument. Sie können sie erneut ausführen, um weiteren Text hinzuzufügen.
  • rag.py: Diese Python-Anwendung dient als Orchestrator.
  • document.txt: Diese Datei enthält die Dokumentation, die Sie in eine Vektordatenbank umwandeln.
  • .env: Hier speichern Sie Ihren Perplexity-API-Schlüssel.

Geben Sie in .env Folgendes ein:

plaintext
PERPLEXITY_API_KEY=your_api_key_here

Ersetzen Sie your_api_key_here durch Ihren tatsächlichen Schlüssel. Rufen Sie dazu das Dashboard auf der Startseite der Perplexity API Console auf und wählen Sie Create an API key aus. Halten Sie diesen API-Schlüssel geheim.

Wir verwenden zwei Modelle: pplx-embed-v1-0.6b von Perplexity, um unsere Dokumentation vor dem Hochladen in ChromaDB einzubetten, und GPT-5.6-Luna (Zugriff über die API von Perplexity), das die abgerufenen Informationen verarbeitet.

Fügen Sie in document.txt den Text ein, den Ihre RAG-Pipeline abrufen soll. Falls Sie gerade kein Dokument zur Hand haben, verwenden Sie für dieses Tutorial William Blakes Songs of Innocence and of Experience von Project Gutenberg. Es handelt sich um einen kurzen, leicht verständlichen und gemeinfreien Text.

Schritt 4: Indexierungsprogramm schreiben

Kopieren Sie den folgenden Code und fügen Sie ihn in index.py ein. Jede Zwischenüberschrift erklärt kurz, was der nachfolgende Codeabschnitt tut. Dieses Programm wandelt Ihr Quellmaterial mithilfe des Embedding-Modells von Perplexity, pplx-embed-v1-0.6b, und der NumPy-Bibliothek in Vektor-Embeddings um und speichert sie in einer ChromaDB-Datenbank.

Das Programm führt folgende Schritte aus:

  1. Öffnet document.txt.
  2. Teilt den Text in überlappende Chunks auf.
  3. Sendet diese Chunks an das Embedding-Modell pplx-embed-v1-0.6b von Perplexity.
  4. Decodiert und normalisiert die zurückgegebenen Vektoren mit NumPy, einer Python-Bibliothek mit Rechenfunktionen.
  5. Speichert die Vektoren und die ursprünglichen Chunks in einer ChromaDB-Datenbank (diese erscheint in Ihrem Projektverzeichnis).

Das Programm ist so eingerichtet, dass Ihr Dokument nicht bei jeder Ausführung neu indexiert wird. Wenn Sie die Informationen in document.txt ergänzen, wird nur der neue Text eingebettet.

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())

Schritt 5: Orchestrator für das RAG-System schreiben

Kopieren Sie den folgenden Code und fügen Sie ihn in rag.py ein. Es handelt sich um eine vollständige, aber minimale RAG-Implementierung, die das Perplexity SDK und die lokale Vektordatenbankfunktion von ChromaDB nutzt.

Folgendes passiert, wenn Sie das Programm ausführen:

  1. Stellt eine Verbindung zu Perplexity und Ihrer neuen ChromaDB-Datenbank her.
  2. Prüft, ob die Datenbank indexierte Dokumente enthält.
  3. Nimmt eine Frage des Nutzers entgegen.
  4. Sendet die Frage an das Embedding-Modell von Perplexity.
  5. Decodiert und normalisiert das resultierende Embedding mit NumPy.
  6. Durchsucht ChromaDB nach den drei ähnlichsten Dokument-Chunks.
  7. Erstellt einen Prompt, der die abgerufenen Chunks und die Frage des Nutzers enthält.
  8. Sendet den Prompt über die Agent API von Perplexity an GPT-5.6 Luna.
  9. Zeigt die Antwort an und wartet auf die nächste Frage.
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)

Schritt 6: Anwendungen ausführen

Geben Sie im PowerShell-Terminal von VS Code Folgendes ein, um den Inhalt von document.txt einzubetten:

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

Wenn Ihr Dokument erfolgreich indexiert wurde, sollten Sie eine Ausgabe ähnlich dieser sehen:

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

Jetzt ist es an der Zeit, die Orchestrator-Hauptanwendung auszuführen. Geben Sie diesen Befehl ein:

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

Wenn die Ausführung erfolgreich ist, sehen Sie Folgendes:

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

Sie können jetzt Fragen stellen, die auf die von Ihnen erstellte Vektordatenbank zurückgreifen, zum Beispiel:

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.

Schritt 7: Verstehen, was Sie erstellt haben

Das von Ihnen erstellte System enthält die beiden zentralen Pipelines, die in jeder RAG-Architektur vorkommen: eine Indexierungs- bzw. „Ingestion“-Pipeline sowie eine Pipeline für Abruf und Generierung.

Ihre einfache RAG-Implementierung leistet jetzt Folgendes:

  • Vektorspeicherung: Embeddings werden auf Grundlage Ihrer Wissensdatei in Ihrer ChromaDB-Datenbank in Ihrer lokalen Umgebung gespeichert.
  • Inkrementelle Indexierung: Unveränderte Dokument-Chunks werden nicht erneut eingebettet, und Sie können das Dokument jederzeit aktualisieren.
  • Semantischer Abruf: ChromaDB ruft Chunks auf Basis der Vektorähnlichkeit ab (vom Orchestrator angestoßen).
  • Fundierte Antwortgenerierung: Das LLM erhält abgerufene Passagen als Kontext und bezieht sie in eine Antwort ein.
  • Unabhängige Indexierung und Abfrage: Sie können Fragen stellen, ohne den gesamten Text Ihrer Wissensdatei erneut verarbeiten zu müssen.
  • API-basierte Modellanbindung: Alle Antworten werden über Perplexity mit GPT-5.6 Luna generiert statt mit einem Modell, das auf Ihrem Rechner läuft.

Nächste Schritte: So entwickeln Sie diese Implementierung weiter

Ziel dieses Tutorials ist es, eine unkomplizierte, einfache Implementierung zu erstellen. Es behandelt einige der zentralen Komponenten der RAG-Architektur, die in modernen Systemen zu finden sind. Wenn Sie noch einen Schritt weiter gehen möchten, gibt es verschiedene Möglichkeiten, darauf aufzubauen.

Hier einige Ideen, wie Sie den Umfang Ihrer Implementierung erweitern können:

  • Experimentieren Sie mit LangChain: LangChain ist eines der beliebtesten Open-Source-Frameworks für die Orchestrierung. Es enthält vorgefertigte Komponenten für die Entwicklung von Apps, die komplexe RAG-Pipelines ausführen können.
  • Erweitern Sie die Suchfunktionen: Kombinieren Sie dichte Vektorsuche mit lexikalischer (stichwortbasierter) Suche wie BM25. Wenn Sie zusätzlich über die Perplexity Search API eine Websuche einbinden, können Sie Antworten auf aktuelle Daten stützen.
  • Fügen Sie ein Reranking-Modell hinzu: Integrieren Sie ein Reranking-Modell, das abgerufene Passagen anhand des ursprünglichen Prompts bewertet und die relevantesten für den Eingabekontext auswählt, der dem LLM bereitgestellt wird.
  • Bauen Sie ein adaptives RAG-System: Adaptives RAG nutzt entweder ein LLM oder einen Klassifikator (eine Komponente innerhalb des Orchestrators), um zu bestimmen, welche Abrufmethode in einem modularen RAG-System verwendet werden soll. Ein solches System zu bauen, ist eine gute Möglichkeit, anspruchsvollere RAG-Implementierungen kennenzulernen.

Auch wenn das alles komplex klingen mag: Gängige KI-Plattformen wie Perplexity können fortgeschrittene Unterstützung beim Programmieren bieten. Sie eignen sich hervorragend für private Projekte und Experimente, und die Kosten lassen sich relativ gering halten.

Hinweis: In diesem Tutorial wird GPT-5.6 Luna verwendet. Die Verfügbarkeit von Modellen kann sich ändern, daher müssen Sie den API-Aufruf möglicherweise anpassen.