Embeddings & RAG
Wie du LLMs externes Wissen gibst: Embeddings, Vector-Datenbanken und Retrieval-Augmented Generation (RAG). Die Basis fuer Doku-Chatbots, Wissens-Apps und mehr.
Inhaltsverzeichnis
Embeddings & RAG
Ein LLM โweissโ nur, was in seinem Training war. Fuer Firmendaten, eigene Dokumente oder aktuelle Infos brauchst du Retrieval-Augmented Generation (RAG) - der Pattern fast aller โChat mit deinen Datenโ-Apps.
Das Problem
Du willst einen Bot, der Fragen zu deiner Doku beantwortet. Der LLM kennt sie nicht. Was tun?
Naive Loesung: Dokumentation einfach in den Prompt packen.
Problem: Kontext-Fenster sind zu klein (oder teuer), und Relevanz leidet.
Richtige Loesung: RAG.
RAG in einem Satz
Finde die relevanten Textstuecke, packe nur die in den Prompt.
Wie findet man die Relevanten? Mit Embeddings und einer Vector-Datenbank.
Embeddings
Ein Embedding ist ein numerischer Vektor, der die Bedeutung eines Texts darstellt. Aehnliche Bedeutung = aehnlicher Vektor.
Beispiel:
- โHundโ โ [0.12, -0.05, 0.88, โฆ]
- โKatzeโ โ [0.10, -0.07, 0.85, โฆ] โ sehr aehnlich
- โStaubsaugerโ โ [0.91, 0.20, -0.15, โฆ] โ ganz anders
Die Vektoren sind typischerweise 768-3000 Dimensionen gross.
Embedding berechnen
Mit OpenAIs Embedding-API:
from openai import OpenAI
client = OpenAI()
response = client.embeddings.create(
model="text-embedding-3-small",
input="Rust ist eine Systemsprache."
)
vector = response.data[0].embedding
print(len(vector)) # 1536
Kosten: winzig (~$0.00002 pro Tausend Tokens).
Aehnlichkeit messen
Die Cosine Similarity zweier Vektoren zeigt, wie aehnlich sie sind:
import numpy as np
def cosine(a, b):
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
a = embed("Der Hund bellt laut.")
b = embed("Ein Wauwau macht Geraeusche.")
c = embed("Der Kuchen schmeckt suess.")
print(cosine(a, b)) # hoch (~0.85)
print(cosine(a, c)) # niedrig (~0.20)
Das Prinzip funktioniert sprachuebergreifend - ein deutscher und englischer Satz mit gleicher Bedeutung haben aehnliche Embeddings.
RAG-Pipeline - die 5 Schritte
Hier der klassische Ablauf:
1. Chunking
Deine Dokumente in kleine Haeppchen aufteilen (500-1500 Tokens pro Chunk):
def chunks(text: str, size: int = 500, overlap: int = 50):
result = []
for i in range(0, len(text), size - overlap):
result.append(text[i:i + size])
return result
Gute Chunks respektieren Absatz- und Satzgrenzen. Libraries wie LangChain oder LlamaIndex haben fertige Splitter.
2. Embedding berechnen
Fuer jeden Chunk einen Vektor erzeugen - einmalig, offline.
3. In Vector-DB speichern
Du brauchst eine Datenbank, die nach Aehnlichkeit sucht:
- Qdrant - Open Source, Rust, top Performance
- Pinecone - Managed Service, sehr einfach
- pgvector - PostgreSQL-Extension
- ChromaDB - simpel, lokal
- Weaviate - Open Source mit Schema-Support
Vereinfacht kannst du auch ohne DB auskommen - fuer Prototypen reichen In-Memory-Vektoren.
4. Query retrieval
Bei einer User-Frage:
- Frage in Embedding umwandeln
- Top-K (z.B. 5) aehnlichste Chunks aus der DB holen
5. Augmented prompt
Die Chunks als Kontext in den LLM-Prompt:
Beantworte die Frage basierend auf dem Kontext.
Wenn die Antwort nicht im Kontext steht, sag das.
Kontext:
[Chunk 1]
[Chunk 2]
[Chunk 3]
Frage: [User-Frage]
Das LLM antwortet nur mit den gegebenen Infos.
Ein minimaler RAG (Python)
from openai import OpenAI
import numpy as np
client = OpenAI()
# 1) Unsere "Datenbank" - drei Texte
docs = [
"Unsere Oeffnungszeiten: Mo-Fr 9-18 Uhr, Sa 10-14 Uhr.",
"Ruecksendungen sind innerhalb 30 Tagen kostenlos moeglich.",
"Unser Flagship-Produkt ist die Pro-Cloud fuer 29 EUR/Monat.",
]
# 2) Embeddings berechnen
def embed(text: str) -> np.ndarray:
r = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return np.array(r.data[0].embedding)
doc_vectors = [embed(d) for d in docs]
# 3) Frage und Retrieval
def answer(frage: str, top_k: int = 2) -> str:
q_vec = embed(frage)
sims = [
(np.dot(q_vec, dv) / (np.linalg.norm(q_vec) * np.linalg.norm(dv)), i)
for i, dv in enumerate(doc_vectors)
]
top = sorted(sims, reverse=True)[:top_k]
context = "\n".join(docs[i] for _, i in top)
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content":
"Beantworte NUR mit den Infos aus dem Kontext. Auf Deutsch."},
{"role": "user", "content":
f"Kontext:\n{context}\n\nFrage: {frage}"}
]
)
return r.choices[0].message.content
# Ausprobieren:
print(answer("Wann habt ihr am Samstag offen?"))
print(answer("Kann ich was zurueckschicken?"))
Drei Zeilen Infrastruktur plus ein Paar API-Calls - fertig ist dein Doku-Bot.
Vector-DB mit Qdrant (Produktion)
Fuer echte Apps nutzt du eine Vector-DB:
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
qdrant = QdrantClient(":memory:") # oder URL
qdrant.create_collection(
collection_name="docs",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
# Insert
points = [
PointStruct(id=i, vector=embed(d).tolist(), payload={"text": d})
for i, d in enumerate(docs)
]
qdrant.upsert(collection_name="docs", points=points)
# Query
hits = qdrant.query_points(
collection_name="docs",
query=embed(frage).tolist(),
limit=3,
).points
for hit in hits:
print(hit.score, hit.payload["text"])
Qdrant skaliert auf Millionen Dokumente und ist extrem schnell.
Chunk-Groessen und Overlap
Zu klein (50 Tokens) โ keine Context Zu gross (2000+ Tokens) โ irrelevanter Fuellstoff
Faustregel: 500-1000 Tokens pro Chunk, mit 50-100 Tokens Overlap (damit wichtige Infos nicht an Grenzen zerrissen werden).
Reranking - die naechste Stufe
Einfaches Top-K liefert oft mittelmaessige Ergebnisse. Bessere Loesung:
- Retrieve: Top 20 Kandidaten per Embedding
- Rerank: Cross-Encoder-Modell bewertet jeden einzeln
- Nimm die besten 3-5 als Kontext
Cohereโs /rerank-API ist dafuer beliebt.
Hybrid Search
Noch besser: Kombiniere Embedding-Search mit klassischer BM25-Volltext-Suche. Beide haben Staerken - zusammen ergeben sie die besten Ergebnisse.
Qdrant, Weaviate und viele andere unterstuetzen das nativ.
Frameworks
Fuer echte Apps lohnen sich Libraries:
- LangChain - der bekannte Baukasten (Python + JS)
- LlamaIndex - speziell fuer RAG
- Haystack - reif, Enterprise-tauglich
Sie bringen Chunking, Retrieval, Prompt-Templates mit.
Wann RAG vs. Fine-Tuning?
- RAG: Wenn du Wissen hinzufuegen willst (Doku, FAQ, Produktkatalog)
- Fine-Tuning: Wenn du Stil oder Verhalten aendern willst
RAG ist fast immer der bessere Start - billiger, flexibler, schneller zu iterieren.
Zusammenfassung
- Embeddings sind numerische Vektoren, die Bedeutung darstellen
- RAG = Relevante Chunks finden + als Kontext in den LLM-Prompt
- Pipeline: Chunk โ Embed โ Store โ Retrieve โ Generate
- Tools: Qdrant, pgvector, Pinecone als Vector-DB
- Reranking und Hybrid Search verbessern die Qualitaet
- Fuer Wissen: RAG statt Fine-Tuning
Im naechsten Kapitel: LLM-Agenten, die Tools nutzen koennen.