GEDOPLAN
Spring

Werkzeuge für die AI – Function calling/Tools in LLMs mit Spring AI

Spring
spring ai jpg

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

2020 07 02 12 09 28 window
Web Security, Spring

Spring Boot Swagger Keycloak

Spring Boot + Swagger sind ein eingespieltes Team, um die eigenen Rest-APIs als OpenApi Beschreibung zur Verfügung zu stellen. Dafür…
IT-Training - GEDOPLAN
DevOps

Docker – “Build, Ship, and Run”

Problemstellung Ein großes Problem stellen beim Testen und Ausliefern von Software die leider häufig unterschiedlich konfigurierten Ablaufumgebungen dar. Die Entwicklerrechner,…

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!