Beliebte Suchanfragen
//

RAG mit SpringAI und LangGraph4J

23.8.2026 | 10 Minuten Lesezeit

RAG mit SpringAI und LangGraph4J

Einleitung

RAG (Retrieval Augmented Generation) ermöglicht es, LLMs mit eigenem Wissen zu erweitern - ohne Retraining oder Finetuning. In diesem Artikel zeige ich, wie sich eine RAG-Anwendung mit Spring AI und LangGraph4J umsetzen lässt, am Beispiel eines Frage-Antwort-Systems rund um die Charaktere des Herrn der Ringe. Neben der Implementierung gehe ich auf grundlegende Konzepte wie Embeddings, Vektorsuche und Textsplitting ein und teile dabei Probleme und Erkenntnisse, die mir während der Umsetzung begegnet sind.

Begriffe und Grundlagen

Dieser Abschnitt erläutert wesentliche Begriffe, die zum Verständnis einer RAG Anwendung notwendig sind.

Embeddings

Mithilfe von Embeddings werden Wörter, Texte in einen Vektorraum überführt, um sie klassifizieren, gruppieren sowie für die Maschine verarbeitbar machen zu können. Die Darstellung von Wörtern und Texten erfolgt als n-dimensionaler Vektor in Form von Fließpunktzahlen (Floats) in einem Array. Zur Einfachheit sind im folgenden Ausschnitt die Embeddings für Cat, Kitten und Car als 3-dimensionaler Vektor dargestellt.

1double[] catEmbedding = {120.0, 60.0, 50.0};
2double[] kittenEmbedding = {130.0, 70.0, 60.0};
3double[] carEmbedding = {-120.0, 20.0, 15.0};

Ausschnitt mit Embeddings zu Cat, Kitten und Car zur Einfachheit als 3-dimensionaler Vektor. Quelle: Eigene Darstellung.

Embedding-Modelle für die Einordnung von Text, dessen Gewichtung anhand von Trainingsdaten, werden ähnlich wie LLMs im OpenSource Raum zur Verfügung gestellt. Innerhalb dieser Arbeit wurde das nomic-embed-text Modell genutzt.

Vektorsuche, Vektorstore und Cosine Similarity

Die erstellten Embeddings werden in einem Vektorstore gespeichert. Bei einem Vektorstore handelt es sich um eine Datenbank, die für die Suche und Vergleich von Vektoren (Similarität) ausgelegt ist. Für die semantische Suche werden die Embeddings im n-dimensionalen Vektorraum anhand ihrer Verteilung und Relation zueinander betrachtet.

Die folgende Abbildung zeigt die Embeddings Cat, Kitten und Car innerhalb eines dreidimensionalen Vektorraums, zur Verdeutlichung vom Konzept der Vektorsuche. Abbildung zeigt die Darstellung von den Embeddings (Cat, Kitten, Car) innerhalb eines 3-dimensionalen Vektorraums. Quelle: Eigene Darstellung.

Die Similarität der Vektoren Cat, Kitten und Car wird mithilfe der Cosine Similarity berechnet, die Aufschluss über die semantische Zugehörigkeit gibt. Abbildung von den Embeddings Cat, Kitten und Car im 2-dimensionalen Raum zur Veranschaulichung der Winkel und damit der potentiellen semantischen Zugehörigkeit, die sich mit Hilfe der Cosine Similarity berechnet. Quelle: Eigene Darstellung.

Durch die Berechnung der Winkel der jeweiligen Vektoren mit Hilfe der Cosine Similarity werden die Absolute der Ergebnisse ermittelt, die Werte zwischen 0 und 1 ergeben. Dabei gibt 1 die vollständige Übereinstimmung der Begriffe oder Texte miteinander an und der Wert 0 den größten Abstand und damit keine semantische Similarität an.

Textsplitting

Um Dokumente für RAG vorzubereiten, wird das Quelldokument, in meinem Fall mein Textdokument mit Informationen zu den Charakteren von den Herrn der Ringe, mittels Textsplitting in kleinere Einheiten (Chunks) zerlegt. Diese Chunks dürfen sich dabei überschneiden (Overlapping), um einen möglichen Kontextverlust zu minimieren. Die Chunks werden anschließend im Vektorstore als Embeddings gespeichert. Bei der semantischen Suche wird mit dem TopK-Wert bestimmt, wie viele Ergebnisse der nächstgelegenen Vektoren zurückgegeben werden sollen.

