Alle Beiträge

RAG verstehen, indem du es baust: die kleinste funktionierende App mit Spring AI

Du brauchst weder LangChain noch eine Vektordatenbank, um RAG zu verstehen. Der ganze Loop passt in zwei Java-Klassen — und genau das ist der Punkt.

Kurz beantwortet

RAG sind drei Schritte zum Zeitpunkt der Frage — die relevantesten Chunks per Ähnlichkeitssuche aus dem Vektorspeicher holen, sie in den Prompt einfügen, das Modell darauf gestützt antworten lassen — plus eine Ingestion-Pipeline, die einmal pro Dokument läuft: Text extrahieren, chunken, embedden, speichern. Teil 1 dieser vierteiligen Serie baut genau das und nichts mehr: Spring Boot mit Spring AIs In-Memory-SimpleVectorStore, Azure OpenAI, Tika und Jsoup für die Extraktion und ein Angular-Dashboard, das zu jeder Antwort zeigt, auf welche Chunks sie sich stützt. Das komplette Projekt liegt auf GitHub, eigenständig lauffähig.

Du brauchst weder LangChain noch eine Vektordatenbank noch einen fünfstündigen Framework-Kurs, um RAG zu verstehen. Der ganze Loop passt in zwei Java-Klassen. Ich weiss es, weil ich ihn gerade so gebaut habe — bewusst so klein wie möglich — als Teil 1 einer vierteiligen Tutorial-Serie. Dieser Post erklärt, was RAG wirklich ist, anhand von Code, den du klonen und starten kannst.

Das Problem, das RAG löst

Ein Sprachmodell kennt seine Trainingsdaten. Es kennt weder euer Wiki noch eure Verträge noch die Meeting-Notizen von gestern. Fragt man trotzdem, tut es das Schlimmstmögliche: Es antwortet selbstbewusst. RAG — Retrieval-Augmented Generation — löst das, ohne das Modell anzufassen. Statt dem Modell eure Dokumente beizubringen, reichst du ihm die relevanten Ausschnitte zum Zeitpunkt der Frage und sagst ihm, es soll daraus antworten.

Drei Schritte, bei jeder einzelnen Frage:

  1. Retrieval — die Dokument-Chunks finden, die der Frage am ähnlichsten sind.
  2. Augmentation — diese Chunks als Kontext in den Prompt einfügen.
  3. Generation — das Modell antworten lassen, gestützt auf diesen Kontext.

Damit Schritt 1 funktioniert, durchläuft jedes Dokument vorher einmal die Ingestion-Pipeline: Text extrahieren (Apache Tika für PDFs und Office-Dateien, Jsoup für Webseiten), in Chunks teilen, für jeden Chunk ein Embedding berechnen — einen Vektor aus Gleitkommazahlen, der seine Bedeutung erfasst — und die Vektoren speichern. Ähnliche Bedeutung, nahe Vektoren. Das ist der ganze Trick.

Der komplette Loop, in Code

Die Ingestion ist eine Pipeline aus vier Aufrufen:

List<Document> chunks = splitter.apply(List.of(new Document(text, metadata)));
vectorStore.add(chunks); // embeddet jeden Chunk und speichert ihn

Und das Antworten sind drei Schritte, auf die du zeigen kannst:

// 1. Retrieval
List<Document> chunks = vectorStore.similaritySearch(
        SearchRequest.builder().query(question).topK(4).build());

// 2. Augmentation
String context = chunks.stream().map(Document::getText)
        .collect(Collectors.joining("\n---\n"));
Prompt prompt = new Prompt(List.of(
        new SystemMessage(SYSTEM_TEMPLATE.formatted(context)),
        new UserMessage(question)));

// 3. Generation
String answer = chatModel.call(prompt).getResult().getOutput().getText();

Das war's. Der „augmentierte Prompt", von dem alle reden, ist ein String mit deinen Chunks drin. Keine Magie, keine Chains, kein Graph aus Runnables. Der Vektorspeicher in Teil 1 ist Spring AIs In-Memory-SimpleVectorStore — nach einem Neustart weg, und das ist okay: Solange du lernst, ist Persistenz nur ein weiteres Ding, das verdeckt, was wirklich passiert.

Was dir das Dashboard beibringt

Das Angular-Frontend sind zwei Panels: links ingestieren (Datei-Upload + URL), rechts chatten. Das Detail, auf das es ankommt: Jede Antwort zeigt, auf welche Chunks sie sich stützt, mit Quelle und Ähnlichkeits-Score. Beim ersten Mal, wenn eine Antwort den falschen Chunk zitiert, hörst du auf, RAG für Magie zu halten, und fängst an, über Retrieval-Qualität nachzudenken — genau das mentale Modell, das du brauchst, bevor irgendetwas Schlaueres dazukommt.

Was ich bewusst weggelassen habe

