Hallo zusammen,

in meinem heutigen Blog wird es wieder einmal etwas technisch. Ich hatte ja zuletzt bereits Blogs zu Large Language Models (LLMs) veröffentlicht. In der heutigen Episode möchte ich das Langchain4j-Framework vorstellen. Wie der Name bereits vermuten lässt, ist dies die Java-Variante zur ebenfalls sehr bekannten Langchain Implementierung für Python.

Mithilfe von Langchain können Best Practises im Bereich der LLMs schnell umgesetzt werden. Es gibt mittlerweile auch einige Alternativen, wie z.B. Spring-AI für Java. Langchain scheint aber das aktivere Projekt zu sein und bietet meiner Meinung nach auch die flexiblere API, wobei das gegebenenfalls auch auf den Anwendungsfall ankommt.

Apropos – heute werde ich an ein paar einfachen Beispielen bzw. Anwendungsfällen zeigen, wie diese mit Langchain umgesetzt werden können. An dieser Stelle sei auch noch auf die hervorragenden Beispiele hingewiesen, die es im offiziellen  Langchain-Beispiel-Repository zu finden gibt.

Wir werden ein paar Standard-Anwendungsfälle behandeln (wie z. B. einfacher Chatbot mit Memory, Bilderkennung, Java-Objekt-Extraktion aus Text) und uns dann auch zu den etwas komplexeren Themen RAG und Function Calling heranwagen. Insbesondere letzteres finde ich persönlich sehr spannend.

Die hier angesprochenen Beispiele befinden sich übrigens auch in folgendem GitHub Repo. Bei Interesse dort einfach mal einen Blick rein werfen.

Langchain4j

Langchain4j bietet einige Module für verschiedene LLMs und Anwendungsfälle an. Über den bekannten spring-starter-Mechanismus können diese Komponenten einfach in die eigene Applikation integriert werden.

Maven-Konfiguration

Wenn die langchain4j-bom importiert wird im DependencyManagement-Bereich der POM.xml, können anschließend die benötigten Starter als Dependencies hinzugefügt werden. Als Beispiel wurde eine Spring Boot-Applikation inklusive Langchain4j erstellt:

POM.xml (zum erweitern klicken) 

<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.jadice.blog.l4j</groupId>

  <artifactId>blog-langchain4j</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <properties>

        <java.version>21</java.version>

        <spring-boot.version>3.3.2</spring-boot.version>

        <langchain4j.version>0.33.0</langchain4j.version>

        <commons-io.version>2.15.1</commons-io.version>

        <maven.compiler.source>${java.version}</maven.compiler.source>

        <maven.compiler.target>${java.version}</maven.compiler.target>

    </properties>

    <dependencies>

        <dependency>

            <groupId>org.projectlombok</groupId>

            <artifactId>lombok</artifactId>

            <scope>provided</scope>

        </dependency>

        <dependency>

            <groupId>dev.langchain4j</groupId>

            <artifactId>langchain4j-spring-boot-starter</artifactId>

        </dependency>

        <dependency>

            <groupId>dev.langchain4j</groupId>

            <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>

        </dependency>

        <dependency>

            <groupId>dev.langchain4j</groupId>

            <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>

        </dependency>

        <dependency>

            <groupId>dev.langchain4j</groupId>

            <artifactId>langchain4j-easy-rag</artifactId>

        </dependency>

        <dependency>

            <groupId>dev.langchain4j</groupId>

            <artifactId>langchain4j-pgvector</artifactId>

        </dependency>

        <dependency>

            <groupId>com.fasterxml.jackson.datatype</groupId>

            <artifactId>jackson-datatype-jsr310</artifactId>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-actuator</artifactId>

        </dependency>

        <dependency>

            <groupId>io.micrometer</groupId>

            <artifactId>micrometer-registry-prometheus</artifactId>

        </dependency>

        <dependency>

            <groupId>io.micrometer</groupId>

            <artifactId>micrometer-registry-jmx</artifactId>

        </dependency>

        <dependency>

            <groupId>org.springframework</groupId>

            <artifactId>spring-aspects</artifactId>

        </dependency>

        <dependency>

            <groupId>commons-io</groupId>

            <artifactId>commons-io</artifactId>

            <version>${commons-io.version}</version>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

        </dependency>

        <dependency>

            <groupId>org.testcontainers</groupId>

            <artifactId>postgresql</artifactId>

            <scope>test</scope>

        </dependency>

    </dependencies>

    <dependencyManagement>

        <dependencies>

            <dependency>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-dependencies</artifactId>

                <version>${spring-boot.version}</version>

                <type>pom</type>

                <scope>import</scope>

            </dependency>

            <dependency>

                <groupId>dev.langchain4j</groupId>

                <artifactId>langchain4j-bom</artifactId>

                <version>${langchain4j.version}</version>

                <type>pom</type>

                <scope>import</scope>

            </dependency>

        </dependencies>

    </dependencyManagement>

