Всем привет, меня зовут Сергей Прощаев, и в этой статье расскажу про то, как Java и Spring за последний год заметно подтянулись в теме, где долго и заслуженно лидировал Python, — продакшен AI‑агентов. Сразу обозначу позицию, чтобы не было разночтений. Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры.

Как Java‑разработчик я искренне благодарен Python. Именно Python дал экосистему, которой в Java не хватало, и годами позволял решать AI‑задачи там, где на Java это было бы медленно и неудобно. Речь ниже не про «кто кого», а про то, что у нас, джавистов, появился ещё один рабочий вариант.

Если у вас в проде Spring Boot, а каждая «умная» фича приезжает отдельным сервисом со своим деплоем и дежурствами, — то это статья про то, как часть такой логики можно держать прямо в своём JVM‑процессе. Не вместо Python, а там, где так удобнее. Узнали себя? Тогда поехали.

Несколько лет подряд расклад был привычный. Хочешь добавить в продукт что‑то «умное» — берёшь Python. Там LangChain, LlamaIndex и вся быстро растущая экосистема, там все примеры из интернета, и это было абсолютно разумно: в Java на тот момент такой экосистемы просто не было.

Помню, как однажды мы выкатывали в FinTech‑продукте маленькую фичу с LLM. Она приехала отдельным сервисом на Python: свой деплой, свой контур безопасности, свой стек наблюдаемости, свои дежурства. Фича была на три экрана кода, а сопровождение — как у полноценного сервиса. И это не претензия к Python, а просто стоимость второго параллельного стека рядом с основным бэкендом — особенно ощутимая там, где всё зарегулировано.

И вот что поменялось к июню 2026-го. Главное — изменилось не столько качество самих LLM, сколько зрелость Java‑инфраструктуры вокруг них. Если раньше Java‑разработчики чаще интегрировали модели через отдельный Python‑сервис, то сегодня многие из этих задач можно решать внутри привычного Spring‑стека — и именно об этом статья.

Маркер зрелости: 12 июня вышел Spring AI 2.0.0 GA — релиз, в котором одним из центральных нововведений стала глубокая интеграция с MCP, и который ориентирован на Spring Boot 4. Тезис, который я хочу разобрать (и с которым, предупреждаю, можно спорить): логика принятия решений и выбор инструментов остаются за LLM, и тут ничего не меняется.

А вот слой исполнения — то, что реально дёргает ваши сервисы, базы и платёжные шлюзы, — теперь можно держать в Java без второго технологического стека. Это не «отказ от Python», а появившийся выбор. Ниже я погружу вас в саму проблему, разберу решение, которое сейчас выглядит самым рабочим, покажу практики команд на июнь 2026-го и честно расскажу, где всё это пока ломается.

Рис. 1. Расцепление слоёв: LLM как часть, которая рассуждает, и Java/Spring как слой исполнения, соединённые единым протоколом
Рис. 1. Расцепление слоёв: LLM как часть, которая рассуждает, и Java/Spring как слой исполнения, соединённые единым протоколом

Часть 1. Почему для агента мы по привычке звали Python — и это было оправдано

Давайте честно про корень боли. Дело было не в том, что на Java «нельзя позвать модель» — HTTP‑клиент есть, позвать OpenAI можно было и три года назад. Проблема была архитектурная. Чтобы агент приносил пользу, он должен не болтать, а действовать: ходить в базу, дёргать внутренние сервисы, проверять лимиты, инициировать платёж. А вся эта логика у энтерпрайза уже написана. На Java. С транзакциями, ролями, аудитом и десятилетием выстраданных edge‑кейсов.

И вот тут возникала чисто инженерная сложность. Чтобы дать AI‑сервису доступ к этой логике, мы шли одним из двух путей. Либо открывали ему пачку внутренних HTTP‑эндпоинтов — а это новая поверхность атаки. Либо дублировали кусок бизнес‑логики во втором стеке — а это рассинхрон и баги. В одном из проектов AI‑ассистент знал про статусы заказов из отдельной копии справочника, и однажды эта копия отстала от основной системы на сутки. Клиентам он бодро рассказывал про заказы, которых уже не было. И это была не вина Python — это была вина архитектуры с двумя источниками правды. Смешно, пока это не твой инцидент.

