TL;DR:
Hauptthema: Integration von Generativer KI und Large Language Models (LLMs) in Java-Anwendungen mit dem Spring AI-Framework ohne Technologiewechsel zu Python.
Kernarchitektur & Features:
-
ChatClient API: Empfohlene, fluent API für Modellaufrufe, Streaming (
Flux<String>) und strukturierten JSON-Output (Mapping auf Java Records/POJOs). -
Anbieter-Abstraktion: Einheitliche Schnittstelle für OpenAI, Anthropic Claude, Google Gemini, Amazon Bedrock und lokale Modelle via Ollama.
-
Tool Calling (Function Calling): Deklarative Methodenausführung durch KI via
@Tool– und@ToolParam-Annotationen (automatisch gesteuert über denToolCallingAdvisor). -
Retrieval Augmented Generation (RAG): Native
VectorStore-Einbindung (z. B. Qdrant, PgVector, Redis) per Builder-Pattern undQuestionAnswerAdvisor. -
Zustand & Kontext: Verwaltung des Gesprächsverlaufs über
MessageChatMemoryAdvisorunter Angabe einer obligatorischenCONVERSATION_ID. -
Observability & Sicherheit: Integrierte Micrometer-/OpenTelemetry-Metriken (inkl. Token-Verbrauch); Absicherung von MCP-Endpunkten via OAuth2 oder API-Keys (
mcp-server-security).
Primärer Einsatzzweck: Ermöglicht Java- und Spring-Boot-Anwendungen die direkte Nutzung von LLM-Features, RAG-Pipelines und Agenten-Workflows innerhalb der bestehenden Unternehmensinfrastruktur.
KI ist in praktisch jedem Produktteam angekommen, und mit ihr eine Frage, die Java Developer lange beschäftigt hat: Muss man für ernsthafte KI-Features eigentlich nach Python wechseln? Die kurze Antwort lautet seit einer Weile: Nein. Die längere liefert Spring AI.
Mit dem GA-Release von Spring AI 2.0 im Juni 2026 hat das Projekt eine stabile Basis erreicht – aufgesetzt auf Spring Boot 4 und Spring Framework 7. Dieser Artikel zeigt, was Spring AI ausmacht, wie ein minimales Setup aussieht und worauf man im Alltag achten sollte. Gedacht ist er für Entwickler, die sich einen fundierten ersten Eindruck verschaffen wollen, bevor sie das Framework in ein bestehendes Projekt holen.
| Spring AI | 2.0.0 (GA, Juni 2026), verfügbar über Maven Central |
| Plattform | Spring Boot 4.0 |
| Java | 17 als Minimum, 21+ empfohlen |
| Bibliotheken | Jackson 3 statt Jackson 2; durchgängig null-annotiert (JSpecify) |
Was ist Spring AI – und welches Problem löst es?
Spring AI ist ein Framework aus der Spring-Community, das KI-Funktionen in Spring-Anwendungen einbindet, ohne den gewohnten Technologie-Stack zu verlassen. Es stellt Abstraktionen bereit, um mit Modellen verschiedener Anbieter zu arbeiten – etwa denen von OpenAI, Anthropic (Claude) oder Google (Gemini).
Konzeptionell borgt sich das Projekt einiges bei Python-Bibliotheken wie LangChain und LlamaIndex. Der Unterschied: Spring AI denkt von Anfang an in Spring-Begriffen. Dependency Injection, Auto-Configuration, die üblichen Design-Patterns – all das bleibt erhalten. Für ein eingespieltes Spring-Team senkt das die Einstiegshürde erheblich, weil kein zweites mentales Modell nötig ist.
Spring AI oder doch Python?
Dass Python im KI- und ML-Umfeld die Nummer eins ist, bestreitet niemand. Warum also Spring AI? Der Grund liegt selten in der reinen KI-Leistung, sondern in der Integration.
Man stelle sich eine über Jahre gewachsene Anwendung vor – die Spring Petclinic ist das klassische Beispiel, aufgebaut auf Spring Boot, Thymeleaf und JPA. Solche Systeme wurden nie für KI entworfen. Die Alternative zu Spring AI wäre, eine zweite Infrastruktur in Python danebenzustellen: eigener Dienst, eigene Authentifizierung, zusätzliche Netzwerk-Hops, eine weitere CI/CD-Strecke. Das alles kostet, bevor das erste Feature überhaupt läuft.
Spring AI geht den anderen Weg. So wie Spring Data eine Abstraktion über verschiedene Datenbanken legt – man schreibt gegen ein Interface, der Starter erledigt den Rest –, legt Spring AI eine Abstraktion über Large Language Models. Wenn Geschäftslogik, Sicherheit und Daten ohnehin in einer Java-Anwendung liegen, ist das meist der pragmatischere Schnitt.
Kernkonzepte
Ein paar Begriffe sollte man kennen, bevor es losgeht.
- Models – die eigentlichen KI-Algorithmen. Spring AI unterstützt die großen Anbieter: OpenAI, Anthropic Claude, Google Gemini, Amazon Bedrock, Ollama für lokale Modelle und weitere. In 2.0 nutzt das Framework für OpenAI, Anthropic und Google direkt die offiziellen Hersteller-SDKs, was neue Modell-Features schneller verfügbar macht.
- Tokens & Embeddings – Modelle rechnen nicht mit Wörtern, sondern mit Tokens, also Textfragmenten. Embeddings übersetzen Text in numerische Vektoren, sodass sich semantische Nähe mathematisch ausdrücken lässt. Das ist die Grundlage für Vektorsuche und RAG.
- Prompts – die Anweisungen an das Modell. Für dynamische Prompts setzt Spring AI auf Templates (per StringTemplate), in die zur Laufzeit Variablen eingesetzt werden.
- ChatClient – die zentrale, fluent gestaltete API für die Kommunikation mit dem Modell. In 2.0 ist der ChatClient ausdrücklich die empfohlene Einstiegsebene; das tiefer liegende ChatModel braucht man nur für Spezialfälle.
- Advisors – eine Art AOP für LLM-Aufrufe. Sie klinken sich in Anfrage und Antwort ein: Der SimpleLoggerAdvisor protokolliert Requests, der MessageChatMemoryAdvisor hängt automatisch den Gesprächsverlauf an.
- Structured Output – LLMs antworten von Haus aus in Fließtext. Spring AI kann das Modell dazu bringen, stattdessen maschinenlesbares JSON zu liefern, das direkt auf ein Java-Record oder POJO gemappt wird.
- Tool Calling – das Modell darf definierte Java-Methoden selbst aufrufen, wenn es dafür Daten braucht: eine Datenbankabfrage, einen Wetterdienst, eine interne API. In 2.0 läuft diese Ausführungsschleife über den ChatClient bzw. den ToolCallingAdvisor.
Das minimale Setup
Dank Auto-Configuration ist der Einstieg kurz.
1. Abhängigkeiten
Seit 2.0 liegen die stabilen Artefakte in Maven Central – ein zusätzliches Snapshot- oder Milestone-Repository braucht man nicht mehr. Versionen verwaltet man am besten über die BOM (hier Gradle Kotlin DSL):
|
1 2 3 4 |
dependencies { implementation(platform("org.springframework.ai:spring-ai-bom:2.0.0")) implementation("org.springframework.ai:spring-ai-starter-model-openai") } |
Für Anthropic oder Google tauscht man nur den Starter (spring-ai-starter-model-anthropic, spring-ai-starter-model-google-genai) – der restliche Code bleibt gleich. Genau das ist der Punkt.
2. Konfiguration
Den API-Key zieht man aus einer Umgebungsvariable.
|
1 2 3 4 5 6 7 8 |
spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-5-mini temperature: 0.0 # 0.0 = deterministisch, höhere Werte = kreativer |
3. Ein erster Controller
Mit dem ChatClient.Builder lässt sich ein einsatzbereiter Client injizieren:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@RestController @RequestMapping("/api/ai") public class TranslationController { private final ChatClient chatClient; public TranslationController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/translate") public Map<String, String> translate(@RequestParam String text) { String response = chatClient.prompt() .user("Übersetze den folgenden Text ins Spanische: " + text) .call() .content(); return Map.of("translation", response); } } |
Der Aufruf erzeugt im Hintergrund den Request an den Anbieter und gibt den reinen Text zurück. Mehr braucht es für den ersten Treffer nicht.
Antworten direkt als Objekt
Oft will man keinen Fließtext, sondern Struktur. Der ChatClient kann die Antwort über .entity(…) direkt auf ein Record mappen:
|
1 2 3 4 5 6 |
record Translation(String original, String translated, String targetLanguage) {} Translation result = chatClient.prompt() .user("Übersetze 'Guten Morgen' ins Spanische.") .call() .entity(Translation.class); |
Spring AI weist das Modell an, passendes JSON zu liefern, und deserialisiert es für uns.
Streaming
Bei längeren Antworten will man nicht warten, bis der ganze Text steht. .stream() liefert die Antwort als Flux, Stück für Stück:
|
1 2 3 4 5 6 7 |
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); } |
Stärken und Schwächen – eine ehrliche Einordnung
Kein Stack ist nur Sonnenseite. Drei Dinge spielt Spring AI klar aus.
Das erste ist die Austauschbarkeit der Modelle. Ein Wechsel von OpenAI zu einem lokal über Ollama betriebenen Modell ist oft eine Sache von Dependency und ein paar Properties; der Anwendungscode bleibt unberührt. Das macht A/B-Tests zwischen Anbietern und das Mischen kommerzieller und lokaler Modelle unaufwändig.
Das zweite ist die Integration in Spring Boot. Vektordatenbanken wie Qdrant, PgVector oder Redis hängen über die gewohnten Auto-Configurations an der Anwendung – man spart sich viel Verdrahtung.
Das dritte ist die Sicherheit. Über das Model Context Protocol (MCP) lassen sich KI-Endpunkte absichern, wahlweise per OAuth2 (so sieht es die Spezifikation vor) oder, wo keine OAuth2-Infrastruktur existiert, per API-Schlüssel.
Dem stehen Einschränkungen gegenüber. Das Tempo des Projekts war lange hoch. Mit 2.0 GA ist die API-Fläche stabilisiert und durchgängig null-annotiert, aber wer aus der 1.x-Welt migriert, sollte die Upgrade Notes wirklich lesen – zwischen den Versionen wurde einiges umbenannt und entfernt. Das Feintuning bleibt anspruchsvoll: Eine gute Gesamtkonfiguration aus Modell, Vector Store und Prompt-Strategie erreicht man nicht per Default, sondern durch Messen und Nachjustieren. Und lokale Open-Source-Modelle sind in der Dialogführung kommerziellen Modellen oft noch unterlegen; das kann das Framework nicht wegabstrahieren.
Praxis: Muster, die sich bewähren
Wer tiefer einsteigt, läuft über dieselben Stellen wie alle anderen. Ein paar Empfehlungen.
RAG statt Fine-Tuning
Soll das Modell internes Firmenwissen kennen, ist Retrieval Augmented Generation (RAG) meist der günstigere Weg als Fine-Tuning. Unstrukturierte Inhalte – Tickets, Wiki-Artikel – landen als Embeddings in einem Vector Store. Zur Laufzeit sucht die Pipeline die semantisch passenden Fragmente und gibt sie dem Modell als Kontext mit.
Den Vector Store konfiguriert man als Bean. Seit 1.0 nutzt Spring AI durchgängig Builder statt vielarmiger Konstruktoren:
|
1 2 3 4 5 6 7 |
@Bean QdrantVectorStore vectorStore(QdrantClient client, EmbeddingModel embeddingModel) { return QdrantVectorStore.builder(client, embeddingModel) .collectionName("incidents") .initializeSchema(true) .build(); } |
Die Suche muss man nicht von Hand bauen. Der QuestionAnswerAdvisor übernimmt Retrieval und das Anreichern des Prompts:
|
1 2 3 |
ChatClient chatClient = builder .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).build()) .build(); |
Wer mehr Kontrolle braucht, greift direkt auf den Store zu – auch hier per Builder:
|
1 2 3 4 5 6 |
SearchRequest request = SearchRequest.builder() .query(text) .topK(5) .build(); List<Document> hits = vectorStore.similaritySearch(request); |
System-Prompts ernst nehmen
Modelle beantworten bereitwillig auch Fragen, die nichts mit der eigentlichen Aufgabe zu tun haben. Über einen System-Prompt gibt man klare Leitplanken vor:
|
1 2 3 4 5 6 7 8 9 |
@Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(""" Du bist ein Assistent für die Verwaltung einer Tierklinik. Beantworte ausschließlich Fragen, die die Tierklinik betreffen. """) .build(); } |
Prompts in eigene Dateien auslagern
Solche Anweisungen werden schnell lang. Im Java-Code verkettete Strings sind schwer zu pflegen – und Product Owner oder Prompt Engineers kommen gar nicht erst heran. Besser: den Prompt in eine Datei legen. Markdown eignet sich gut, weil es Struktur erlaubt, die Modelle zuverlässig verarbeiten. Spring AI lädt .st-Templates von Haus aus; eine schlichte Markdown-Datei tut es genauso.
Datei unter src/main/resources/prompts/chat-system.md:
|
1 2 3 4 5 6 7 |
# Rolle Du bist ein Assistent für eine Tierklinik. # Regeln - Beantworte nur Fragen zur Tierklinik. - Gib niemals sensible Kundendaten preis. - Wenn du etwas nicht weißt, sage das offen. |
Laden per @Value als Classpath-Ressource:
|
1 2 3 4 5 6 7 8 9 10 |
@Configuration public class ChatClientConfig { @Value("classpath:prompts/chat-system.md") private Resource systemPrompt; @Bean ChatClient chatClient(ChatClient.Builder builder) { return builder.defaultSystem(systemPrompt).build(); } } |
Vorteil: Die Java-Klasse bleibt sauber, und der Prompt lässt sich unabhängig vom Code anpassen.
Tools statt blinder Antworten
Mit Tool Calling gibt man dem Modell Zugriff auf eigene Systeme – es entscheidet selbst, ob und wann es eine Methode aufruft. Der schlanke Weg führt über die @Tool-Annotation an ganz normalen Service-Methoden. Entscheidend ist die Beschreibung: An ihr erkennt das Modell, wann das Tool passt.
Häufiger Stolperstein: Die Annotation liegt in org.springframework.ai.tool.annotation.Tool – nicht im chat.model-Paket.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; @Service public class WeatherService { @Tool(description = "Liefert aktuelle Temperatur und Wetter für eine Stadt.") public String getCurrentWeather( @ToolParam(description = "Name der Stadt") String city) { return new ExternalWeatherApiClient().fetchWeatherForCity(city); } } |
Registriert wird das Tool, indem man die Bean übergibt – nicht den Methodennamen als String. Pro Request über .tools(…), für alle Requests am Builder über .defaultTools(…):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@RestController public class WeatherChatController { private final ChatClient chatClient; public WeatherChatController(ChatClient.Builder builder, WeatherService weatherService) { this.chatClient = builder .defaultTools(weatherService) .build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } } |
Spring AI erzeugt aus der Methodensignatur automatisch das JSON-Schema, das das Modell erwartet, ruft die Methode bei Bedarf auf und reicht das Ergebnis zurück. In 2.0 führen die ChatModels diese Schleife nicht mehr selbst aus – der ChatClient registriert dafür automatisch einen ToolCallingAdvisor. Wer direkt gegen das ChatModel arbeitet, muss die Tool-Ausführung selbst steuern.
Die ältere Variante über java.util.function.Function-Beans mit @Description existiert weiterhin; für neue Projekte ist @Tool aber der direktere Weg.
Gesprächsverlauf verwalten
LLMs sind zustandslos – ohne Verlauf versteht das Modell keine Folgefragen. Der MessageChatMemoryAdvisor hängt die letzten Nachrichten automatisch an. Spring Boot konfiguriert dafür bereits eine ChatMemory-Bean (eine MessageWindowChatMemory mit In-Memory-Repository); man muss sie nur einklinken:
|
1 2 3 4 5 6 7 8 |
@Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build(), new SimpleLoggerAdvisor()) .build(); } |
Wer das Fenster selbst dimensionieren will, baut die Memory explizit:
|
1 2 3 |
ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .build(); |
Ein Detail, das in aktuellen Versionen Pflicht ist: Jeder Aufruf über den Memory-Advisor braucht eine Conversation-ID, damit Gespräche getrennt bleiben:
|
1 2 3 4 5 |
chatClient.prompt() .user(message) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); |
Die früher verbreitete Schreibweise mit new InMemoryChatMemory() ist entfallen – Konstruktor und Klasse wurden zugunsten der Builder-API abgelöst.
Endpunkte absichern
In Enterprise-Umgebungen ist die Absicherung der KI-Integration kein Nice-to-have. Bindet man Systeme über MCP an, sollen MCP-Server laut Spezifikation geschützt werden – primär über OAuth2. Wo das nicht praktikabel ist, bietet das Community-Modul mcp-security eine API-Key-Variante. Sie stammt nicht aus Spring Security Core, sondern aus org.springaicommunity:mcp-server-security (für Spring AI 2.x die Versionslinie 0.1.x).
|
1 2 3 4 5 6 7 8 9 |
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // McpApiKeyConfigurer stammt aus dem Community-Modul mcp-server-security return http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .with(McpApiKeyConfigurer.mcpServerApiKey(), apiKey -> apiKey.apiKeyRepository(apiKeyRepository())) .build(); } |
Das apiKeyRepository ist Pflicht – ohne Schlüsselquelle startet die Konfiguration nicht. Aufgerufen wird der Server dann mit einem Header X-API-key: id.secret; der Secret-Teil liegt serverseitig bcrypt-gehasht.
Was 2.0 sonst noch mitbringt
Ein paar Punkte, die über das Tagesgeschäft hinaus interessant sind.
MCP ist in 2.0 vollständig in den Kern gewandert. Eine Anwendung kann gleichzeitig MCP-Client sein – und externe Tools wie Dateisystem- oder Datenbankzugriffe konsumieren – und MCP-Server, der eigene Geschäftslogik als Tools anbietet. Als Transport ist Streamable HTTP der neue Standard; SSE gilt als veraltet, stdio bleibt für lokale Prozesse.
Observability ist eingebaut. Spring AI erzeugt Micrometer-Spans und OpenTelemetry-kompatible Metriken für Modell- und Tool-Aufrufe, inklusive Token-Verbrauch – was bei der Kostenkontrolle hilft.
Die gesamte API ist über JSpecify null-annotiert. Für Kotlin-Nutzer übersetzt sich das in echte nullable- und non-nullable-Typen, die der Compiler prüft.
Produktiv lohnt es sich außerdem, Retry- und Rate-Limit-Verhalten sowie sinnvolles Error-Handling von Anfang an einzuplanen: LLM-APIs antworten nicht immer und nicht immer schnell. Und beim Aufsetzen eines neuen Projekts nimmt einem start.spring.io die Auswahl der Modell- und Vector-Store-Starter ab.
Fazit
Spring AI schließt die Lücke zwischen der etablierten Java-Welt und der schnellen Entwicklung bei generativer KI, ohne dass man das Ökosystem verlassen muss. Über Abstraktionen wie den ChatClient, unkompliziertes Tool Calling und die Integration von Vector Stores lässt sich Wert schaffen, statt Infrastruktur nachzubauen.
Mit 2.0 steht das Ganze auf einer stabilen, konsistenten Basis. Wer in der Spring-Boot-Welt zu Hause ist und LLMs einbinden will, hat damit einen geradlinigen Weg. Der pragmatische Einstieg: klein anfangen mit dem ChatClient, strukturierten Output ausprobieren und die Architektur später in Richtung RAG ausbauen.