Вы хотите спланировать поездку в Гоа. У вас есть пять дней, бюджет в 25 000 рупий и явное предпочтение пляжам и морепродуктам. Обычно это означает открытие десяти вкладок в браузере, чтение устаревших постов на форумах и ручную сборку маршрута. Вместо этого представьте, что вы отправляете один POST-запрос и получаете структурированный по дням план с предложениями по питанию, списками активностей и точным распределением бюджета. Именно это и реализует данный проект.

Мы создадим REST API, используя Spring Boot и Azure OpenAI. API принимает пункт назначения, бюджет, продолжительность и интересы. В ответ он возвращает чистый JSON, который фронтенд или мобильное приложение может отобразить мгновенно. Никакого скрапинга, никаких захардкоженных маршрутов. Только модель ИИ, которой дали инструкцию действовать как планировщик путешествий.

Что возвращает API

Ответ — это не блок текста Markdown, который вам придется разбирать с помощью регулярных выражений. Это структурированный JSON-объект, содержащий ежедневные мероприятия, рекомендации по еде и разбивку бюджета. Для поездки в Гоа вы можете получить сегмент для первого дня, который выделяет 500 рупий на завтрак в пляжном ресторанчике, утро в Палолеме и вечерний ужин с морепродуктами в определенном районе. Каждый день содержит временные интервалы, оценочную стоимость и теги, такие как "beach" или "food". Такая структура важна, потому что современным приложениям для путешествий не нужно парсить абзацы. Им нужны объекты, которые они могут сопоставить с RecyclerView или React-компонентами.

Стек технологий и почему он подходит

В проекте используется Spring Boot 3.5 со Spring AI. Spring AI — это критически важный элемент. Он предоставляет унифицированную абстракцию ChatModel, поэтому вам не нужно писать «сырые» HTTP-клиенты для Azure OpenAI. Вы меняете зависимости и свойства, а не код сервиса.

В вашем файле сборки должны быть четыре зависимости:

  • spring-boot-starter-web для уровня REST.
  • spring-ai-starter-model-azure-openai для подключения к LLM через интерфейс Spring AI.
  • springdoc-openapi для автоматической документации Swagger.
  • Lombok для сокращения шаблонного кода в ваших POJO для запросов и ответов.

Spring AI располагается между вашей бизнес-логикой и провайдером LLM. Такое позиционирование намеренно. Оно позволяет вашим классам @Service оставаться чистыми и независимыми от конкретного провайдера.

Промпт-инжиниринг с использованием PromptTemplates

Хардкодить промпты внутри Java-строк — это быстрый способ создать неподдерживаемое ПО. Если команда продукта решит, что ИИ должен звучать более непринужденно или отказываться от оценки бюджета выше определенного порога, вам не придется перекомпилировать свой сервис.

Spring AI предоставляет PromptTemplate. Вы храните скелет промпта в ресурсном файле или в выделенной строке шаблона, оставляя плейсхолдеры для таких переменных, как {destination}, {budget}, {days} и {interests}. Во время выполнения сервис создает объект Prompt и внедряет в него значения пользователя.

Разделяйте системные сообщения и сообщения пользователя. Используйте системное сообщение для определения роли (persona). Например, вы говорите модели, что она — планировщик путешествий, специализирующийся на индийских направлениях, внимательный к бюджету и строго придерживающийся возврата только JSON без markdown-разметки. Используйте сообщение пользователя для передачи конкретных деталей поездки. Такое разделение поможет, когда вы захотите провести A/B-тестирование различных ролей, не меняя контракт API.

Сервисный слой: взаимодействие с Azure OpenAI

У класса @Service одна задача. Он формирует промпт, вызывает модель, очищает ответ и парсит результат.

Внедрите (inject) ChatClient или ChatModel из Spring AI. Отрисуйте PromptTemplate с помощью входящих значений запроса, а затем вызовите метод чата. Ответ приходит в виде строки String. Именно здесь многие туториалы заканчиваются, а начинается реальный продакшн-код.

