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.