На мой взгляд, для исследований, ML и дата‑сайенса Python был и остаётся королём, и я не собираюсь это оспаривать. Но когда речь про агента, который живёт внутри корпоративного бэкенда и трогает боевые данные, ключевой вопрос другой. Не «на чём удобнее звать модель», а «где живёт исполнение и кто за него отвечает». И для FinTech и e‑commerce ответ у меня почти всегда один. Там же, где и всё остальное: в JVM‑процессе, который уже умеет в конфиги, health‑чеки, graceful shutdown и привычную нам безопасность.

Часть 2. MCP — и история про то, кто на самом деле написал Java SDK

Чтобы подключать инструменты к LLM единообразно, нужен общий стандарт. Одним из самых перспективных таких стандартов сегодня стал MCP — Model Context Protocol. Важная оговорка: сам по себе MCP ничего не знает про агентов. Это протокол взаимодействия клиента и сервера; агент может пользоваться им, а может — обычным HTTP, gRPC или function calling вообще без MCP. Если совсем коротко: сервер выставляет наружу три вещи. Tools — функции, которые модель может вызвать. Resources — данные, которые модель может прочитать.

Prompts — шаблоны запросов. Клиент получает их описание и вызывает по стандартному JSON‑RPC (список может обнаруживаться динамически, а может быть известен заранее). Один хост может держать пачку клиентов и подключаться сразу к множеству серверов: один обёрнут вокруг вашего PostgreSQL, другой — вокруг внешнего API, третий — вокруг внутренней логики заказов. И что важно для этой статьи: MCP языконезависим. Сервер на Python и клиент на Java прекрасно понимают друг друга, и наоборот. То есть выбор языка для каждого куска перестаёт быть идеологическим — берёте тот, что удобнее под задачу.

А теперь история, которая лучше всего объясняет, как именно Java оказалась в этой теме всерьёз. MCP многие до сих пор считают «питоновской» или «антроповской» темой. Но официальный Java‑SDK протокола во многом вырос из работы команды Spring. В декабре 2024 года команда Spring передала свою Java‑реализацию Anthropic.

Сегодня этот проект развивается как официальный Java SDK для MCP под эгидой организации Model Context Protocol. Важно: это не «спринговый» SDK и не однокомандный проект. Он развивается совместно — в контрибьюторах помимо Spring есть инженеры из Broadcom, Oracle, Google и других. Поверх этого SDK Spring AI собрал свои Boot‑стартеры, а с версии 1.1 завезли аннотации — и поднять MCP‑сервер стало почти так же просто, как написать обычный сервис.

Суть поворота, по‑моему, такая: не «перенесите логику к ИИ», а «дайте ИИ безопасный стандартный вход в логику, которая у вас уже есть» — ничего при этом не переписывая.

Чтобы это не звучало абстрактно, посмотрим на схему расцепления слоёв (см. Рис. 2). Она показывает, кто за что отвечает и где проходит сетевая граница между агентным приложением и сервисом, который реально исполняет действия.

Рис. 2. Принципиальная схема Spring AI + MCP: расцепление рассуждения и исполнения через сетевую границу
Рис. 2. Принципиальная схема Spring AI + MCP: расцепление рассуждения и исполнения через сетевую границу

Главная мысль из этой схемы простая. В этой архитектуре LLM не получает прямого доступа к вашим базам и сервисам. Она лишь принимает решение: какой инструмент вызвать и с какими параметрами. Всё реальное действие происходит на стороне MCP‑сервера — там же, где живут ваши транзакции, роли и аудит. Это не косметика, а другая модель ответственности: периметр безопасности остаётся там же, где живёт исполнение, и не размазывается по второму отдельному контуру.

Часть 3. Как это выглядит в коде (немного DIY)

Порог входа здесь действительно низкий, иногда даже подозрительно. Серверная часть — это обычный спринговый бин, у которого метод помечен аннотацией. Вот каркас инструмента, который отдаёт статус заказа. Сразу с тем, чего часто не хватает в туториалах: с DTO на выходе, явной бизнес‑ошибкой и пометкой про доступ.

@Component
public class OrderTools {

    private final OrderService orderService;

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    @McpTool(
        name = "get_order_status",
        description = "Возвращает текущий статус заказа. Используй, когда " +
                      "пользователь спрашивает про состояние или доставку заказа."
    )
    @PreAuthorize("hasAuthority('SCOPE_orders:read')") // доступ к инструменту — явный, не по умолчанию
    public OrderStatusResponse getOrderStatus(
        @McpToolParam(description = "ID заказа, например ORD-00123", required = true)
        String orderId
    ) {
        try {
            Order order = orderService.getOrder(orderId);   // бросит OrderNotFoundException
            return OrderStatusResponse.from(order);         // отдаём DTO, а не внутренний enum/сущность
        } catch (OrderNotFoundException e) {
            // ожидаемая бизнес-ошибка: пусть модель получит структурированный ответ, а не упадёт весь цикл
            throw new ToolExecutionException("Заказ " + orderId + " не найден", e);
        }
    }
}

