Überspringen und zum Inhalt gehen →

30 Tage DSPy-Challenge – Tag 21: Wochenrückblick und Vertiefung – RAG-Optimierung mit HyDE

Der Tag 21 der DSPy-Challenge dient dem Rückblick auf die dritte Woche und der technischen Vertiefung der Retrieval Augmented Generation (RAG). Nachdem ich in den vergangenen Tagen die Grundlagen von RAG, den Umgang mit Multi-Hop-Fragen und die Integration eigener Daten behandelt habe, liegt der Fokus nun auf der qualitativen Verbesserung des Retrieval-Schritts mit Hilfe von HyDE.

Hypothetical Document Embeddings (HyDE)

Eine häufige Schwachstelle in RAG-Systemen ist die Diskrepanz zwischen der Suchanfrage und den gespeicherten Dokumenten. Während Fragen oft kurz und prägnant sind, beinhalten die Dokumente detaillierte Kontexte. Dies kann dazu führen, dass die Vektorähnlichkeitssuche keine optimalen Ergebnisse liefert. Eine Lösung für dieses Problem werde ich heute mit Hypothetical Document Embeddings (HyDE) implementieren. Die Kernidee von HyDE besteht darin, nicht die Frage selbst für die Vektorsuche zu verwenden, sondern eine vom Sprachmodell generierte, hypothetische Antwort.

Der Ablauf gestaltet sich wie folgt:

Generierung
Das LLM erhält die Frage und wird angewiesen, eine plausible (hypothetische) Antwort zu verfassen, auch wenn diese faktisch nicht korrekt sein muss. Hier kann das LLM ruhig etwas halluzinieren.

Embedding
Diese hypothetische Antwort wird vektorisiert. Da sie strukturell und inhaltlich eher den gesuchten Dokumenten in der Datenbank ähnelt als die ursprüngliche Frage, liegt der Vektor im semantischen Raum näher an den relevanten Treffern.

