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)
*/
interface
AssistantWithMemoryId {
String chat(
@MemoryId
int
memoryId,
@UserMessage
String 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(
new
File(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
public
static
class
Person {
String firstName;
String lastName;
LocalDate birthDate;
Address address;
}
@Data
public
static
class
Address {
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).
*/
interface
PersonExtractor {
@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
new
journey.
He was welcomed into the world at
345
Whispering 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 =
new
ArrayList<>();
// Load documents
documents.addAll(FileSystemDocumentLoader.loadDocuments(
new
File(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
int
maxResults =
3
;
double
minScore =
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.1
Messages + 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.1
Messages + 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
*/
interface
RagAssistant {
@SystemMessage
(
"Always provide the source of knowledge"
)
String chat(
@UserMessage
String 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(
new
File(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 =
new
InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
// EmbeddingModel from rag-simple
EmbeddingModel embeddingModel =
new
BgeSmallEnV15QuantizedEmbeddingModel();
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
class
Tools {
boolean
kwigglydigglyCalled =
false
;
boolean
addCalled =
false
;
boolean
multiplyCalled =
false
;
@Tool
(
"Adds two values, returning the sum"
)
int
add(
int
a,
int
b) {
addCalled =
true
;
logger.info(
"# Calling tool function add() with parameters {} + {}"
, a, b);
return
a + b;
}
@Tool
(
"Multiply two values"
)
int
multiply(
int
a,
int
b) {
multiplyCalled =
true
;
logger.info(
"# Calling tool function multiply() with parameters {} * {}"
, a, b);
return
a * b;
}
@Tool
(
"Compute the kwigglydiggly value of a number"
)
int
computeKwigglydiggly(
double
a) {
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 =
new
Tools();
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 :-)