Retrieval Augmented Generation (RAG)

Large Language Models (LLMs) sind auf einer bestimmten Wissensbasis trainiert und antworten aus dieser heraus. Um unter anderem Halluzinationen zu reduzieren sowie Antworten zu generieren, deren Daten das LLM nicht besitzt, wie z. B. unternehmensinterne Dokumente, wird RAG verwendet. Bei RAG wird der relevante Kontext zur Laufzeit aufgerufen und dem LLM im Prompt mitgegeben, um darauf basierend eine fundierte Antwort zu generieren. Der relevante Kontext ergibt sich aus dem Retrieval Schritt - Ergebnisse aus der Suche von Dokumenten im Vektorstore.

Architekturüberblick

Die folgende Abbildung gibt eine Übersicht der Anwendung und zeigt die Verarbeitung vom eingehenden UserQuery bis hin zur generierten Antwort in 7 Schritten auf. Die beteiligten Komponenten:

  • API (Schnittstelle) - Entgegennahme von Anfragen und Rückgabe der Antworten
  • SpringAI und LangGraph4J - Abstraktionsschicht für die Integration von LLM
  • Textdokument (LotR) - Textdokument mit Informationen zu LotR Charakteren
  • Vektorstore - Datenbank für Vektoren und Suche von Vektoren
  • LLM - Large Language Model zur Generierung von Antworten Abbildung zum Ablauf der Anwendung in (7) Schritten. Quelle: Eigene Darstellung.

Umsetzung

In den folgenden Abschnitten gehe ich auf die Implementierungsaspekte, den Aufbau vom Graphen mit LangGraph4J sowie den die Umsetzung von RAG mit Textsplitting und Retrieval.

Gerichteter Graph mit LangGraph4J

LangGraph4J ist eine Java-Bibliothek für die Implementierung von AI Workflows, die Orchestrierung von unterschiedlichen AI Agents und die Ermöglichung der Kommunikation miteinander.

Nodes und Edges

In LangGraph4J wird ein Workflow als Graph mit Nodes und Edges dargestellt. Nodes stellen eine Entität dar. Sie sind in der Lage Funktionen auszuführen und die Ergebnisse davon zurück zu geben - als Beispiel dient die Node "Klassifizierung" in der unteren Abbildung. Edges stellen die Kanten im Graphen dar und verbinden Nodes miteinander, sie geben die Beziehung zwischen Nodes wieder. Sie können weiterhin Konditionen enthalten, wie es in der unteren Abbildung mit "isLotr" ausgedrückt wird. Abbildung vom Anwendungsgraphen mit Nodes und Conditional Edges. Quelle: Eigene Darstellung.

Innerhalb der Anwendung sieht die Umsetzung vom Graphen mit LangGraph4J wie folgt aus :

1public CompiledGraph<LotrState> buildGraph() throws GraphStateException {  
2    // Erstellen vom Graphen mit Zustand
3    StateGraph<LotrState> workflow = new StateGraph<>(LotrState.SCHEMA, LotrState::new);  
4  
5    // Hinzufügen der Nodes  
6    workflow.addNode("classifyQuery", AsyncNodeAction.node_async(this::classifyQueryNode)); // Klassifizierung  
7    workflow.addNode("retrieveContext", AsyncNodeAction.node_async(this::retrieveContextNode)); // Retrieval 
8    workflow.addNode("generateAnswer", AsyncNodeAction.node_async(this::generateAnswerNode)); // Antwort Generierung  
9    workflow.addNode("handleOffTopic", AsyncNodeAction.node_async(this::handleOffTopicNode)); // Off-Topic 
10  
11    // Hinzufügen der Edges und verbinden der Nodes  
12    workflow.addEdge(StateGraph.START, "classifyQuery"); // Start -> Klassifizierung  
13    workflow.addConditionalEdges("classifyQuery",  
14            AsyncEdgeAction.edge_async(  
15                    state -> (boolean) state.value(LotrState.IS_LOTR).orElse(false) ? "LOTR" : "OFF_TOPIC"),  
16            Map.of(  
17                    "LOTR", "retrieveContext",  
18                    "OFF_TOPIC", "handleOffTopic"  
19            )  
20    ); // conditional Edge - ist LotR Thema oder kein LotR Thema  
21    workflow.addEdge("retrieveContext", "generateAnswer"); // Retrieval -> Antwort 
22    workflow.addEdge("generateAnswer", StateGraph.END); // Ende  
23    workflow.addEdge("handleOffTopic", StateGraph.END); // Ende 
24    return workflow.compile();  
25}