</project>

Mit diesem Setup können nun sehr kleine und übersichtliche Testklassen erstellt werden, um die Anwendungsfälle zu verdeutlichen.

Use Case: Einfacher Chatbot mit Memory

Wir wollen einen Chatbot erstellen, der die letzten Nachrichten des Chatverlaufs kennt und somit auf Rückfragen besser antworten kann. Im Beispiel testen wir dies mit 2 Benutzern, die in ihrer „Session“ jeweils den Namen nennen und dieser dann in einer zweiten Anfrage an das LLM abgefragt wird.

Erstellen wir zunächst den Assistenten. In Langchain4j werden Assistenten dynamisch erzeugt. Man gibt selbst das Interface für den Chatbot vor, z.B:

Definition Chat bot mit Memory (zum erweitern klicken) 

/**

 * An assistant with a chat memory which can be accessed via its memoryId number

 * (e.g. user id)

 */

interfaceAssistantWithMemoryId {

    String chat(@MemoryIdintmemoryId, @UserMessageString message);

}

Die Magie entsteht dann später bei der Erstellung der Instanz; dort werden die hier angebrachten Langchain4j-Annotationen @MemoryId und @UserMessage erkannt / verwendet. 

Zunächst wird das Model benötigt. In unserem Fall verwenden wir das Ollama-Chat-Model für einen lokal laufenden Ollama Server.

Model erzeugen (via Ollama aus ModelName)

OllamaChatModel model = OllamaChatModel.builder().baseUrl(ollamaUrl).modelName(modelName)
.timeout(Duration.ofMinutes(5)).temperature(0.0).build();

Dieses Interface wird dann über einen Builder von Langchain4j als Instanz erzeugt:

Chat bot erzeugen

AssistantWithMemoryId assistant = AiServices.builder(AssistantWithMemoryId.class).chatLanguageModel(model)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)).build();

Dat wars. Damit können wir nun via „assistant.chat(…)“ mit dem LLM kommunizieren.

Test

Wir haben das „Memory“ bzw. unser Chat-Gedächtnis als „int“-Zahl definiert. Es wären auch andere Datentypen möglich. Hier verwenden wir jetzt aber einfach die Zahlen 1 und 2, um 2 verschiedene „Chat-Sessions“ mit dem Modell zu simulieren.

Wir werden hier in 2 Sessions einen Namen nennen und diesen dann abfragen. „Fritz“ hat Session 1, „Francine“ hat Session 2:

Beispiel: Test Chat Memory (zum erweitern klicken) 

logger.debug("Chat for Fritz ongoing...");

String answerToFritz = assistant.chat(1, "Hello, my name is Fritz");

logger.debug("Chat for Francine ongoing...");

String answerToFrancine = assistant.chat(2, "Hello, my name is Francine");

logger.info("Answer to Fritz: {}, Answer to Francine: {}", answerToFritz, answerToFrancine);

// Now ask for the name in each session

String nameOfFritz = assistant.chat(1, "Say my name");

String nameOfFrancine = assistant.chat(2, "Say my name");

logger.info("Name of Fritz: {}, Name of Francine: {}", nameOfFritz, nameOfFrancine);

assertTrue(nameOfFritz.toLowerCase().contains("fritz"), "Fritz not detected");

assertTrue(nameOfFrancine.toLowerCase().contains("francine"), "Francine not detected");

So viel sei verraten: Dieser Test läuft am Ende dann erfolgreich durch. Durch Angabe der jeweiligen „MemoryID“ (hier 1 oder 2) wird dem LLM automatisch die jeweilige Chat-History übergeben und die Frage nach dem Namen erfolgreich beantwortet.

Allerdings scheint das LLM (bzw. in meinem Fall das verwendete Llama3.1 Modell) mit Breaking Bad trainiert worden zu sein, deswegen wird der Name nach der Aufforderung „say my name“ manchmal komplett in Großbuchstaben geantwortet und der Test musste dies berücksichtigen 😉

