Überspringen und zum Inhalt gehen →

30 Tage DSPy-Challenge – Tag 20: Ein RAG-System eigene Daten

Die Implementierung eines Retrieval Augmented Generation (RAG) Systems ermöglicht es großen Sprachmodellen (LLMs), auf spezifisches, nicht im Trainingsdatensatz enthaltenes Wissen zuzugreifen. Während in den vorangegangenen Tagen der Challenge grundlegende RAG-Konzepte behandelt wurden, liegt der Fokus an Tag 20 auf der Integration eigener, lokaler Daten.

Dieser Beitrag beschreibt die Erstellung einer RAG-Pipeline in DSPy, die lokale Dokumente indiziert und mithilfe derQdrant Vektordatenbank durchsuchbar macht.

Konzept und Architektur

Ein RAG-System für eigene Daten besteht in DSPy aus drei Hauptkomponenten:

Die Datenquelle
Unstrukturierter Text, der in Vektoren (Embeddings) umgewandelt wird.

Das Retrieval Model (RM)
Eine Komponente, die semantisch relevante Abschnitte sucht. Hierfür wird Qdrant verwendet, eine leistungsfähige Open-Source-Vector-Search-Engine mit der ich in der Vergangenheit schon viel gute Erfahrungen habe.

Das DSPy-Programm
Ein Modul, das den Retriever aufruft, den Kontext in den Prompt integriert und die Antwort generiert.

Vorbereitung der Umgebung

Für die Umsetzung werden neben dspy die Bibliotheken qdrant-client für die Datenbank und ollama für die Erstellung der Embeddings benötigt.

pip install qdrant-client ollama 

Zusätzlich muss in ollama das EmbeddingGemma Modell installiert werden.

ollama pull embeddinggemma

Das Sprachmodell muss initial konfiguriert werden (hier mit Ollama Qwen3-30B).

import dspy

local_llm = dspy.LM(
    "openai/qwen3:30b", 
    api_base="http://localhost:11434/v1", 
    api_key="no_key_needed"
)

dspy.configure(lm=local_llm,  cache=False)
dspy.configure_cache(enable_disk_cache=False)
dspy.configure_cache(enable_memory_cache=False)

Datenaufbereitung und Indizierung in Qdrant

Zunächst müssen die eigenen Daten vektorisiert und in Qdrant gespeichert werden. Der folgende Code simuliert eine Wissensbasis, initialisiert einen lokalen Qdrant-Client (im Arbeitsspeicher) und speichert die Vektoren.

# Beispieldaten definieren
documents = [
    "Titus Skates wurde in den späten 1970er Jahren von dem deutschen Skateboard-Pionier Titus Dittmann gegründet. Er trug maßgeblich zur Popularisierung der Skateboard-Kultur in Deutschland bei und etablierte mehrere Skate-Shops.",
    "Der Münsteraner Unternehmer Titus Dittmann war nicht nur Gründer der Firma Titus, sondern organisierte später auch den legendären Münster Monster Mastership, einen der wichtigsten Skateboard-Contests Europas.",
    "Skateboards bestehen typischerweise aus sieben Lagen kanadischem Ahornholz. Die Rollen sind meist aus Polyurethan, und die Achsen werden aus Aluminium gefertigt.",
    "In den 1980er Jahren entstanden in Deutschland zahlreiche Skate-Shops, darunter bekannte Marken wie Titus, City Skates und weitere unabhängige Stores. Allerdings wurden nicht alle von denselben Personen gegründet.",
    "Titus Dittmann setzte sich später vermehrt für soziale Projekte ein, unter anderem für Skate-Aid, ein weltweites Kinderhilfsprojekt, das Skateboarding als pädagogisches Werkzeug einsetzt.",
	"Apple wurde 1976 von Steve Jobs, Steve Wozniak und Ronald Wayne gegründet. Das Unternehmen begann in einer Garage in Los Altos.",
	"Die Stadt Münster ist bekannt für ihre historische Altstadt, das Picasso-Museum und eine der größten Universitäten Deutschlands.",
	"Die Firma Vans, eine der einflussreichsten Marken im Skateboarding, wurde 1966 von Paul Van Doren gegründet.",
    "Die Skateboard-Kultur in Deutschland wurde stark von amerikanischen Profi-Skatern beeinflusst, die in den 1980ern auf Europa-Tour waren.",
    "Der Mond umkreist die Erde in etwa 27,3 Tagen. Seine Gravitation verursacht die Gezeiten auf unserem Planeten."
]

# Qdrant initialisieren (In-Memory für Tests, persistent via Pfad möglich)
qdrant_client = QdrantClient(":memory:")
collection_name = "my_knowledge_base"