Ausschnitt zur Erstellung vom Graphen mit LangGraph4J. Quelle: Eigene Darstellung.

Der Graph wird in zwei Schritten aufgebaut. Zunächst werden die jeweiligen Nodes mit ihren Funktionalitäten registriert und anschließend über Edges verbunden. Die Conditional Edge stellt die Verzweigung dar, handelt es sich um eine Frage bzgl. den Herrn der Ringe, wird der Node "retrieveContext" ausgeführt, anderenfalls "handleOffTopic".

Retrieval und Vektorstore

Neben dem Graphen ist das Retrieval der zweite zentrale Baustein der Anwendung. Es umfasst das Textsplitting, die Speicherung im Vektorstore sowie die lexikalische und semantische Suche.

Textsplitting und Chunks

Als Wissensbasis für die Beantwortung der Fragen rund um den Herrn der Ringe, ein von mir erzeugtes Textdokument mit Informationen zu den Charakteren. Der folgende Text-Ausschnitt zeigt dessen Struktur, um anschließend den Chunking-Prozess nachvollziehbar zu machen.

1=== ARAGORN (STREICHER) ===  
2Aragorn, Sohn von Arathorn, ist der 16te Stammesführer der Dúnedain...
3
4=== GANDALF DER GRAUE / DER WEISSE ===  
5Gandalf ist einer der Istari (Zauberer), die von den Valar...

Ausschnitt aus dem Textfile als Basis für die Beantwortung von Fragen rund um den Herrn der Ringe. Quelle: Eigene Beschreibung.

Der Aufbau vom Textsplitter erfolgt mit dem Builder vom TokenTextSplitter.

1public CommandLineRunner ingestLotrData(  
2        ElasticHybridService elasticHybridService,  
3        @Value("classpath:lotr-knowledge.txt")Resource lotrResource  
4        ) {  
5    return args -> {
6        // Einlesen vom Textdokument  
7        TextReader textReader = new TextReader(lotrResource);  
8        List<Document> documents = textReader.read();  
9  
10        // Erstellen vom Textsplitter mit notwendigen Parametern
11        TokenTextSplitter splitter = TokenTextSplitter.builder()  
12                .withChunkSize(chunkSize)  
13                .withMinChunkSizeChars(minChunkSize)  
14                .withKeepSeparator(true)  
15                .build(); 
16        // Textsplitting mit Hilfe vom TokenTextSplitter
17        List<Document> chunks = splitter.apply(documents);  
18  
19        // Chunks aus dem Splitting im VektorStore speichern
20        elasticHybridService.getElasticVectorStore().add(chunks);   
21    };  
22}

Ausschnitt zum Einlesen vom Textdokument sowie Textsplitting mittels des TokenTextSplitter. Quelle: Eigene Darstellung.

Das Textdokument wird zunächst über einen TextReader eingelesen. Das Ergebnis sind Dokument-Objekte (Container), die den Inhalt sowie Metadaten vom Text enthalten. Der TokenTextSplitter übernimmt anschließend das Splitting und benötigt dafür die gewünschte Chunkgröße.

Bestimmung der Chunksize (Problematik)

Die Bestimmung der Chunksize stellt innerhalb einer RAG-Anwendung eine Herausforderung dar. Zu kleine Chunks verlieren Kontext, zu große Chunks verschlechtern die Präzision und erhöhen den Rechenaufwand. Relevante Einflussgrößen sind das Context Window des Modells, die erwartete Länge der Anfrage, die Größe der gewünschten Antwort sowie der TopK-Wert. In meinem Fall habe ich bewusst nicht das volle Context Window von gemma4:e4b ausgenutzt, weil mein lokaler Rechner bei maximaler Auslastung deutlich längere Antwortzeiten aufweist.