Use Case: Bilderkennung

Ein weiterer mittlerweile Standard-Anwendungsfall für LLMs. Bilderkennung kann mit dem Multi-modalen (Text+Bild) Modell „llava“ einfach umgesetzt werden. Hier im Beispiel wird ein JPG eines Papageien (Parrot.jpg) übergeben, und dies wird auch als Ergebnis im Test erwartet auf die Frage, was das LLM sieht:

Beispiel: Test Chat Memory (zum erweitern klicken) 

String ollamaUrl = "http://localhost:11434";

String modelName = "llava";

OllamaChatModel model = OllamaChatModel.builder().baseUrl(ollamaUrl).modelName(modelName).timeout(

    Duration.ofMinutes(5)).temperature(0.0).build();

String prompt = "What do you see?";

UserMessage userMessage = UserMessage.from(TextContent.from(prompt),

    ImageContent.from(newFile(System.getProperty("user.dir"), "/src/test/resources/images/parrot.jpg").toURI()));

logger.debug("Sending image request...");

Response<AiMessage> response = model.generate(userMessage);

String answer = response.content().text();

assertTrue(answer.toLowerCase().contains("bird") || answer.toLowerCase().contains("parrot"),

    "Bird/parrot not recognized");

Das llava Model liegt wie die meisten Modelle auch in unterschiedlichen Quantisierungsstufen / Größen vor. Meist reicht das Standard-Modell für gute Ergebnisse, ggf kann durch ein anderes Modell auch eine bessere Erkennung erreicht werden.

Use Case: Ein Java Pojo Objekt vom LLM aus Text extrahieren lassen

Ein aus meiner Sicht verblüffender Anwendungsfall ist die Erzeugung eines Java POJO Objekts aus einem Text heraus. Als „POJO“ wird ein „Plain old Java Object“ bezeichnet, also ein Java Objekt, welches meist für den Transport von Informationen zuständig ist.

In unserem Beispiel extrahieren wir ein „Person“-Objekt aus einem Text. Hierbei soll die Person und das zugehörige Address-Objekt befüllt werden.

Die Klassen sehen dann so aus:

Pojo Klassen (zum erweitern klicken) 

/**

 * Our simple pojo is a person with an address.

 */

@Data

publicstaticclassPerson {

    String firstName;

    String lastName;

    LocalDate birthDate;

    Address address;

}

@Data

publicstaticclassAddress {

    String street;

    Integer streetNumber;

    String city;

}

Für die Extraktion der Objekte definieren wir ebenfalls noch einen „Extraktor“. Diesem sagen wir über die @UserMessage Annotation, dass eine Person aus dem Prompt extrahiert werden soll:

Pojo Extraktor Interface (zum erweitern klicken) 

/**

 * This interface will be populated by lang4j AiServices mechanism. If there is

 * only one parameter in the method, the value can be referenced in

 * the @UserMessage Annotation with

 *

 * <pre>

 * it

 * </pre>

 *

 * (in double curly brackets).

 */

interfacePersonExtractor {

    @UserMessage("Extract information about a person from {{it}}")

    Person extractPersonFrom(String text);

}

Und schon kann es los gehen und wir können Personen-Objekte aus Texten extrahieren.

Pojo Extraktion Beispiel (zum erweitern klicken) 

OllamaChatModel model = OllamaChatModel.builder().baseUrl(ollamaUrl).modelName(modelName)

                .timeout(Duration.ofMinutes(5)).temperature(0.0).format("json").build();

PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, model);

String text = """

        In 1968, amidst the fading echoes of Independence Day,

        a child named John arrived under the calm evening sky.

        This newborn, bearing the surname Doe, marked the start of a newjourney.

        He was welcomed into the world at 345Whispering Pines Avenue

        a quaint street nestled in the heart of Springfield

        an abode that echoed with the gentle hum of suburban dreams and aspirations.

        """;

logger.debug("Extracting person instance from input text\n{}", text);

Person person = personExtractor.extractPersonFrom(text);

// // Person { firstName = "John", lastName = "Doe",

// birthDate = 1968-07-04,// address = Address { ... } }

logger.info(om.writeValueAsString(person));

assertNotNull(person);

assertEquals("John", person.getFirstName());