# Collection erstellen
qdrant_client.create_collection(
    collection_name=collection_name,
    vectors_config=VectorParams(size=768, distance=Distance.COSINE)
)

# Daten vektorisieren und hochladen
points = []
for idx, doc in enumerate(documents):
    vector = ollama.embed(model="embeddinggemma", input=doc).embeddings
    points.append(PointStruct(id=idx, vector=vector, payload={"text": doc}))

qdrant_client.upsert(
    collection_name=collection_name,
    points=points
)

In diesem Schritt werden die Texte durch das embeddinggemma-Modell in numerische Repräsentationen umgewandelt und zusammen mit dem Originaltext (Payload) in Qdrant abgelegt.

Implementierung eines Qdrant-Retrievers für DSPy

Um Qdrant mit DSPy zu verbinden, wird eine benutzerdefinierte Retriever-Klasse erstellt. Diese Klasse erbt von dspy.Retrieve und implementiert die Methode forward. Sie übernimmt die Aufgabe, die Eingabefrage zu vektorisieren und die ähnlichsten Dokumente aus Qdrant abzurufen.

from typing import List, Union

# Implementierung eines Qdrant-Retrievers für DSPy
class QdrantRM(dspy.Retrieve):
    def __init__(self, client, collection_name, encoder, k=3):
        super().__init__(k=k)
        self.client = client
        self.collection_name = collection_name
        self.encoder = encoder

    def forward(self, query_or_queries: Union[str, List[str]], k=None) -> dspy.Prediction:
        k = k if k is not None else self.k

        # Falls mehrere Queries kommen, nehmen wir hier vereinfacht die erste oder verarbeiten eine Liste
        # DSPy übergibt oft einen einzelnen String
        query = query_or_queries if isinstance(query_or_queries, str) else query_or_queries[0]

        # Query vektorisieren
        query_vector = ollama.embed(model="embeddinggemma", input=doc).embeddings[0]

        # Suche in Qdrant
        search_result = qdrant_client.query_points(
            collection_name=self.collection_name,
            query=np.array(query_vector),
            limit=k
        )
        
        # Extrahieren der Text-Passagen aus dem Payload
        passages = [hit.payload['text'] for hit in search_result.points]

        # Rückgabe als DSPy Prediction
        return dspy.Prediction(passages=passages)

Diese Klasse fungiert als Brücke: Sie übersetzt die Textanfrage von DSPy in eine Vektorsuche für Qdrant und gibt die gefundenen Ergebnisse in Form einer dspy.Prediction an den Aufrufer zurück.

Definition der Signatur und des RAG-Moduls

Die Signatur definiert die Schnittstelle für das Sprachmodell, während das RAG-Modul den Retriever und den Generator verknüpft.

Die Signatur

# Signatur
class GenerateAnswer(dspy.Signature):
    """Beantworte Fragen basierend auf dem gegebenen Kontext."""

    context = dspy.InputField(desc="Fakten aus der Wissensdatenbank")
    question = dspy.InputField(desc="Die Frage des Nutzers")
    answer = dspy.OutputField(desc="Die präzise Antwort")

Das RAG-Modul

# RAG Modul
class RAG(dspy.Module):
    def __init__(self, retriever_model):
        super().__init__()
        self.retrieve = retriever_model
        self.generate = dspy.ChainOfThought(GenerateAnswer)

    def forward(self, question):
        # Informationen abrufen (Retrieval)
        retrieval_result = self.retrieve(question)
        context = retrieval_result.passages

        # Antwort generieren (Generation)
        prediction = self.generate(context=context, question=question)

        return dspy.Prediction(context=context, answer=prediction.answer)

Ausführung und Test

Nach der Definition aller Komponenten wird das System instanziiert und getestet. Es werden die 2 relevantesten Ergebnisse aus der Qdrant Datenbank geladen und anhand der gefundenen Texte die Antwort auf die Frage „Wer gründete die Firma Titus Skates?“ ausgegeben.

# Initialisieren des Retrievers
my_retriever = QdrantRM(
    client=qdrant_client, 
    collection_name=collection_name, 
    encoder=ollama, 
    k=2
)

# Initialisieren des RAG-Systems
rag_system = RAG(retriever_model=my_retriever)

# Testfrage stellen
question = "Wer gründete die Firma Titus Skates?"

# Ausführung
response = rag_system(question)

# Ausgabe der Ergebnisse
print(f"Frage: {question}")
print(f"Gefundener Kontext: {response.context}")
print(f"Antwort: {response.answer}")

Interpretation der Ergebnisse

