{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# RAG-Praxis: Retrieval-Augmented Generation (lokal)\n", "\n", "Dieses Notebook führt den **RAG-Ablauf** Schritt für Schritt aus – **vollständig lokal**, ohne externe LLM-API. Dabei gehen wir durch:\n", "\n", "1. Einen kleinen Dokumentenkorpus zu chunken und zu embedden\n", "2. Semantische Suche (Retrieval) mit Kosinusähnlichkeit\n", "3. Den RAG-Prompt zu bauen (Kontext + Frage)\n", "4. Eine lokale „Antwort“ ohne API (Mock-Generierung auf Basis des Kontexts)\n", "\n", "**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.\n", "\n", "**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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Abhängigkeiten und Embedding-Modell" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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.\n", "\n", "**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**." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.620114Z", "iopub.status.busy": "2026-03-24T18:20:46.619960Z", "iopub.status.idle": "2026-03-24T18:20:46.660334Z", "shell.execute_reply": "2026-03-24T18:20:46.659624Z", "shell.execute_reply.started": "2026-03-24T18:20:46.620096Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dummy-Embedding aktiv (kein Download). RAG-Ablauf läuft sofort.\n", "Bereit für die nächsten Zellen.\n" ] } ], "source": [ "import numpy as np\n", "\n", "# True = sofort lauffähig, ohne Download. False = echtes Modell (erster Lauf: Download von Hugging Face)\n", "USE_DUMMY_EMBEDDINGS = True\n", "\n", "if USE_DUMMY_EMBEDDINGS:\n", " # Dummy-Embedding: kein Download, läuft sofort auf CPU. Nur für Demo des RAG-Ablaufs.\n", " EMBED_DIM = 384\n", "\n", " class DummyEmbedder:\n", " def encode(self, texts, **kwargs):\n", " out = np.zeros((len(texts), EMBED_DIM))\n", " for i, t in enumerate(texts):\n", " np.random.seed(hash(t) % (2**32))\n", " out[i] = np.random.randn(EMBED_DIM)\n", " return out.astype(np.float32)\n", "\n", " model = DummyEmbedder()\n", " print(\"Dummy-Embedding aktiv (kein Download). RAG-Ablauf läuft sofort.\")\n", "else:\n", " import os\n", " os.environ[\"TRANSFORMERS_NO_TF\"] = \"1\"\n", " try:\n", " from sentence_transformers import SentenceTransformer\n", " except ImportError:\n", " raise ImportError(\"Bitte installieren: pip install sentence-transformers\")\n", " except Exception as e:\n", " if \"Keras\" in str(e) or \"tf_keras\" in str(e):\n", " raise ImportError(\n", " \"Konflikt transformers/Keras 3. Option: pip install tf-keras oder TRANSFORMERS_NO_TF=1 setzen.\"\n", " ) from e\n", " raise\n", " print(\"Modell wird geladen (beim ersten Mal Download, bitte ggf. 2–8 Min. warten) …\")\n", " model = SentenceTransformer(\"all-MiniLM-L6-v2\") \n", " print(\"Embedding-Modell geladen (https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2).\")\n", "\n", "print(\"Bereit für die nächsten Zellen.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Beispiel-Dokumente und Chunking\n", "\n", "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)." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.660776Z", "iopub.status.busy": "2026-03-24T18:20:46.660650Z", "iopub.status.idle": "2026-03-24T18:20:46.663633Z", "shell.execute_reply": "2026-03-24T18:20:46.663185Z", "shell.execute_reply.started": "2026-03-24T18:20:46.660767Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Anzahl Chunks: 5\n" ] } ], "source": [ "docs = [\n", " \"RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.\",\n", " \"Embeddings sind Vektoren, die die Bedeutung von Text erfassen. Ähnliche Texte haben ähnliche Vektoren und liegen im Vektorraum nah beieinander.\",\n", " \"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.\",\n", " \"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.\",\n", " \"Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen.\",\n", "]\n", "\n", "# Einfaches Chunking: hier jeder Absatz = ein Chunk\n", "chunks = docs\n", "print(f\"Anzahl Chunks: {len(chunks)}\")" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.664152Z", "iopub.status.busy": "2026-03-24T18:20:46.664039Z", "iopub.status.idle": "2026-03-24T18:20:46.667280Z", "shell.execute_reply": "2026-03-24T18:20:46.666939Z", "shell.execute_reply.started": "2026-03-24T18:20:46.664143Z" } }, "outputs": [ { "data": { "text/plain": [ "['RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.',\n", " 'Embeddings sind Vektoren, die die Bedeutung von Text erfassen. Ähnliche Texte haben ähnliche Vektoren und liegen im Vektorraum nah beieinander.',\n", " '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.',\n", " '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.',\n", " 'Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen.']" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "chunks" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Embeddings berechnen und speichern" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.667677Z", "iopub.status.busy": "2026-03-24T18:20:46.667598Z", "iopub.status.idle": "2026-03-24T18:20:46.676694Z", "shell.execute_reply": "2026-03-24T18:20:46.676302Z", "shell.execute_reply.started": "2026-03-24T18:20:46.667669Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Shape der Embeddings: (5, 384)\n" ] } ], "source": [ "chunk_embeddings = model.encode(chunks)\n", "chunk_embeddings = np.array(chunk_embeddings)\n", "print(f\"Shape der Embeddings: {chunk_embeddings.shape}\")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.677140Z", "iopub.status.busy": "2026-03-24T18:20:46.677041Z", "iopub.status.idle": "2026-03-24T18:20:46.680624Z", "shell.execute_reply": "2026-03-24T18:20:46.679872Z", "shell.execute_reply.started": "2026-03-24T18:20:46.677132Z" } }, "outputs": [ { "data": { "text/plain": [ "array([[ 0.35093144, -2.0653076 , 0.8655082 , ..., -0.6377263 ,\n", " -1.4079658 , -0.39661685],\n", " [ 2.281467 , -0.55625576, 0.28869125, ..., -1.0337354 ,\n", " 0.22436005, 0.973891 ],\n", " [-1.3814495 , 2.2624822 , -0.8642466 , ..., -1.9703931 ,\n", " 0.6935014 , -1.2209647 ],\n", " [-0.52167743, 0.05087733, -0.6374621 , ..., 0.5776518 ,\n", " -0.61855644, -1.3274955 ],\n", " [-0.7547236 , -0.6995751 , 0.8159137 , ..., -0.2962538 ,\n", " -0.3425144 , 0.46511042]], shape=(5, 384), dtype=float32)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "chunk_embeddings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Retrieval: Kosinusähnlichkeit und Top-k\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.681334Z", "iopub.status.busy": "2026-03-24T18:20:46.681078Z", "iopub.status.idle": "2026-03-24T18:20:46.684762Z", "shell.execute_reply": "2026-03-24T18:20:46.684308Z", "shell.execute_reply.started": "2026-03-24T18:20:46.681322Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Top-2 Chunks zur Frage: 'Wie funktioniert RAG?'\n", " 1. (Score 0.038) Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle...\n", " 2. (Score 0.022) RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstel...\n" ] } ], "source": [ "def cosine_similarity(a, b):\n", " return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))\n", "\n", "def retrieve(query: str, top_k: int = 2):\n", " q_emb = model.encode([query])[0]\n", " scores = [cosine_similarity(q_emb, c) for c in chunk_embeddings]\n", " idx = np.argsort(scores)[::-1][:top_k]\n", " return [(i, scores[i], chunks[i]) for i in idx]\n", "\n", "# Testabfrage\n", "query = \"Wie funktioniert RAG?\"\n", "results = retrieve(query, top_k=2)\n", "print(\"Top-2 Chunks zur Frage:\", repr(query))\n", "for i, (idx, score, text) in enumerate(results, 1):\n", " print(f\" {i}. (Score {score:.3f}) {text[:80]}...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. RAG-Prompt bauen\n", "\n", "**Ü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.\n", "\n", "**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.\n", "\n", "**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." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.685182Z", "iopub.status.busy": "2026-03-24T18:20:46.685089Z", "iopub.status.idle": "2026-03-24T18:20:46.687695Z", "shell.execute_reply": "2026-03-24T18:20:46.687241Z", "shell.execute_reply.started": "2026-03-24T18:20:46.685172Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "=== RAG-Prompt (würde an ein LLM gesendet) ===\n", "Antworte nur auf Basis des folgenden Kontexts. Wenn die Antwort nicht im Kontext steht, sage das.\n", "\n", "Kontext:\n", "Ohne RAG kennt das Modell nur sein Trainingswissen. Mit RAG kann es auf aktuelle Dokumente und firmeneigenes Wissen zugreifen.\n", "RAG bedeutet Retrieval-Augmented Generation. Man sucht zuerst relevante Textstellen in eigenen Dokumenten und gibt sie dem Sprachmodell als Kontext mit.\n", "\n", "Frage: Wie funktioniert RAG?\n", "\n", "Antwort:\n" ] } ], "source": [ "def build_rag_prompt(query: str, retrieved_chunks: list, max_context_len: int = 500):\n", " context = \"\\n\".join(r[2] for r in retrieved_chunks)\n", " if len(context) > max_context_len:\n", " context = context[:max_context_len] + \"...\"\n", " return (\n", " \"Antworte nur auf Basis des folgenden Kontexts. \"\n", " \"Wenn die Antwort nicht im Kontext steht, sage das.\\n\\n\"\n", " \"Kontext:\\n\" + context + \"\\n\\n\"\n", " \"Frage: \" + query + \"\\n\\n\"\n", " \"Antwort:\"\n", " )\n", "\n", "rag_prompt = build_rag_prompt(query, results)\n", "print(\"=== RAG-Prompt (würde an ein LLM gesendet) ===\")\n", "print(rag_prompt)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Lokale „Antwort“ ohne externe API\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.688167Z", "iopub.status.busy": "2026-03-24T18:20:46.688076Z", "iopub.status.idle": "2026-03-24T18:20:46.690482Z", "shell.execute_reply": "2026-03-24T18:20:46.690026Z", "shell.execute_reply.started": "2026-03-24T18:20:46.688160Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[Lokale Mock-Antwort – ohne LLM-API]\n", "Basierend auf dem abgerufenen Kontext:\n", "\n", "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.\n" ] } ], "source": [ "def mock_answer(query: str, retrieved_chunks: list) -> str:\n", " \"\"\"Erzeugt eine lokale Antwort ohne API: Kurze Zusammenfassung der Chunks.\"\"\"\n", " parts = [r[2] for r in retrieved_chunks]\n", " summary = \" \".join(parts)\n", " if len(summary) > 400:\n", " summary = summary[:400] + \"...\"\n", " return (\n", " \"[Lokale Mock-Antwort – ohne LLM-API]\\n\"\n", " \"Basierend auf dem abgerufenen Kontext:\\n\\n\" + summary\n", " )\n", "\n", "answer = mock_answer(query, results)\n", "print(answer)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Andere Fragen ausprobieren" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2026-03-24T18:20:46.691020Z", "iopub.status.busy": "2026-03-24T18:20:46.690911Z", "iopub.status.idle": "2026-03-24T18:20:46.693384Z", "shell.execute_reply": "2026-03-24T18:20:46.693045Z", "shell.execute_reply.started": "2026-03-24T18:20:46.691010Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Frage: Was sind Embeddings?\n", "[Lokale Mock-Antwort – ohne LLM-API]\n", "Basierend auf dem abgerufenen Kontext:\n", "\n", "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 ...\n", "--------------------------------------------------\n", "Frage: Wozu braucht man semantische Suche?\n", "[Lokale Mock-Antwort – ohne LLM-API]\n", "Basierend auf dem abgerufenen Kontext:\n", "\n", "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 ...\n", "--------------------------------------------------\n" ] } ], "source": [ "for q in [\"Was sind Embeddings?\", \"Wozu braucht man semantische Suche?\"]:\n", " res = retrieve(q, top_k=2)\n", " ans = mock_answer(q, res)\n", " print(\"Frage:\", q)\n", " print(ans[:300], \"...\" if len(ans) > 300 else \"\")\n", " print(\"-\" * 50)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Zusammenfassung\n", "\n", "* **Embeddings** und **Retrieval** laufen vollständig lokal (sentence-transformers).\n", "* Der **RAG-Prompt** (Kontext + Frage) kann bei Bedarf an ein lokales LLM (z. B. Ollama) oder eine API gesendet werden.\n", "* Die **Mock-Antwort** zeigt den Ablauf ohne externe API; in der echten Anwendung wird sie durch den LLM-Aufruf ersetzt." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }