GEDOPLAN
JavaKISpring

Structured Output mit LangChain4j und Spring Boot

JavaKISpring
spring structured output

In den bisherigen Blogposts zum Thema Large Language Models haben wir uns primär mit der naheliegenden Anwendung der Chat-Bots auseinandergesetzt. Ein Benutzer gibt eine Textnachricht ein, sie wird an das LLM weitergeleitet und die generierte Textantwort wieder an den Benutzer weitergereicht. Large Language Models lassen sich aber nicht nur für das Generieren von Chat-Anworten verwenden, sondern auch für andere Zwecke in der Textverarbeitung. Wir werfen diesmal einen Blick auf das Structured Output Feature von LangChain4j, mit dem aus der Ausgabe des Large Language Models direkt ein Java Objekt in unserer Anwendung erzeugt wird. Als Basis dient uns diesmal eine Spring-Boot Anwendung in der wir die Spring-Boot-Integration von LangChain4j und Ollama verwenden.

[...]
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>1.0.0-beta3</version>
</dependency>

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
    <version>1.0.0-beta3</version>
</dependency>
[...]

pom.xml

Die konkrete Ollama-Instanz und das zu verwendende AI-Model können wir dann in der application.properties spezifizieren.

langchain4j.ollama.chat-model.base-url=http://localhost:11434
langchain4j.ollama.chat-model.model-name=gemma3:4b

src/main/resources/application.properties

Informationsgewinnung aus einem Fließtext

Wir schauen uns ein Beispiel an in dem wir bestimmte Informationen aus einem Text extrahieren und in strukturierter Form weiter verarbeiten möchten. Konkret haben wir eine textuelle Beschreibung einer Person vorliegen und möchten aus der Beschreibung wenn möglich die Eigenschaften Vorname, Nachname, Ehestatus und Wohnort herausfinden. Die Datenstruktur beschreiben wir mit dem Record PersonInformation. Mit @Description können wir die Attribute noch weiter beschreiben.

public record PersonInformation(
    String givenName,
    String surname,
    Boolean married,
    @Description("place of residence")
    String city
) {}

src/main/java/de/gedoplan/showcase/langchain4jdemo/PersonInformation.java

Das Structured Output Feature können wir verwenden indem wir den Record in unserer AiService-Interface-Methode anstatt String als Ausgabetyp verwenden. In der System-Message oder User-Message können wir dem LLM die Aufgabe zudem beschreiben. Ähnlich wie bei der Quarkus-Integration gibt es auch in der Spring-Integration eine Annotation mit der uns die Bibliothek auf Basis des Interfaces automatisch einen entsprechenden AiService bereitstellen kann. Hier ist das @AiService.

@AiService
public interface PersonInformationExtractor {

  @SystemMessage("""
    Du bist ein Assistent der Informationen über eine Person aus einem Text extrahierst und strukturiert zurückgibst.
    Stelle dabei sicher, dass du keine Informationen vergisst, aber auch keine neuen Informationen hinzufügst die nicht vorhanden sind.
    Wenn eine Informationen nicht im Text vorkommt gebe den Wert null zurück.""")
  PersonInformation extractPersonInformationSystemMessage(String input);

  @UserMessage("""
    Der folgende Text enthält Informationen über eine Person.
    Bitte extrahiere die Informationen aus dem Text und gebe sie strukturiert zurück.
    Stelle dabei sicher, dass du keine Informationen vergisst, aber auch keine neuen Informationen hinzufügst die nicht vorhanden sind.
    Wenn eine Informationen nicht im Text vorkommt gebe den Wert null zurück.
    ---
    {{it}}
    ---""")
  PersonInformation extractPersonInformationUserMessage(String input);

}

src/main/java/de/gedoplan/showcase/langchain4jdemo/PersonInformationExtractor.java

Wir können diesen AiService nun zum Beispiel in einen Rest-Controller injizieren und testweise über einen HTTP-Endpunkt aufrufen. Wir erhalten dann ein PersonInformation Objekt zurück.

@RestController
public class StructuredOutputController {
  private PersonInformationExtractor personInformationExtractor;
  
  public StructuredOutputController(PersonInformationExtractor personInformationExtractor) {
    this.personInformationExtractor = personInformationExtractor;
  }

  @PostMapping(value = "/extractPersonInformationSystem", consumes = "text/plain")
  public PersonInformation extractPersonInformationSystem(@RequestBody String message) {
    return personInformationExtractor.extractPersonInformationSystemMessage(message);
  }

  @PostMapping(value = "/extractPersonInformationUser", consumes = "text/plain")
  public PersonInformation extractPersonInformationUser(@RequestBody String message) {
    return personInformationExtractor.extractPersonInformationUserMessage(message);
  }
}

