Головна / Статті

LangChain4j для команд Java: інструменти, агенти, навички та A2A у сервісах Spring

Як інтегрувати виклик інструментів, конвеєри агентів, навички за запитом, робочі процеси з можливістю паузи та продовження, а також віддалених агентів типу A2A у існуючий Java-сервіс за допомогою LangChain4j.

Видавець: REACTAPP.TOP7 хв читання1718 слів

Джерела: Java AI in 2026, LangChain4j Turns Agents into Ordinary Interfaces

Уявіть собі компанію, яка працює на Java. Замовлення, запаси, контроль ризиків та обслуговування клієнтів вже багато років реалізуються за допомогою Spring Boot, і тепер потрібен співробітник підтримки, який зможе перевірити замовлення, оформити повернення грошей та передати справу іншій особі. Туторіали в Інтернеті починаються з FastAPI, потім йдуть про LangChain та налаштування середовища Python, тоді як Java-сервіс залишається неробочим на фоні.

LangChain4j об’єднує ці функції в проект на Java. Це не порт Python-фреймворку — його перебудували з урахуванням способу роботи розробників на Java. Коротко кажучи: створення агентів за допомогою цього інструменту означає поєднання трьох шарів у вже існуючому сервісі — інструментів, навичок та агентів — без необхідності спочатку змінювати мову програмування.

Інструменти: можливість для моделі викликати бізнес-методи

Багато команд застрягають саме тут, не у самій моделі, а у способі її навчання щодо вибору методу Java для виклику. Анотація @Tool вирішує цю проблему. Якщо позначити метод нею, фреймворк перетворює його на JSON Schema, який може прочитати модель.

Почніть з залежностей. Модуль, сумісний з OpenAI, — це лише один із варіантів. Перехід на Azure, Ollama чи Qwen здебільшого означає заміну реалізації чат-моделі, тоді як бізнес-код залишається майже незмінним.

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

Далі — клас інструментів порядку. Нічого екзотичного: звичайні методи Java, параметри та значення повернення яких чітко описані.

Для коду потрібна версія Java 17 або новіша. У звичайному проекті Maven увімкніть флаг компілятора -parameters; інакше назва параметра, подібна до замінника запитання, може бути скомпільована як arg0 та не мати жодного значення для моделі. Spring та Quarkus за замовчуванням самі вирішують цю проблему.

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

Потім під’єднайте інструменти до інтерфейсу. Клієнт просто запитує, а модель вирішує, чи шукати замовлення, чи розпочати повернення грошей.

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

Це той рівень, де розробники Java почуваються як удома, адже створення кінцевої точки запиту та використання методу @Tool у моделі виявляються дуже схожими процедурами.

Агенти: інтерфейс та спільний діапазон

Оркестрація зводиться до двох основних ідей. Анотація @Agent розміщується на методі, а не на класі, а об’єкт AgenticScope передає результати від одного агента до іншого.

Візьмемо три невеликі агенти. Вони ніколи не викликають один одного безпосередньо. Кожен записує свої результати у спільну змінну, а наступний їх бере.

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

Кожен агент зберігає свій результат під змінною, названою outputKey. Наступний агент читає цю змінну через @V, і таким чином утворюється конвеєр обробки даних.

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

Перевага полягає у тому, що оркестрація перетворюється на повторно використовувані елементи. Процеси розробки, відшліфовування та присвоєння назв можна тестувати окремо та змінювати їх порядок. Моделі циклічної, паралельної та умовної обробки також постачаються разом із фреймворком, тож для постійного вдосконалення результату до досягнення певного рівня якості використовуйте loopBuilder разом із агентом оцінювання та умовою завершення.

Навички: інструкції, завантажувані за потребою

Навички LangChain4j більше схожі на пакети навичок Claude Code, ніж на будь-які анотації. Навичка — це каталог, який містить файл SKILL.md. За замовчуванням модель не бачить повного тексту. Лише тоді, коли вона вирішує, що завдання відповідає, вона викликає activate_skill для завантаження контенту, що дозволяє зберігати невеликий початковий контекст та робити кожен виклик дешевшим.

