Привет! Меня зовут Бромбин Андрей, и сегодня я разберу на практике, что такое RAG-системы и как они помогают улучшать поиск. Покажу, как использовать Spring AI, векторные базы данных и LLM. Ты получишь теорию и пример реализации на Java и Spring Boot — от идеи до работающего сервиса. Без сложных формул — только чёткие объяснения и код.

О чём эта статья?

Ответим на следующие вопросы:

  • Что такое векторные базы данных и почему они незаменимы для приложений с ИИ.

  • Что такое embeddings и почему без них RAG-системы теряют свою силу.

  • Как реализовать сервис для хранения и поиска знаний по смыслу с помощью Spring Boot, Spring AI и Qdrant.

  • Как реализовать сервис для подкреплённой генерации ответов LLM с помощью Spring AI.

  • Способы улучшить RAG: как повысить точность и полезность ответов.

  • Как собрать всё вместе в работающую систему, о которой можно честно написать, например, в резюме.

Проблема и решение: зачем нужен RAG?

Представьте: у вас есть LLM (большая языковая модель), которая умеет отлично генерировать текст, но без контекста не знает, что именно хочет пользователь. Допустим, сотрудник спрашивает: «Как исправить ошибку X в системе Y?».

Если у модели нет доступа к внутренним инструкциям, тикетам и документации, она либо выдумает ответ (галлюцинация), либо честно признается: «Не знаю». RAG решает эту проблему, добавляя модели нужный контекст.

Он ничего не стёр, он просто блефует — он вообще не в курсе про наш payment service
Он ничего не стёр, он просто блефует — он вообще не в курсе про наш payment service

RAG решает эту проблему, комбинируя два этапа:

  1. Retrieval (поиск): Находит релевантные данные в базе знаний с помощью векторного поиска. Это быстрее и точнее, чем поиск по ключевым словам, так как учитывает семантику.

  2. Augmented Generation (подкреплённая генерация): Передаёт найденные данные LLM, чтобы она сгенерировала точный и полезный ответ.

Первый ключ к успеху RAG — это эмбеддинги и векторные базы данных. Давайте разберём, как это работает, пошагово внедрив её в систему инцидентов. Зелёным цветом я выделил Retrieval блок, который будет отвечать за поиск данных, а синим — блок Agumented Generation для подкреплённой генерации ответа.

Структурная схема интеграции RAG-системы
Структурная схема интеграции RAG-системы

Когда использовать RAG?

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

Основные понятия

Что такое embeddings?

Эмбеддинги — это числовые векторы, которые описывают смысл текста. Например:

«Поезд прибыл на станцию» = [0.27, -0.41, 0.88, ...]
«Поезд подъехал к платформе» = [0.26, -0.40, 0.87, ...]

«База данных легла под нагрузкой» = [-0.11, 0.17, -0.56, ...]
«БД ушла в закат на пике» = [-0.08, 0.16, -0.51, ...]

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

  • Косинусное сходство: cos(θ) = (A·B)/(|A|·|B|), где A и B — векторы. Значение от -1 (противоположный смысл) до 1 (идентичный смысл).

  • Евклидово расстояние: √Σ(Ai - Bi)², где меньшее расстояние означает большую схожесть.

Как создаются эмбеддинги? Модель, например BERT, обучается на огромном массиве текстов, чтобы улавливать контекст и смысл слов. Текст сначала разбивается на токены — слова или их части. Эти токены проходят через слои трансформера, и на выходе получается векторное представление.

Зачем нужны векторные базы данных?

Векторные базы данных хранят специальные числовые представления объектов — эмбеддинги. Их можно представить как точки на карте: близкие точки означают схожий смысл, а далёкие друг от друга — разный. Благодаря этому поиск работает не по буквальному совпадению слов, а по смыслу. Эмбеддинги создаются один раз и сохраняются, поэтому нужный фрагмент находится быстро и без лишних вычислений. Это делает векторные БД удобным инструментом для работы с LLM и большими массивами данных.

Векторное пространство IT-компетенций и их расположение по семантике
Векторное пространство IT-компетенций и их расположение по семантике