src/main/java/de/gedoplan/showcase/langchain4jdemo/StructuredOutputController.java

Wir verwenden zum Testen folgende Beschreibung.

Peter ist 45 Jahre alt. Sein Hund heißt Pedro und seine Frau Maria Schmidt.
Sie machen gerne lange Spaziergänge am Strand.

Von den gewünschten Informationen ist nur der Vorname direkt ersichtlich, der Ehestatus ist impliziert. Es gibt keine genaue Information über den Wohnort und nur der Nachname der Ehefrau ist spezifiziert. Das verwendete LLM erkennt den Vornamen und Ehestatus korrekt. Als Nachname wählt es den Nachnamen der Frau und gibt korrekt zurück, dass der Wohnort nicht spezifiziert ist.

{
  "givenName": "Peter",
  "surname": "Schmidt",
  "married": true,
  "city": null
}

Wenn wir das Request-Logging aktiviert haben können wir auch sehen, wie LangChain4j die gewünschte Datenstruktur an das LLM kommuniziert.

[...]
"messages" : [ {
  "role" : "system",
  "content" : "Du bist ein Assistent der Informationen über eine Person aus einem Text extrahierst und strukturiert zurückgibst.\nStelle dabei sicher, dass du keine Informationen vergisst, aber auch keine neuen Informationen hinzufügst die nicht vorhanden sind.\nWenn eine Informationen nicht im Text vorkommt gebe den Wert null zurück."
}, {
  "role" : "user",
  "content" : "Peter ist 45 Jahre alt. Sein Hund heißt Pedro und seine Frau Maria Schmidt.\nSie machen gerne lange Spaziergänge am Strand.\nYou must answer strictly in the following JSON format: {\n\"givenName\": (type: string),\n\"surname\": (type: string),\n\"married\": (type: boolean),\n\"city\": (place of residence; type: string)\n}"
} ],
[...]

Im einfachsten Fall wird einfach ein JSON-Format am Ende der User-Message definiert an das sich das LLM bei der Antwort strikt halten soll. Abhängig vom LLM Betreiber können aber auch zusätzliche Optionen aktiviert werden um die Struktur auf anderem Wege zu kommunizieren. Für Ollama können wir die Capability RESPONSE_FORMAT_JSON_SCHEMA über eine Property aktivieren.

langchain4j.ollama.chat-model.supported-capabilities=response_format_json_schema

Nun haben wir auch die Möglichkeit bestimmte Attribute als optional oder notwendig zu kennzeichnen. In der aktuellen LangChain4j Version sind Attribute standardmäßig optional. Dies kann mit @JsonProperty(required = true) angepasst werden. Im Log können wir nun beobachten, dass das Format für die Antwort nicht als Teil der User-Message definiert wurde.

[...]
"messages" : [ {
  "role" : "system",
  "content" : "Du bist ein Assistent der Informationen über eine Person aus einem Text extrahierst und strukturiert zurückgibst.\nStelle dabei sicher, dass du keine Informationen vergisst, aber auch keine neuen Informationen hinzufügst die nicht vorhanden sind.\nWenn eine Informationen nicht im Text vorkommt gebe den Wert null zurück."
}, {
  "role" : "user",
  "content" : "Peter ist 45 Jahre alt. Sein Hund heißt Pedro und seine Frau Maria Schmidt.\nSie machen gerne lange Spaziergänge am Strand."
} ],
"format" : {
  "type" : "object",
  "description" : "personal information",
  "properties" : {
    "givenName" : {
      "type" : "string"
    },
    "surname" : {
      "type" : "string"
    },
    "married" : {
      "type" : "boolean"
    },
    "city" : {
      "type" : "string",
      "description" : "place of residence"
    }
  },
  "required" : [ "givenName", "surname", "married", "city" ]
},
[...]

Abhängig vom eingesetzten Modell können unterschiedliche Einstellungen zu den besten Ergebnissen führen. So kann man ausprobieren ob das Aktivieren der RESPONSE_FORMAT_JSON_SCHEMA Capability oder das Festlegen von notwendigen Attributen einen positiven oder negativen Effekt hat. Genauso kann das Beschreiben der Aufgabe in der System-Message oder User-Message einen Unterschied machen.

Das Beispielprojekt gibt es natürlich wie immer auch 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

IT-Training - GEDOPLAN
Jakarta EE (Java EE)

JPA 2.1 Goodies: Converter

Die neue Version Java Persistence 2.1 bietet einige kleine aber feine Neuerungen, von denen ich hier zunächst mal eine kurz…

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!