Главная / Статьи / Основы Spring AI: ChatClient, стриминг, память и инструменты с Ollama

Основы Spring AI: ChatClient, стриминг, память и инструменты с Ollama

Запускаем AI-приложение на Spring Boot со Spring AI и локальной моделью Ollama: ChatClient, шаблоны промптов, стриминг, структурированный вывод, память чата и вызов инструментов.

1888 слов

Представьте AI-функцию, которая сегодня работает на OpenAI. В следующем квартале растёт счёт, команда хочет попробовать Ollama на ноутбуках, или компания стандартизируется на Azure OpenAI, Bedrock или Gemini. Без слоя абстракции смена провайдера может прокатиться волной по всему коду: настройка клиента, формат запросов, код стриминга, эмбеддинги, описания инструментов и разбор ответов. Spring AI даёт разработчикам на Spring Boot единый набор API для разных провайдеров, и эта волна остаётся маленькой.

Здесь речь о практических основах: что даёт фреймворк, настройка проекта, конфигурация Ollama, ChatClient, шаблоны промптов, стриминг, структурированный вывод, память чата и вызов инструментов. Поиск, векторные хранилища, ETL, фильтрация, наблюдаемость, оценка, мультимодальный ввод, MCP и советы для продакшена оставлены на следующую тему.

Все примеры работают на Ollama, так что API-ключ для повторения никому не нужен. Пример проекта собран на Spring Boot 3.5.5 и Spring AI 1.0.9. Существует и стабильная линейка Spring AI 2.0.x, но она рассчитана на Spring Boot 4.0.x и 4.1.x, поэтому смешивать два набора версий в одной сборке не стоит.

Словарь до кода

Новичкам терминология может даваться тяжелее кода. Вот термины, которые встретятся ниже.

  • LLM (большая языковая модель): модель, которая генерирует текст. Сюда относятся GPT, Claude, Gemini и локальные модели вроде Llama. На вход подаётся текст, а модель предсказывает полезный текст на выходе.
  • Модель: конкретный движок, который вызывается. Здесь его локально запускает Ollama. В облаке это может быть OpenAI, Anthropic, Azure OpenAI, Amazon Bedrock или Google Vertex AI.
  • Промпт: инструкция или вопрос, отправляемый модели, например «Explain Spring AI in one paragraph» (объясни Spring AI в одном абзаце). Чёткие, конкретные промпты с достаточным контекстом дают ответы лучше.
  • Токен: небольшая единица текста — слово, часть слова, знак препинания или пробел. Цены, размер контекста и длина ответа обычно считаются в токенах.
  • Контекстное окно: максимальный объём текста, который модель может учесть за раз. В него входят промпт, история чата, найденные документы, результаты инструментов и сам ответ.
  • ChatClient: fluent API Spring AI для чат-моделей. Вместо того чтобы вручную собирать HTTP-вызовы под конкретного провайдера, код читается как chatClient.prompt().user(...).call().content().
  • Шаблон промпта: многоразовый промпт с плейсхолдерами. «Explain {topic} in {language}» во время выполнения превращается в «Explain RAG in Java».
  • Структурированный вывод: запрос данных у модели в предсказуемой форме и их отображение на Java-record или класс. Полезно, когда приложению нужны поля, а не проза.
  • Память чата: история разговора, которой управляет приложение. По умолчанию модели не хранят состояние, поэтому прошлые сообщения нужно отправлять заново всякий раз, когда модель должна их помнить.
  • Advisor: компонент Spring AI, который меняет или дополняет запрос до того, как он попадёт к модели. Например, memory advisor добавляет прошлые сообщения, а RAG advisor, о котором позже, добавил бы найденные документы.
  • Вызов инструментов: модель просит приложение выполнить Java-метод. Сама она к базе данных или API не обращается. Она запрашивает инструмент, приложение его выполняет, и результат возвращается модели.

Что входит в Spring AI

Фреймворк — не просто обёртка над chat completions. Для Java-команд это полноценный прикладной фреймворк для AI-нагрузок. Основные части:

  • переносимые API моделей для чата, эмбеддингов, изображений, распознавания и синтеза речи и модерации
  • интеграции с OpenAI, Anthropic, Azure OpenAI, Amazon Bedrock, Google Vertex AI и Gemini, Mistral, Ollama и другими
  • ChatClient — fluent API в духе WebClient и RestClient
  • шаблоны промптов и типы сообщений для системного, пользовательского, ассистентского и медиаконтента
  • отображение ответов модели на Java-record или классы
  • стриминг через Project Reactor
  • advisor’ы для повторяющихся паттернов вроде памяти и RAG
  • вызов инструментов, чтобы модель могла запрашивать Java-методы
  • модели эмбеддингов за абстракциями векторных хранилищ
  • дополнения для продакшена: ETL, наблюдаемость, оценка и MCP