Основные составляющие конечной JSON-подобной структуры в векторной БД:

  • ID — уникальный идентификатор объекта.

  • Vector (Embedding) — сам вектор, который хранится в специальной структуре для быстрого поиска ближайших соседей.

  • Payload (Metadata) — дополнительные данные в формате ключ-значение (часто JSON): путь к файлу, тип документа, автор, дата и т.д.

  • Оригинальный текст — сам фрагмент или ссылка на внешний источник.

Про безопасность: для обеспечения безопасности документов в векторных хранилищах, в первую очередь, следует использовать встроенные механизмы аутентификации, а также управление API-ключами. Кроме того, для разграничения доступа можно присваивать каждому документу метку access_level или access_role, определяющую, кто имеет право на просмотр, либо кластеризировать данные по уровням доступа.

Порядок обработки документа и конечная JSON-подобная структура
Порядок обработки документа и конечная JSON-подобная структура

Как работает поиск: данные переводятся в векторы и индексируются. Запрос пользователя также конвертируется в вектор и база ищет самые близкие к нему по смыслу. Алгоритмы вроде HNSW (обход многоуровневого графа) или IVF (обход векторных кластеров) делают это очень быстро — даже при миллионах записей.

Реализация на Java и Spring Ai

Для работы с RAG на Java и Spring AI нужны несколько библиотек. В проект необходимо добавить следующие зависимости:

pom.xml
<dependencies>
		<dependency>
			<groupId>io.grpc</groupId>
			<artifactId>grpc-services</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.grpc</groupId>
			<artifactId>spring-grpc-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
		</dependency>
        // остальные базовые: lombok, spring-boot и тд
	</dependencies>
