25 июня 2025
Тэги: Docker, Kotlin, rest, Spring AI, нейросети.
В статье Spring AI: пишем telegram-bot для ChatGPT мы научились общаться с нейросетью в диалоговом режиме, сохраняя контекст беседы. А в статье Spring AI: retrieval augmented generation мы добавляли в контекст модели произвольные данные из векторного хранилища. Теперь давайте пойдём ещё дальше и посмотрим, как можно добавлять в контекст модели сторонние инструменты.
Протокол контекста модели (Model Context Protocol, MCP) — это открытый стандарт, разработанный и представленный компанией Anthropic 25 ноября 2024 года. Основная цель MCP — создание унифицированного протокола взаимодействия между большими языковыми моделями (LLM) и внешними источниками данных и инструментами. MCP унифицирует определения вызовов интерфейса для доступа к возможностям различных инструментов.
Архитектура с использованием MCP состоит из MCP-клиента, который обращается к одному или нескольким MCP-серверам. Эти сервера интегрированы с целевыми инструментами и источниками данных. Spring AI позволяет выполнить эту интеграцию в простом декларативном стиле. Вам даже не потребуется разбираться с протоколом, т.к. Spring будет генерить описания инструментов автоматически. Также MCP-клиент одновременно является связующим звеном с LLM.
Какие инструменты можно интегрировать в контекст LLM? Давайте рассмотрим простой пример. При этом он очень хорошо иллюстрирует плюсы, которые вы получаете от использования MCP.
Как известно, модель в общем случае вещь статическая. Её тренировали на каком-то наборе данных, который был актуальным на определённую дату. Отсюда следует, что LLM не обладает текущим контекстом времени. Если мы спросим LLM, сколько сейчас времени или какой сегодня день, она вам ответить не сможет или что-то попытается нафантазировать. Но мы можем создать MCP-инструмент, возвращающий текущую дату и время.
Создадим пустой спринговый проект (например, с помощью Spring Initializr). Выбираем тип проекта – Gradle-Kotlin, язык – Kotlin и версию Java – 21. Из зависимостей добавим только spring-ai-starter-mcp-server-webmvc. В качестве альтернативы также можно использовать spring-ai-starter-mcp-server-webflux, если вы хотите использовать неблокирующий стек. И тот, и другой стартер автоматически поднимает mcp-сервер и делает доступными для интеграции все методы, помеченные специальными аннотациями. Пример MCP-сервера вы можете посмотреть на моём github.
Чтобы в дальнейшем запускать и mcp-сервер и mcp-клиент на одной машине, давайте сразу в application.yml переопределим дефолтный порт на 8081.
Теперь создадим спринговый сервис и в нём метод, возвращающий текущую дату и время.
Этот метод мы снабжаем аннотацией @Tool, которая как раз и указывает, что данный метод должен быть доступен как MCP-инструмент. В параметрах аннотации обязательно определяем description – именно на это описание будет опираться LLM, чтобы понять, какой именно метод ей нужно вызывать.
Вторым шагом нам нужно определить бин toolsProvider, в котором будут перечислены все mcp-сервисы.
Созданный выше toolService мы сюда инжектим через параметр стандартными средствами Spring. Внутри конструируем ToolCallbackProvider с помощью билдера, подставляя этот сервис.
Теперь запускаем приложение и если всё сделано правильно, в логах увидим сообщение «Registered tools: 1». Наш MCP-сервер готов!
Создадим второе приложение с помощью Spring Initializr. Добавляем в проект 3 зависимости: Spring Web, OpenAI и Model Context Protocol Client. Пример MCP-клиента также доступен на моём github.
Как и в предыдущих статьях, в application.yml настраиваем параметры подключения к LLM.
Для подключения к OpenAI нам потребуется Api-Key, который можно сгенерировать в личном кабинете https://platform.openai.com/api-keys. Также, если вы подключаетесь через какой-либо прокси-сервис OpenAI или используете любую другую LLM с совместимым протоколом, надо ещё прописать целевой хост в переменной OPEN_AI_BASE_URL. Если же подключаетесь напрямую – этот параметр можно вообще не указывать и будет использовано значение по умолчанию.
Далее пропишем MCP-сервер (их может быть несколько, но в нашем случае один).
Каждому серверу мы присваиваем произвольное имя (в данном случае mcp-server-example) и прописываем его урл. Обратите внимание, что тут мы указываем ровно тот порт, который переопределили выше, т.е. 8081. Взаимодействие с MCP-серверами происходит через SSE (server-side events). Каждый сервер должен предоставлять определённый эндпоинт, куда клиент будет слать сообщения. Но всё это взаимодействие берёт на себя Spring.
Теперь создадим конфигурацию с бином chatClient, который позволяет взаимодействовать с LLM в диалоговом режиме (см. также Spring AI: пишем telegram-bot для ChatGPT).
Помимо уже традиционного SimpleLoggerAdvisor, который логирует запрос и ответ LLM, добавляем с помощью метода defaultToolCallback() бин toolCallbackProvider. Именно этот бин представляет собой реестр всех MCP-инструментов, доступных для LLM.
Теперь создадим AiService, который отвечает за взаимодействие с LLM.
В этот сервис подтягиваем chatClient, который сконфигурировали выше. Создадим здесь единственный метод processUserMessage(), принимающий текстовый запрос от пользователя и возвращающий ответ от LLM.
Внутри этого метода делаем всё очень похожим образом, как мы делали в других статьях про Spring AI: указываем, что ответ ожидается в виде текста без форматирования, затем указываем целевую модель и температуру выставляем в 0, чтобы ответы были максимально точными. Затем указываем какой-то системный промт с базовыми инструкциями для LLM и передаём сюда chatOptions.
Тут важно отметить, что для корректной работы MCP сюда нужно передавать не просто какой-то объект, реализующий ChatOptions, а его более частный случай – ToolCallingChatOptions. Если сделаете иначе – ошибки не будет, но и MCP не заработает. Благо OpenAiChatOptions реализует нужный нам интерфейс, а также поддерживает перечисление OpenAiApi.ChatModel со всеми доступными в OpenAI моделями.
В целях демонстрации создадим rest-контроллер, чтобы взаимодействовать с LLM.
Теперь можно приступать к тестированию. Сначала запускаем mcp-сервер, затем mcp-клиент. С помощью Postman отправляем POST-запрос на эндпоинт http://127.0.0.1:8080/ai/tools.
В это время в логах mcp-сервера мы также увидим значение текущего времени. То есть LLM действительно выполнила запрос нашего инструмента.
Вы можете закомментировать строку с вызовом defaultToolCallbacks() в конфигурации chatClient и ещё раз спросить время. MCP-сервер выйдет из контекста и нейросеть ответит ожидаемо.
Получение текущего времени не предполагает наличие каких-то параметров запроса. Давайте сделаем более комплексный пример и создадим метод, который будет создавать напоминание с определённым текстом на определённое время. Здесь нас не интересует сама логика создания такого напоминания, а только факт вызова метода и параметры, которые придут на вход.
Добавим метод createReminder() в сервис ToolService в mcp-сервере:
Тут мы снабжаем описанием не только сам метод, но и каждый его параметр с помощью аннотаций @ToolParam. Эти описания также крайне важны, чтобы LLM понимала, что от неё ожидается.
Теперь мы можем попросить LLM, чтобы она создала напоминание. Причём мы можем указывать не абсолютное время, а относительное. Это вынудит LLM сначала узнать текущее время с помощью метода, который мы сделали ранее.
Тут мы попросили создать напоминание и указали время его срабатывания как +2 часа от текущего. В логах сервера убеждаемся, что были вызваны оба метода:
Прибавить два часа к текущему времени – это далеко не всё, на что способна LLM. Давайте создадим ещё более комплексный пример, в котором LLM будет принимать решение. Например, мы хотим купить хлеб по самой низкой цене. Для этого добавим третий метод, возвращающий цены на хлеб в разных магазинах.
Метод просто возвращает мапу, где ключом является название магазина, а значением – цена на хлеб.
Тогда LLM должна выяснить название магазина с самой низкой ценой, затем запросить текущее время и создать соответствующее напоминание.
В логах mcp-сервера мы увидим, что были вызваны все три метода в правильной последовательности:
Spring AI предоставляет очень простой декларативный подход для добавления любых инструментов в контекст LLM с помощью Model Context Protocol. Вам даже не требуется разбираться с форматом этого протокола. Однако очень важно делать подробные описания методов и их параметров, чтобы LLM понимала, что ей требуется вызывать и в какой последовательности.
Рассмотренные в статье примеры MCP-сервера и MCP-клиента содержат Dockerfile и полностью готовы к деплою. Если захотите развернуть их в облаке буквально в один клик – воспользуйтесь услугами dockhost.ru. Более подробно читайте в статье Как быстро развернуть Spring Boot в облаке.
Kotlin, Java, Spring, Spring Boot, Spring Data, Spring AI, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.