Einer der ersten Stolpersteine bei der Integration von AI Schnittstellen in Anwendungen ist häufig, dass das Modell nicht auf aktuelle Informationen zurückgreifen kann, da diese zum Trainingszeitpunkt noch nicht existiert haben. Im schlechtesten Fall generiert das Modell dann einfach falsche Informationen und gibt diese als Realität aus. In diesem Beitrag schauen wir uns an, wie wir mit Spring AI einem LLM zur Laufzeit ermöglichen können Informationen an einer definierten Schnittstelle zu erfragen und diese in der Generierung einer Antwort zu berücksichtigen.
Das Feature das wir dazu in diesem Beitrag verwenden nennt sich Function Calling oder Tools und wird bereits von einigen Modellen unterstützt. Eine andere Möglichkeit um dem Modell Kontext zu einer Anfrage bereitzustellen (RAG) schauen wir uns in einem zukünftigen Blogpost an. In unserem Beispielprojekt verwenden wir das Modell mistral, das lokal mit Ollama ausgeführt werden kann. Konkret möchten wir hier dem LLM ermöglichen aktuelle Wetter- und Verkehrsinformationen abzurufen. Wir könnten entsprechende Schnittstellen anfragen um dem LLM die tatsächlichen aktuellen Informationen bereit zu stellen. Für dieses Beispiel reicht es allerdings aus, wenn wir diese Schnittstellen zunächst nur mit Beispieldaten füllen.
Um die Funktionen mit Spring AI bereitzustellen müssen sie als Function
Objekte modelliert werden, diese können wir als Bean in unserer Anwendung bereitstellen. Dabei müssen wir eine Beschreibung der gebotenen Funktionalität für das LLM hinzufügen. In den Code-Snippets beschränken wir uns hier auf die Implementierung der Verkehrsinformationsschnittstelle, die Wetterinformationsschnittstelle ist analog implementiert.
@Configuration
public class AiClientConfig {
[...]
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
return chatClientBuilder.build();
}
@Bean
@Description("Get traffic jam information for motorway.")
public Function<TrafficRequest, List<TrafficResponse>> trafficJamFunction() {
return new MockTrafficInformationService();
}
[...]
}
Zur Modellierung der Anfrage und Antwort verwenden wir dabei diese Typen. Die definierte Struktur der Anfrage und Antwort teilt Spring AI dem LLM bei der Anfrage automatisch mit.
public record TrafficRequest(Motorway motorway, LengthUnit unit) {}
public record TrafficResponse(String from, String to, double trafficJamLength, LengthUnit unit) {[...]}
public enum Motorway { A1, A2; [...] }
public enum LengthUnit { KILOMETRES, MILES; [...] }
Die Mock-Schnittstelle liefert dann ein paar vordefinierte Verkehrsinformationen.
public class MockTrafficInformationService
implements Function<TrafficRequest, List<TrafficResponse>> {
private final Map<Motorway, List<TrafficResponse>> trafficJamMap = Map.ofEntries(
Map.entry(Motorway.A1, List.of(
new TrafficResponse("Dortmund", "Euskirchen", 2.0, LengthUnit.KILOMETRES),
new TrafficResponse("Köln", "Dortmund", 4.0, LengthUnit.KILOMETRES)
)),
Map.entry(Motorway.A2, List.of(
new TrafficResponse("Oberhausen", "Dortmund", 8.5, LengthUnit.KILOMETRES),
new TrafficResponse("Bielefeld", "Hannover", 3.0, LengthUnit.KILOMETRES)
))
);
@Override
public List<TrafficResponse> apply(TrafficRequest request) {
if (request.motorway() == null) return List.of();
var trafficJams = trafficJamMap.getOrDefault(request.motorway(), List.of())
.stream().map(response -> response.withLengthUnit(response.unit())).toList();
return trafficJams;
}
}
Unsere Anwendung beinhaltet auch eine einfache HTTP-Schnittstelle zum Testen, in der eine Anfrage an das LLM gesendet wird. Die Antwort bekommen wir dann entsprechend zurück.
@RestController
@RequestMapping("/ai")
public class ToolsAiResource{
@Autowired
ChatClient chatClient;
@PostMapping(consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.TEXT_PLAIN_VALUE, value = "/tools")
public String tools(@RequestBody String question) {
return chatClient
.prompt()
.user(question)
.functions("trafficJamFunction", "weatherFunction")
.call()
.content();
}
}
Wenn wir diese Schnittstelle nun aufrufen bekommen wir (in den meisten Fällen) eine sinnvolle Antwort, die die Mock Daten berücksichtigt. Auch unterschiedliche Sprachen sind je nach LLM oft kein Problem. Für Debugzwecke ist im Projekt ein HttpLoggingInterceptor
angelegt, der die kompletten HTTP Anfragen und Antworten an das LLM ausgibt. Der von Spring AI bereitgestellte SimpleLoggingAdvisor
liefert leider keinen direkten Einblick auf die tatsächliche Kommunikation mit dem LLM.
Q: What is the current traffic information for the A1?
A: On the A1, there is currently a traffic jam of 2 kilometers from Dortmund to Euskirchen and a traffic jam of 4 kilometers from Köln to Dortmund.
Q: Wie ist die aktuelle Verkehrssituation auf der A1?
A: Es gibt Verkehrsstau auf der A1 zwischen Dortmund und Euskirchen mit einer Länge von 2 Kilometern sowie zwischen Köln und Dortmund mit einer Länge von 4 Kilometern.
Allerdings liefern die LLMs prinzipiell keine Garantien. Sie sind hochkomplexe stochastische Maschinen und so kann es durchaus mal vorkommen, dass das LLM die mitgegebenen Tools anders aufruft, als man erwartet hat. So müssen wir z.B. beachten, dass die Einheit bei der Anfrage eventuell den Wert null
hat oder das LLM nach Stauinformationen auf der A5 fragt obwohl dieser Wert in der Enumeration nicht dargestellt werden kann und uns keine Informationen zu dieser Autobahn vorliegen.
Im Falle der Stauinformationen ist hier ein Fehlerfall nicht spezifisch modelliert. In der Schnittstelle zur Wetterinformation sind zwei mögliche Antwortstrukturen im Interface WeatherResponse
definiert um einen Fehlerfall an das LLM zu kommunizieren.
public sealed interface WeatherResponse {
record Success(String location, double temp, TempUnit unit) implements WeatherResponse {[...]}
record Failure(String failureMessage) implements WeatherResponse {}
}
Letztlich sind wir in der Programmierung dieser Tools nicht besonders eingeschränkt und so können wir damit z.B. auch Daten aus unserer Datenbank abfragen und bereitstellen oder dem LLM sogar ermöglichen Änderungen in unserer Datenbank vorzunehmen. Ob das im jeweiligen Geschäftsprozess eine gute Idee ist sollte man natürlich sorgfältig abwägen.
Das Beispiel gibt es wie immer auf GitHub.