<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>
		<dependency>
			<groupId>org.springframework.grpc</groupId>
			<artifactId>spring-grpc-dependencies</artifactId>
			<version>${spring-grpc.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

Превращаем текст в вектор

Spring AI предоставляет готовые реализации для интеграции с популярными моделями эмбеддингов через интерфейс EmbeddingModel. Среди них: OpenAiEmbeddingModel, OllamaEmbeddingModel, PostgresMlEmbeddingModel и другие из пакетов spring-ai-starter-model-<modelName>. Это избавляет от ручной реализации методов call() или embed(), предлагая единый API для генерации векторов.

Для русского языка мы в каждый момент времени ориентируемся на актуальные бенчмарки. Сейчас хороший выбор — модель ru-en-RoSBERTa на Hugging Face: она обучена на русском и английском и подходит для двуязычных задач.

Конфиг клиента для модели RoSBERTa
@Configuration
@FieldDefaults(level = AccessLevel.PRIVATE)
public class RosbertaClientConfig {

    @Value("${huggingface.token}")
    String hfToken;

    @Value("${huggingface.rosberta.url}")
    String rosbertaUrl;

    @Bean
    public RestClient ruEnHuggingFaceRestClient() {
        if (hfToken == null || hfToken.isBlank()) {
            throw new IllegalStateException("huggingface token is not set");
        }
        
        return RestClient.builder()
                .baseUrl(rosbertaUrl)
                .defaultHeader("Authorization", "Bearer " + hfToken)
                .build();
    }
}

Класс RosbertaEmbeddingModel расширяет абстрактный класс и переопределяет его методы для работы с моделью. В call формируется запрос к API Hugging Face: текст запроса дополняется префиксом "search_query", упаковывается в payload и отправляется POST-запросом через RestClient. В ответ приходит массив чисел (эмбеддинг). Метод embed делегирует работу методу call, передавая текст из документа. При необходимости модель можно развернуть локально, а не использовать облачный API.

Класс RosbertaEmbeddingModel
@Service
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class RosbertaEmbeddingModel extends AbstractEmbeddingModel {
    RestClient restClient;

    @Override
    public @NotNull EmbeddingResponse call(@NotNull EmbeddingRequest request) {
        var payload = Map.of(
            "inputs", "search_query: " + request.getInstructions().get(0),
            "parameters", 
            Map.of("pooling_method", "cls", "normalize_embeddings", true)
        );

        List<Double> responseList = restClient.post()
                .contentType(MediaType.APPLICATION_JSON)
                .body(payload)
                .retrieve()
                .body(new ParameterizedTypeReference<>() {});

        float[] floats = convertDoubleListToFloatArray(responseList);

        return new EmbeddingResponse(List.of(new Embedding(floats, 0)));
    }

    @Override
    public @NotNull float[] embed(@NotNull Document document) {
        return call(new EmbeddingRequest(List.of(document.getFormattedContent()), null))
                .getResults().getFirst().getOutput();
    }
}

Теперь можно проверить результат через Postman, отправив запрос к нашему API:

Пример: текст и его числовой вектор
Пример: текст и его числовой вектор

Сохранение в векторное хранилище

Поднимем Qdrant локально с помощью docker-compose.yml:

docker-compose.yml
services:
  qdrant:
    image: qdrant/qdrant:latest
    restart: always
    container_name: qdrant
    ports:
      - 6333:6333
      - 6334:6334
    expose:
      - 6333
      - 6334
      - 6335
    configs:
      - source: qdrant_config
        target: /qdrant/config/production.yaml
    volumes:
      - ./qdrant_data:/qdrant/storage

configs:
  qdrant_config:
    content: |
      log_level: INFO

Ранее мы подключили пакеты Spring AI и gRPC — его использует Qdrant для общения с клиентом. Для конфигурации есть два варианта: задать её вручную или использовать автоконфигурацию. Spring умеет автоматически настраивать QdrantVectorStore, если в application.yml указать параметры вида:»

spring:
  ai:
    vectorstore:
      qdrant:
        host: <qdrant host>
        port: <qdrant grpc port>
        api-key: <qdrant api key>
        collection-name: <collection name>
        use-tls: false
        initialize-schema: true

Ручная настройка несложна и пригодится нам, так как мы используем не стандартную реализацию пакетного EmbeddingModel, а собственную — RosbertaEmbeddingModel, которую мы указываем в @Bean QdrantVectorStore.

class QdrantConfig
@Slf4j
@Configuration
@FieldDefaults(level = AccessLevel.PRIVATE)
public class QdrantConfig {

    @Value("${qdrant.host:localhost}")
    String qdrantHost;

    @Value("${qdrant.port:6334}")
    int qdrantPort;

    @Value("${qdrant.collection-name:incidents}")
    String collectionName;

    @Value("${qdrant.api-key:}")
    String apiKey;

    @Bean
    @Primary
    public QdrantClient qdrantClient() {
        QdrantGrpcClient.Builder builder = QdrantGrpcClient
                  .newBuilder(qdrantHost, qdrantPort, false);
        if (apiKey != null && !apiKey.trim().isEmpty()) {
            builder.withApiKey(apiKey);
        }
        return new QdrantClient(builder.build());
    }

    @Bean
    @Primary
    public QdrantVectorStore qdrantVectorStore(QdrantClient qdrantClient,
                                               RosbertaEmbeddingModel rosbertaEmbeddingModel) {
        QdrantVectorStore voStore = QdrantVectorStore
                .builder(qdrantClient, rosbertaEmbeddingModel)
                .collectionName(collectionName)
                .initializeSchema(true)
                .build();

        return voStore;
    }
}

На завершающем этапе мы формируем документ с метаданными и сохраняем его. При сохранении текст преобразуется в embedding с помощью указанной EmbeddingModel в конфигурации.

Класс сохранения документа в векторное хранилище Qdrant
@Slf4j
@Service
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class IncidentEmbeddingService {

    QdrantVectorStore qdrantVectorStore;

    public void storeIncident(String text, List<String> tags) {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("tags", tags);
        metadata.put("timestamp", System.currentTimeMillis());

        Document document = new Document(text, metadata);
        qdrantVectorStore.doAdd(List.of(document));
    }
}

После вызова метода открываем Qdrant Web UI и проверяем данные:

Сохранённый Point и его данные в Qdrant
Сохранённый Point и его данные в Qdrant

Обогатим хранилище данными тем же способом. Чтобы граф был понятнее, оставим самые важные связи, построив остовое дерево.

Минимальное/максимальное остовое дерево из векторов в Qdrant
Минимальное/максимальное остовое дерево из векторов в Qdrant

Поиск похожих по семантике данных

Создали эндпоинт /incidents/similar, который принимает текстовый запрос и возвращает список документов из векторного хранилища. Параметр limit задаёт максимальное количество результатов.

Эндпоинт для поиска семантически похожих документов
@PostMapping(path = "/incidents/similar", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Document>> getSimilarIncidents(
        @RequestBody String query,
        @RequestParam(defaultValue = "3") int limit) {
    try {
        List<Document> responseDocuments = incidentEmbeddingService
                                        .searchSimilarIncidents(query, limit);
        return ResponseEntity.ok(responseDocuments);
    } catch (Exception e) {
        return ResponseEntity.internalServerError().build();
    }
}

Метод searchSimilarIncidents собирает запрос (SearchRequest) с текстом и topK. Система превращает текст в эмбеддинг и сравнивает его с векторами в базе. Внутри Qdrant для оценки близости используется косинусное расстояние — угол между векторами.

Метод поиска семантически похожих документов в Qdrant
public List<Document> searchSimilarIncidents(String query, Integer limit) {
    SearchRequest searchRequest = SearchRequest.builder()
            .query(query)
            .topK(limit)
            //.filterExtension("key == 'value'")
            //.similarityThreshold(0.6)
            .build();
    return qdrantVectorStore.similaritySearch(searchRequest);
}

При запросе к API мы получаем k документов, наиболее близких по смыслу. Для каждого документа доступны метрики: distance — косинусное расстояние до запроса (чем меньше, тем ближе по смыслу) иscore = 1 - distance. Эти показатели можно использовать для ранжирования результатов.

Ответ API: список релевантных документов
Ответ API: список релевантных документов

Минимальное значение косинусного сходства не всегда значит лучший результат. Поэтому мы увеличиваем выборку — берём k ближайших соседей и улучшаем поиск с помощью RAG Fusion. О нём подробнее будет в разделе про повышение качества поиска.

Подкреплённая генерация ответа

Spring AI поддерживает работу с разными LLM, например, OpenAI, Gemini, LLaMA, Amazon AI и другие. Вместо того чтобы писать специфичный код для каждой модели, можно использовать интерфейсы ChatModel и ChatClient и переключать модели через конфигурацию. Это упрощает RAG-сценарии и работу с несколькими поставщиками одновременно. Из российских LLM-моделей не без труда подключаются GigaChat и YandexGPT.

Конфигурация

Для работы с Open AI достаточно добавить соответствующую зависимость в pom.xml:

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

Далее нужно указать ключевые параметры: модель, температуру генерации, лимит токенов в ответе, API-ключ и URL для вызовов.

spring:
  ai:
    openai:
      api-key: sk-<OPEN_AI_API_KEY>
      base-url: <OPEN_AI_API_URL>
      chat:
        completions-path: /v1/chat/completions
        options:
          model: gpt-5
          temperature: 1
          max-completion-tokens: 1000

Параметр temperature в LLM (в том числе в OpenAI) управляет степенью «случайности» или креативности генерации текста. При temperature = 0 модель работает детерминировано и выбирает самый вероятный вариант. При temperature > 1 генерация становится более свободной — появляются нестандартные, иногда неожиданные ответы.

Не все модели поддерживают параметр temperature, например, gpt-5 вернул мне
Unsupported value. Only the default (1) value is supported.

Завершающий этап — создание бинаChatClient, который будет использоваться для работы с LLM.

@Configuration
public class ChatClientConfig {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultAdvisors(
                        new SimpleLoggerAdvisor()
                )
                .build();
    }
}

Полный цикл: от обработки запроса до ответа LLM

Процесс включает четыре шага:

  1. Предобработка: приводим вопрос в удобный для анализа вид — нормализуем текст, исправляем ошибки, убираем лишние символы, уточняем формулировки. Также создаём n альтернативных вариантов вопроса для дальнейшей работы.

  2. Трансформация и поиск: преобразуем запросы в векторы и ищем k наиболее близких документов. В результате имеем n × k кандидатов.

  3. Ранжирование: оцениваем найденные документы по релевантности, удаляем дубликаты, формируем упорядоченный список.

  4. Подкреплённая генерация: передаём отобранный контекст в LLM, которая на основе него составляет ответ для пользователя.

Базовый цикл работы RAG-системы
Базовый цикл работы RAG-системы

Проще всего попросить LLM нормализовать запрос пользователя, передав системный промпт, где указываем требования к нормализации, семантическому уплотнению, контекстуализации и формату вывода.

Системный промпт и метод предобработки запроса
private static final String PREPROCESSED_SYSTEM_PROMPT = 
    "Ты специалист по семантической оптимизации пользовательских вопросов для поиска по эмбеддингам. "
  + "Преобразуй входной ВОПРОС по правилам:\n"
  + "1) Нормализация:\n"
  + "   - Удали спецсимволы (оставь только !?.,)\n"
  + "   - Убери лишние пробелы и переносы строк\n"
  + "   - Приведи все кавычки к виду \\\"\\\"\n"
  + "   - Расшифруй сокращения: «н-р» → «например», «т.д.» → «и так далее»\n"
  + "2) Семантическое уплотнение:\n"
  + "   - Сохрани ключевые термины, числа, имена собственные без изменений\n"
  + "   - Удали стоп-слова («очень», «просто», «ну») и вводные фразы («кстати», «в общем»)\n"
  + "   - Устрани повторы, сделай формулировку точной и ёмкой\n"
  + "   - Заменяй местоимения на конкретные референсы (напр. «он» → «алгоритм авторизации»)\n"
  + "3) Контекстуализация:\n"
  + "   - Добавь недостающие уточнения в [квадратных скобках], если это повышает однозначность\n"
  + "   - Делай вопрос самодостаточным: «Как он работает?» → «Как работает алгоритм авторизации?»\n"
  + "Формат вывода:\n"
  + "   - Сначала выведи ТОЛЬКО итоговый очищенный и уточнённый вопрос (без комментариев)\n"
  + "   - Если вопрос состоит из нескольких смысловых частей, раздели их пустой строкой\n"
  + "   - Затем выведи 3 альтернативные формулировки, сохраняя смысл:\n"
  + "Вывод ТОЛЬКО в JSON с полями:\n"
  + "{\\\"normalized\\\": \\\"<строка>\\\",\\\"alternatives\\\": [\\\"<строка>\\\",\\\"<строка>\\\",\\\"<строка>"]}"
  + "Без пояснений и текста вне JSON";