Retrieval & Generation
Mit diesem Vektor werden die tatsächlichen, faktisch korrekten Dokumente abgerufen. Anschließend generiert das Modell die finale Antwort basierend auf dem realen Kontext und der ursprünglichen Frage.

    Implementierung der erweiterten RAG-Pipeline

    Für die Umsetzung wird die Pipeline von Tag 20 erweitert. Die Basis bilden weiterhin Qdrant als Vektordatenbank und Ollama für Embeddings und Textgenerierung.

    Setup und Daten-Ingestion

    Zunächst erfolgt die Initialisierung des LLMs und der Aufbau der Wissensdatenbank. Dieser Schritt ist identisch zum Vortag, stellt jedoch die notwendige Basis für das HyDE-Modul dar.

    import dspy
    from qdrant_client import QdrantClient
    from qdrant_client.models import PointStruct, VectorParams, Distance
    import ollama
    from typing import List, Union
    import numpy as np
    
    # Konfiguration des lokalen LLMs
    local_llm = dspy.LM(
        "openai/qwen3:30b",
        api_base="http://localhost:11434/v1", 
        api_key="no_key_needed"
    )
    
    dspy.configure(lm=local_llm)
    # Beispieldaten (Wissensbasis)
    documents = [
        "Titus Skates wurde in den späten 1970er Jahren von dem deutschen Skateboard-Pionier Titus Dittmann gegründet.",
        "Der Münsteraner Unternehmer Titus Dittmann organisierte den legendären Münster Monster Mastership.",
        "Skateboards bestehen typischerweise aus sieben Lagen kanadischem Ahornholz, Rollen aus Polyurethan und Achsen aus Aluminium.",
        "Apple wurde 1976 von Steve Jobs, Steve Wozniak und Ronald Wayne gegründet.",
        "Die Stadt Münster ist bekannt für ihre historische Altstadt und das Picasso-Museum.",
        "Der Mond umkreist die Erde in etwa 27,3 Tagen."
    ]
    
    # Qdrant In-Memory Client initialisieren
    qdrant_client = QdrantClient(":memory:")
    collection_name = "my_knowledge_base"
    
    qdrant_client.create_collection(
        collection_name=collection_name,
        vectors_config=VectorParams(size=768, distance=Distance.COSINE)
    )
    
    # Vektorisierung und Upload der Dokumente
    points = []
    for idx, doc in enumerate(documents):
        # Embedding-Modell muss lokal in Ollama verfügbar sein (z.B. 'nomic-embed-text' oder 'embeddinggemma')
        vector = ollama.embed(model="embeddinggemma", input=doc).embeddings[0]
        points.append(PointStruct(id=idx, vector=vector, payload={"text": doc}))
    
    qdrant_client.upsert(collection_name=collection_name, points=points)

    Definition des Retrievers

    Der Retriever benötigt eine Korrektur gegenüber der Version von Tag 20: Beim Vektorisieren der Query muss die Variable query und nicht doc verwendet werden.

    class QdrantRM(dspy.Retrieve):
        def __init__(self, client, collection_name, k=3):
            super().__init__(k=k)
            self.client = client
            self.collection_name = collection_name
    
        def forward(self, query_or_queries: Union[str, List[str]], k=None) -> dspy.Prediction:
            k = k if k is not None else self.k
            query = query_or_queries if isinstance(query_or_queries, str) else query_or_queries[0]
    
            # Korrektur: Nutzung der 'query' Variable für das Embedding
            query_vector = ollama.embed(model="embeddinggemma", input=query).embeddings[0]
    
            search_result = self.client.query_points(
                collection_name=self.collection_name,
                query=np.array(query_vector),
                limit=k
            )
    
            passages = [hit.payload['text'] for hit in search_result.points]
            return dspy.Prediction(passages=passages)
    
    # Initialisierung des Retrievers
    my_retriever = QdrantRM(client=qdrant_client, collection_name=collection_name, k=2)

    HyDE-Signatur und Modul

    Hier implementiere ich die eigentliche Erweiterung. Es wird eine neue Signatur GenerateHypothetical erstellt, die das Modell anweist, eine vorläufige Antwort zu formulieren. Das Modul RAGWithHyDE integriert diesen Schritt vor dem eigentlichen Retrieval.

    # Signatur für die Erzeugung der hypothetischen Antwort
    class GenerateHypothetical(dspy.Signature):
        """Schreibe eine hypothetische Antwort auf die Frage. Sie muss nicht faktisch korrekt sein, 
        sondern Keywords und Struktur enthalten, die in einem relevanten Dokument vorkommen könnten."""
    
        question = dspy.InputField(desc="Die Frage des Nutzers")
        hypothetical_answer = dspy.OutputField(desc="Eine plausible, hypothetische Antwort")
    
    # Signatur für die finale Antwort (wie an Tag 20)
    class GenerateAnswer(dspy.Signature):
        """Beantworte Fragen präzise 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 erweiterte RAG-Modul
    class RAGWithHyDE(dspy.Module):
        def __init__(self, retriever_model):
            super().__init__()
            # HyDE Generator
            self.generate_hyde = dspy.ChainOfThought(GenerateHypothetical)
    
            # Retriever
            self.retrieve = retriever_model
    
            # Finale Antwort
            self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
    
        def forward(self, question):
            # Generiere hypothetisches Dokument
            hyde_output = self.generate_hyde(question=question)
            hypothetical_doc = hyde_output.hypothetical_answer
    
            # Nutze das hypothetische Dokument für die Suche (statt der Frage)
            # Dies hilft, Dokumente zu finden, die der Antwortstruktur ähneln
            retrieval_result = self.retrieve(hypothetical_doc)
            context = retrieval_result.passages
    
            # Generiere die finale Antwort mit dem gefundenen Kontext und der Originalfrage
            prediction = self.generate_answer(context=context, question=question)
    
            return dspy.Prediction(
                context=context, 
                answer=prediction.answer, 
                hypothetical_used=hypothetical_doc
            )

    Test und Analyse der Gedankengänge

    Nach der Instanziierung des Systems wird eine Frage gestellt.

    # System initialisieren
    rag_hyde_system = RAGWithHyDE(retriever_model=my_retriever)
    
    # Frage, die Kontextwissen erfordert
    question = "Was hat der Gründer von Titus Skates außer dem Verkauf von Hardware noch organisiert?"
    
    # Ausführung
    response = rag_hyde_system(question)
    
    print(f"--- ERGEBNIS ---\n")
    print(f"Frage: {question}")
    print(f"Hypothetische Antwort (für Suche genutzt): {response.hypothetical_used}\n")
    print(f"Gefundener Kontext: {response.context}\n")
    print(f"Finale Antwort: {response.answer}\n")

    Die Ausgabe des Programms sieht dann wie folgt aus:

    --- ERGEBNIS ---

    Frage: Was hat der Gründer von Titus Skates organisiert?
    Hypothetische Antwort (für Suche genutzt): Der Gründer von Titus Skates hat das jährliche Titus Skates Open in Los Angeles organisiert, welches Street-Skate-Contests und urban-künstlerische Ausstellungen für lokale und internationale Talent kombiniert. Das Event umfasste Workshops zur Skateboard-Design-Entwicklung und förderte die Zusammenarbeit zwischen Skateboardern und Street-Art-Künstlern seit 2018.

    Gefundener Kontext: ['Titus Skates wurde in den späten 1970er Jahren von dem deutschen Skateboard-Pionier Titus Dittmann gegründet.', 'Der Münsteraner Unternehmer Titus Dittmann organisierte den legendären Münster Monster Mastership.']

    Finale Antwort: Den legendären Münster Monster Mastership

    Das Ergebnis „Münster Monster Mastership“ ist natürlich absolut korrekt.

    Analyse der Ergebnisse

    Die Ausgabe von inspect_history zeigt die Prompt-Struktur, die DSPy generiert hat. Durch die Verwendung von dspy.ChainOfThought in beiden Schritten (HyDE und Finale Antwort) enthält der Prompt jeweils ein Feld Rationale.

    --- ANALYSE DER GEDANKENGÄNGE ---

    System message:

    Your input fields are:
    1. `question` (str): Die Frage des Nutzers
    Your output fields are:
    1. `reasoning` (str):
    2. `hypothetical_answer` (str): Eine plausible, hypothetische Antwort
    All interactions will be structured in the following way, with the appropriate values filled in.

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

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

    [[ ## hypothetical_answer ## ]]
    {hypothetical_answer}

    [[ ## completed ## ]]
    In adhering to this structure, your objective is:
    Schreibe eine hypothetische Antwort auf die Frage. Sie muss nicht faktisch korrekt sein,
    sondern Keywords und Struktur enthalten, die in einem relevanten Dokument vorkommen könnten.

    User message:

    [[ ## question ## ]]
    Was hat der Gründer von Titus Skates organisiert?

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

    Response:

    [[ ## reasoning ## ]]
    Der Begriff "Titus Skates" ist nicht in öffentlichen Datenbanken oder bekannten Skate-Brands nachweisbar. Daher wird eine hypothetische Antwort erstellt, die typische Begriffe aus dem Skateboarding-Szenario und Marken-Organisationen enthält, wie "Jahrescontests", "Urban-Art-Ausstellungen" und "Community-Workshops", um plausibel zu wirken.

    [[ ## hypothetical_answer ## ]]
    Der Gründer von Titus Skates hat das jährliche Titus Skates Open in Los Angeles organisiert, welches Street-Skate-Contests und urban-künstlerische Ausstellungen für lokale und internationale Talent kombiniert. Das Event umfasste Workshops zur Skateboard-Design-Entwicklung und förderte die Zusammenarbeit zwischen Skateboardern und Street-Art-Künstlern seit 2018.

    [[ ## completed ## ]]

    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 präzise basierend auf dem gegebenen Kontext.


    User message:

    [[ ## context ## ]]
    [1] «Titus Skates wurde in den späten 1970er Jahren von dem deutschen Skateboard-Pionier Titus Dittmann gegründet.»
    [2] «Der Münsteraner Unternehmer Titus Dittmann organisierte den legendären Münster Monster Mastership.»

    [[ ## question ## ]]
    Was hat der Gründer von Titus Skates organisiert?

    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 gibt an, dass Titus Dittmann, der Gründer von Titus Skates, als "Münsteraner Unternehmer" den "legendären Münster Monster Mastership" organisiert hat. Die Frage bezieht sich direkt auf das Organisatorische des Gründers, und der Kontext nennt ausschließlich diese Veranstaltung.

    [[ ## answer ## ]]
    Den legendären Münster Monster Mastership

    [[ ## completed ## ]]

    Analyse der History

    Die vorliegenden Protokolle veranschaulichen die Funktionsweise der HyDE-Architektur in zwei deutlich voneinander abgegrenzten Phasen, die den Übergang von einer semantischen Suchhilfe zur faktischen Extraktion markieren.

    Im ersten Schritt, der Generierung des hypothetischen Dokuments, offenbart der reasoning-Block zunächst eine Wissenslücke des Modells, da es die spezifische Marke „Titus Skates“ nicht in seinem internen Datensatz verorten kann. Das Modell reagiert jedoch instruktionskonform auf die DSPy-Signatur und konstruiert eine plausible, wenn auch faktisch falsche Antwort über ein „Titus Skates Open“ in Los Angeles. Diese Halluzination erfüllt den technischen Zweck des HyDE-Ansatzes ideal, da sie das Embedding mit semantisch relevanten Begriffen wie „Contests“, „Events“ und „Workshops“ anreichert, ohne dass die faktische Korrektheit der Geografie oder des Eventnamens für den Suchprozess relevant wäre.

    Der zweite Abschnitt demonstriert anschließend die erfolgreiche Erdung (Grounding) durch den Retrieval-Prozess. Dem Modell werden nun spezifische Kontext-Schnipsel über Titus Dittmann und den „Münster Monster Mastership“ bereitgestellt, die durch die Ähnlichkeit zum zuvor generierten hypothetischen Vektor gefunden wurden. Das Modell ändert in dieser Phase seine Strategie fundamental von kreativer Erfindung hin zu logischer Deduktion. Der reasoning-Teil zeigt auf, wie das System die Verbindung zwischen der Rolle des Gründers im ersten Kontext und der organisierten Veranstaltung im zweiten Kontext herstellt. Das Endergebnis belegt die Robustheit der RAG-Pipeline, da das Modell sein internes Nichtwissen beziehungsweise die vorangegangene Halluzination vollständig verwirft und ausschließlich die extern bereitgestellten, korrekten Fakten für die finale Antwort nutzt.

    Zusammenfassung

    Mit der Integration von HyDE wurde die RAG-Pipeline robuster gegenüber komplexen Fragestellungen gemacht, bei denen die direkte lexikalische oder einfache semantische Ähnlichkeit zwischen Frage und Dokument nicht ausreicht. Die Analyse mittels inspect_history bestätigt, dass DSPy komplexe Prompt-Ketten abstrahiert, aber dennoch volle Transparenz über die logischen Schritte des Modells gewährt.

    Veröffentlicht in Allgemein