RAG-Praxis: Retrieval-Augmented Generation (lokal)

Dieses Notebook führt den RAG-Ablauf Schritt für Schritt aus – vollständig lokal, ohne externe LLM-API. Dabei gehen wir durch:

  1. Einen kleinen Dokumentenkorpus zu chunken und zu embedden

  2. Semantische Suche (Retrieval) mit Kosinusähnlichkeit

  3. Den RAG-Prompt zu bauen (Kontext + Frage)

  4. Eine lokale „Antwort“ ohne API (Mock-Generierung auf Basis des Kontexts)

Zwei Modi: (1) Dummy-Embedding (Standard): Kein Download, läuft sofort auf CPU – ideal zum Durchklicken. (2) Echtes Modell (sentence-transformers): Beim ersten Lauf Download von Hugging Face (~90 MB, 2–5 Min.), danach läuft es auf normaler CPU.

Hinweis bei Keras-3-Fehler: Wenn beim Import ein Fehler zu „Keras 3“ oder „tf_keras“ erscheint, setzt die erste Code-Zelle automatisch TRANSFORMERS_NO_TF=1. Falls der Fehler trotzdem auftritt, vor dem Start des Notebooks in der Konsole ausführen: pip install tf-keras oder die Umgebungsvariable TRANSFORMERS_NO_TF=1 setzen.

1. Abhängigkeiten und Embedding-Modell

Option A – Ohne Download (empfohlen für schnellen Durchlauf): USE_DUMMY_EMBEDDINGS = True setzen (Standard). Das Notebook läuft sofort mit Dummy-Vektoren; der RAG-Ablauf (Retrieval, Prompt, Mock-Antwort) bleibt derselbe.

Option B – Echtes Embedding-Modell: USE_DUMMY_EMBEDDINGS = False setzen. Beim ersten Lauf wird das Modell von Hugging Face geladen (~90 MB, 2–5 Min.). Danach läuft alles auf normaler CPU.

[1]:
import numpy as np

# True = sofort lauffähig, ohne Download. False = echtes Modell (erster Lauf: Download von Hugging Face)
USE_DUMMY_EMBEDDINGS = True

if USE_DUMMY_EMBEDDINGS:
    # Dummy-Embedding: kein Download, läuft sofort auf CPU. Nur für Demo des RAG-Ablaufs.
    EMBED_DIM = 384

    class DummyEmbedder:
        def encode(self, texts, **kwargs):
            out = np.zeros((len(texts), EMBED_DIM))
            for i, t in enumerate(texts):
                np.random.seed(hash(t) % (2**32))
                out[i] = np.random.randn(EMBED_DIM)
            return out.astype(np.float32)

    model = DummyEmbedder()
    print("Dummy-Embedding aktiv (kein Download). RAG-Ablauf läuft sofort.")
else:
    import os
    os.environ["TRANSFORMERS_NO_TF"] = "1"
    try:
        from sentence_transformers import SentenceTransformer
    except ImportError:
        raise ImportError("Bitte installieren: pip install sentence-transformers")
    except Exception as e:
        if "Keras" in str(e) or "tf_keras" in str(e):
            raise ImportError(
                "Konflikt transformers/Keras 3. Option: pip install tf-keras oder TRANSFORMERS_NO_TF=1 setzen."
            ) from e
        raise
    print("Modell wird geladen (beim ersten Mal Download, bitte ggf. 2–8 Min. warten) …")
    model = SentenceTransformer("all-MiniLM-L6-v2")
    print("Embedding-Modell geladen (https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2).")

print("Bereit für die nächsten Zellen.")
Dummy-Embedding aktiv (kein Download). RAG-Ablauf läuft sofort.
Bereit für die nächsten Zellen.

2. Beispiel-Dokumente und Chunking

Für den Showcase verwenden wir einen kleinen deutschen Text aus verschiedenen Absätzen. In der Praxis ersetzen wir ihn durch die entsprechenden inhaltlichen Dokumente (z. B. FAQs, Handbücher).

