Zum Hauptinhalt springen
Lektion 14 von 22 · Embeddings, RAG und semantische Suche

Erweiterter RAG: Multi-Query, HyDE, Step-Back

Lektion 14 von 22 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Lernziele
  2. Das Problem: Eine einzelne Abfrage reicht nicht aus
  3. Multi-Query: Die Frage anders stellen
  4. HyDE: Hypothetische Dokument-Embeddings
  5. Step-Back-Prompting
  6. Vergleich der Techniken
  7. Kombination von Techniken
  8. Zusammenfassung

Lernziele

  • Verbesserung der Abrufung durch fortgeschrittene Techniken
  • Implementierung von Multi-Query, HyDE und Step-Back-Prompting
  • Kenntnis der Anwendungsfälle für jede Technik

Das Problem: Eine einzelne Abfrage reicht nicht aus

Das Reranking aus der vorherigen Lektion verbesserte zwar die Rangfolge der Kandidaten, konnte jedoch keine Dokumente helfen, die der Abruf gar nicht erst in die Kandidatenliste aufgenommen hat. Die häufigste Ursache für diese Fehlschläge liegt weder im Modell noch im Index, sondern in der Abfrage selbst. Ein Nutzer könnte beispielsweise eingeben: „Es funktioniert nicht auf meinem Handy”, während die Dokumentation über „Inkompatibilität mit Android-Versionen vor 12” spricht. Kein Embedding-Modell wird diese beiden Formulierungen von sich aus näher zusammenrücken lassen.

Die folgenden Techniken teilen ein einziges Prinzip: Anstatt die Abfrage so zu akzeptieren, wie sie ist, transformieren oder vervielfältigen wir sie vor der Suche. Alle wirken auf die Recall-Rate – also die Fähigkeit, das richtige Dokument in die Kandidatenliste zu bringen.

Multi-Query: Die Frage anders stellen

Der erste Ansatz ist der direkteste. Wir bitten das LLM, die Frage aus mehreren Perspektiven neu zu formulieren, führen mit jeder Variante eine Suche durch und verschmelzen die Ergebnisse. Die Frage „Wie kündige ich mein Abo?” wird auch zu „Kündigungsablauf”, „Monatsgebühr stoppen” und „Mein bezahltes Konto löschen” – drei verschiedene Vokabulare, drei Chancen, die Formulierung zu treffen, die der Dokumentationsautor verwendet hat.

Der Verschmelzungsschritt verdient eine Anmerkung: Die Zeile np.maximum behält für jedes Dokument die beste Punktzahl, die über alle Abfragen hinweg erzielt wurde. Ein Dokument, das nur mit einer Neuformulierung gut übereinstimmt, steigt ebenfalls an die Spitze, was nicht der Fall wäre, wenn wir die Punktzahlen durchschnitten hätten. Die Temperatur von 0,7 ist hier absichtlich gewählt, da wir Vielfalt und keine Reproduzierbarkeit anstreben.

from openai import OpenAI
import numpy as np

client = OpenAI()

def generer_multi_queries(frage: str, n: int = 3) -> list[str]:
    """Generiert n Neuformulierungen der Frage."""
    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[{
            "role": "user",
            "content": (
                f"Generieren Sie {n} verschiedene Neuformulierungen dieser Frage "
                f"für die Dokumentensuche. Jede Neuformulierung "
                f"soll einen anderen Aspekt abdecken.\n\n"
                f"Ursprüngliche Frage: {frage}\n\n"
                f"Geben Sie nur die Neuformulierungen zurück, jede auf einer neuen Zeile."
            )
        }],
        temperature=0.7
    )
    queries = response.choices[0].message.content.strip().split("\n")
    return [q.strip().lstrip("0123456789.-) ") for q in queries if q.strip()]

