LangChain4j für Java-Teams: Tools, Agenten, Fähigkeiten und A2A in Spring-Diensten
Wie man Tool-Aufrufe, Agent-Pipelines, auf Anfrage verfügbare Fähigkeiten, Workflows mit Pause und Fortsetzung sowie entfernte A2A-Agenten mithilfe von LangChain4j in einen bestehenden Java-Dienst integriert.
Quellen: Java AI in 2026, LangChain4j Turns Agents into Ordinary Interfaces
Stellen Sie sich ein Unternehmen vor, das auf Java basiert. Bestellungen, Lagerverwaltung, Risikokontrolle und Kundenservice werden bereits seit Jahren mit Spring Boot abgewickelt, und nun wird ein Support-Mitarbeiter gesucht, der in der Lage ist, eine Bestellung zu überprüfen, einen Rückzahlungsauftrag auszustellen und den Fall an eine andere Person weiterzuleiten. Die online verfügbaren Tutorials beginnen mit FastAPI, wechseln dann zu LangChain und schließlich zur Einrichtung einer Python-Umgebung, während der Java-Dienst im Hintergrund untätig bleibt.
LangChain4j bringt diese Funktionen in ein Java-Projekt. Es handelt sich dabei nicht um eine Portierung des Python-Frameworks, sondern um eine Neuentwicklung, die auf der Arbeitsweise von Java-Entwicklern basiert. Kurz gesagt: Mit ihm können Agenten erstellt werden, indem drei Schichten in einen bereits vorhandenen Dienst integriert werden – nämlich Tools, Fähigkeiten und Agenten – ohne vorher den Sprachwechsel vorzunehmen.
Tools: Ermöglichen des Aufrufs von Geschäftsmethoden durch das Modell
Viele Teams stecken hier fest – nicht am Modell selbst, sondern daran, wie dieses ermittelt, welche Java-Methode aufgerufen werden soll. Die @Tool-Anmerkung kümmert sich darum. Markieren Sie eine Methode damit, und das Framework wandelt sie in ein JSON Schema um, das das Modell lesen kann.
Beginnen Sie mit den Abhängigkeiten. Das mit OpenAI kompatible Modul ist nur eine Möglichkeit. Der Wechsel zu Azure, Ollama oder Qwen bedeutet in der Regel nur den Ersatz der Chat-Modell-Implementierung, während der Geschäftskodex im Großen und Ganzen unverändert bleibt.
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-skills</artifactId>
<version>1.19.0-beta29</version>
</dependency>
Dann gibt es die Klasse der Order-Tools. Nichts Exotisches: gewöhnliche Java-Methoden, deren Parameter und Rückgabewerte klar beschrieben sind.
Der Code benötigt Java 17 oder neuer. In einem einfachen Maven-Projekt sollte die Compiler-Flagge -parameters aktiviert werden; andernfalls könnte ein Parametername wie ein Platzhalter für eine Frage als arg0 kompiliert werden und für das Modell keine Bedeutung haben. Spring und Quarkus kümmern sich standardmäßig darum.
class OrderTools {
@Tool("Look up the status of an order by ID")
String queryOrder(@P("Order ID") String orderId) {
return "Order " + orderId + " has shipped and should arrive tomorrow";
}
@Tool("Refund the specified order")
String refund(@P("Order ID") String orderId, @P("Refund reason") String reason) {
return "Order " + orderId + " refund initiated. Reason: " + reason;
}
}
Dann werden die Tools an eine Schnittstelle angebunden. Der Kunde stellt einfach eine Anfrage, und das Modell entscheidet, ob ein Auftrag abgerufen oder eine Rückerstattung eingeleitet wird.
interface CustomerServiceAssistant {
@UserMessage("You are an e-commerce customer service assistant. Answer questions about orders and refunds. The customer's question is {{question}}")
String answer(String question);
}
CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class)
.chatModel(model)
.tools(new OrderTools())
.chatMemory(MessageWindowChatMemory.withMaxMessages(20))
.build();
String reply = assistant.answer("Where is order 20260907001?");
Das ist die Ebene, in der sich Java-Entwickler wohlfühlen, denn das Erstellen eines Abfrageendpunkts sowie das Bereitstellen einer @Tool-Methode für ein Modell sind im Grunde eng miteinander verbunden.
Agenten: eine Schnittstelle plus ein gemeinsamer Gültigkeitsbereich
Die Orchestrierung reduziert sich auf zwei Konzepte. Die @Agent-Anmerkung wird anstelle einer Klasse auf eine Methode gesetzt, und ein AgenticScope überträgt Ergebnisse von einem Agenten zum nächsten.
Nehmen wir drei kleine Agenten. Sie rufen sich niemals direkt gegenseitig auf. Jeder schreibt sein Ergebnis in eine gemeinsame Variable, und der nächste Agent holt es ab.
public interface DraftWriter {
@UserMessage("""
You are a technical editor. Write an opening of no more than 200 words on the topic {{topic}}.
Return only the opening, with no explanation.
""")
@Agent(outputKey = "draft", description = "Writes an opening based on a topic")
String write(@V("topic") String topic);
}
public interface Sharpener {
@UserMessage("""
You are a reviewer. Make the following opening more specific and remove filler.
The opening is {{draft}}.
Return only the revised text.
""")
@Agent(outputKey = "draft", description = "Makes the opening more specific")
String polish(@V("draft") String draft);
}
public interface TitlePicker {
@UserMessage("""
Write 3 headlines suitable for a technical blog for the following content, one per line.
The content is {{draft}}.
""")
@Agent(outputKey = "titles", description = "Writes headlines for the content")
String pickTitle(@V("draft") String draft);
}
Jeder Agent speichert seine Ausgabe unter der mit outputKey benannten Variablen. Der folgende Agent liest diese Variable über @V ein, und die Kette bildet so eine Pipeline.
DraftWriter writer = AgenticServices
.agentBuilder(DraftWriter.class)
.chatModel(model)
.build();
Sharpener sharpener = AgenticServices
.agentBuilder(Sharpener.class)
.chatModel(model)
.build();
TitlePicker titlePicker = AgenticServices
.agentBuilder(TitlePicker.class)
.chatModel(model)
.build();
UntypedAgent editor = AgenticServices
.sequenceBuilder()
.subAgents(writer, sharpener, titlePicker)
.outputKey("titles")
.build();
Object result = editor.invoke(Map.of("topic", "Building agents with LangChain4j"));
Der Vorteil ist, dass die Orchestrierung in wiederverwendbare Komponenten umgewandelt wird. Das Erstellen, Feinschleifen und Benennen können jeweils getrennt getestet und neu geordnet werden. Zudem sind Schleifen-, parallele und bedingte Muster im Framework enthalten; um die Ausgabe ständig weiter zu verbessern, bis sie einen Qualitätsstandard erfüllt, verwendet man loopBuilder zusammen mit einem Bewertungsmechanismus und einer Abbruchbedingung.
Fähigkeiten: Anweisungen auf Anfrage geladen
Die LangChain4j-Fähigkeiten ähneln eher den Fähigkeitspaketen von Claude Code als irgendwelchen Annotationen. Eine Fähigkeit ist ein Verzeichnis, das eine SKILL.md-Datei enthält. Standardmäßig sieht das Modell den vollständigen Text nicht. Erst wenn es eine Aufgabe für passend hält, ruft es activate_skill auf, um den Inhalt zu laden – dadurch bleibt der Ausgangskontext klein und jede Aufrufkosten geringer.
Beginnen Sie damit, eine Fähigkeitsdatei zu erstellen:
skills/refund/SKILL.md
---
name: refund
description: Handle refund requests. Look up the order, check the conditions, then either refund or escalate to a human.
---
When a customer requests a refund:
1. First call queryOrder to check the order status.
2. Only call refund when the order has shipped or is complete.
3. If the order is still awaiting payment, tell the customer to cancel it.
4. If the amount exceeds 500, escalate to a human instead of refunding directly.
Führen Sie anschließend die Fähigkeiten mit dem Service zusammen. Der Tool-Modus ist der offiziell empfohlene Weg, und das Modell kann nur auf die von Ihnen registrierten Tools zugreifen, ohne Zugang zu Dateisystemen oder der Shell zu haben.
List<FileSystemSkill> loadedSkills = FileSystemSkillLoader.loadSkills(Path.of("skills"));
Skills skillSet = Skills.from(loadedSkills);
CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class)
.chatModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(20))
.tools(new OrderTools())
.toolProvider(skillSet.toolProvider())
.systemMessage("You have access to the following skills. When a request relates to one of them, activate it first.\n"
+ skillSet.formatAvailableSkills())
.build();
Die Alternative ist der Shell-Modus, der dem Modell ein run_shell_command-Tool bietet. Er ist nützlich für Prototypen, erfordert in der Produktion jedoch Vorsicht: Die Befehle werden direkt auf dem Host ausgeführt, ohne jeglichen Sandbox. Sofern die Eingaben nicht vollständig vertrauenswürdig sind, sollte dieser Modus deaktiviert bleiben.
Pause und Wiederaufnahme langer Workflows
In einem Unternehmensumfeld lässt sich das schwierige Szenario nicht mit einer einfachen Antwort lösen, sondern es handelt sich um Workflows, die mitten in Dutzenden von Schritten unterbrochen werden. Persistenz und menschliche Genehmigung in LangChain4j richten sich genau darauf aus. Um sicherzustellen, dass ein Workflow wiederherstellbar ist, muss seine Schnittstelle AgenticScopeAccess erweitern und das Methodenattribut @MemoryId verwenden, wodurch ein persistenter Scope aktiviert wird.
public interface OrderWorkflow extends AgenticScopeAccess {
@Agent
String processOrder(@MemoryId String orderId, @V("order") String orderDetails);
}
Dann folgen drei Schritte in Folge: Validierung, menschliche Freigabe und Versand. Der Freigabeschritt gibt eine SuspendedResponse zurück, wodurch das System den aktuellen Zustand speichert und den Thread freigibt, anstatt ihn weiterhin zu halten.
AgenticScopeAction validateOrder = AgenticServices.agentAction(scope -> {
String order = scope.readState("order", "");
scope.writeState("validated_order", "VALIDATED: " + order);
});
HumanInTheLoop approvalGate = AgenticServices.humanInTheLoopBuilder()
.description("Large orders require manual approval")
.outputKey("approval")
.responseProvider(scope -> new SuspendedResponse<>("manager-approval"))
.build();
AgenticScopeAction shipOrder = AgenticServices.agentAction(scope -> {
String validated = scope.readState("validated_order", "");
String approval = scope.readState("approval", "");
scope.writeState("result", "Order processed " + approval);
});
OrderWorkflow workflow = AgenticServices.sequenceBuilder(OrderWorkflow.class)
.subAgents(validateOrder, approvalGate, shipOrder)
.outputKey("result")
.build();
Der Aufrufer kümmert sich um die Aussetzung mithilfe von ResultWithAgenticScope. Sobald ein Manager zustimmt, setzt der Workflow von seinem Checkpoint fort.
ResultWithAgenticScope<String> result =
workflow.processOrder("order-10001", "1000 units");
if (result.suspended()) {
result = result.completePendingResponse("Manager approved");
}
Der Zustand im Arbeitsspeicher allein überlebt einen Neustart nicht, daher sollte ein AgenticScopeStore unter Verwendung von Redis, einer Datenbank oder dem Dateisystem implementiert werden. Das Framework erstellt nach Abschluss jeder Aufgabe Checkpoints und speichert die Position des Planers, sodass bei Wiederherstellung bereits ausgeführte Schritte nicht wiederholt werden müssen.
AgenticScopePersister.setStore(new MyAgenticScopeStore());
Es sieht unbedeutend aus, doch genau das unterscheidet einen Demo-Agenten von einem Produktions-Agenten. Ausfälle, Neustarts und Verzögerungen bei der Freigabe sind alltägliche Ereignisse, keine Ausnahmefälle.
A2A: Remote-Agenten als lokale Komponenten
Über verschiedene Teams hinweg kann der Risiko-Agent in einem Service laufen, während der Kundenservice einer anderen Gruppe zugeordnet ist. A2A dient als Verbindungsstück für solche Remote-Agenten: Durch das Einbinden des langchain4j-agentic-a2a-Moduls behandelt der Orchestrierungskodex einen entfernten Agenten genauso wie einen im selben Prozess befindlichen.
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic-a2a</artifactId>
<version>1.19.0</version>
</dependency>
Deklarieren Sie eine lokale Schnittstelle, die auf den entfernten A2A-Dienst verweist.
public interface RemoteRiskAgent {
@Agent
String check(@V("transaction") String transaction);
}
UntypedAgent riskAgent = AgenticServices
.a2aBuilder("https://risk.internal/a2a", RemoteRiskAgent.class)
.outputKey("risk_result")
.build();
Bei mehrstufigen Chats sollten contextId und taskId wieder in den gemeinsamen Bereich geschrieben werden: Der Server erzeugt sie in der ersten Nachricht, und der Client gibt sie in der zweiten zurück.
public interface ChatAgent {
@A2AClientAgent(a2aServerUrl = "http://localhost:8080", outputKey = "response")
ResultWithAgenticScope<String> chat(
@V("question") String question,
@A2AContextId @V("contextId") String contextId,
@A2ATaskId @V("taskId") String taskId);
}
ResultWithAgenticScope<String> first = chatAgent.chat("Hello", null, null);
String contextId = (String) first.agenticScope().readState("contextId");
String taskId = (String) first.agenticScope().readState("taskId");
ResultWithAgenticScope<String> second = chatAgent.chat("Continue", contextId, taskId);
Mehrere Agenten können dann nicht mehr erfordern, dass alles in einem einzigen Repository gespeichert ist. Teams können den A2A-Service für ihre jeweiligen Agenten selbst betreiben, während eine höhere Schicht die Komponenten miteinander verbindet.
Ratschläge für Java-Teams
Fangen Sie klein an. Vermeiden Sie vollständig autonome Operationen-Agenten und entwickeln Sie stattdessen einen Support-Agenten, der Bestellungen überprüft und Rückerstattungen ausstellt, bevor Sie schrittweise weitere Funktionen sowie menschliche Genehmigungen hinzufügen.
Trennen Sie Tools von Fähigkeiten. Tools beschreiben, was der Code leisten kann. Fähigkeiten sind die schrittweisen Verfahren, die dem Modell vorgegeben werden. Halten Sie die Tools klein und stabil, während die Fähigkeiten je nach Geschäftsregeln häufig geändert werden können, ohne dass sich die einen in die anderen einmischen.
Fügen Sie frühzeitig Persistenz für kritische Arbeitsabläufe hinzu. Wenn ein Ablauf Zustimmungsaufenthalte, Fernanrufe oder Schritte umfasst, die länger als einige Dutzend Sekunden dauern, konfigurieren Sie vorab einen AgenticScopeStore, da die Nachrüstung von Wiederherstellungsmöglichkeiten in der Regel teurer ist.
Der Vorteil von LangChain4j besteht darin, dass Agenten zu herkömmlicher Java-Entwicklung zurückkehren. Keine einzelne auffällige Funktion macht es aus. Schnittstellen, Abhängigkeitsinjektion, Tests, Verpackung und Überwachung bleiben erhalten, und KI wird zu einer weiteren Fähigkeit anstelle eines Grundes, alles neu aufzubauen.