Почніть зі створення файлу навички:

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.

Потім пов’яжіть ці навички із сервісом. Режим інструментів є офіційно рекомендованим способом, і модель може отримувати доступ лише до зареєстрованих вами інструментів, без можливості доступу до файлової системи чи оболонки.

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

Альтернативою є режим шелу, який надає моделі інструмент run_shell_command. Він зручний для прототипів, але у продакшені вимагає обережності: команди виконуються безпосередньо на хості без жодного сандбоксу. Якщо вхідні дані не є повністю надійними, краще його вимкнути.

Паузування та відновлення довгих робочих процесів

У корпоративному середовищі складні ситуації — це не однозначні відповіді, а робочі процеси, які перериваються на середині десятків кроків. Стабільність та людське схвалення в LangChain4j саме на це спрямовані. Щоб робочий процес можна було відновити, його інтерфейс має розширювати AgenticScopeAccess та додавати атрибут @MemoryId до методу, що активує постійний діапазон.

public interface OrderWorkflow extends AgenticScopeAccess {

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

Потім виконується три кроки: перевірка, схвалення людиною та відправка. На кроці схвалення повертається SuspendedResponse, тому система зберігає поточний стан та звільняє потік замість того, щоб тримати його у своєму розпорядженні.

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

Автор дзвінка керує процесом призупинення за допомогою ResultWithAgenticScope. Як тільки менеджер схвалить це, робочий процес продовжиться з попереднього стану.

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

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

Лише стан у пам’яті не виживе після перезапуску, тому необхідно впровадити AgenticScopeStore, підтримуваний Redis, базою даних чи файловою системою. Фреймворк створює контрольні точки після завершення роботи кожного агента та записує стан планувальника, щоб після відновлення не доводилося повторювати вже виконані кроки.

AgenticScopePersister.setStore(new MyAgenticScopeStore());

Це може здатися незначним, проте саме це відрізняє демонстраційного агента від агента у продакшені. Перерви у роботі, перезапуски та затримки затверджень є звичайними явищами, а не винятковими ситуаціями.

A2A: віддалені агенти як локальні елементи

У різних командах агент ризиків може працювати в одній службі, тоді як обслуговування клієнтів належить до іншої групи. A2A є з’єднувальним елементом для таких віддалених агентів: достатньо використати модуль langchain4j-agentic-a2a, і код оркестрації буде ставитися до агента, що знаходиться на відстані, так само, як до агента в межах того ж процесу.

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

Оголосіть локальний інтерфейс, який вказує на віддалений сервіс 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();

Для багатокрокових чатів введіть contextId та taskId назад у спільний діапазон: сервер генерує їх під час першого кроку, а клієнт повертає їх під час другого.

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

У співпраці кількох агентів більше не потрібно, щоб усе знаходилось у одному репозиторії. Команди можуть володіти та керувати сервісом A2A для своїх агентів, тоді як вищий рівень об’єднує всі ці елементи.

Поради для команд, що працюють з Java

Почніть з простого. Пропустіть можливість повністю автономних операційних агентів та створіть агента підтримки, який перевіряє замовлення та видає повернення грошей, а потім додайте додаткові функції та людське схвалення.

Тримайте інструменти та навички окремо. Інструменти відповідають за те, що може робити код. Навички – це поетапна процедура, яка подається моделі. Зробіть інструменти простими та стабільними, а навички – часто змінюваними відповідно до бізнес-правил, при цьому нехай жодні з них не впливають на інші.

Якомога раніше впроваджуйте механізми зберігання даних для критичних робочих процесів. Якщо процес включає очікування схвалень, віддалені виклики чи кроки, які тривають більше кількох десятків секунд, налаштуйте AgenticScopeStore заздалегідь, адже пізніше впровадження механізмів відновлення зазвичай коштує дорожче.

Перевагою LangChain4j є те, що агенти повертаються до звичайної роботи з Java. Жодна окрема яскрава функція не є його головною особливістю. Інтерфейси, вставка залежностей, тести, пакування та моніторинг залишаються без змін, а ШІ стає ще однією можливістю, а не причиною для повної перебудови всього.