Hybrid-Suche, Reranking, Metadaten-Filter, semantisches Chunking — alles echte Techniken, alles Verbesserungen, die du irgendwann willst, alles nicht in Teil 1. Nicht weil sie egal wären, sondern weil jede davon eine der vier Pipeline-Stufen tunt — und du keine Stufe tunen kannst, die du nie isoliert gesehen hast. Wenn ein produktives RAG-System eine schlechte Antwort gibt, sitzt der Fehler in einem dieser Schritte: Die Extraktion hat Müll produziert, das Chunking hat mitten im Satz geschnitten, das Retrieval hat die falschen Chunks geholt, oder der Prompt hat dem Modell erlaubt, seinen Kontext zu ignorieren. Um das zu debuggen, musst du die naive Version auswendig kennen.

Dasselbe gilt für die Framework-Frage. Spring AIs ChatModel, EmbeddingModel und VectorStore sind gewöhnliche Spring-Beans — wer JVM-Services in Produktion betreibt, bekommt RAG als Bibliothek, nicht als neue Plattform. Wer Python-nativ ist, nimmt LangChain oder LlamaIndex; die Konzepte dieser Serie übertragen sich eins zu eins.

Der Takeaway

Bau den kleinsten RAG-Loop, bevor du irgendeinen RAG-Stack übernimmst. Es ist ein Nachmittag Arbeit, und er verwandelt RAG von einem Buzzword in vier debugbare Funktionen. Das komplette Projekt — Backend, Dashboard, Walkthrough auf Deutsch und Englisch — ist eigenständig lauffähig mit einem einzigen Azure-OpenAI-Key.

Teil 2 macht die beiden grossen Infrastruktur-Entscheidungen austauschbar: Azure OpenAI vs. normale OpenAI-API vs. ein komplett offline laufendes lokales Mistral, und in-memory vs. persistentes pgvector — per Spring-Profil, ohne diesen Anwendungscode anzufassen. Der Tausch klingt trivial. War er nicht, und die Details sind der interessante Teil.

Code: github.com/halviclabs/rag-tutorials — Teil 1 ist rag-tutorial-01-basics. Und wenn dein Agent-Tooling über Dokumente hinaus an Systeme muss, gilt dieselbe Regel „klein, explizit, debugbar" — siehe deinen ersten MCP Server bauen.

Häufige Fragen

Was ist Retrieval-Augmented Generation (RAG)?

RAG lässt ein Sprachmodell Fragen zu Dokumenten beantworten, die es nie im Training gesehen hat. Zum Zeitpunkt der Frage passieren drei Schritte: Retrieval (eine Ähnlichkeitssuche findet die relevantesten Dokument-Chunks im Vektorspeicher), Augmentation (diese Chunks kommen als Kontext in den Prompt) und Generation (das Modell antwortet gestützt auf diesen Kontext statt zu raten). Vorher durchläuft jedes Dokument einmal die Ingestion-Pipeline: Text extrahieren, in Chunks teilen, jeden Chunk embedden, die Vektoren speichern.

Brauche ich eine Vektordatenbank für eine RAG-Anwendung?

Nicht für den Anfang. Ein Vektorspeicher ist jede Struktur, die die Frage beantwortet, welche gespeicherten Chunks einem Frage-Vektor am nächsten sind — Spring AIs In-Memory-SimpleVectorStore tut das in wenigen Zeilen und ist zum Lernen ideal, weil nichts versteckt ist. Auf einen persistenten Speicher wie PostgreSQL mit pgvector wechselt man, wenn Daten Neustarts überleben müssen — genau das ergänzt Teil 2 der Serie.

Wozu dient Chunking in einer RAG-Pipeline?

Chunking teilt lange Dokumente in Stücke, die klein genug sind, um sinnvoll embedded zu werden, und von denen mehrere in einen Prompt passen. Zu gross, und das Embedding eines Chunks mittelt über zu viele Themen; zu klein, und der Kontext geht verloren. Das Tutorial nutzt Spring AIs TokenTextSplitter mit Defaults — ein vernünftiger Startpunkt, den man erst tunt, wenn die Retrieval-Qualität es verlangt.

Warum RAG mit Spring AI statt mit LangChain bauen?

Wenn dein Team auf der JVM arbeitet, bekommst du RAG mit den Abstraktionen, die du ohnehin betreibst: ChatModel, EmbeddingModel und VectorStore sind Spring-Beans, Anbieter sind per Konfiguration austauschbar, und es kommt keine zusätzliche Runtime dazu. LangChain und LlamaIndex sind im Python-Ökosystem exzellent — die Konzepte dieser Serie übertragen sich eins zu eins.

AI-Code ohne Tech-Schuld — die Checkliste

Trag dich ein: die Checkliste plus neue Beiträge zu KI-Engineering. Kein Spam, jederzeit abbestellbar.