Обратите внимание на ключевую деталь: вся ценность здесь не в аннотации, а в description. Это, по сути, документация для модели — по ней она решает, когда дёрнуть инструмент. Я для себя сформулировал правило: пиши description как короткую инструкцию для нового джуна, который видит твой сервис впервые. Не описывай реализацию — описывай намерение: когда этот инструмент стоит вызвать. Именно по нему модель и выбирает. Расплывчато напишешь — модель будет вызывать инструмент не вовремя или не будет вовсе.

Пара оговорок по коду, чтобы не выглядело догмой. Авторизацию я повесил на сам инструмент через @PreAuthorize, но это лишь один из вариантов — её точно так же можно держать глубже, в доменных сервисах. И обработку ошибок в боевом коде обычно делают централизованно (глобальный обработчик), а локальный try/catch я оставил только ради наглядности примера.

Клиентская сторона в Spring AI 2.0 тоже узнаваемо спринговая. Объявляете адрес сервера в application.yml, а авто‑сконфигурированный ToolCallbackProvider отдаёте в ChatClient. Один нюанс именно для 2.0: в релизных заметках исполнение инструментов вынесли наружу. Теперь его ведёт ChatClient с ToolCallingAdvisor, а внутренний флаг internalToolExecutionEnabled убрали.

И вот что было, когда я первый раз поднял это у себя, а не на бумаге. Сервер с одним инструментом завёлся минут за пятнадцать. А клиент к нему молча не подключился: соединение просто не вставало, в логе пусто. Полчаса я грешил на порты и фаервол, пока не дошло: клиент ждал stdio, а сервер поднялся на HTTP‑транспорте — я взял конфигурацию из примера, не вчитавшись (об этой ловушке ниже). Поправил транспорт — ожило с первого раза. И знаете, момент, когда мой обычный сервис заказов вдруг ответил агенту как полноценный инструмент, у меня вызвал больше эмоций, чем весь хайп вокруг agentic AI за предыдущие полгода.

Мой вариант, который я обычно советую командам на старте: не лезть сразу в свой агентный цикл. Возьмите ChatClient, которому MCP‑инструменты подключены через ToolCallbackProvider (сам ChatClient знает не про MCP, а про ToolCallback — MCP лишь один из их источников), соберите два‑три инструмента вокруг реального сценария и посмотрите, как оно ведёт себя под вашими данными. Кастомные циклы и мульти‑агентность подождут — там, как увидим ниже, пока больше всего острых углов.

Часть 4. Грабли: почему до прода доезжают единицы

А теперь холодный душ, без которого статья была бы рекламой. Лёгкость старта обманчива. По открытым обсуждениям сообщества — в GitHub Issues, Discord и блогах — складывается впечатление: большинство примеров и обсуждений пока заканчиваются локальными прототипами, а до настоящего прода доходят единицы. Это наблюдение, а не строгая статистика, но разрыв между «работает на демо» и «работает в проде» здесь заметно шире, чем рассказывают туториалы. Разберу четыре места, где спотыкаются чаще всего.

  • Первое — транспорт, на котором я уже споткнулся выше. Та история с молчащим соединением — не разовая неудача, а системная ловушка. В конфигурациях из многих примеров транспортом оказывается SSE, тогда как локальные CLI‑клиенты вроде Claude Code ждут stdio, и несовпадение даёт тихий отказ соединения. Хуже того, stdio плохо ведёт себя под нагрузкой: в одном из тестов под десятком одновременных соединений почти все запросы отвалились.