public PreprocessedQuestion preprocessQuestion(String question) {
    String raw = chatClient.prompt(new Prompt(
        List.of(new SystemMessage(PREPROCESSED_SYSTEM_PROMPT),
                new UserMessage(question))
    )).call().content();

    // Парсим ответ LLM на основной вопрос и варианты
    return extractVariants(raw);
}

Для получения семантически похожих документов для каждого из вариантов запросов достаточно вызвать раннее реализованный метод — searchSimilarDocuments(variant, topK). Далее необходимо ранжировать полученные документы. Простой способ — убрать дубликаты и отсортировать по убыванию score.

Метод ранжирования
private List<Document> rankDocuments(List<Document> documents) {
    Map<String, Document> uniqueDocs = documents.stream()
            .collect(Collectors.toMap(
                Document::getId,
                d -> d,
                (d1, d2) -> d1.getScore() >= d2.getScore() ? d1 : d2
            ));

    List<Document> ranked = uniqueDocs.values().stream()
            .sorted(Comparator.comparingDouble(Document::getScore).reversed())
            .toList();

    return ranked;
}

Финальный этап — передать вопрос и контекст в LLM, взамен получив ответ.

Системный промпт и метод подкреплённой генерации
String AG_SYSTEM_PROMPT = """
    Ты эксперт по написанию лаконичных и понятных ответов для пользователей.
    На вход получаешь:
    1) Вопрос пользователя.
    2) Контекст, состоящий из релевантных документов.
    
    Задача:
    - Сформулировать ответ на вопрос, опираясь на предоставленный контекст.
    - Если информации недостаточно — честно сообщи об этом.
    - Ответ должен быть ясным, структурированным и по возможности кратким.
    - Не добавляй лишние комментарии.
""";

