Künstliche Intelligenz (KI) wird für moderne Anwendungen immer wichtiger. Während KI unterschiedliche Technologien umfasst, ist der Fokus derzeit aufgrund der jüngsten Fortschritte bei großen Sprachmodellen (LLMs) auf Generative KI (GenAI).
Traditionell ist Python die dominierende Sprache für KI. Doch für Java-Entwickler, die Generative KI nutzen möchten, bietet das Spring AI Projekt eine attraktive Alternative. Es vereinfacht die Entwicklung von KI-gestützten Enterprise-Anwendungen erheblich und ermöglicht es, so mit der rasant fortschreitenden KI-Landschaft Schritt zu halten.
Spring AI abstrahiert komplexe Interaktionen mit verschiedenen KI-Anbietern, die REST APIs bereitstellen, darunter OpenAI, Anthropic, Microsoft, Google, Amazon und sogar lokalen LLMs. Durch diese Abstraktionen ist ein einfacher Wechsel zwischen verschiedenen Modellen möglich, gleichzeitig ist aber, wie in Spring üblich, gewährleistet, auf spezifische Funktionen und Konfigurationen einzelner Modelle zugreifen zu können.
Das Framework bietet eine Vielzahl an Funktionen, wie die Konvertierung von Modellausgaben in Java Objekte, Multimodalität, KI-bezogene Observability und Testunterstützung zur Bewertung von Modellausgaben, bereit.
Darüber hinaus unterstützt Spring AI fortgeschrittene Techniken wie Tool Calling, Retrieval-Augmented Generation (RAG) und das Model Context Protocol (MCP), um den Kontext von LLMs anzureichern.
Spring AI baut auf den Kernfunktionalitäten des Spring Frameworks und weiteren Spring Projekten wie Spring Data zur Integration von Vektor-Datenbanken auf. Durch die automatische Konfiguration von Spring Boot, wird die Entwicklung von KI-gestützten Funktionen erheblich vereinfacht und beschleunigt.
Erste Schritte mit Spring AI
Nach dem groben Überblick zu Spring AI, widmen wir uns nun der Umsetzung im Code! Um Spring AI in deiner Spring Boot Anwendung zu nutzen, muss zunächst die entsprechende Bibliothek für den gewünschten KI-Anbieter hinzugefügt werden. In diesem Beispiel wird Ollama verwendet, das die lokale Ausführung von verschiedene LLMs unterstützt. Es wird außerdem empfohlen, die Spring AI Bill of Materials (BOM) hinzuzufügen, bis Spring Boot kompatible Versionen der Spring AI-Abhängigkeiten verwalten.
implementation platform("org.springframework.ai:spring-ai-bom:${springAiVersion}")
implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
Die Standart-Konfiguration für Ollama geht davon aus, dass Mistral als LLM verwendet wird und die API zur Interaktion mit dem Modell auf demselben Host unter Port 11434 zu erreichen ist.
ollama pull mistral
Die automatische Konfiguration lässt sich einfach per Konfigurationsdatei oder Code anpassen. In diesem Beispiel wird z.B. über Ollama anstatt Mistral das Llama 3.2 Modell bereitgestellt.
spring.ai.ollama.chat.model=llama3.2
Die ChatClient API
Mit der ChatClient
API kann mit wenigen Zeilen Code mit dem KI-Modell der Wahl kommuniziert werden. Es wird sowohl die in diesem Beispiel verwendete synchrone Kommunikation, als auch Streaming unterstützt.
String answer = this.chatClient.prompt()
.user("Was ist die Hauptstadt von Deutschland?")
.call()
.content();
Die prompt()
Methode initialisiert eine Interaktion mit einem KI-Modell und ermöglicht die Zusammenstellung der Anweisungen für dieses.
Mit der call()
Methode werden die Anweisungen, die als “Prompt” bezeichnet werden, an das KI-Modell bzw. die API des Anbieters gesendet, das eine Antwort generiert und zurücksendet.
Die content()
Methode extrahiert schließlich die vom Modell erzeugte Antwort von den restlichen Daten und gibt sie als String
zurück.
Eine ChatClient
Instanz wird mithilfe des Builder-Patterns von einem ChatClient.Builder
erstellt. Durch die automatische Konfiguration steht dieser als Spring Bean für Dependency Injection zur Verfügung.
@Component
class MyComponent {
private final ChatClient chatClient;
// Konstruktor-basierte Dependency Injection
public MyComponent(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
...
}
Anweisungen können Platzhalter enthalten, die durch geschweifte Klammern gekennzeichnet sind und zur Laufzeit mit dynamischen Werten befüllt werden. Statt einer Zeichenkette wird hierfür eine Funktion vom Typ Consumer<PromptUserSpec>
and die user()
Methode übergeben, um die Werte für die Platzhalter zu definieren. Ein Consumer
ist eine funktionale Schnittstelle in Java, die eine Eingabe, in diesem Fall ein Objekt des Typs PromptUserSpec
, verarbeitet, ohne einen Rückgabewert zu liefern.
String country = "Deutschland";
String answer = this.chatClient.prompt()
.user(promptUserSpec -> promptUserSpec
.text("Was ist die Hauptstadt von {country}?")
.param("country", country))
.call()
.content();
Für KI-Modelle, die multimodale Eingaben unterstützen, also neben Text auch Bilder oder Audiodateien verarbeiten können, bietet die PromptUserSpec
zusätzlich die Methode media()
. Damit lassen sich Bild- und Audiodateien in den Prompt integrieren und gemeinsam mit der Anweisung in Textform an das Modell senden.
var imageResource = new ClassPathResource("berlin.png");
String answer = this.chatClient.prompt()
.user(promptUserSpec -> promptUserSpec
.text("Was ist der Name dieser Stadt?")
.media(MimeTypeUtils.IMAGE_PNG, imageResource))
.call()
.content();
Strukturierte Ausgaben
Die ChatClient
API bietet mehrere Möglichkeiten, mit KI-generierten Ausgaben umzugehen. Im vorherigen Beispiel wurde die Methode content()
verwendet, um die Antwort des Modells als String
bereitzustellen. Die API unterstützt jedoch auch die direkte Umwandlung der Ausgabe in Java Objekte.
Dies wird durch Systemanweisungen erreicht, bei denen es sich um spezielle Anweisungen handelt, deren Zweck es ist, das Verhalten des KI-Modells zu steuern. Mit der system()
Methode der ChatClient
API können solche Systemanweisungen ähnlich wie Benutzeranweisungen in den Prompt integriert werden.
String answer = this.chatClient.prompt()
.user("Was ist die Hauptstadt von Deutschland?")
.system("Du bist ein Geschichtsexperte. Verwende ausführliche Erklärungen.")
.call()
.content();
Zusätzlich kann über die Methode defaultSystem()
in der ChatClient.Builder
Klasse eine Systemanweisung für alle ChatClient
API Interaktionen festgelegt werden.
Um die automatische Umwandlung von KI-generierten Ausgaben zu Java Objekten im ChatClient
zu ermöglichen, fügt ein StructuredOutputConverter
im Hintergrund eine Systemanweisung zum Prompt hinzu. Diese Anweisung fordert das Modell auf, die Antwort in einem bestimmten Format, beispielsweise JSON, zurückzugeben, das anschließend in ein Java Objekt überführt wird.
record City(String name, String zipcode) {}
City capitalOfGermany = this.chatClient.prompt()
.user("Was ist die Hauptstadt von Deutschland?")
.call()
.entity(City.class);
Fortgeschrittene KI-Techniken
Während Prompt Engineering hilft, die Antworten eines KI-Modells zu steuern, hat es dennoch seine Grenzen. Egal wie gut ein Prompt formuliert ist, ein LLM kann nur Antworten auf Basis seines vorab trainierten Wissens und des im Prompt bereitgestellten Kontexts generieren.
Um diese Einschränkungen zu überwinden, kommen in KI-Anwendungen zunehmend Agenten zum Einsatz. Dies sind autonome, intelligente Systeme, die bestimmte Aufgaben ohne menschliches Eingreifen ausführen können. Diese Agenten erweitern die Fähigkeiten eines LLMs, indem sie planen, Aktionen ausführen und dynamisch neue Informationen abrufen oder generieren. Spring AI stellt die wesentlichen Bausteine für die Implementierung und Nutzung von KI-Agenten bereit.
Tool Calling
Ein solcher Bausteine ist Tool Calling, auch bekannt als Function Calling. Dabei können LLMs externe APIs nutzen, um bestimmte Aufgaben auszuführen und aktuelle Informationen bei der Generierung ihrer Antwort einzubeziehen. Wenn ein Modell eine Benutzeranweisungen erhält, prüft es, ob eine vorab definierte externe Funktion aufgerufen werden muss, um diese zu erfüllen. Falls dies der Fall ist, generiert das Modell eine strukturierte Antwort mit den Details zur aufrufenden Funktion sowie den benötigten Parametern. Das Client System verarbeitet diese Antwort, führt die entsprechende Funktion aus und gibt das Ergebnis an das LLM zurück, das auf dieser Grundlage dann die finale Antwort generiert.
Spring AI bietet mehrere Möglichkeiten, aufrufbare Funktionen zu definieren. Eine davon ist der deklarative Ansatz mit der @Tool
Annotation, die ein Beschreibungselement enthält, um dem LLM den Zweck und die Funktionalität dieser Funktion zu erklären. Ebenso kann die @ToolParam
Annotation verwendet werden, um einzelne Parameter der Funktion zu beschreiben.
@Service
class WeatherService {
@Tool(description = "Liefert die aktuelle Temperatur in einer Stadt")
Double fetchCurrentTemperature(
@ToolParam(description = "Name der Stadt") String city) {
...
}
}
Um eine definierte Funktion in einer bestimmten Anweisung zu nutzen, wird eine Instanz der Klasse, welche die Funktion enthält, mit der tools()
Methode an die ChatClient
API übergeben. Um Funktionen für alle ChatClient
API Interaktionen verfügbar zu machen, können sie in der defaultTools()
Methode des ChatClient.Builder
registriert werden.
Intern sendet Spring AI die Funktionsdefinitionen zusammen mit dem Prompt an das LLM. Das Modell entscheidet dann, ob ein Funktionsaufruf erforderlich ist. Falls ja, gibt es den Namen der Funktion sowie die individuellen Werte der Parameter zurück. Spring AI führt die Funktion mit den übergebenen Eingaben aus und gibt das Ergebnis an das LLM zurück.
Die Tool Calling Funktionalität von LLMs ist außerdem ein zentraler Bestandteil des Model Context Protocols (MCP). Dieses Protokoll standardisiert die Kommunikation zwischen KI-Modellen und verschiedenen Datenquellen sowie externen Funktionen und vereinfacht es so Agenten auf Basis von LLMs zu erstellen. MCP folgt einer Client-Server Architektur, bei der eine KI-Anwendung (MCP-Host) über eingebettete MCP-Clients mit MCP-Servern kommuniziert, um auf spezifische Ressourcen oder Funktionen zuzugreifen.
Spring AI bietet seit Kurzem Unterstützung für die Implementierung von MCP-Clients und MCP-Servern basierend auf dem offiziellen MCP Java SDK. Da dieses Thema den Rahmen dieses Artikels sprengt, wird es hier nicht weiter behandelt.
Retrieval-Augmented Generation
Eine weitere fortgeschrittene Technik zur Verbesserung der Antworten von LLMs durch externe Daten ist Retrieval-Augmented Generation (RAG). Hierbei werden externe Daten durch Embedding-Modelle als Vektoren repräsentiert und in einer Vektor-Datenbank gespeichert. Um relevante Informationen abzurufen, wird auch die Benutzeranweisung in Vektoren konvertiert. Anschließend werden semantisch passende Daten aus der Datenbank abgerufen und in den Kontext des LLMs integriert, um die Antworten zu verbessern.
Um RAG in seiner einfachsten Form zu implementieren, müssen für den Use-Case relevante Daten zuerst in eine Vektor-Datenbank integriert werden. Spring AI erleichtert diesen Prozess durch eine Extract-Transform-Load (ETL) Pipeline. Zunächst extrahiert ein DocumentReader
Inhalte aus verschiedenen Quellen wie PDFs und wandelt sie in strukturierte Document
Objekte um. Anschließend werden diese Dokumente mit einem DocumentTransformer
unterteilt, um den Kontextfensterbeschränkungen der KI-Modelle zu genügen. Schließlich speichert ein DocumentWriter
, der durch das VectorStore
Interface erweitert wird, diese Dokumentenfragmente in einer Vektor-Datenbank.
DocumentReader documentReader = new PagePdfDocumentReader(pdfResource);
List<Document> documents = new TokenTextSplitter().apply(documentReader.get());
vectorStore.accept(documents);
Das VectorStore
Interface stellt eine Abstraktionsschicht bereit, die eine flexible Integration verschiedener Vektor-Datenbanken mit minimalen Code-Änderungen ermöglicht. Jede unterstützte Vektor-Datenbank verfügt über eine eigene Spring Boot Starter Bibliothek. In diesem Beispiel wird PGvector, eine Erweiterung für die Speicherung von Vektoren in PostgreSQL, verwendet.
implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter'
Eine VectorStore
Instanz in Spring AI benötigt ein Embedding-Modell, das über die EmbeddingModel
API verfügbar ist. Da die meisten KI-Anbieter bereits Embedding-Modelle anbieten, sind in der Regel keine zusätzlichen Abhängigkeiten erforderlich. Dank der automatischen Konfiguration von Spring Boot ist sowohl eine VectorStore als auch eine EmbeddingModel
Instanz mit minimalem Setup verfügbar.
spring.ai.ollama.embedding.model=nomic-embed-text
Die Unterstützung von RAG in Spring AI basiert auf der Advisor
API, mit der Entwickler die Kommunikation zwischen der Anwendung und LLMs abfangen und anpassen können. Für gängige RAG Workflows bietet Spring AI den QuestionAnswerAdvisor
und, mit einer modularen Architektur, den RetrievalAugmentationAdvisor
an. Mit ihm lassen sich fortgeschrittene RAG-Workflows realisieren, die z.B. Routing zwischen mehreren Vektor-Datenbanken nutzen.
Advisors können für eine spezifische ChatClient
API Interaktionen über die advisors()
Methode konfiguriert werden oder global über den ChatClient.Builder
.
String answer = this.chatClient.prompt()
.user("What are the best cities to visit based on my travel guides?")
.advisors(new QuestionAnswerAdvisor(vectorStore))
.call()
.content();
In diesem Beispiel wird ein QuestionAnswerAdvisor
mit einer automatisch konfigurierten VectorStore Instanz verwendet. Über einen optionalen SearchRequest
Parameter kann feinjustiert werden, wie relevante Daten innerhalb der Vektor-Datenbank gesucht werden. Ein weiterer Parameter ermöglicht es, dem LLM zusätzliche Anweisungen zu geben, wie es die abgerufenen Daten im Kontext interpretieren und nutzen soll.
KI-Modelle für Bilder und Audio
Die ChatClient
API basiert auf der Model
API, die unterschiedliche Arten von KI-Modellen, wie neben Text z.B. Embedding-, Bild- und Audiomodelle, unterstützt. Während die ChatClient
API sinnvoll ist, um komplexe Prompts für Chat-Modelle zu vereinfachen, erfordern Bild- und Audiomodelle in der Regel weniger aufwändige Prompts. In diesen Fällen reicht die direkte Interaktion mit der Model
API aus. Wie gewohnt werden durch die automatische Konfiguration von Spring Boot Model
API Instanzen für alle Modelltypen vorkonfiguriert und so für Dependency Injection bereitgestellt.
String prompt = new PromptTemplate("Generate a picture of {city}")
.render(Map.of("city", "Berlin"));
ImageGeneration imageGeneration = imageModel.call(prompt).getResult();
Image image = imageGeneration.getOutput();
String imageUrl = image.getUrl();
Fazit
Zusammenfassend ermöglicht Spring AI es Ihnen Gen-AI-Funktionen, ähnlich wie beispielsweise Datenbanken, mit minimalem Aufwand in Spring Boot Anwendungen zu integrieren. Es unterstützt alle gängigen KI-Modelle mit einem Fokus auf Portabilität und bietet grundlegende Funktionen wie strukturierte Ausgaben und Multimodalität. Darüber hinaus stellt Spring AI die wesentlichen Bausteine für Agenten bereit und unterstützt das Model Context Protocol, das eine reibungslose Integration mit Drittanbieter Tools und Ressourcen ermöglicht. Durch diesen Funktionen stellt Spring AI sicher, dass Sie mit der Geschwindigkeit, der sich stetig weiterentwickelnden KI-Technologien mithalten können.