Für die Bestimmung der Chunksize sollte beachtet werden, dass der Context Window des Modells nicht überschritten wird. Formel zur theoretischen Orientierung - Addition von Token sollte kleiner gleich dem Context Window vom KI-Modell entsprechen. Quelle: Eigene Darstellung.

Um einen ungefähren Wert für den Umfang der Token zu erhalten, die beim Einlesen meines Dokumentes verwendet werden, stelle ich meine Berechnung unter den folgenden Annahmen:

  • ein Wort im Deutschen entspricht ca. 1,3 bis 1,5 Token
  • ein Absatz in meinem Text-Dokument fasst ca. 130 Wörter
  • ein semantisch aussagefähiger Satz fasst ca. 160 Wörter
  • unter Verwendung von RRF liegen die Treffer pro Suchart bei ca. 4 Dokumenten, TopK ~ 8 Formel zum Ermitteln der Tokenanzahl mit $F_t$ als Tokenfaktor mit 1.5 Token, $a_{Absatz}$ für Anzahl Wörter vom semantisch aussagefähigen Absatz und $topK$ für die Treffer pro Suchart mit RRF. Quelle: Eigene Darstellung.

Unter Anwendung der Formel erhalte ich Tokenanzahl Token=1920 und runde das Ergebnis auf 2000 auf. Zur Bestimmung der potentiell geeigneten Chunksize nehme ich das Ergebnis Token=2000 und dividiere es durch die topK=8. Formel für die Berechnung der potentiell geeigneten Chunkgröße. Quelle: Eigene Darstellung.

Mit den genannten Annahmen ergibt sich Chunksize=250. Bei den aufgeführten Formeln handelt es sich um eigene Heuristiken, abgeleitet aus Recherchen und Experimenten und stellen deshalb keine allgemeingültigen Aussagen dar. Für tiefergehende Analysen weise ich auf die wissenschaftlichen Arbeiten im Quellenverzeichnis hin.

Problematik mit semantischer Suche

Beim Experimentieren startete ich zunächst mit dem SimpleVectorStore, der ausschließlich die semantische Suche unterstützt. Dabei zeigte sich schnell ein Problem: Einzelne Begriffe ließen sich nicht zuverlässig finden. Der Beiname "Streicher" von Aragorn als Query für die Suche lieferte kein Ergebnis und wurde außerhalb vom Herrn der Ringe Kontext verortet. Das lag wahrscheinlich aufgrund der Assoziation von "Streicher" im Kontext vom Orchester und Musik. Damit zusammenhängend lieferten neben einzelnen Begriffen, wie "Streicher" ebenso Fragen "Wer ist Streicher?" keine Ergebnisse.

Hybride Suche mit Reciprocal Rank Fusion (RRF)

Die Lösung lag in der Kombination aus semantischer und lexikalischer Suche (Volltextsuche). ElasticSearch bietet diese Kombination an und fasst diese beiden Sucharten innerhalb des Algorithmus Reciprocal Rank Fusion (RRF) zusammen. Vereinfachte Abbildung der lexikalischen und semantischen Suche mittels RRF, dem Ranking von Dokumenten und die Ausgabe der Suchergebnisse. Quelle: Eigene Darstellung.

Bei der hybriden Suche laufen beide Sucharten parallel ab. Die lexikalische Suche matcht auf Basis von exakten Begriffen, die semantische Suche auf Basis von Bedeutungsähnlichkeit. RFF kombiniert die jeweiligen Ergebnislisten und bewertet Dokumente höher, die in beiden Listen auftauchen. Das Ergebnis von RRF im Rahmen der hybriden Suche ist die Fusion von den neu bewerteten Listen basierend auf dem Ranking vom Algorithmus.

Fazit