assertEquals("Doe", person.getLastName());

Ergebnis

Aus dem Text

In 1968, amidst the fading echoes of Independence Day
a child named John arrived under the calm evening sky.
This newborn, bearing the surname Doe, marked the start of a newjourney.
He was welcomed into the world at 345Whispering Pines Avenuea
quaint street nestled in the heart of Springfieldan
abode that echoed with the gentle hum of suburban dreams and aspirations.

Wird ein Person Java Objekt erzeugt, welches im json Format so aussieht:

json Ergebnis

{"firstName":"John","lastName":"Doe","birthDate":null,"address":{"street":"Whispering Pines Avenue","streetNumber":345,"city":"Springfield"}}

In diesem Beispiel wird das Person-Objekt prinzipiell korrekt erzeugt. Allerdings scheinen nicht alle LLMs dahinterzukommen, dass das Geburtsdatum am 4. Juli (Independence Day) ist. Dieses Feld ist gegebenenfalls manchmal leer.

Dieser Mechanismus scheint sehr interessant für manche Anwendungsfälle, wo bestimmte Daten (hier die Person) aus einem Text direkt strukturiert abfragbar sein sollen.

Use Case: Test der VectorDB

Wenn man nun einem LLM weitere Zusatzinformationen bereitstellen möchte, ergeben sich mit der Zeit ein paar Problemstellungen:

  • Die Menge an Hintergrundwissen kann sehr groß sein
  • LLMs können nur eine begrenzte Anzahl Tokens verarbeiten

Die Idee ist nun also, dem LLM beim Aufruf möglichst nur die „passenden“ Informationen mitzuliefern. Hier werden aktuell oftmals Vector-Datenbanken verwendet.

Dies sind Datenbanken, die speziell für die Behandlung von mathematischen Vektoren, also im Endeffekt langen Zahlenreihen, verwendet werden.

Vektoren kennt man ja aus verschiedenen Bereichen. In der Physik kann mit einem Vektor z.B. Geschwindigkeit dargestellt werden, die in eine bestimmte Richtung wirkt.

In unserem Fall werden Wissensquellen, also z.B. PDF Dokumente, Textdateien etc mithilfe eines „Embedding Modells“ quasi in Vektoren umgewandelt (siehe auch Einbettung). Diese Vektoren werden in der Vektor-Datenbank abgelegt.

Wenn nun Wissen abgefragt wird, wird aus der Frage ebenfalls ein Vektor erzeugt. Die Ergebnisse sind dann die dem Fragevektor naheliegenden Wissens-Vektoren.

Beispiel:

Wir legen zunächst Daten in der Vektor-DB ab. In diesem Fall verwenden wir PDF-Dokumente mit IBM DB2 Fehlercodes sowie die PDF-Formatspezifikation.

Testdokumente laden (zum erweitern klicken) 

List<Document> documents = newArrayList<>();

// Load documents

documents.addAll(FileSystemDocumentLoader.loadDocuments(

    newFile(System.getProperty("user.dir"), "/src/test/resources/testdocs").toPath()));

// Split documents into segments

DocumentSplitter splitter = DocumentSplitters.recursive(300, 0);

// Embed segments (convert them into vectors that represent the meaning) using

// embedding model

for(Document document : documents) {

  List<TextSegment> segments = splitter.split(document);

  List<Embedding> embeddings = embeddingModel.embedAll(segments).content();

  embeddingStore.addAll(embeddings, segments);

}

Man kann dann Abfragen gegen die Vektor-DB durchführen. Wir fragen jetzt etwas über ungültige Attribut-IDs. Es wäre zu erwarten, dass wir also Ergebnisse aus den DB2-Fehlercode Dokumenten erhalten.

VectorDB abfragen (zum erweitern klicken) 

String question = "I am getting 'Invalid attribute with ID' error messages. What to do?";

// Embed the question

Embedding questionEmbedding = embeddingModel.embed(question).content();

// Find relevant embeddings in embedding store by semantic similarity

// You can play with parameters below to find a sweet spot for your specific use

// case

intmaxResults = 3;

doubleminScore = 0.7;

// Search the closest vectors

EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder().queryEmbedding(

    questionEmbedding).maxResults(maxResults).minScore(minScore).build();

EmbeddingSearchResult<TextSegment> embeddingSearchResult = embeddingStore.search(embeddingSearchRequest);