[2]:
docs = [
    "RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.",
    "Embeddings sind Vektoren, die die Bedeutung von Text erfassen. Ähnliche Texte haben ähnliche Vektoren und liegen im Vektorraum nah beieinander.",
    "Bei der semantischen Suche wird nicht nach exakten Wörtern gesucht, sondern nach inhaltlich passenden Stellen. Dafür nutzt man Embeddings und ein Ähnlichkeitsmaß wie die Kosinusähnlichkeit.",
    "Ein Large Language Model erzeugt Text Token für Token. Es sagt das nächste Wort vorher und fügt es an den bisherigen Kontext an.",
    "Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen.",
]

# Einfaches Chunking: hier jeder Absatz = ein Chunk
chunks = docs
print(f"Anzahl Chunks: {len(chunks)}")
Anzahl Chunks: 5
[3]:
chunks
[3]:
['RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.',
 'Embeddings sind Vektoren, die die Bedeutung von Text erfassen. Ähnliche Texte haben ähnliche Vektoren und liegen im Vektorraum nah beieinander.',
 'Bei der semantischen Suche wird nicht nach exakten Wörtern gesucht, sondern nach inhaltlich passenden Stellen. Dafür nutzt man Embeddings und ein Ähnlichkeitsmaß wie die Kosinusähnlichkeit.',
 'Ein Large Language Model erzeugt Text Token für Token. Es sagt das nächste Wort vorher und fügt es an den bisherigen Kontext an.',
 'Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen.']

3. Embeddings berechnen und speichern

[4]:
chunk_embeddings = model.encode(chunks)
chunk_embeddings = np.array(chunk_embeddings)
print(f"Shape der Embeddings: {chunk_embeddings.shape}")
Shape der Embeddings: (5, 384)
[5]:
chunk_embeddings
[5]:
array([[ 0.35093144, -2.0653076 ,  0.8655082 , ..., -0.6377263 ,
        -1.4079658 , -0.39661685],
       [ 2.281467  , -0.55625576,  0.28869125, ..., -1.0337354 ,
         0.22436005,  0.973891  ],
       [-1.3814495 ,  2.2624822 , -0.8642466 , ..., -1.9703931 ,
         0.6935014 , -1.2209647 ],
       [-0.52167743,  0.05087733, -0.6374621 , ...,  0.5776518 ,
        -0.61855644, -1.3274955 ],
       [-0.7547236 , -0.6995751 ,  0.8159137 , ..., -0.2962538 ,
        -0.3425144 ,  0.46511042]], shape=(5, 384), dtype=float32)

4. Retrieval: Kosinusähnlichkeit und Top-k

Zur Frage (z. B. „Wie funktioniert RAG?“) ermitteln wir die semantisch ähnlichsten Chunks aus dem Korpus. Die Variable results enthält die Top-k Treffer (Index, Score, Text). Diese abgerufenen Chunks brauchen wir im nächsten Schritt, um den RAG-Prompt zu bauen.

[6]:
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def retrieve(query: str, top_k: int = 2):
    q_emb = model.encode([query])[0]
    scores = [cosine_similarity(q_emb, c) for c in chunk_embeddings]
    idx = np.argsort(scores)[::-1][:top_k]
    return [(i, scores[i], chunks[i]) for i in idx]

# Testabfrage
query = "Wie funktioniert RAG?"
results = retrieve(query, top_k=2)
print("Top-2 Chunks zur Frage:", repr(query))
for i, (idx, score, text) in enumerate(results, 1):
    print(f"  {i}. (Score {score:.3f}) {text[:80]}...")
Top-2 Chunks zur Frage: 'Wie funktioniert RAG?'
  1. (Score 0.038) Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle...
  2. (Score 0.022) RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstel...

5. RAG-Prompt bauen

Übergang von Retrieval zu RAG: In Schritt 4 haben wir die Frage (z. B. „Wie funktioniert RAG?“) gestellt und mit der semantischen Suche die relevanten Chunks abgerufen – die Variable results enthält genau diese Treffer. Diese Texte liegen aber noch einzeln vor; ein Sprachmodell sieht sie noch nicht.

