Spring-AI-Grundlagen: ChatClient, Streaming, Memory und Tools mit Ollama
Eine Spring-Boot-AI-App mit Spring AI und einem lokalen Ollama-Modell starten: ChatClient, Prompt-Templates, Streaming, strukturierte Ausgabe, Chat-Memory und Tool Calling.
Stellen Sie sich ein AI-Feature vor, das heute auf OpenAI läuft. Im nächsten Quartal steigt die Rechnung, das Team will Ollama auf Laptops ausprobieren, oder das Unternehmen standardisiert auf Azure OpenAI, Bedrock oder Gemini. Ohne Abstraktionsschicht kann ein Anbieterwechsel Wellen durch die gesamte Codebasis schlagen: Client-Setup, Request-Formate, Streaming-Code, Embeddings, Tool-Definitionen und das Parsen der Antworten. Spring AI gibt Spring-Boot-Entwicklern einen einheitlichen API-Satz über Anbieter hinweg, sodass diese Wellen klein bleiben.
Im Mittelpunkt stehen die praktischen Grundlagen: was das Framework bietet, Projekt-Setup, Ollama-Konfiguration, ChatClient, Prompt-Templates, Streaming, strukturierte Ausgabe, Chat-Memory und Tool Calling. Retrieval, Vector Stores, ETL, Filterung, Observability, Evaluation, multimodale Eingaben, MCP und Produktionstipps bleiben einem Folgethema vorbehalten.
Alle Beispiele laufen auf Ollama, zum Mitmachen braucht also niemand einen API-Key. Das Beispielprojekt basiert auf Spring Boot 3.5.5 und Spring AI 1.0.9. Es gibt auch eine stabile Linie Spring AI 2.0.x, die aber auf Spring Boot 4.0.x und 4.1.x zielt, weshalb man die beiden Versionssätze nicht in einem Build mischen sollte.
Begriffe vor dem Code
Für Einsteiger ist die Terminologie manchmal schwieriger als der Code. Diese Begriffe tauchen unten auf.
- LLM (Large Language Model): das Modell, das Text erzeugt. GPT, Claude, Gemini und lokale Modelle wie Llama gehören dazu. Text geht hinein, und das Modell sagt nützlichen Text als Ausgabe voraus.
- Modell: die konkrete Engine, die aufgerufen wird. Hier läuft sie lokal über Ollama. In der Cloud könnte sie bei OpenAI, Anthropic, Azure OpenAI, Amazon Bedrock oder Google Vertex AI liegen.
- Prompt: die Anweisung oder Frage an das Modell, zum Beispiel „Explain Spring AI in one paragraph“ (erkläre Spring AI in einem Absatz). Klare, konkrete Prompts mit genug Kontext liefern bessere Antworten.
- Token: eine kleine Texteinheit, etwa ein Wort, ein Wortteil, ein Satzzeichen oder ein Leerzeichen. Preise, Kontextgröße und Ausgabelänge werden normalerweise in Tokens gezählt.
- Kontextfenster: die maximale Textmenge, die das Modell auf einmal berücksichtigen kann. Prompt, Chatverlauf, abgerufene Dokumente, Tool-Ergebnisse und die Antwort zählen alle dazu.
- ChatClient: die Fluent API von Spring AI für Chatmodelle. Statt anbieterspezifische HTTP-Aufrufe von Hand zu bauen, liest sich der Code wie chatClient.prompt().user(...).call().content().
- Prompt-Template: ein wiederverwendbarer Prompt mit Platzhaltern. Aus „Explain {topic} in {language}“ wird zur Laufzeit „Explain RAG in Java“.
- Strukturierte Ausgabe: Daten in vorhersehbarer Form vom Modell anfordern und auf einen Java-Record oder eine Klasse abbilden. Nützlich, wenn eine Anwendung Felder statt Fließtext braucht.
- Chat-Memory: der Gesprächsverlauf, den die Anwendung verwaltet. Modelle sind standardmäßig zustandslos, frühere Nachrichten müssen also jedes Mal erneut gesendet werden, wenn das Modell sich daran erinnern soll.
- Advisor: eine Spring-AI-Komponente, die eine Anfrage verändert oder anreichert, bevor sie beim Modell ankommt. Ein Memory-Advisor ergänzt etwa frühere Nachrichten, ein RAG-Advisor, um den es später geht, würde abgerufene Dokumente hinzufügen.
- Tool Calling: Das Modell bittet die Anwendung, eine Java-Methode auszuführen. Es greift selbst nie auf eine Datenbank oder API zu. Es fordert das Tool an, die Anwendung führt es aus, und das Ergebnis geht zurück ans Modell.
Was Spring AI mitbringt
Das Framework ist mehr als ein Wrapper um Chat Completions. Für Java-Teams ist es ein vollwertiges Anwendungsframework für AI-Workloads. Die wichtigsten Bausteine:
- portable Modell-APIs für Chat, Embeddings, Bilder, Speech-to-Text, Text-to-Speech und Moderation
- Integrationen für OpenAI, Anthropic, Azure OpenAI, Amazon Bedrock, Google Vertex AI und Gemini, Mistral, Ollama und weitere
- ChatClient, eine Fluent API im Stil von WebClient und RestClient
- Prompt-Templates und Nachrichtentypen für System-, User-, Assistant- und Medieninhalte
- Abbildung von Modellantworten auf Java-Records oder Klassen
- Streaming über Project Reactor
- Advisors für wiederkehrende Muster wie Memory und RAG
- Tool Calling, damit das Modell Java-Methoden anfordern kann
- Embedding-Modelle hinter Vector-Store-Abstraktionen
- Extras für den Produktivbetrieb: ETL, Observability, Evaluation und MCP
Das ist eine breite Oberfläche, deshalb bleibt der Durchgang unten beim zentralen Anwendungsfluss.
Projekt-Setup
Legen Sie ein Maven-Projekt auf Java 21 mit Spring Boot 3.5.x, Spring Web und dem Spring-AI-Starter für Ollama an. Eine Bill of Materials hält alle Spring-AI-Module auf kompatiblen Versionen.
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.9</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Für die lokale Ollama-Demo fügen Sie den Web-Starter zusammen mit dem Ollama-Starter hinzu.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
Wer OpenAI bevorzugt, kann den Modell-Starter austauschen.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
Eine Warnung: Behalten Sie nur einen Chatmodell-Starter im Classpath, sofern keine primäre ChatModel-Bean konfiguriert ist. Sind OpenAI und Ollama beide vorhanden, findet Spring zwei Kandidaten und kann den Standard-ChatClient.Builder nicht erzeugen, und der Start schlägt fehl.
Ein lokales Modell betreiben
Installieren Sie Ollama und laden Sie ein Chatmodell herunter.
ollama pull llama3.2
Tragen Sie es dann in application.properties ein.
spring.application.name=spring-boot-ollama
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.options.model=llama3.2
Starten Sie die App.
mvn spring-boot:run
Sie lauscht hier:
http://localhost:8080
Einfacher Chat mit ChatClient
ChatClient ist die High-Level-API, mit der die meisten Spring-Anwendungen beginnen sollten.
@RestController
public class SpringAiDemoController {
private final ChatClient chatClient;
public SpringAiDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/api/demo/simple")
public String simpleChat(@RequestParam(defaultValue = "Tell me a joke") String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
Ausprobieren im Terminal:
curl "http://localhost:8080/api/demo/simple?message=Explain%20Spring%20AI%20in%20one%20paragraph"
Der Aufruf läuft in vier Schritten ab. prompt() öffnet eine neue Anfrage, user(...) hängt die Nachricht an, call() wählt die blockierende Ausführung, und content() liefert den erzeugten Text.
Prompt-Templates
Hartcodierte Prompts werden schnell mühsam in der Pflege. Ein Template enthält wiederverwendbaren Text mit Parametern.
@GetMapping("/api/demo/template")
public String templateChat(
@RequestParam(defaultValue = "Microservices") String topic,
@RequestParam(defaultValue = "Java") String language) {
PromptTemplate template = new PromptTemplate(
"Explain the concept of {topic} using examples in {language}. Keep it brief.");
String renderedPrompt = template.render(Map.of(
"topic", topic,
"language", language
));
return chatClient.prompt()
.user(renderedPrompt)
.call()
.content();
}
Den Endpoint mit einem Thema und einer Sprache aufrufen:
curl "http://localhost:8080/api/demo/template?topic=Retrieval-Augmented%20Generation&language=Java"
Templates passen am besten, wenn der Prompt Teil des Anwendungsverhaltens ist und kein einmaliger String.
Streaming
Lange Antworten sollten gestreamt werden, damit Nutzer sofort eine Ausgabe sehen und nicht auf den ganzen Text warten.
@GetMapping(value = "/api/demo/stream", produces = "text/event-stream")
public Flux<String> streamChat(
@RequestParam(defaultValue = "Write a short story about a brave robot.") String message) {return chatClient.prompt()
.user(message)
.stream()
.content();
}
Testen Sie es im Browser oder mit curl ohne Pufferung.
curl -N "http://localhost:8080/api/demo/stream?message=Write%20a%20short%20Spring%20AI%20intro"
Spring MVC schreibt den Flux aus Strings als Server-Sent Events heraus.
Strukturierte Ausgabe
Text passt zum Chat, doch Business-Software braucht meist strukturierte Daten. Spring AI kann eine Antwort anfordern, die sich auf einen Java-Typ abbilden lässt. Bei lokalen Modellen funktioniert es meist besser, das JSON-Format explizit vorzugeben.
public record MovieReview(String title, double rating, String shortSummary) {
}
@GetMapping("/api/demo/structured")
public MovieReview structuredOutput(@RequestParam(defaultValue = "Inception") String movie) {
return chatClient.prompt()
.user(u -> u.text("Review the movie {movie}.")
.param("movie", movie))
.call()
.entity(MovieReview.class);
}
Ausprobieren:
curl "http://localhost:8080/api/demo/structured?movie=The%20Matrix"
Ruft man entity mit der Record-Klasse auf, ergänzt Spring AI den Prompt um Formatanweisungen und wandelt das zurückgegebene JSON direkt in den Record um. Das ist sauberer und verlässlicher, als JSON-Strings von Hand zu parsen.
Gesprächsgedächtnis
Modelle erinnern sich nicht an frühere Anfragen, also liefert die Anwendung den Verlauf. Spring AI erledigt das mit Memory-Abstraktionen und Advisors, die als Standard am Client-Builder registriert werden.
this.chatClient = chatClientBuilder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(
MessageWindowChatMemory.builder().build()
).build()
)
.build();
Jede Anfrage trägt dann eine Konversations-ID.
@GetMapping("/api/demo/memory")
public String chatWithMemory(
@RequestParam String message,
@RequestParam(defaultValue = "chat_session_1") String chatId) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
.call()
.content();
}
Zwei Aufrufe mit derselben ID zeigen den Effekt: zuerst einen Namen nennen, dann fragen, wie er lautete.
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=My%20name%20is%20Yash"
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=What%20is%20my%20name?"
In Produktion ersetzen Sie die In-Memory-Speicherung durch ein persistentes Repository wie JDBC, Redis, Cassandra, MongoDB oder Neo4j.
Tool Calling
Tools erlauben dem Modell, von der Anwendung definierte Arbeit anzufordern. Das Modell hat keinen direkten Weg zu einer Datenbank oder API. Es fragt, der Anwendungscode führt das Tool aus, und das Ergebnis geht zurück ans Modell.
Ein Tool ist eine normale Methode mit Beschreibungen.
public static class WeatherTools {
@Tool(description = "Get the current weather for a city")
public WeatherResponse getCurrentWeather(
@ToolParam(description = "City name") String city) {
return new WeatherResponse(city, 22.5, "Sunny");
}
}
public record WeatherResponse(
String location,
double temperatureCelsius,
String condition) {
}
Es wird als Bean bereitgestellt.
@Bean
public WeatherTools weatherTools() {
return new WeatherTools();
}
Dann aktiviert eine Anfrage es.
@GetMapping("/api/demo/function")
public String functionCalling(
@RequestParam(defaultValue = "What is the weather in Paris?") String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools)
.call()
.content();
}
Eine Frage nach dem Wetter in einer Stadt durchläuft die komplette Schleife.
curl "http://localhost:8080/api/demo/function?message=What%20is%20the%20weather%20in%20Paris?"
Sinnvolle Einsätze sind etwa Lagerbestände prüfen, Wetter abrufen, einen Kunden nachschlagen, ein Support-Ticket eröffnen oder einen Workflow anstoßen. Bei destruktiven Aktionen ist Strenge gefragt: Ergänzen Sie Autorisierung, Validierung, Auditing und, wo nötig, eine Bestätigung durch den Nutzer.
Die ganze Demo ausführen
Starten Sie die Anwendung erneut.
mvn spring-boot:run
Rufen Sie dann die Endpoints nacheinander auf.
curl "http://localhost:8080/api/demo/simple?message=Explain%20Spring%20AI%20in%20one%20paragraph"
curl "http://localhost:8080/api/demo/template?topic=Retrieval-Augmented%20Generation&language=Java"
curl -N "http://localhost:8080/api/demo/stream?message=Write%20a%20short%20Spring%20AI%20intro"
curl "http://localhost:8080/api/demo/structured?movie=Inception"
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=My%20name%20is%20Yash"
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=What%20is%20my%20name?"
curl "http://localhost:8080/api/demo/function?message=What%20is%20the%20weather%20in%20Paris?"
Häufige Fehler
Mehrere Modell-Starter hinzuzufügen, ohne einen auszuwählen, führt zu diesem Startfehler:
No qualifying bean of type 'ChatModel' available: expected single matching bean but found 2
Behalten Sie in der Demo nur einen Starter oder benennen Sie explizit ein primäres Modell.
Wer Chat-Memory ohne Konversations-ID nutzt, bekommt bei jedem Aufruf scheinbar einen neuen Chat. Übergeben Sie die ID als Advisor-Parameter.
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
Die dritte Falle ist, Tools zu großzügig freizugeben. Tools sind echte Anwendungsaktionen, also validieren Sie ihre Eingaben, setzen Sie Autorisierung durch und vermeiden Sie destruktive Operationen, solange es keinen Freigabeprozess gibt.
Wo wir jetzt stehen
Damit steht das Fundament einer Spring-AI-Anwendung: Chat, Prompts, Streaming, strukturierte Ausgabe, Memory und Tools. Diese Bausteine decken bereits viele nützliche Szenarien ab, darunter Chat-Assistenten, Zusammenfasser, Endpoints zur strukturierten Extraktion und kontrollierte Tool-Calling-Abläufe. Nach dem Chat folgt eine Wissensschicht auf Basis von Embeddings und Vector Stores, darauf RAG, ETL-Pipelines, Metadatenfilter, Observability, Evaluation, multimodale Eingaben, MCP und die Härtung für die Produktion.
Ein vollständiges Demoprojekt lässt sich von GitHub klonen.
git clone https://github.com/Yash2462/blog-demos.git
Öffnen Sie nach dem Klonen den Ordner spring-ai-demo und folgen Sie dem README darin.
Weiterführende Quellen
- die Referenzdokumentation von Spring AI
- der Getting-started-Leitfaden von Spring AI
- die Seite zum Ollama-Chatmodell in der Spring-AI-Dokumentation
- die Dokumentation zu ChatClient
- die Dokumentation zu Tool Calling