Strona główna / Artykuły

LangChain4j dla zespołów Java: narzędzia, agenci, umiejętności oraz komunikacja A2A w usługach Spring

Jak wdrożyć funkcje takie jak wywoływanie narzędzi, pipeline agentów, umiejętności na żądanie, przerwanie i wznowienie procesów oraz zdalnych agentów A2A do istniejącego serwisu Java za pomocą LangChain4j.

Wydawca: REACTAPP.TOP7 min czytania1718 słów

Źródła: Java AI in 2026, LangChain4j Turns Agents into Ordinary Interfaces

Wyobraź sobie firmę działającą w środowisku Java. Zamówienia, zapasy, kontrola ryzyka oraz obsługa klienta od lat są realizowane za pomocą Spring Boot, a teraz szukany jest pracownik obsługi klienta, który potrafi sprawdzić zamówienie, wydać zwrot pieniędzy i przekazać sprawę odpowiedniej osobie. Tutoryale dostępne w Internecie zaczynają się od FastAPI, przechodzą do LangChain, a następnie do konfiguracji środowiska Python, podczas gdy usługa napisana w Java pozostaje nieużywana w tle.

LangChain4j łączy te funkcjonalności w projekcie napisanym w Java. Nie jest to port frameworku Python, lecz nowa implementacja dostosowana do sposobu pracy programistów Java. Krótko mówiąc: tworzenie agentów za jego pomocą oznacza połączenie trzech warstw w istniejącą już usługę – narzędzi, umiejętności i agentów – bez konieczności najpierw zmiany języka programowania.

Narzędzia: umożliwienie modelowi wywoływania metod biznesowych

Wiele zespołów utyka właśnie w tym momencie, nie w samym modelu, ale w sposobie, w jaki ten określa, który metodę w Javie powinien wywołać. Adnotacja @Tool rozwiązuje ten problem – wystarczy oznaczyć metodę tą adnotacją, a framework przekształca ją w schemat JSON, który model może odczytać.

Zacznij od zależności. Moduł kompatybilny z OpenAI to tylko jedna z opcji. Przejście na Azure, Ollamę lub Qwen oznacza głównie zastąpienie implementacji modelu do rozmów, podczas gdy kod biznesowy pozostaje w dużej mierze niezmieniony.

<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>

Następnie klasa narzędzi typu order. Nic egzotycznego: zwykłe metody w Javie, których parametry i wartości zwracane są jasno opisane.

Kod wymaga Javy 17 lub nowszej. W zwykłym projekcie Maven należy włączyć flagę kompilatora -parameters; nazwa parametru, taka jak zastępcza w pytaniu, może zostać skompilowana jako arg0 i nie mieć żadnego znaczenia dla modelu. Spring i Quarkus automatycznie zajmują się tym przez domyślną konfigurację.

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;
    }
}

Następnie przymocuj narzędzia do interfejsu. Klient po prostu o to prosi, a model decyduje, czy wyszuka zamówienie, czy rozpocznie zwrot pieniędzy.

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?");

To jest warstwa, w której deweloperzy Java czują się jak w domu, ponieważ tworzenie punktu końcowego do wysyłania zapytań oraz udostępnianie metody @Tool modelowi okazują się być blisko spokrewnionymi czynnościami.

Agenty: interfejs plus wspólny zakres

Orchestracja sprowadza się do dwóch zasad. Adnotacja @Agent znajduje się na metodzie, a nie na klasie, natomiast AgenticScope przenosi wyniki od jednego agenta do drugiego.

Weźmy trzy małe agenty. One nigdy nie dzwonią do siebie bezpośrednio. Każdy zapisuje swój wynik do wspólnej zmiennej, a następny go pobiera.

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);
}

Każdy agent przechowuje swój wynik w zmiennej nazwanej outputKey. Następny agent odczytuje tę zmienną za pomocą @V, a łańcuch staje się przepływem danych.

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"));

Zaletą jest to, że orkiestracja przekształca się w elementy wielokrotnie używalne. Proces tworzenia, dopracowywania i nadawania tytułów można testować osobno oraz modyfikować ich kolejność. W ramach tego frameworka dostępne są również wzory pętli, równoległe i warunkowe, więc aby nieustannie doskonalić wynik aż do osiągnięcia określonego standardu jakości, należy użyć loopBuilder w połączeniu z agentem oceny i warunkiem zakończenia.

Umiejętności: instrukcje ładowane na żądanie

Umiejętności LangChain4j przypominają bardziej pakiety umiejętności Claude Code niż jakiekolwiek inne formy anotacji. Umiejętność to katalog zawierający plik SKILL.md. Domyślnie model nie widzi pełnego tekstu; dopiero gdy oceni, że zadanie jest odpowiednie, wywołuje activate_skill w celu załadowania treści, co zapewnia mały początkowy kontekst i obniża koszt każdego wywołania.

Najpierw należy stworzyć plik umiejętności:

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.

Następnie powiąż umiejętności z usługą. Tryb narzędziowy jest oficjalnie zalecaną metodą, a model może uzyskać dostęp tylko do zarejestrowanych narzędzi, bez możliwości dostępu do systemu plików ani wiersza poleceń.

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();