def recherche_multi_query(
    frage: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5,
    n_queries: int = 3
) -> list[dict]:
    """Suche mit mehreren Neuformulierungen."""
    queries = [frage] + generer_multi_queries(frage, n_queries)
    print(f"Abfragen: {queries}")

    alle_punktzahlen = np.zeros(len(documents))
    for abfrage in queries:
        abfrage_emb = client.embeddings.create(
            input=abfrage, model="text-embedding-3-large"
        ).data[0].embedding
        punktzahlen = corpus_embeddings @ np.array(abfrage_emb)
        alle_punktzahlen = np.maximum(alle_punktzahlen, punktzahlen)

    top_k = np.argsort(alle_punktzahlen)[-k:][::-1]
    return [
        {**documents[i], "score": float(alle_punktzahlen[i])}
        for i in top_k
    ]

HyDE: Hypothetische Dokument-Embeddings

HyDE greift das Problem von der anderen Seite an. Statt Fragen zu vervielfältigen, lässt das Modell eine plausible Antwort generieren – selbst wenn diese teilweise falsch ist, ist das unerheblich – und sucht dann nach Dokumenten, die dieser Antwort nahekommen. Die Frage „Wie konfiguriere ich SSO?” erzeugt einen Absatz über Identitätsanbieter, SAML und Metadaten, genau das Vokabular, das in der echten Dokumentation zu finden ist.

def recherche_hyde(
    frage: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5
) -> list[dict]:
    """Suche mit einem hypothetischen Dokument (HyDE)."""
    # 1. Generierung eines hypothetischen Dokuments
    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[{
            "role": "user",
            "content": (
                f"Schreiben Sie einen kurzen Absatz (3-5 Sätze), der diese Frage "
                f"beantwortet. Schreiben Sie so, als wäre es ein Auszug "
                f"aus einer technischen Dokumentation.\n\n"
                f"Frage: {frage}"
            )
        }],
        temperature=0.3
    )
    doc_hypothetique = response.choices[0].message.content

    # 2. Kodierung des hypothetischen Dokuments
    hyde_emb = client.embeddings.create(
        input=doc_hypothetique,
        model="text-embedding-3-large"
    ).data[0].embedding

    # 3. Suche nach ähnlichen Dokumenten
    punktzahlen = corpus_embeddings @ np.array(hyde_emb)
    top_k = np.argsort(punktzahlen)[-k:][::-1]

    return [
        {**documents[i], "score": float(punktzahlen[i])}
        for i in top_k
    ]

Wenn diese Technik funktioniert, liegt es daran, dass das Embedding einer Antwort dem Embedding eines Dokuments ähnlicher ist als das Embedding einer Frage. Eine Frage ist kurz, fragend und oft in den eigenen Worten des Nutzers formuliert; ein Dokument ist lang, affirmativ und in Fachjargon geschrieben. HyDE überbrückt diese Lücke zwischen Abfrage und Dokument, indem es die Abfrage auf das Terrain des Korpus verlagert. Genau deshalb spezifiziert der Prompt auch „als ob es sich um einen Auszug aus einer technischen Dokumentation handelt”: Wir imitieren das Zielsprachniveau.

Step-Back-Prompting

Die dritte Technik behandelt den gegenteiligen Fall: Fragen, die so spezifisch sind, dass kein Dokument sie direkt adressiert. „Kann ich ein im Februar gekauftes E-Bike absetzen?” taucht nirgendwo auf, doch der Abschnitt über abzugsfähige Betriebsausgaben tut es. Wir bitten das Modell daher um die allgemeine Frage, die sich hinter der spezifischen verbirgt, und suchen mit beiden, gewichtet mit 0,7 für die ursprüngliche Frage und 0,3 für die breitere – der weite Kontext hilft, sollte aber nicht dominieren.