Важно не передёрнуть: устаревшим объявлен не «SSE как технология», а конкретный HTTP+SSE‑транспорт из ранней спецификации MCP — в самой спецификации его заменили на Streamable HTTP, и ряд крупных MCP‑клиентов заявил о сворачивании поддержки старого SSE к середине 2026-го. Вывод практический: для новых серверов берём Streamable HTTP, особенно на WebFlux — он нормально держит конкуренцию и живёт за обычным балансировщиком.

  • Второе — аутентификация, и это, пожалуй, самая нерешённая часть. Жизненный цикл OAuth 2.1-токенов плохо ложится на долгоиграющие сессии агентов: токен протухает прямо посреди диалога. На практике я вижу такую тенденцию: команды, которые тащат авторизацию в каждый сервер по отдельности, на масштабе захлёбываются. В большинстве корпоративных систем удобнее вынести её на уровень шлюза — централизованно, с управлением токенами и аудитом. Это не единственный путь (есть service mesh, workload identity через SPIFFE, OAuth token exchange), но как решение по умолчанию шлюз обычно даёт меньше всего боли.

  • Третье — обработка ошибок, и тут была показательная боевая история. В ранних сборках Spring AI 1.0 был баг (issue #2857): если инструмент бросал исключение, отличное от ToolExecutionException, оно прорывалось наверх и роняло весь агентный цикл. Модель при этом не получала никакого сообщения об ошибке — чат просто зависал. Исправление позже вошло в последующие релизы Spring AI. Вывод, который я забрал себе: в текущей реализации Spring AI 2.0 для ожидаемых бизнес‑ошибок имеет смысл бросать ToolExecutionException, чтобы модель получила структурированный ответ и могла среагировать, а не повисла. Именно это и сделано в коде выше.

  • Четвёртое — надёжность самого вызова, и про неё часто забывают. Вспомните пример с «инициировать платёж». Модель недетерминированна и вполне может выдать один и тот же Tool Call дважды — например, после таймаута. Поэтому инструменты, меняющие состояние, должны быть либо идемпотентными, либо иметь собственную защиту от повторного исполнения: ключ идемпотентности, дедупликация по бизнес‑идентификатору.

И ещё одна мысль, очевидная, но регулярно упускаемая: MCP‑инструмент — это обычный сетевой вызов, и к нему применимы те же правила, что и к любому внешнему сервису. Таймауты обязательны, retry — только для идемпотентных операций, а перед нестабильным инструментом не лишним будет circuit breaker. Именно поэтому MCP‑инструменты стоит проектировать как обычные интеграционные API, а не как «магические функции для ИИ». Без этого один зависший вызов способен подвесить весь агентный цикл.

Чтобы свести это в маршрут, я набросал план действий — как тащить MCP‑сервер с ноутбука в прод по шагам (см. Рис. 3).

Рис. 3. План действий: маршрут MCP‑сервера от прототипа на ноутбуке до продакшена
Рис. 3. План действий: маршрут MCP‑сервера от прототипа на ноутбуке до продакшена

Главная мысль из этого маршрута: путь в прод — это не «дописать пару строк», а пять‑шесть инженерных ворот, каждое из которых закрывает свой риск. Транспорт снимает риск падения под нагрузкой, virtual threads упрощают масштабирование блокирующих операций, шлюз держит безопасность, observability не даёт ослепнуть на инциденте, контракт ошибок — повиснуть агенту. Кто проходит все ворота, тот и попадает в те самые единицы, что доезжают.

Часть 5. Best practices команд на сегодня

Если собрать вместе то, что делают команды, реально доехавшие до прода, получается стройный набор.

Транспорт — Streamable HTTP, для реактивных стеков на WebFlux; stdio и старый SSE оставляем для локальных экспериментов. С конкурентностью аккуратнее, чем любят писать в заголовках: виртуальные потоки Java 21 упрощают масштабирование блокирующих операций (JDBC, синхронные HTTP‑вызовы), а на реактивном WebFlux‑пайплайне выигрыш уже другой природы — подменять одно другим не стоит. Авторизацию, аудит и управление токенами выносим на шлюз (с оговорками из Части 4).

Наблюдаемость берём из коробки: в новом поколении Spring интеграцию с OpenTelemetry заметно упростили — в Spring Boot 4 появился единый стартер spring‑boot‑starter‑opentelemetry, а авто‑сконфигурированные HTTP‑клиенты сами протаскивают trace‑контекст через межсервисные вызовы. Практический смысл — протащить один correlation‑id через всю цепочку: запрос к LLM → Tool Call → MCP → доменный сервис → БД. Когда агентная цепочка включает пять вызовов и упал третий, без сквозного идентификатора не понять, какой именно и почему; со сквозным трейсом вся цепочка видна как единое целое.

Отдельно про гранулярность инструментов — это сейчас одна из самых обсуждаемых тем. Соблазн обернуть каждый REST‑эндпоинт в отдельный tool велик, но практика показывает: слишком мелкие инструменты (свой tool на каждую CRUD‑операцию) быстро перегружают модель — ей становится сложнее выбрать нужный из десятка похожих, и вероятность неподходящего вызова растёт. Обычно лучше публиковать инструмент уровня бизнес‑действия — «оформить возврат», «рассчитать лимит», — а не инфраструктурной операции. Модель оперирует намерениями, и инструмент, описанный как намерение, она выбирает заметно точнее.

И, пожалуй, главная не‑техническая практика: относиться к MCP‑серверу не как к скрипту, а как к продукту‑платформе. Реестр инструментов, RBAC, observability, внутренний SDK для команд. Тогда каждая команда в организации публикует свои инструменты, не трогая ваше ядро, а агент обнаруживает их все на старте без единой правки кода. Не «прикрутить чат к продукту», а превратить уже существующий бэкенд в управляемую AI‑нативную поверхность.

Личная позиция, на которой я настаиваю: если Java здесь и стала удобной для агентов, то не потому, что стала модной. Агент в продакшене — это в первую очередь вопрос эксплуатации и безопасности, а не красоты примера в туториале. И вот тут JVM сильна давно — как, к слову, Python силён в другом.

Часть 6. Заключение: теперь это выбор — и это хорошо для всех

Если убрать пафос, вывод у меня такой. Раньше, чтобы добавить в Java‑продукт интеллект, почти всегда нужен был отдельный сервис на Python — и спасибо ему, что он был и выручал. Теперь, по моему опыту, у нас просто появился выбор. Команды, которым важна операционная простота одного JVM‑процесса, привычная модель безопасности и понятный стек наблюдаемости, получили зрелый путь в прод внутри Spring, без второго деплоя. А там, где сильнее экосистема Python, никто не мешает оставить исполнение на Python и подключить его тем же MCP — протокол это позволяет, языки тут не воюют.

Но я обещал честные ограничения, и вот они. Базовые кирпичи — ChatClient, MCP, Advisors — стабильны и боевые. А вот всё, что выше: агентные циклы, оркестрация нескольких агентов — пока живёт в community‑preview проекте Spring AI Agents и активно меняется. Структурированный вывод иногда подводит по надёжности, на краях бывают шероховатости с GraalVM. И ещё один прозаичный, но жёсткий нюанс именно для этого лета.

Spring AI 2.0 построен на Spring Boot 4, а Spring Boot 3.5 и Spring Framework 6.2 уходят в end‑of‑life 30 июня 2026-го. То есть «попробовать новый стек агентов» для многих автоматически означает «сначала закрыть большую миграцию». Могу себе представить, сколько команд столкнутся с этим в ближайший месяц.

Что я предложил бы сделать на этой неделе: возьмите один существующий сервис, оберните один метод в @McpTool, поднимите MCP‑сервер на Streamable HTTP и подключите любого MCP‑клиента. После этого разговор «Java или Python для агентов» станет для вас сильно предметнее.

Интереснее всего здесь, по‑моему, не сам факт, что «на Java теперь можно ИИ». Интереснее, где проходит граница ответственности. Рассуждает модель — действует ваш код, и неважно, на каком он языке. И мне как инженеру из продакшена такое чистое разделение нравится гораздо больше, чем сервис с прямым доступом к боевой базе — на каком бы стеке он ни жил.


Если проводить эту границу ответственности в реальном Spring‑приложении, быстро всплывают вполне земные вопросы: как не сломать транзакции, где держать доступ к данным, как встроить AI‑логику так, чтобы она не превратилась в отдельный хрупкий контур рядом с основным бэкендом.

Чтобы глубже погрузиться в тему, приходите на бесплатные уроки от преподавателей-практиков:

  • 29 июня в 20:00 — «Как работает @Transactional в Spring: границы транзакций и типовые ошибки». Записаться
    Расскажем, где заканчивается «магия» аннотации и начинаются реальные ограничения транзакций в Spring.

  • 14 июля в 20:00 — «Spring AI: как превратить изображение документа в JSON в Java‑приложении». Записаться
    Покажем, как встроить AI‑распознавание документов в Spring‑приложение и получить структурированный JSON на выходе.

Что почитать по теме:

Миграция на Spring Boot 4 и Java 25: пошаговый план, чтобы обновиться и не уронить прод — как подготовить Java-сервис к переходу на новый стек, пройти через Spring Boot 4 и Java 25 без аврала и заранее закрыть основные риски миграции.