Поверхность широкая, поэтому разбор ниже держится основного потока приложения.

Настройка проекта

Создайте Maven-проект на Java 21 со Spring Boot 3.5.x, Spring Web и стартером Spring AI для Ollama. Bill of materials держит все модули Spring AI на совместимых версиях.

<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.0.9</spring-ai.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Для локального демо с Ollama добавьте веб-стартер вместе со стартером Ollama.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>

Кто предпочитает OpenAI, может заменить стартер модели.

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

Одно предупреждение: держите в classpath только один стартер чат-модели, если не настроен основной бин ChatModel. Когда там есть и OpenAI, и Ollama, Spring находит двух кандидатов и не может собрать ChatClient.Builder по умолчанию, и приложение не стартует.

Запуск локальной модели

Установите Ollama и скачайте чат-модель.

ollama pull llama3.2

Затем укажите её в application.properties.

spring.application.name=spring-boot-ollama
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.options.model=llama3.2

Запустите приложение.

mvn spring-boot:run

Оно слушает здесь:

http://localhost:8080

Простой чат через ChatClient

ChatClient — высокоуровневый API, с которого стоит начинать большинству Spring-приложений.

@RestController
public class SpringAiDemoController {

    private final ChatClient chatClient;

    public SpringAiDemoController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/api/demo/simple")
    public String simpleChat(@RequestParam(defaultValue = "Tell me a joke") String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

Пробуем из терминала:

curl "http://localhost:8080/api/demo/simple?message=Explain%20Spring%20AI%20in%20one%20paragraph"

Вызов раскладывается на четыре шага. prompt() открывает новый запрос, user(...) прикрепляет сообщение, call() выбирает блокирующее выполнение, а content() возвращает сгенерированный текст.

Шаблоны промптов

Захардкоженные промпты тяжело поддерживать. Шаблон хранит многоразовый текст с параметрами.

@GetMapping("/api/demo/template")
public String templateChat(
        @RequestParam(defaultValue = "Microservices") String topic,
        @RequestParam(defaultValue = "Java") String language) {

    PromptTemplate template = new PromptTemplate(
            "Explain the concept of {topic} using examples in {language}. Keep it brief.");

    String renderedPrompt = template.render(Map.of(
            "topic", topic,
            "language", language
    ));

    return chatClient.prompt()
            .user(renderedPrompt)
            .call()
            .content();
}

Вызываем эндпоинт с темой и языком:

curl "http://localhost:8080/api/demo/template?topic=Retrieval-Augmented%20Generation&language=Java"

Шаблоны лучше всего подходят, когда промпт — часть поведения приложения, а не разовая строка.

Стриминг

Длинные ответы стоит стримить, чтобы пользователь сразу видел вывод, а не ждал весь текст.

@GetMapping(value = "/api/demo/stream", produces = "text/event-stream")
public Flux<String> streamChat(
        @RequestParam(defaultValue = "Write a short story about a brave robot.") String message) {return chatClient.prompt()
            .user(message)
            .stream()
            .content();
}

Проверьте в браузере или через curl в режиме без буферизации.

curl -N "http://localhost:8080/api/demo/stream?message=Write%20a%20short%20Spring%20AI%20intro"

Spring MVC отдаёт Flux строк как Server-Sent Events.

Структурированный вывод

Текст хорош для чата, но бизнес-софту обычно нужны структурированные данные. Spring AI умеет запросить ответ, который отображается на Java-тип. С локальными моделями лучше работает явное описание формата JSON.

public record MovieReview(String title, double rating, String shortSummary) {
}

@GetMapping("/api/demo/structured")
public MovieReview structuredOutput(@RequestParam(defaultValue = "Inception") String movie) {
    return chatClient.prompt()
            .user(u -> u.text("Review the movie {movie}.")
                    .param("movie", movie))
            .call()
            .entity(MovieReview.class);
}

Пробуем:

curl "http://localhost:8080/api/demo/structured?movie=The%20Matrix"

Если вызвать entity с классом record, Spring AI добавит в промпт инструкции по формату и сразу преобразует полученный JSON в record. Это аккуратнее и надёжнее, чем разбирать JSON-строки вручную.

Память разговора

Модели не помнят прошлые запросы, поэтому историю передаёт приложение. Spring AI решает это через абстракции памяти и advisor’ы, которые регистрируются по умолчанию в билдере клиента.

this.chatClient = chatClientBuilder
        .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(
                        MessageWindowChatMemory.builder().build()
                ).build()
        )
        .build();

После этого каждый запрос несёт идентификатор разговора.

@GetMapping("/api/demo/memory")
public String chatWithMemory(
        @RequestParam String message,
        @RequestParam(defaultValue = "chat_session_1") String chatId) {
        return chatClient.prompt()
            .user(message)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
            .call()
            .content();
}

Эффект видно на двух вызовах с одним id: сначала назовите имя, потом спросите, каким оно было.

curl "http://localhost:8080/api/demo/memory?chatId=demo&message=My%20name%20is%20Yash"
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=What%20is%20my%20name?"

В продакшене замените хранение в памяти на постоянный репозиторий: JDBC, Redis, Cassandra, MongoDB или Neo4j.

Вызов инструментов

Инструменты позволяют модели запрашивать работу, определённую приложением. Прямого доступа к базе данных или API у модели нет. Она просит, код приложения запускает инструмент, и результат возвращается модели.

Инструмент — это обычный метод с описаниями.

public static class WeatherTools {
@Tool(description = "Get the current weather for a city")
    public WeatherResponse getCurrentWeather(
            @ToolParam(description = "City name") String city) {
        return new WeatherResponse(city, 22.5, "Sunny");
    }
}
public record WeatherResponse(
        String location,
        double temperatureCelsius,
        String condition) {
}

Он публикуется как бин.

@Bean
public WeatherTools weatherTools() {
    return new WeatherTools();
}

Затем запрос его подключает.

@GetMapping("/api/demo/function")
public String functionCalling(
        @RequestParam(defaultValue = "What is the weather in Paris?") String message) {
        return chatClient.prompt()
            .user(message)
            .tools(weatherTools)
            .call()
            .content();
}

Вопрос о погоде в городе прогоняет весь цикл целиком.

curl "http://localhost:8080/api/demo/function?message=What%20is%20the%20weather%20in%20Paris?"

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

Запуск всего демо

Снова запустите приложение.

mvn spring-boot:run

Потом по очереди проверьте эндпоинты.

curl "http://localhost:8080/api/demo/simple?message=Explain%20Spring%20AI%20in%20one%20paragraph"
curl "http://localhost:8080/api/demo/template?topic=Retrieval-Augmented%20Generation&language=Java"
curl -N "http://localhost:8080/api/demo/stream?message=Write%20a%20short%20Spring%20AI%20intro"
curl "http://localhost:8080/api/demo/structured?movie=Inception"
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=My%20name%20is%20Yash"
curl "http://localhost:8080/api/demo/memory?chatId=demo&message=What%20is%20my%20name?"
curl "http://localhost:8080/api/demo/function?message=What%20is%20the%20weather%20in%20Paris?"

Частые ошибки

Если добавить несколько стартеров моделей и не выбрать один, при запуске будет такая ошибка:

No qualifying bean of type 'ChatModel' available: expected single matching bean but found 2

Оставьте в демо один стартер или явно укажите основную модель.

Если пользоваться памятью чата без идентификатора разговора, каждый вызов будет выглядеть как новый чат. Передавайте id как параметр advisor’а.

.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))

Третья ловушка — слишком свободно открывать инструменты. Инструменты — это реальные действия приложения, поэтому валидируйте их входные данные, проверяйте права и избегайте разрушительных операций, если нет процесса согласования.

Что имеем на данный момент

Фундамент Spring AI-приложения готов: чат, промпты, стриминг, структурированный вывод, память и инструменты. Этих частей уже хватает на множество полезных сценариев: чат-ассистенты, суммаризаторы, эндпоинты для структурированного извлечения данных и контролируемые сценарии с вызовом инструментов. После чата идёт слой знаний на эмбеддингах и векторных хранилищах, а поверх него — RAG, ETL-конвейеры, фильтры по метаданным, наблюдаемость, оценка, мультимодальный ввод, MCP и подготовка к продакшену.

Полный демо-проект можно клонировать с GitHub.

git clone https://github.com/Yash2462/blog-demos.git

После клонирования откройте папку spring-ai-demo и следуйте README в ней.

Дополнительные материалы

  • справочная документация Spring AI
  • руководство по началу работы со Spring AI
  • страница о чат-модели Ollama в документации Spring AI
  • документация по ChatClient
  • документация по вызову инструментов