public String generateAnswerFromContext(String userQuestion, String context) {
    SystemMessage systemMessage = new SystemMessage(AG_SYSTEM_PROMPT);
    String userContent = "Вопрос пользователя: " + userQuestion 
                              + "\n\nКонтекст документов:\n" + context;

    UserMessage userMessage = new UserMessage(userContent);
    Prompt prompt = new Prompt(List.of(systemMessage, userMessage));

    String response = chatClient.prompt(prompt).call().content();

    return response.strip();
}

Результаты

Сделаем несколько запросов и посмотрим, как ведёт себя система. Для начала — вопрос к LLM без RAG-составляющей:

Без RAG LLM не даёт полезного ответа на вопрос
Без RAG LLM не даёт полезного ответа на вопрос

Теперь сделаем запрос на api реализованной RAG-системы:

Прямого ответа в базе нет, но модель вернула контакты ответственных — честный, полезный результат
Прямого ответа в базе нет, но модель вернула контакты ответственных — честный, полезный результат

Посмотрим, как ведёт себя модель, когда у неё есть весь нужный контекст:

Отличный вопрос, отличный ответ — всё, как и должно быть
Отличный вопрос, отличный ответ — всё, как и должно быть

Способы повышения качества поиска

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

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

Предобработка и очистка пользовательского вопроса