def recherche_step_back(
    frage: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5
) -> list[dict]:
    """Zweistufige Suche: allgemein dann spezifisch."""
    # 1. Generierung einer Step-Back-Frage
    response = client.chat.completions.create(
        model="gpt-5.6-terra",
        messages=[{
            "role": "user",
            "content": (
                f"Was ist die allgemeinere Frage oder das zugrundeliegende Konzept "
                f"hinter dieser spezifischen Frage?\n\n"
                f"Spezifische Frage: {frage}\n\n"
                f"Geben Sie nur die allgemeine Frage zurück."
            )
        }],
        temperature=0.3
    )
    question_generale = response.choices[0].message.content.strip()

    # 2. Suche mit beiden Fragen
    resultats_combines = {}

    for q, poids in [(frage, 0.7), (question_generale, 0.3)]:
        q_emb = client.embeddings.create(
            input=q, model="text-embedding-3-large"
        ).data[0].embedding
        punktzahlen = corpus_embeddings @ np.array(q_emb)

        for i, s in enumerate(punktzahlen):
            if i not in resultats_combines:
                resultats_combines[i] = 0.0
            resultats_combines[i] += float(s) * poids

    top_k = sorted(
        resultats_combines.items(),
        key=lambda x: x[1], reverse=True
    )[:k]

    return [
        {**documents[idx], "score": score}
        for idx, score in top_k
    ]

Vergleich der Techniken

Jede Technik adressiert eine spezifische Abrufpathologie und kostet durch zusätzliche Aufrufe mehr Latenz.

TechnikWann einsetzenZusätzlicher Aufwand
Multi-QueryMehrdeutige Abfragen, variierendes Vokabularn Embedding-Aufrufe + 1 LLM-Aufruf
HyDELücke zwischen Abfrage und Dokument, technische Domänen1 LLM-Aufruf + 1 Embedding-Aufruf
Step-BackSehr spezifische Fragen, Bedarf an Kontext1 LLM-Aufruf + 2 Embedding-Aufrufe

Kombination von Techniken

Man muss sich nicht für nur eine entscheiden. Die folgende Funktion akzeptiert eine Liste von Strategien, sammelt die Punktzahlen, die jede erzeugt, in einem Dictionary, das nach Dokumenten indiziert ist, und behält das Maximum bei. Ein Dokument, das allein durch HyDE gefunden wurde, wird daher neben einem Dokument, das durch direkte Suche gefunden wurde, gerankt. Denken Sie jedoch daran, dass das Stapeln von drei Strategien das Budget verdreifacht und die Antwortzeit verlängert: Reservieren Sie diese Kombination für schwierige Abfragen oder für die Offline-Verarbeitung, bei der Latenz keine Rolle spielt.

def recherche_avancee(
    frage: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5,
    strategies: list[str] = ["direct", "multi_query", "hyde"]
) -> list[dict]:
    """Kombinieren Sie mehrere Suchstrategien."""
    alle_resultate = {}

    if "direct" in strategies:
        q_emb = client.embeddings.create(
            input=frage, model="text-embedding-3-large"
        ).data[0].embedding
        punktzahlen = corpus_embeddings @ np.array(q_emb)
        for i, s in enumerate(punktzahlen):
            alle_resultate.setdefault(i, []).append(float(s))

    if "hyde" in strategies:
        res_hyde = recherche_hyde(
            frage, corpus_embeddings, documents, k=k*2
        )
        for r in res_hyde:
            alle_resultate.setdefault(r["id"], []).append(r["score"])

    # Endgültige Punktzahl = Maximum der Punktzahlen
    scores_finaux = {
        idx: max(scores) for idx, scores in alle_resultate.items()
    }

    top_k = sorted(
        scores_finaux.items(), key=lambda x: x[1], reverse=True
    )[:k]

    return [
        {**documents[idx], "score": score}
        for idx, score in top_k
    ]

Zusammenfassung

  • Multi-Query formuliert die Frage aus mehreren Perspektiven, um mehr Vokabular abzudecken
  • HyDE generiert eine hypothetische Antwort, die den Zieldokumenten ähnlicher ist
  • Step-Back stellt eine allgemeinere Frage, um breiten Kontext einzufangen
  • Diese Techniken erhöhen die Latenz, verbessern aber die Recall-Rate erheblich