List<EmbeddingMatch<TextSegment>> relevantEmbeddings = embeddingSearchResult.matches();

String information = relevantEmbeddings.stream().map(match -> match.embedded().text()).collect(

    Collectors.joining("\n\n"));

Über die Parameter „maxResults=3“ kann die Ergebnis-Anzahl beschränkt werden. Über die „minScore“ (0-1) kann festgelegt werden, wie nah das Ergebnis an der Frage dran sein soll. So können eher weiter entfernte Vektoren ausgelassen werden.

Tatsächlich liefert die Abfrage hier dann die passendsten Ergebnisse (aus dem DB2-PDF Dokument). 

Ergebnis (zum erweitern klicken) 

Relevant RAG Vector DB information:

CM8.4.1Messages + Codes.pdf : Index: 1007->

DGL3638A  Invalid  attribute  ID.

Explanation:

An invalid  attribute  ID was  used.  The  attribute  might not  exist  in the  database.

User  response:

Define  a valid  attribute  ID.

DGL5060A  An  item  with  the  version  ID was  not

CM8.4.1Messages + Codes.pdf : Index: 2072-> Source:

Java  or C++  APIs

DGL7096A  Invalid  attribute  with  ID [nnn]  cannot appear  after  the  REFERENCEDBY component  type  view.

Explanation:

Only  the  REFERENCER  attribute  can  appear  in the  join between  the  REFERENCEDBY  and  the  destination

Damit kann nun z.B. ein Chatbot mit RAG implementiert werden, der bei Abfragen weitere Hintergrundinformationen zur Verfügung hat. Dies machen wir im nächsten Abschnitt.

Use Case: Chatbot mit Retrieval Augmented Generation (RAG) aus der VectorDB

Um nun einen „schlaueren“ Chatbot zu erzeugen, gehen wir folgendermaßen vor:

Erstellen des Chatbot Interfaces. Wir möchten, dass unser Chatbot immer die Quelle des Wissens (also z.B. einen Dateinamen) mit in das Ergebnis schreibt. Dies kann über eine SystemMessage definiert werden:

Chatbot RAG (zum erweitern klicken) 

/**

  * This assistant will add the source of knowledge to the response

  */

 interfaceRagAssistant {

   @SystemMessage("Always provide the source of knowledge")

   String chat(@UserMessageString message);

 }

Zunächst laden wir wieder unsere RAG Dokumente und erzeugen einen „RetrievalAugmentor“, über diesen intern in langchain4j das Wissen bereitgestellt wird:

RAG Dokumente laden (zum erweitern klicken) 

// Load documents

List<Document> documents = FileSystemDocumentLoader.loadDocuments(

    newFile(System.getProperty("user.dir"), "/src/test/resources/testdocs").toPath());

logger.info("{} RAG documents loaded. Getting embeddings (this might take a while)...", documents.size());

InMemoryEmbeddingStore<TextSegment> embeddingStore = newInMemoryEmbeddingStore<>();

EmbeddingStoreIngestor.ingest(documents, embeddingStore);

// EmbeddingModel from rag-simple

EmbeddingModel embeddingModel = newBgeSmallEnV15QuantizedEmbeddingModel();

EmbeddingStoreContentRetrieverBuilder builder = EmbeddingStoreContentRetriever.builder().embeddingStore(

    embeddingStore).embeddingModel(embeddingModel);

ContentRetriever contentRetriever = builder.build();

// Each retrieved segment should include "file_name" and "index" metadata values

// in the prompt

ContentInjector contentInjector = DefaultContentInjector.builder()

    // .promptTemplate(...) // Formatting can also be changed

    .metadataKeysToInclude(Arrays.asList("file_name", "index")).build();

RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder().contentRetriever(

    contentRetriever).contentInjector(contentInjector).build();

Nun kann man den Chatbot einfach verwenden. In dem Beispiel habe ich eine Datei „Nelly.txt“ hinzugefügt, in der ein langsamer Hund namens Nelly beschrieben ist. Die Frage sollte also zum Ergebnis führen, dass Nelly ein Hund ist und diese Information aus der Datei Nelly.txt stammt:

Chat mit RAG (zum erweitern klicken) 

String answer = assistant.chat("who is Nelly?");

Und in der Tat ist dies nun der Fall:

RAG Ergebnis (zum erweitern klicken) 

Nelly is a slow dog who was a beloved pet in a small town.