Первый шаг — нормализовать вопрос: убрать «шум», исправить опечатки, провести лемматизацию. Это даёт более точный запрос и повышает релевантность поиска.

Часто применяют подход RAG Fusion: просим LLM предложить несколько вариантов очищенного запроса и по каждому выполняем семантический поиск. В результате получаем выборку размером [число предобработанных запросов] × [число релевантных документов], которую можно ранжировать и объединять в общий контекст.

В больших системах на этапе предобработки также классифицируют запросы: например, как просьбу, вопрос или жалобу, либо по отделам (бухгалтерия, HR). В нашей Java-реализации такую метаинформацию мы сохраняли в ключе tags.

Ансамбли

Помимо ансамблирования разных формулировок запроса, используют и ансамбли из нескольких LLM или нескольких сервисов-ретриверов. В нашем простом примере была связка «трансформер — вектор — векторное хранилище» (Dense Retriever). Также можно подключать и традиционные методы поиска (Sparse Retriever), где релевантность оценивается по частоте вхождений терминов в документ.

RAG с ансамблем бурлаков: несколько моделей тянут поиск к точности
RAG с ансамблем бурлаков: несколько моделей тянут поиск к точности

Дополнительный шаг поиска — динамическое обучение

Прежде чем выдавать клиенту финальный ответ, можно сгенерировать несколько черновых вариантов на основе ранжированных документов и использовать их как подсказки для LLM. Это позволяет модели опираться на конкретные примеры и давать более релевантный ответ. На этом этапе полезно применять zero-shot, few-shot и похожие методики.

Оценка результата

Оценка RAG — задача непростая: важно считать, сколько из извлечённых документов действительно релевантные, и насколько ответ опирается на них.
Для извлечения precision и recall. Для генерации — faithfulness/groundedness, совпадение с эталоном или оценка результата другой LLM-моделью.

Кроме того, смотрят порог уверенности по logits — распределение вероятностей для токенов в сгенерированном тексте. Это помогает понять, насколько модель уверена в ответе, и при низкой уверенности подставлять честное сообщение: «На данный вопрос не найден ответ».

Заключение

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

Он больше не бурлак, не рыбак, не Сизиф, а путник, оставивший пустыню позади
Он больше не бурлак, не рыбак, не Сизиф, а путник, оставивший пустыню позади

Надеюсь, статья оказалась полезной и вдохновляющей. Если у вас есть вопросы, идеи или замечания — делитесь ими в комментариях. Буду рад конструктивной критике: вместе мы сделаем наш путь ещё интереснее и полезнее.

Присоединяйтесь к моему Telegram-каналу, чтобы не пропустить следующие материалы.
До встречи в новой статье!

© 2025 ООО «МТ ФИНАНС»

Комментарии (3)


  1. ekzes
    28.08.2025 16:14

    Очень интересно, здорово, что на пальцах объяснена математика векторного поиска по тексту. И здорово, когда у компании файлы базы знаний находятся в txt формате или хотя бы адекватно сделан pdf. Правда куча геморроя начинается, когда начинаешь использовать OCR для документов, особенно когда в ПДФках есть таблицы :) Так что если автор решит проблему адекватной векторизации ПДФ, буду раз почитать)


  1. Smollett322
    28.08.2025 16:14

    Статья по делу и наглядно, уважаемо


  1. federini123
    28.08.2025 16:14

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