LangChain4j для Java-каманд: інструменты, агенты, навыкі і A2A у сэрвісах Spring
Як дадаць у існы Java-сэрвіс выклік інструментаў, канвееры агентаў, навыкі па запыце, працэсы з паўзай і аднаўленнем і аддаленых A2A-агентаў з дапамогай LangChain4j.
Крыніцы: Java AI in 2026, LangChain4j Turns Agents into Ordinary Interfaces
Уявіце сябе компаніяй, якая працуе на Java. Замовленні, запасы, кантроль рызыка і абслуга кліента ўжо багаты гады знаходзяцца ў Spring Boot, і цяпер трэба адпаведны агент падтрымкі, які можа пераглядзець замовленне, выдаваць вярненне грошаў і перадаваць справу іншай адзеўцы. Урокі, якія можна знайсці ў Інтэрнэце, пачынаюцца з FastAPI, далей пераходзяць да LangChain, а потым да налаштавання сераўніка Python, тады калі Java-сервіс застаецца недзейным у фоне.
LangChain4j аднесае гэтыя можлівасці ў праект на Java. Гэта не пераклад пайтонавскага фрэймворку, а ўсё перабудаванне па прынцыпах работы распрацоўшчыкаў на 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();
Альтэрнатыва — рэжым shell, які дае модэлю інструмент run_shell_command. Ён падходзіць для пратотыпаў, але ў працоўнай суперактуальнасці трэба быць абераглівым: каманды выкананыя безпосерэдна на хосце, без жаднага sandbox. Як толькі інпут не ўпэўнены на 100%, лепшая яго не выкорыстоўваць.
Паўза і продаж часу дзейнасці дзейнасцяў
У корпаратыўнай асяроддзі складныя сітуацыі не маюць простай адпаведзі, а ўскладненыя дзейнасці, якія перарываюцца на срэдзі дзесяткаў крокаў. Стойкасць і падтрымка з боку чалавека ў 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, адколькі пазнейшая наладка зворачэння да нормальнага стану зазвычай коштуе больш.
Прыемнае ў Лангчэйн4джэ тое, што агенты вяртаюцца да звычнай працы ў Java. Жадна адзінка яркіх функцый не ўзначае яго перавагу. Інтэрфейсы, ввод залежнасцяў, тэсты, пакетаванне і манітарынг застаюцца без змян, а ШЫ стае ўсьмо толькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтолькі ўтоль