She was a golden retriever with a heart of gold and a wagging tail, but she had one major flaw: she was incredibly slow.

Despite her slowness, Nelly had a big personality and was a loyal companion to her owners.

Source: The text file "Nelly.txt"(index 0)

Use Case: Function calling – das LLM verwendet unsere bereitgestellten Java Klassen

Und zu guter Letzt hatte mich noch interessiert, wie man mit langchain4j FunctionCalling durchführen kann. Hier kann man selbst dem LLM Methoden bereitstellen, die dann verwendet werden.

Wir definieren also zunächst unsere „Tools“, die dem LLM zur Verfügung stehen sollen. Wir erstellen 3 Methoden:

  • add(int a, int b) : Zum Addieren von 2 Zahlen
  • multiply(int a, int b) : Für Multiplikation
  • computeKwigglyDiggly(double value) : Errechnet den Pseudowert „kwigglydiggly“ – im Endeffekt ist dies der gerundete Wert vom Eingangswert * 42
Tool-Definition (zum erweitern klicken) 

/**

* Simple Tools for this test. Each @Tool method will also set a boolean whether it was called.

* Can be used in tests to check if a tool was used.

*/

@Getter

classTools {

booleankwigglydigglyCalled = false;

booleanaddCalled = false;

booleanmultiplyCalled = false;

@Tool("Adds two values, returning the sum")

intadd(inta, intb) {

  addCalled = true;

  logger.info("# Calling tool function add() with parameters {} + {}", a, b);

  returna + b;

}

@Tool("Multiply two values")

intmultiply(inta, intb) {

  multiplyCalled = true;

  logger.info("# Calling tool function multiply() with parameters {} * {}", a, b);

  returna * b;

}

@Tool("Compute the kwigglydiggly value of a number")

intcomputeKwigglydiggly(doublea) {

  kwigglydigglyCalled = true;

  // In the end, the "kwigglydiggly" value is the original value * 42; rounded to

  // int

  logger.info("# Calling tool function computeKwigglydiggly() with parameters {}", a);

  return((Long) Math.round(a * 42d)).intValue();

}

}

Die Abfrage sieht nun so aus. Wir fragen im Endeffekt: 

What is 1+2 and 3*4?
Tool Abfrage (zum erweitern klicken) 

OllamaChatModel model = OllamaChatModel.builder().baseUrl(ollamaUrl).modelName(modelName).timeout(

    Duration.ofMinutes(5)).temperature(0.0).build();

Tools tools = newTools();

ChatBot assistant = AiServices.builder(ChatBot.class).chatLanguageModel(model).tools(tools).build();

String answer = assistant.chat("What is 1+2 and 3*4?");

Das LLM liefert uns hier die korrekten Antworten und ruft auch unsere 2 Methoden auf:

Tool Antwort (Math) (zum erweitern klicken) 

The results of the calculations are:

1+ 2= 3

3* 4= 12

Im Log sichtbar die Log-Meldungen aus unseren Tool-Methoden:

Calling tool function add() with parameters 1+ 2

Calling tool function multiply() with parameters 3* 4

Auch für die KwigglyDiggly-Methode funktioniert dies. Hier die Antwort zur Frage „What is the kwigglydiggly value of pi?“ (132):

KwigglyDiggly-Ergebnis (zum erweitern klicken) 

I called the `kwigglydiggly` function with the argument `pi`, which is a mathematical constant representing the ratio of a circle's circumference to its diameter.

The function returned the kwigglydiggly value of pi, which is 132.

Die Frage „Compute the kwigglydiggly value of 5 and multiply by 2.0“ wurde in meinem Test allerdings nicht korrekt beantwortet. Hier hätte das LLM zuerst den k-Wert ermitteln und dann multiplizieren sollen. Das hat hier aber nicht funktioniert (aufgrund eines internen Problems mit den Datentypen).

Fazit

Mit Langchain4j können sehr schnell Best Practises umgesetzt und Prototypen erstellt werden. Wenn die Verkettung von Funktionen beim FunctionCalling noch optimiert wird oder man die Prompts noch weiter verfeinert, können damit auch schon interessante Anwendungsfälle ermöglicht werden.

So, damit habt ihr es für heute mal wieder geschafft. Schaut euch bei Interesse die weiteren offiziellen Beispiele an.

Bis zum nächsten Mal und danke für die Aufmerksamkeit :-)