Was jetzt fehlt: Bei RAG soll das LLM die Antwort aus genau diesem Kontext ableiten. Dafür müssen wir die abgerufenen Chunks und die Frage zu einem einzigen Prompt zusammenbauen: „Hier ist der Kontext: … Hier ist die Frage: … Antworte nur auf Basis des Kontexts.“ Diesen fertigen Text würde man in der Praxis an ein LLM senden; das LLM liest Kontext + Frage und generiert die Antwort.

In dieser Zelle bauen wir genau diesen Prompt aus results und query – ohne echten API-Aufruf, damit alles lokal läuft. So wird sichtbar, was später an ein echtes LLM gehen würde.

[7]:
def build_rag_prompt(query: str, retrieved_chunks: list, max_context_len: int = 500):
    context = "\n".join(r[2] for r in retrieved_chunks)
    if len(context) > max_context_len:
        context = context[:max_context_len] + "..."
    return (
        "Antworte nur auf Basis des folgenden Kontexts. "
        "Wenn die Antwort nicht im Kontext steht, sage das.\n\n"
        "Kontext:\n" + context + "\n\n"
        "Frage: " + query + "\n\n"
        "Antwort:"
    )

rag_prompt = build_rag_prompt(query, results)
print("=== RAG-Prompt (würde an ein LLM gesendet) ===")
print(rag_prompt)
=== RAG-Prompt (würde an ein LLM gesendet) ===
Antworte nur auf Basis des folgenden Kontexts. Wenn die Antwort nicht im Kontext steht, sage das.

Kontext:
Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen.
RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.

Frage: Wie funktioniert RAG?

Antwort:

6. Lokale „Antwort“ ohne externe API

Damit das Notebook ohne LLM-API auskommt, erzeugen wir eine Mock-Antwort, die die abgerufenen Chunks zusammenfasst. In der Praxis wird dieser Schritt durch einen Aufruf an ein lokales LLM ersetzt (z. B. Ollama) oder eine Cloud-API.

[8]:
def mock_answer(query: str, retrieved_chunks: list) -> str:
    """Erzeugt eine lokale Antwort ohne API: Kurze Zusammenfassung der Chunks."""
    parts = [r[2] for r in retrieved_chunks]
    summary = " ".join(parts)
    if len(summary) > 400:
        summary = summary[:400] + "..."
    return (
        "[Lokale Mock-Antwort – ohne LLM-API]\n"
        "Basierend auf dem abgerufenen Kontext:\n\n" + summary
    )

answer = mock_answer(query, results)
print(answer)
[Lokale Mock-Antwort – ohne LLM-API]
Basierend auf dem abgerufenen Kontext:

Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen. RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.

7. Andere Fragen ausprobieren

[9]:
for q in ["Was sind Embeddings?", "Wozu braucht man semantische Suche?"]:
    res = retrieve(q, top_k=2)
    ans = mock_answer(q, res)
    print("Frage:", q)
    print(ans[:300], "..." if len(ans) > 300 else "")
    print("-" * 50)
Frage: Was sind Embeddings?
[Lokale Mock-Antwort – ohne LLM-API]
Basierend auf dem abgerufenen Kontext:

Embeddings sind Vektoren, die die Bedeutung von Text erfassen. Ähnliche Texte haben ähnliche Vektoren und liegen im Vektorraum nah beieinander. RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textste ...
--------------------------------------------------
Frage: Wozu braucht man semantische Suche?
[Lokale Mock-Antwort – ohne LLM-API]
Basierend auf dem abgerufenen Kontext:

RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit. Ein Large Language Model erzeugt Text Token für Token. Es sagt das näc ...
--------------------------------------------------

Zusammenfassung

  • Embeddings und Retrieval laufen vollständig lokal (sentence-transformers).

  • Der RAG-Prompt (Kontext + Frage) kann bei Bedarf an ein lokales LLM (z. B. Ollama) oder eine API gesendet werden.

  • Die Mock-Antwort zeigt den Ablauf ohne externe API; in der echten Anwendung wird sie durch den LLM-Aufruf ersetzt.