Die Integration eines LLM mit SpringAI und LangGraph4J war aufgrund der hilfreichen und umfassenden Dokumentation gut umsetzbar. Die eigentliche Herausforderung lag bei der Bestimmung der Chunksize und die Konfiguration der Suche. Konkrete, allgemeingültige Formeln habe ich bei meinen Recherchen nicht gefunden. Die optimalen Größen hängen vom Modell, der Anfragelänge, dem Antwortumfang und der verfügbaren Rechenleistung ab. Der entscheidende Schritt nach vorne war der Wechsel von der reinen semantischen Suche zur hybriden Suche mit RRF. Erst damit ließen sich exakte Begriffe, wie z. B. "Streicher", zuverlässig matchen. Für zukünftige RAG-Projekte empfehle ich daher die hybride Suche von Anfang an einzuplanen bzw. in Betracht zu ziehen.

Hinweise und Einschränkungen

Die Ergebnisse und Empfehlungen dieses Artikels sind im Kontext des durchgeführten Experiments zu verstehen. Ich habe keine Memory eingesetzt, d. h. Chat-Verläufe wurden nicht gespeichert, jede Anfrage wurde für sich beantwortet - Zustandslosigkeit der Anwendung. Alle ausgeführten Versuche wurden mit gemma4:e4b vorgenommen, es fand kein Modellvergleich statt. Chunksize und TopK-Werte basieren auf Grundlage meiner Formeln und sind über die Anwendung statisch. Eine dynamische Ermittlung und Festlegung der Parameter wurde nicht untersucht. Alle Abbildungen, Code-Ausschnitte und Formeln basieren auf meinem eignen Verständnis und erheben keinen Anspruch auf Allgemeingültigkeit.

Technologien und Versionen

Bei der Umsetzung kamen folgende Technologien in Einsatz: SpringAI Version 2.0 als Abstraktionsschicht für die LLM-Integration, LangGraph4J Version 1.9.0 für die Orchestrierung vom Workflow sowie ElasticSearch (SpringAI Starter Vector Store ElasticSearch Version 2.0) als Vektorstore mit Unterstützung für die hybride Suche. Als Sprachmodell wurde gemma4:e4b über Ollama Version 0.32.8 lokal bertrieben. Für die Embedding-Models wurde nomic-embed-text verwendet.

Quellenverzeichnis

Bücher

Soto Bueno, A. (2025). Applied AI for Enterprise Java Development. O'Reilly Media.

Videos

Computerphile (2023). Vector Search with LLMs [Video]. YouTube. https://www.youtube.com/watch?v=YDdKiQNw80c (abgerufen: 18.08.2026)

Computerphile (2023). A Helping Hand for LLMs - Retrieval Augmented Generation [Video]. YouTube. https://www.youtube.com/watch?v=of4UDMvi2Kw&t=628s (abgerufen: 18.08.2026)

Online-Dokumentationen

Spring AI Documentation. https://docs.spring.io/spring-ai/reference/index.html (abgerufen: 18.08.2026)

Elasticsearch Documentation. https://www.elastic.co/search-labs/blog/spring-ai-elasticsearch-application#getting-started-with-elasticsearch (abgerufen: 18.08.2026)

LangChain4J Documentation. https://docs.langchain4j.dev/ (abgerufen: 18.08.2026)

LangGraph4J Documentation. https://langgraph4j.github.io/langgraph4j/main/ (abgerufen: 18.08.2026)

Microsoft Azure. Reciprocal Rank Fusion (RRF). https://learn.microsoft.com/de-de/azure/search/hybrid-search-ranking (abgerufen: 18.08.2026)

TolkienGateway. https://tolkiengateway.net/wiki/Main_Page (abgerufen: 18.08.2026)

Wissenschaftliche Arbeiten

Ramakanth Bhat, S., Rudat, M., Spiekermann, J. & Flores-Herr, N. (2025). Rethinking Chunk Size For Long-Document Retrieval: A Multi-Dataset Analysis. arXiv. https://doi.org/10.48550/arXiv.2505.21700

Zhong, Z., Liu, H., Cui, X., Zhang, X. & Qin, Z. (2024). Mix-of-Granularity: Optimize the Chunking Granularity for Retrieval-Augmented Generation. arXiv. https://doi.org/10.48550/arXiv.2406.00456

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//
Jetzt für unseren Newsletter anmelden

Alles Wissenswerte auf einen Klick:
Unser Newsletter bietet dir die Möglichkeit, dich ohne großen Aufwand über die aktuellen Themen bei codecentric zu informieren.