LangChain4j для команд на Java: инструменты, агенты, навыки и взаимодействие между компонентами в сервисах Spring
Как внедрить вызов инструментов, конвейеры агентов, навыки по запросу, возможность приостановки и возобновления рабочих процессов, а также удаленных агентов типа A2A в существующий Java-сервис с помощью LangChain4j.
Источники: 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. Ни одна яркая функция не делает его уникальным: интерфейсы, внедрение зависимостей, тестирование, пакетирование и мониторинг остаются прежними, а ИИ становится лишь еще одним инструментом, а не причиной для полной переработки всего кода.