Der Ablauf gestaltet sich wie folgt:

Vektorisierung
Die Frage wird durch das Ollama Modell embeddinggemma in einen Vektor umgewandelt.

Suche
Qdrant berechnet die Kosinus-Ähnlichkeit zwischen dem Frage-Vektor und den Dokumenten-Vektoren und liefert die beiden ähnluchsten Antworten.

    Generierung
    Das LLM erhält diesen Kontext und formuliert die Antwort: „Titus Dittmann“

    Analyse der Ergebnisse

    Die Ausgabe der generierten Prompts und der Antworten vom LLM sieht wie folgt aus:


    System message:

    Your input fields are:
    1. `context` (str): Fakten aus der Wissensdatenbank
    2. `question` (str): Die Frage des Nutzers
    Your output fields are:
    1. `reasoning` (str):
    2. `answer` (str): Die präzise Antwort
    All interactions will be structured in the following way, with the appropriate values filled in.

    [[ ## context ## ]]
    {context}

    [[ ## question ## ]]
    {question}

    [[ ## reasoning ## ]]
    {reasoning}

    [[ ## answer ## ]]
    {answer}

    [[ ## completed ## ]]
    In adhering to this structure, your objective is:
    Beantworte Fragen basierend auf dem gegebenen Kontext.

    User message:

    [[ ## context ## ]]
    [1] «Der Mond umkreist die Erde in etwa 27,3 Tagen. Seine Gravitation verursacht die Gezeiten auf unserem Planeten.»
    [2] «Der Münsteraner Unternehmer Titus Dittmann war nicht nur Gründer der Firma Titus, sondern organisierte später auch den legendären Münster Monster Mastership, einen der wichtigsten Skateboard-Contests Europas.»

    [[ ## question ## ]]
    Wer gründete die Firma Titus Skates?

    Respond with the corresponding output fields, starting with the field `[[ ## reasoning ## ]]`, then `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.


    Response:

    [[ ## reasoning ## ]]
    Der Kontext [2] gibt an, dass Titus Dittmann der Gründer der Firma "Titus" war. Die Frage bezieht sich auf die Firma "Titus Skates", doch im Kontext wird lediglich die Firma "Titus" erwähnt. Da der Zusammenhang mit Skateboard-Contests (Münster Monster Mastership) hergestellt wird, ist die Firma "Titus" in diesem Kontext mit "Titus Skates" identisch, wie es im Umfeld der Skateboard-Szene üblich ist. Somit ist Titus Dittmann der Gründer.

    [[ ## answer ## ]]
    Titus Dittmann

    [[ ## completed ## ]]

    Die Ausgabe zeigt, dass die Pipeline den strukturierten Prompt korrekt verarbeitet und aus dem übergebenen Kontext eine schlüssige Schlussfolgerung ableitet. Interessant ist, dass der relevante Hinweis ausschließlich im zweiten Kontextfragment steckt, während das erste keinerlei Bezug zur Frage hat. Trotzdem gelingt es dem Modell, den irrelevanten Mond-Abschnitt vollständig zu ignorieren und sich auf die Passage zu konzentrieren, in der Titus Dittmann als Gründer genannt wird. Die reasoning-Sektion ist sauber konstruiert, denn das Modell erkennt, dass im Kontext zwar nur von der Firma „Titus“ gesprochen wird, die Frage jedoch „Titus Skates“ lautet. Das Modell stellt eigenständig den gedanklichen Zusammenhang her, dass sich beide Namen innerhalb der Skateboard-Szene auf die gleiche Marke beziehen. Es zeigt also eine Fähigkeit zur leichten Normalisierung von Markennamen, was für realistische RAG-Szenarien hilfreich ist. Die Antwort bleibt präzise und enthält weder unnötige Zusatzinformationen noch Spekulationen. Die Strukturvorgaben des dspy-Prompts werden eingehalten, inklusive der Abschlussmarkierung. Insgesamt wirkt die Pipeline stabil, da sie korrekt filtert, relevante Inhalte extrahiert, eine nachvollziehbare Begründung liefert und das gewünschte Format vollständig erfüllt.

    Zusammenfassung

    An Tag 20 konnte ich ein RAG-System mit eigenen Daten unter Verwendung von Qdrant und DSPy aufgebauen. Die Kombination aus einer Vektordatenbank einem Embedding Modell und einem LLM in DSPy ermöglicht ein universell einsetzbares Antwortsystem. Die Verwendung von QdrantRM als benutzerdefiniertes Modul demonstriert die Flexibilität von DSPy, sich in bestehende Infrastrukturen zu integrieren.

    Veröffentlicht in Allgemein