LLM иногда добавляют вежливые вступления. Вы можете получить ответ, который начинается с фразы "Here is your itinerary" (Вот ваш маршрут), а затем выдает JSON, обернутый в тройные обратные кавычки. Если вы попытаетесь десериализовать это напрямую с помощью Jackson, ваше приложение упадет. Добавьте небольшой вспомогательный метод, который сканирует необработанную строку, находит первую открывающую фигурную скобку и последнюю закрывающую скобку и извлекает только полезную нагрузку JSON. Затем провалидируйте извлеченный блок. Проверьте наличие обязательных полей и адекватность числовых значений, прежде чем возвращать объект в контроллер.

Такой защищенный парсинг не является опциональным. Это граница между демо-версией и надежным API.

Обработка ошибок как в зрелой системе

Внешние API могут давать сбои. Azure OpenAI может возвращать ошибки ограничения частоты запросов (rate limit), ошибки аутентификации или временные ошибки 500. Если вы позволите этим ошибкам всплывать до пользователя в виде стек-трейсов, вы потеряете доверие.

Используйте @RestControllerAdvice для глобального перехвата исключений. Сопоставьте исключения Spring AI, HttpClientErrorException и общие RuntimeException с единообразными ответами об ошибках. Возвращайте JSON-тело с понятным сообщением, HTTP-статусом (например, 429 для ограничений частоты запросов) и достаточной детализацией, чтобы клиент мог повторить запрос или залогировать проблему. Пользователь должен видеть что-то вроде «Сервис временно занят. Пожалуйста, повторите попытку через 30 секунд», а не экран, заполненный названиями Java-классов.

Никогда не зашивайте секреты в код

Ваш ключ Azure OpenAI API не должен находиться в application.properties, который попадает в Git. Вынесите его вовне. Используйте переменные окружения, на которые ссылаются ваши конфигурации Spring, такие как ${AZURE_OPENAI_KEY} и ${AZURE_OPENAI_ENDPOINT}. Для разработки используйте локальный файл .env, добавьте его в .gitignore и загружайте через механизм relaxed binding в Spring Boot. Если ключ будет скомпрометирован, вы сможете сменить его в одном месте, а не пересобирать весь артефакт.

Тестирование через Swagger

Зависимость springdoc-openapi предоставляет эндпоинт Swagger UI во время выполнения. Как только приложение запустится, откройте /swagger-ui.html в браузере. Вы можете заполнить пример для Гоа напрямую: destination — «Goa», budget — 25000, days — 5, interests — «beaches, food». Нажмите execute и наблюдайте, как появляется JSON-маршрут. Это позволяет валидировать изменения в промптах, проверять сериализацию и делиться живой песочницей с фронтенд-разработчиками еще до того, как кто-либо из них напишет юнит-тест.

Смена провайдеров без переписывания кода

Стартапы меняют провайдеров. Возможно, закончатся кредиты Azure или вы захотите запустить инференс на локальном экземпляре Ollama, чтобы сократить расходы. Поскольку Spring AI абстрагирует интерфейс ChatModel, замена происходит механически. Измените Maven-зависимость с spring-ai-starter-model-azure-openai на другой стартер, обновите файл свойств новым эндпоинтом и ключом, и не трогайте свой сервисный класс. API-контракт, который видит ваше мобильное приложение, останется прежним.

Такая переносимость делает эту архитектуру особенно полезной для реальных продуктов. Вы не «женитесь» на Azure. Вы используете его как один из двигателей, подключенных к чистому конвейеру Spring.

Главный вывод

AI-модель — это не ваше приложение. Это внешний сервис, который возвращает непредсказуемый текст. Относитесь к нему с той же строгостью, с какой вы относились бы к платежному шлюзу или стороннему API погоды. Выносите учетные данные вовне. Валидируйте каждый ответ. Очищайте полезную нагрузку перед парсингом. Обрабатывайте ошибки глобально, чтобы ваши пользователи никогда не видели stack trace.

Пусть AI берет на себя творческую работу по составлению маршрута по Гоа с бюджетом 25 000 рупий. Вы же занимаетесь «трубами» (инфраструктурой). Когда эти две части разделены, вы получаете систему, которую можно реально выпускать в продакшн.

Оригинальный разбор, вдохновивший эту статью, можно найти здесь.

Хотите обсудить Spring AI и подобные проекты? Присоединяйтесь к обучающему сообществу GyaanSetu.