Alternatywą jest tryb shell, który daje modelowi narzędzie run_shell_command. Jest przydatny do prototypowania, ale w środowisku produkcyjnym wymaga ostrożności: polecenia są wykonywane bezpośrednio na serwerze host bez żadnego sandboxu. Chyba że dane wejściowe są w pełni wiarygodne, lepiej go wyłączyć.

Zatrzymywanie i wznowianie długich procesów pracy

W środowisku korporacyjnym trudny przypadek to nie odpowiedź na jedno pytanie, lecz proces pracy przerwany w trakcie dziesiątek kroków. Trwałość danych i zatwierdzenie przez człowieka w LangChain4j są właśnie na to przeznaczone. Aby proces pracy mógł zostać przywrócony, jego interfejs powinien rozszerzać AgenticScopeAccess, a na metodzie umieścić @MemoryId, co aktywuje trwały zakres danych.

public interface OrderWorkflow extends AgenticScopeAccess {

    @Agent
    String processOrder(@MemoryId String orderId, @V("order") String orderDetails);
}

Następnie następują trzy kroki: walidacja, zatwierdzenie przez człowieka i wysyłka. Krok zatwierdzenia zwraca SuspendedResponse, więc system zapisuje obecny stan i uwalnia wątek zamiast go utrzymywać.

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();

Osoba dzwoniąca zajmuje się wstrzymaniem poprzez ResultWithAgenticScope. Gdy menedżer to zatwierdzi, proces pracy kontynuuje od swojego punktu kontrolnego.

ResultWithAgenticScope<String> result =
        workflow.processOrder("order-10001", "1000 units");

if (result.suspended()) {
    result = result.completePendingResponse("Manager approved");
}

Samo stan w pamięci nie przetrwa restartu, dlatego należy zaimplementować AgenticScopeStore wspierany przez Redis, bazę danych lub system plików. Framework tworzy punkty kontrolne po zakończeniu pracy każdego agenta i zapisuje pozycję planera, dzięki czemu po przywróceniu nie są powtarzane już wykonane kroki.

AgenticScopePersister.setStore(new MyAgenticScopeStore());

Wygląda to niepozornie, ale to właśnie odróżnia agenta demonstracyjnego od agenta produkcyjnego. Przerwy w działaniu, restarty oraz opóźnienia w zatwierdzaniu to zwykłe zdarzenia, a nie wyjątki.

A2A: agenci zdalni jako elementy lokalne

W różnych zespołach agent ryzyka może działać w jednej usłudze, podczas gdy obsługa klienta należy do innego zespołu. A2A pełni rolę łącznika dla takich agentów zdalnych: wystarczy włączyć moduł langchain4j-agentic-a2a, a kod orkiestracji traktuje agenta znajdującego się daleko tak samo, jak tego w tym samym procesie.

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-agentic-a2a</artifactId>
  <version>1.19.0</version>
</dependency>

Deklaruj lokalną interfejs, który wskazuje na zdalną usługę A2A.

public interface RemoteRiskAgent {


@Agent
    String check(@V("transaction") String transaction);
}
UntypedAgent riskAgent = AgenticServices
        .a2aBuilder("https://risk.internal/a2a", RemoteRiskAgent.class)
        .outputKey("risk_result")
        .build();

W przypadku rozmów wieloetapowych należy ponownie wpisać contextId oraz taskId do wspólnego zakresu: serwer generuje je w pierwszym etapie, a klient zwraca je w drugim.

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);

Współpraca między wieloma agentami nie wymaga już, aby wszystko znajdowało się w jednym repozytorium. Zespoły mogą posiadać i zarządzać usługą A2A dla swoich agentów, podczas gdy wyższy poziom łączy te elementy ze sobą.

Porady dla zespołów Java

Zacznij od czegoś prostego. Pomiń agent pełnie autonomicznych operacji i stwórz agenta wsparcia, który sprawdza zamówienia i wypłaca zwroty, a następnie dodaj warstwę specjalistycznych umiejętności oraz ludzkiej aprobaty.

Trzymaj narzędzia i umiejętności oddzielnie. Narzędzia odpowiadają za to, co może zrobić kod, natomiast umiejętności to krok po kroku przedstawiona modelowi procedura. Utrzymuj narzędzia w małym i stabilnym formacie, a umiejętności niech zmieniają się często wraz z zasadami biznesowymi, przy czym żadne z nich nie powinno wpływać na drugie.

Należy jak najszybciej wdrożyć mechanizmy utrzymywania stanu w kluczowych procesach pracy. Jeśli proces obejmuje oczekiwanie na zatwierdzenia, połączenia zdalne lub kroki trwające dłużej niż kilkadziesiąt sekund, należy z góry skonfigurować AgenticScopeStore, ponieważ późniejsza implementacja mechanizmów odzyskiwania jest zwykle droższa.

LangChain4j oferuje wygodę polegającą na tym, że agenci mogą być używane w standardowej praktyce programowania w Javie. Nie jest to osiągnięte dzięki jednej wyjątkowej funkcji – interfejsy, iniekcja zależności, testy, pakietowanie i monitorowanie pozostają bez zmian, a sztuczna inteligencja staje się jedynie dodatkową możliwością, a nie powodem do całkowitej przebudowy systemu.