RAG ohne Framework: Chunk, Score, Budget, Prompt
RAG als schlichte Token-Auswahl: eine kurze Python-Skizze mit Wortüberlappungs-Scoring, einem Token-Budget und dem, was Embeddings und Vektordatenbanken wirklich verbessern.
RAG ist Token-Auswahl
Ein Modell kennt weder Ihr Wiki noch Ihre Codebasis noch die Support-Tickets von gestern. Seine Gewichte wurden beim Training eingefroren, und ein Gigabyte Text passt niemals in ein Fenster mit 128.000 Tokens. Fine-Tuning kostet Zeit und Geld und ist für Fakten, die sich wöchentlich ändern, das falsche Werkzeug. Bleibt eine Option, und die ist fast schon peinlich direkt: Durchsuchen Sie Ihre Dokumente nach den wenigen Abschnitten, die für diese Frage zählen, fügen Sie sie in den Prompt ein und sagen Sie dem Modell, es soll daraus antworten.
Mehr ist RAG nicht: erst abrufen, dann generieren. Es ist keine neue Art von Modell, kein Gedächtnis und keine Magie. Es ist eine Methode, auszuwählen, welche Tokens in die übliche Next-Token-Schleife gelangen. Das Modell sagt weiterhin Tokens auf Basis des Kontexts voraus, den Sie gebaut haben – RAG verbessert nur diesen Kontext.
Jedes RAG-System, ob Spielzeug oder Produktion, läuft auf vier Schritte hinaus:
- Dokumente in Chunks aufteilen.
- Jeden Chunk gegen die Frage bewerten.
- Die besten paar behalten, die ins Token-Budget passen.
- Aus diesen Notizen und der Frage einen Prompt bauen.
Frameworks unterscheiden sich darin, wie sie den Bewertungsschritt umsetzen. Die Grundform bleibt immer gleich.
Die Idee in schlichtem Code
Die folgende Skizze nutzt Wortüberlappung als Ersatz für eine echte Suche. Sie ist bewusst primitiv, denn es geht um die Struktur.
DOCS = [
"Tokens are subword chunks; a model "
"sees token IDs, never words.",
"The context window caps prompt plus "
"output, measured in tokens.",
"Temperature rescales the next-token "
"distribution before sampling.",
"Ollama serves local models behind an "
"OpenAI-style API on port 11434.",
]
def score(q: str, doc: str) -> int:
qw = set(q.lower().split())
return len(qw & set(doc.lower().split()))
def retrieve(q: str, k: int = 2) -> list:
ranked = sorted(
DOCS,
key=lambda d: score(q, d),
reverse=True,
)
return ranked[:k]
q = "What does the context window cap?"
notes = retrieve(q)
prompt = (
"Answer using only these notes:\n"
+ "\n".join(f"- {n}" for n in notes)
+ f"\n\nQuestion: {q}"
)
print(prompt)
Entscheidend ist der String, der ganz unten zusammengesetzt wird. Er enthält Anweisungen, dann die abgerufenen Notizen, dann die Frage – und dieser String ist RAG. Übergeben Sie ihn einem beliebigen Chat-Client, und die Retrieval-Augmented Generation ist erledigt. Auch die Formulierung „nutze nur diese Notizen“ ist wichtig. Sie ist Ihr wichtigster Schutz gegen ein Modell, das lieber selbstbewusst aus seinen Trainingsdaten antwortet als aus Ihren Dokumenten.
Echte Systeme verbessern hier genau eine Funktion: score(). Wortüberlappung übersieht Synonyme, „laptop“ trifft also nie auf „notebook“. Produktivsysteme verwenden Embeddings, also Vektoren, die nach Bedeutung ranken, dazu eine Vektordatenbank, um sie in großem Maßstab zu durchsuchen, und sie zerlegen Text geschickter als in einen Satz pro Dokument. All das sind Verbesserungen des Bewertungsschritts. Die Pipeline, die Sie gerade gelesen haben, ist die gesamte Architektur.
Das Budget prüfen
Warum nimmt man die Top-k-Chunks statt aller relevanten? Weil das Kontextfenster ein Budget ist und der Abruf es überziehen kann. Selbst Text, der hineinpasst, ist nicht gratis: Jeder Token kostet Geld, und ein vollgestopfter Kontext macht die Aufmerksamkeit des Modells weniger verlässlich. Echter RAG-Code zählt deshalb die Tokens, bevor er den Aufruf macht.
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
def fits(prompt: str, window: int = 8_000,
reserve: int = 500) -> bool:
used = len(enc.encode(prompt))
print(f"prompt uses {used} tokens")
return used <= window - reserve
if fits(prompt):
pass # call the API (Parts 2-5)
else:
notes = retrieve(q, k=1) # shrink, retry
Genau dieses if unterscheidet eine Demo von einem System. Abschneidefehler, stillschweigend verworfener Kontext und Antworten, die Ihre Dokumente ignorieren, gehen meist darauf zurück, dass diese Prüfung fehlt. Planen Sie außerdem eine Reserve ein. Das Fenster muss Prompt und Ausgabe fassen, also lassen Sie Platz für die Antwort, die Sie angefordert haben.
Das Beispiel ausführen
pip install -r requirements.txt
python examples/part06_rag_idea.py
Das Skript führt das komplette Spielzeugbeispiel aus. Es gibt die Retrieval-Scores für mehrere Fragen aus, darunter eine, bei der Wortüberlappung kläglich scheitert (eine gute Demonstration, warum es Embeddings gibt), und zeigt dann den Prompt-Aufbau und die Budgetprüfung. Es läuft offline, ohne Schlüssel und ohne API-Aufruf, denn die Lektion ist die Idee. Den zusammengesetzten Prompt an ein echtes Modell anzubinden, ist ein einziger Funktionsaufruf aus jedem einfachen Chat-Client, und das Skript zeigt die Stelle, an der dieser Aufruf stehen würde.
Was bewusst weggelassen wurde
Embeddings, Vektordatenbanken, Chunking-Strategie und Reranking sind die Mechanik, die retrieve() gut macht. Sie gehören zu einer richtigen RAG-Implementierung, und genau dort beginnt LangChain, seine Installation zu rechtfertigen. Diese Mechanik zuerst zu lernen hat allerdings einen Haken. Wer dort anfängt, sieht RAG oft als Produkt, das man konfiguriert. Wer mit der Idee anfängt, sieht ein Dutzend Zeilen Kontrollfluss rund um eine Suchfunktion und kann jedes Teil untersuchen, wenn die Abrufqualität enttäuscht.
Die Kurzfassung
Auf das Wesentliche reduziert, wählt RAG aus, welche Tokens das Modell zu sehen bekommt. Dokumente aufteilen, die Teile gegen die Frage ranken, die besten unter Beachtung des Budgets in einen Prompt einfügen – und die übliche Next-Token-Schleife erledigt den Rest. Dafür braucht es kein Framework, und wenn Sie es einmal gesehen haben, erkennen Sie es in jedem Framework wieder. Der nächste Schritt ist, RAG in eine echte Pipeline mit den Mustern ReAct und Plan-and-Execute einzubauen und zu sehen, wo LangChain und LangGraph ihren Platz haben.