Documnet Parsing Example
Отлично! Двигаемся дальше. После того как мы освоили Spring AI
на уровне абстракций, самое время применить эти знания для решения конкретной и очень распространенной бизнес-задачи. Этот раздел покажет, как LLM могут заменить хрупкие и сложные парсеры.
Конспект лекции. Тема 7: Практический пример — эффективный парсинг документов
1. Введение: Проблема "темных данных" и хрупких парсеров
В любой компании огромное количество ценной информации заперто в так называемых "темных данных" — неструктурированных документах, таких как PDF-файлы, email-сообщения, сканы контрактов и отчеты. Традиционно для извлечения данных из них использовались:
- Регулярные выражения (Regex): Мощно, но невероятно сложно в написании и поддержке. Ломаются от малейшего изменения в форматировании.
- Шаблонные парсеры: Работают только для документов с абсолютно одинаковой структурой. Новый шаблон от поставщика — и нужно писать новый парсер.
- OCR-системы с правилами: Извлекают текст, но требуют сложных правил для поиска нужных полей.
LLM меняют правила игры. Они понимают текст на семантическом уровне, а не на уровне символов. Для них неважно, написано "Итого к оплате", "Сумма" или "Всего", они поймут, что речь идет об общей стоимости. Мы можем использовать эту способность для создания универсального и гибкого парсера.
2. Наша задача: Превратить PDF-счет в Java-объект
Представим, что нам в систему поступают счета-фактуры в виде текста (предположим, что текст из PDF мы уже извлекли с помощью библиотеки типа Apache Tika).
Входные данные (Input):
СЧЕТ-ФАКТУРА № 123-А
от 15 августа 2025 года
Поставщик: ООО "Техно-Мир"
Покупатель: ООО "Наш Проект"
Таблица товаров:
1. Ноутбук "Горизонт-Про" - 1 шт. - 95000.00 руб.
2. Монитор "Кристалл 27" - 2 шт. - 25000.00 руб.
ИТОГО К ОПЛАТЕ: 145000.00 руб.
Желаемый результат (Output): Java-объект, который мы можем сохранить в базу данных или отправить в другую систему.
public record InvoiceData(
String invoiceNumber,
LocalDate issueDate,
BigDecimal totalAmount,
List<InvoiceItem> items
) {}
public record InvoiceItem(
String description,
int quantity,
BigDecimal price
) {}
3. Инструмент: Spring AI ChatClient
+ Конвертеры вывода
Для этой задачи нам не нужно ничего, кроме ChatClient
из предыдущей лекции и одной из самых мощных "фишек" Spring AI — способности автоматически преобразовывать ответ модели в Java-объект.
Это работает благодаря специальным конвертерам, самый удобный из которых — BeanOutputConverter
. Он анализирует структуру вашего Java-класса (или record
) и "объясняет" модели, в каком формате JSON нужно вернуть результат.
4. Ключевая техника: Промпт, требующий JSON
Секрет успеха — в правильно составленном промпте. Мы должны четко указать модели, что она должна выступить в роли "извлекателя данных" и вернуть результат в строго определенном формате.
Пример эффективного промпта:
private final String promptTemplate = """
Твоя задача — извлечь структурированную информацию из текста счета-фактуры.
Проанализируй текст, который предоставит пользователь, и верни результат в формате JSON.
JSON должен строго соответствовать предоставленной схеме. Не добавляй никаких
пояснений или комментариев, только чистый JSON.
Вот схема JSON, которой нужно следовать:
{json_schema}
Если какая-то информация в тексте отсутствует, используй для соответствующего поля значение null.
ТЕКСТ СЧЕТА-ФАКТУРЫ:
---
{invoice_text}
---
""";
{json_schema}
: Это специальный плейсхолдер, куда Spring AI автоматически подставит JSON-схему, сгенерированную из нашего классаInvoiceData
.{invoice_text}
: Сюда мы подставим текст нашего документа.
Четкие инструкции ("строго соответствовать", "только чистый JSON", "используй null") минимизируют риск получения невалидного или "замусоренного" ответа.
5. Собираем все вместе: InvoiceParserService
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.parser.BeanOutputConverter;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Service
public class InvoiceParserService {
// Record-ы для структурирования данных
public record InvoiceData(/* ... поля ... */) {}
public record InvoiceItem(/* ... поля ... */) {}
private final ChatClient chatClient;
private final String promptTemplate = /* ... шаблон промпта выше ... */;
public InvoiceParserService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public InvoiceData parseInvoice(String invoiceText) {
// Создаем конвертер для нашего целевого класса
var outputConverter = new BeanOutputConverter<>(InvoiceData.class);
// Получаем JSON-схему из конвертера
String jsonSchema = outputConverter.getFormat();
// Создаем итоговый промпт, подставляя схему и текст
PromptTemplate template = new PromptTemplate(this.promptTemplate);
Prompt prompt = template.create(Map.of(
"json_schema", jsonSchema,
"invoice_text", invoiceText
));
// Это магия Spring AI!
// Мы отправляем промпт и говорим: "Результат должен быть типа InvoiceData"
// Spring AI сам получит JSON и десериализует его в наш объект.
return chatClient.prompt(prompt)
.call()
.entity(outputConverter); // Указываем конвертер
}
// Более короткий и современный способ с версии Spring AI 0.8.1
public InvoiceData parseInvoiceShort(String invoiceText) {
// Внутренне он делает то же самое, что и метод выше
return chatClient.prompt()
.user(userSpec -> userSpec
.text("Извлеки информацию из этого счета: {invoice_text}")
.param("invoice_text", invoiceText)
)
.call()
.entity(InvoiceData.class); // Просто передаем класс!
}
}
6. Заключение: Почему это революционный подход?
- Гибкость: Этот парсер не сломается, если в новом счете поля будут в другом порядке или названы немного иначе. Модель поймет семантику.
- Скорость разработки: Написать такой сервис — дело нескольких часов, а не недель, как в случае с традиционными парсерами.
- Поддерживаемость: Код предельно прост и декларативен. Вся "логика" парсинга находится в промпте и структуре целевого DTO. Если нужно извлекать новое поле — вы просто добавляете его в
record
и всё.
Этот пример демонстрирует, как LLM и такие фреймворки, как Spring AI, превращают сложные задачи по обработке неструктурированных данных в решаемые и элегантные инженерные задачи.
Основные типы языковых моделей
Transformer-архитектура
Основа современных LLM. Использует механизм внимания (attention) для понимания контекста и связей между токенами в тексте. Позволяет параллельную обработку последовательностей, в отличие от RNN.
Generative Pre-trained Transformers (GPT)
Авторегрессионные модели, генерирующие текст слева направо. Обучены предсказывать следующий токен по предыдущим. Примеры: GPT-3.5, GPT-4, Claude.
Encoder-Decoder модели
Модели типа T5, BART. Encoder понимает входной текст, decoder генерирует выход. Хорошо подходят для задач преобразования: перевод, суммаризация, переписывание.
Embedding модели
Специализированные модели для создания векторных представлений текста. Используются в RAG, семантическом поиске, кластеризации. Примеры: text-embedding-ada-002, sentence-transformers.
Ключевые концепции
Промпт-инжиниринг
Искусство создания эффективных запросов к LLM. Включает:
- System prompt - базовые инструкции для модели
- Few-shot learning - примеры входа-выхода для обучения без дообучения
- Chain-of-thought - пошаговое рассуждение для сложных задач
- Role prompting - задание роли модели (эксперт, ассистент, критик)
Контекстное окно (Context Window)
Максимальное количество токенов, которое модель может обработать за один запрос. У GPT-4 ~8K-128K токенов, у Claude ~200K. Влияет на объем данных, которые можно передать модели.
Токенизация
Процесс разбиения текста на минимальные единицы (токены). Один токен ≈ 0.75 слова в английском, ≈ 0.5 слова в русском. Влияет на стоимость API-запросов.
Temperature и Top-p
Параметры контроля случайности генерации:
- Temperature (0.0-2.0) - креативность ответа. 0 = детерминированный, 1+ = творческий
- Top-p (0.0-1.0) - ядерная выборка. Рассматривает только токены с суммарной вероятностью до p
Архитектурные паттерны интеграции
RAG (Retrieval-Augmented Generation)
Паттерн дополнения LLM внешними знаниями через векторный поиск:
// Упрощенная схема RAG
public String answerWithRAG(String question) {
List<String> relevantDocs = vectorSearch.findSimilar(question, 5);
String context = String.join("\n", relevantDocs);
String prompt = "Контекст: " + context + "\nВопрос: " + question;
return llmClient.generate(prompt);
}
Компоненты RAG:
- Векторная БД - Pinecone, Weaviate, Chroma для хранения эмбеддингов
- Embedding модель - преобразование текста в векторы
- Retrieval - поиск релевантных документов
- Generation - генерация ответа с учетом найденного контекста
Агенты (AI Agents)
LLM с доступом к внешним инструментам и способностью планировать действия:
@Component
public class LLMAgent {
private final List<Tool> tools;
public String executeTask(String task) {
String plan = llm.createPlan(task, tools);
return executePlan(plan);
}
}
Типы инструментов агентов:
- Function calling - вызов внешних API/методов
- Code execution - выполнение сгенерированного кода
- Web search - поиск в интернете
- Database queries - запросы к БД
Pipeline обработки
Цепочка LLM-операций для сложных задач:
public ProcessingResult processDocument(Document doc) {
String summary = llm.summarize(doc.getContent());
List<String> entities = llm.extractEntities(doc.getContent());
Classification category = llm.classify(summary);
return new ProcessingResult(summary, entities, category);
}
Практические задачи и решения
Структурированный вывод (JSON/XML)
Принуждение LLM генерировать валидный JSON через схемы и промпты:
String jsonSchema = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"skills": {"type": "array", "items": {"type": "string"}}
}
}
""";
String prompt = "Извлеки данные в JSON согласно схеме: " + jsonSchema;
Классификация и сентимент-анализ
Категоризация текста по заданным классам:
public enum Sentiment { POSITIVE, NEGATIVE, NEUTRAL }
public Sentiment analyzeSentiment(String text) {
String prompt = "Определи тональность текста (POSITIVE/NEGATIVE/NEUTRAL): " + text;
String result = llm.generate(prompt);
return Sentiment.valueOf(result.trim());
}
Извлечение информации (Information Extraction)
Выделение структурированных данных из неструктурированного текста:
public Invoice parseInvoice(String invoiceText) {
String prompt = """
Извлеки из счета следующие поля:
- Номер счета
- Дата
- Сумма
- Поставщик
Текст счета: %s
""".formatted(invoiceText);
return llm.parseToObject(prompt, Invoice.class);
}
Генерация кода и SQL
Автоматическое создание кода по описанию:
public String generateSQL(String naturalLanguageQuery, String schema) {
String prompt = """
База данных со схемой: %s
Создай SQL запрос для: %s
Верни только SQL без объяснений.
""".formatted(schema, naturalLanguageQuery);
return llm.generate(prompt);
}
Мультимодальные возможности
Обработка изображений
Анализ, описание и OCR изображений:
public String analyzeImage(byte[] imageData, String question) {
MultimodalRequest request = MultimodalRequest.builder()
.image(imageData)
.prompt("Проанализируй изображение: " + question)
.build();
return visionLLM.process(request);
}
Распознавание документов (OCR)
Извлечение текста и структуры из PDF, чеков, форм:
public DocumentData extractFromReceipt(byte[] receiptImage) {
String prompt = """
Извлеки из чека:
- Дату покупки
- Магазин
- Список товаров с ценами
- Общую сумму
""";
return visionLLM.extractStructured(receiptImage, prompt, DocumentData.class);
}
Оптимизация производительности
Батчевая обработка
Группировка запросов для экономии времени и ресурсов:
public List<String> processBatch(List<String> texts) {
String batchPrompt = texts.stream()
.map(text -> "Текст: " + text)
.collect(Collectors.joining("\n---\n"));
return llm.generateBatch(batchPrompt);
}
Кеширование результатов
Сохранение ответов LLM для повторных запросов:
@Cacheable("llm-responses")
public String generateWithCache(String prompt) {
return llm.generate(prompt);
}
Асинхронная обработка
Неблокирующие вызовы LLM API:
@Async
public CompletableFuture<String> generateAsync(String prompt) {
return CompletableFuture.supplyAsync(() -> llm.generate(prompt));
}
Мониторинг и отказоустойчивость
Rate Limiting
Контроль частоты запросов к API:
@RateLimiter(name = "llm-api", fallbackMethod = "fallbackGenerate")
public String generate(String prompt) {
return llmApi.call(prompt);
}
Circuit Breaker
Защита от сбоев LLM сервиса:
@CircuitBreaker(name = "llm-service")
@Retry(name = "llm-service")
public String generateWithFallback(String prompt) {
return llmService.generate(prompt);
}
Логирование и метрики
Отслеживание использования токенов, стоимости, качества ответов:
public String generateWithMetrics(String prompt) {
long startTime = System.currentTimeMillis();
String response = llm.generate(prompt);
meterRegistry.counter("llm.requests").increment();
meterRegistry.timer("llm.response.time")
.record(System.currentTimeMillis() - startTime, TimeUnit.MILLISECONDS);
return response;
}
Безопасность и комплаенс
Фильтрация контента
Проверка входящих и исходящих данных на безопасность:
public String safeGenerate(String prompt) {
if (contentFilter.isUnsafe(prompt)) {
throw new UnsafeContentException("Небезопасный промпт");
}
String response = llm.generate(prompt);
if (contentFilter.isUnsafe(response)) {
return "Извините, не могу предоставить этот ответ";
}
return response;
}
Анонимизация данных
Удаление персональных данных перед отправкой в LLM:
public String processPersonalData(String text) {
String anonymized = piiDetector.anonymize(text);
String result = llm.generate(anonymized);
return piiDetector.deanonymize(result);
}
Стоимость и оптимизация
Контроль токенов
Мониторинг расхода токенов для управления затратами:
public class TokenManager {
public String generateWithBudget(String prompt, int maxTokens) {
int estimatedTokens = tokenizer.countTokens(prompt);
if (estimatedTokens > maxTokens) {
prompt = truncatePrompt(prompt, maxTokens);
}
return llm.generate(prompt);
}
}
Выбор подходящей модели
Использование разных моделей для разных задач:
- GPT-3.5 - простые задачи, быстро и дешево
- GPT-4 - сложные задачи, высокое качество
- Claude - длинные тексты, анализ
- Специализированные модели - code-davinci для кода, text-embedding для эмбеддингов
Ключевой принцип: начинать с простой модели, масштабировать по необходимости.
Архитектура LLM-сервиса
Основные компоненты системы
API Gateway - точка входа для всех запросов к LLM-сервису. Отвечает за аутентификацию, авторизацию, rate limiting, маршрутизацию запросов. Обычно используется Nginx, Kong, AWS API Gateway или собственное решение на Spring Cloud Gateway.
Orchestrator (Оркестратор) - центральный компонент, который координирует выполнение запросов. Определяет какую модель использовать, какие инструменты вызывать, управляет цепочками промптов. Реализуется как Spring Boot приложение с использованием паттернов Strategy и Chain of Responsibility.
LLM Providers - адаптеры для работы с различными поставщиками моделей (OpenAI, Anthropic, локальные модели). Абстрагируют различия в API и форматах ответов. Обычно реализуются через паттерн Adapter с использованием RestTemplate или WebClient.
Embeddings Service - отдельный сервис для генерации векторных представлений текста. Используется для семантического поиска и RAG (Retrieval-Augmented Generation). Часто deployed как отдельный микросервис с кэшированием результатов.
Retrieval System - система извлечения релевантной информации из знаниевой базы. Включает векторные БД (Pinecone, Weaviate, Qdrant), полнотекстовый поиск (Elasticsearch), гибридный поиск.
Tools Integration - интеграция с внешними инструментами и API. Позволяет LLM выполнять действия: поиск в интернете, работа с БД, вызов внешних сервисов. Реализуется через паттерн Command.
Storage Layer - хранение диалогов, промптов, метаданных. Обычно комбинация реляционной БД (PostgreSQL) для структурированных данных и NoSQL (MongoDB, Redis) для сессий и кэша.
Observability - мониторинг, логирование, трейсинг всех операций. Включает метрики производительности, качества ответов, стоимости запросов. Prometheus + Grafana для метрик, ELK stack для логов.
Управление промптами и конфигурациями
Версионирование промптов
Промпт-шаблоны хранятся в специальной системе управления (prompt management system). Каждый промпт имеет версию, теги, метаданные. Поддерживается откат к предыдущим версиям и постепенное развертывание новых версий.
Конфигурация моделей включает параметры temperature, max_tokens, top_p, frequency_penalty. Эти параметры могут быть разными для разных типов задач и пользователей. Хранятся в конфигурационном сервисе (Spring Cloud Config, Consul).
A/B тестирование позволяет сравнивать эффективность разных промптов и моделей. Трафик распределяется между вариантами по заданным правилам. Метрики качества собираются и анализируются для принятия решений.
Цепочки промптов (Prompt Chaining)
Последовательные цепочки - когда результат одного промпта используется как входные данные для следующего. Например: анализ → суммаризация → перевод.
Условные цепочки - выбор следующего промпта зависит от результата предыдущего. Реализуется через state machine или workflow engine.
Параллельные цепочки - несколько промптов выполняются одновременно, результаты агрегируются. Используется для повышения надежности или получения разных точек зрения.
Типы интеграции и потоки данных
Синхронные потоки
Server-Sent Events (SSE) - для потоковой передачи ответов LLM в реальном времени. Клиент получает частичные ответы по мере их генерации. Реализуется через Spring WebFlux с SseEmitter.
WebSocket - для интерактивных диалогов с низкой задержкой. Поддерживает двустороннюю связь, позволяет прерывать генерацию, отправлять дополнительные инструкции.
REST API - для простых запрос-ответ взаимодействий. Подходит для коротких ответов, когда потоковая передача не нужна.
Асинхронные потоки
Message Queues - для обработки длительных задач в фоне. Используются RabbitMQ, Apache Kafka, AWS SQS. Запрос помещается в очередь, клиент получает task_id, проверяет статус через polling или webhook.
Event-driven архитектура - события публикуются в брокер сообщений, подписчики обрабатывают их асинхронно. Позволяет масштабировать обработку и легко добавлять новую функциональность.
Batch Processing - для массовой обработки документов или данных. Задачи группируются и обрабатываются пакетами для оптимизации использования ресурсов.
Стратегии кэширования
Кэш промптов
Semantic Caching - кэширование на основе семантического сходства промптов. Даже если промпты текстуально отличаются, но семантически похожи, используется кэшированный результат.
Exact Match Caching - точное совпадение промпта и параметров модели. Быстрее семантического, но менее гибкий.
Hierarchical Caching - многоуровневое кэширование: L1 (in-memory), L2 (Redis), L3 (persistent storage).
Кэш контекста
Context Window Caching - кэширование обработанного контекста для длинных диалогов. Позволяет избежать повторной обработки больших объемов текста.
Conversation State Caching - сохранение состояния диалога между запросами. Включает историю сообщений, пользовательские предпочтения, текущий контекст.
Кэш эмбеддингов
Vector Caching - кэширование векторных представлений документов и запросов. Эмбеддинги дорого вычислять, поэтому кэширование критично для производительности.
Index Caching - кэширование индексов для быстрого поиска похожих векторов. Особенно важно для больших коллекций документов.
Управление состоянием диалогов
Архитектура хранения
Redis для короткой памяти - активные сессии, временные данные, кэш. TTL настраивается в зависимости от типа данных. Поддерживает pub/sub для real-time уведомлений.
База данных для долгой памяти - полная история диалогов, пользовательские профили, аналитика. PostgreSQL с JSONB колонками для гибкого хранения метаданных.
Hybrid Storage - комбинация in-memory, Redis и persistent storage. Горячие данные в памяти, теплые в Redis, холодные в БД.
Стратегии управления памятью
Sliding Window - ограничение размера контекста скользящим окном. Старые сообщения удаляются, важная информация сохраняется в сжатом виде.
Hierarchical Summarization - периодическое создание саммари старых частей диалога. Позволяет сохранить контекст при ограниченном размере окна.
Selective Memory - выборочное сохранение важных фактов и предпочтений пользователя. Определяется через анализ важности или явные указания пользователя.
Мониторинг и обсервабилити
Ключевые метрики
Производительность: время ответа, throughput (запросов в секунду), время до первого токена (TTFT), latency percentiles (p50, p95, p99).
Качество: user satisfaction ratings, успешность выполнения задач, coherence scores, factual accuracy metrics.
Стоимость: tokens per request, cost per conversation, cost per user, эффективность использования контекста.
Надежность: error rates, availability, successful completion rate, retry patterns.
Инструменты мониторинга
Distributed Tracing - отслеживание запросов через все компоненты системы. Jaeger или Zipkin для визуализации цепочек вызовов.
Custom Metrics - специфичные для LLM метрики через Micrometer и Prometheus. Dashboards в Grafana для визуализации.
Log Analysis - структурированное логирование всех LLM взаимодействий. ELK stack или Splunk для анализа паттернов и проблем.
Паттерны масштабирования
Горизонтальное масштабирование
Load Balancing - распределение нагрузки между инстансами. Round-robin, weighted, или intelligent routing на основе типа запроса.
Model Sharding - разные модели на разных серверах. Routing по типу задачи или специализации модели.
Async Processing - разделение быстрых и медленных операций. Быстрые обрабатываются синхронно, медленные - асинхронно.
Оптимизация ресурсов
Model Quantization - уменьшение размера модели за счет снижения точности весов. 8-bit или 4-bit quantization для inference.
Batching - группировка запросов для более эффективного использования GPU. Dynamic batching с учетом размера промптов.
Connection Pooling - пул соединений к внешним LLM API для избежания overhead создания новых соединений.
Безопасность и соответствие требованиям
Защита данных
Data Encryption - шифрование данных в покое и в движении. TLS для транспорта, AES для хранения.
PII Detection - автоматическое обнаружение и маскировка персональных данных в промптах и ответах.
Access Control - ролевая модель доступа, аудит всех операций, принцип минимальных привилегий.
Контроль контента
Content Filtering - фильтрация нежелательного контента на входе и выходе. Машинное обучение + rule-based фильтры.
Prompt Injection Protection - защита от атак через промпт-инъекции. Валидация и санитизация входных данных.
Output Validation - проверка ответов модели на соответствие политикам компании и требованиям безопасности.
Основные понятия и архитектура
LLM (Large Language Model) — большая языковая модель, обученная на огромных массивах текста для генерации и понимания естественного языка. Примеры: GPT-4, Claude, Gemini, LLaMA.
Инференс — процесс получения ответа от обученной модели. Может быть локальным (модель работает на ваших серверах) или удаленным (через API).
Токены — базовые единицы текста для LLM. Примерно 1 токен = 0.75 слова. Важно для подсчета стоимости и ограничений.
Context Window — максимальное количество токенов, которое модель может обработать за один запрос (включая промпт и ответ).
Temperature — параметр креативности модели (0.0-1.0). Низкие значения дают более предсказуемые ответы.
Spring AI Framework
Spring AI — официальный фреймворк от Spring для интеграции с AI-сервисами. Предоставляет унифицированный API для разных провайдеров.
Основные компоненты
- ChatClient — основной интерфейс для взаимодействия с чат-моделями
- EmbeddingClient — для работы с векторными представлениями текста
- VectorStore — хранилище векторов для RAG (Retrieval-Augmented Generation)
@Configuration
public class AiConfig {
@Bean
public OpenAiChatClient chatClient() {
return new OpenAiChatClient(
new OpenAiApi("your-api-key")
);
}
}
Основные возможности
- Автоконфигурация через Spring Boot стартеры
- Retry механизмы с экспоненциальным backoff
- Метрики и мониторинг через Micrometer
- Structured Output — автоматическое преобразование ответов в Java объекты
Провайдеры и SDK
OpenAI Java SDK
Официальная библиотека для работы с OpenAI API. Поддерживает GPT-4, DALL-E, Whisper и другие модели.
OpenAiService service = new OpenAiService("your-api-key");
CompletionRequest request = CompletionRequest.builder()
.model("gpt-4")
.prompt("Объясни что такое микросервисы")
.maxTokens(500)
.build();
Особенности:
- Поддержка streaming ответов
- Function calling для интеграции с внешними API
- Fine-tuning возможности
- Batch API для массовой обработки
Google Vertex AI
Платформа Google Cloud для машинного обучения. Включает Gemini Pro, PaLM и другие модели.
VertexAI vertexAi = new VertexAI("project-id", "region");
GenerativeModel model = new GenerativeModel("gemini-pro", vertexAi);
Преимущества:
- Интеграция с Google Cloud экосистемой
- Multimodal возможности (текст + изображения)
- Enterprise-grade безопасность
- Предсказуемые цены
AWS Bedrock SDK
Сервис AWS для доступа к различным foundation models через единый API.
BedrockRuntimeClient client = BedrockRuntimeClient.builder()
.region(Region.US_EAST_1)
.build();
Поддерживаемые модели:
- Claude (Anthropic)
- Llama 2 (Meta)
- Titan (Amazon)
- Jurassic (AI21 Labs)
Локальный инференс
ONNX Runtime Java
ONNX (Open Neural Network Exchange) — открытый формат для представления моделей машинного обучения. Позволяет запускать модели локально без интернета.
OrtEnvironment env = OrtEnvironment.getEnvironment();
OrtSession session = env.createSession("model.onnx");
Преимущества:
- Полный контроль над данными
- Низкая задержка
- Отсутствие зависимости от внешних сервисов
DJL (Deep Java Library)
Фреймворк от Amazon для deep learning на Java. Поддерживает PyTorch, TensorFlow, MXNet.
Model model = Model.newInstance("bert-base");
Predictor<String, String> predictor = model.newPredictor();
Возможности:
- Поддержка различных бэкендов
- Готовые предобученные модели
- Интеграция с Spring Boot
Архитектурные паттерны
Streaming ответы (SSE)
Server-Sent Events — технология для передачи данных от сервера к клиенту в реальном времени. Критично для LLM, чтобы показывать ответ по мере генерации.
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.stream(message)
.map(chunk -> "data: " + chunk + "\n\n");
}
Circuit Breaker Pattern
Паттерн для предотвращения каскадных отказов. Если внешний сервис недоступен, circuit breaker "размыкается" и возвращает fallback ответ.
@Component
public class LlmService {
@CircuitBreaker(name = "openai", fallbackMethod = "fallbackResponse")
public String generateResponse(String prompt) {
return openAiClient.complete(prompt);
}
public String fallbackResponse(String prompt, Exception ex) {
return "Сервис временно недоступен";
}
}
Retry механизмы
LLM API могут быть нестабильными из-за высокой нагрузки. Важно реализовать retry с экспоненциальным backoff.
@Retryable(
value = {HttpServerErrorException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String callLlm(String prompt) {
return llmClient.generate(prompt);
}
Работа с JSON Schema
Structured Output
Современные LLM могут генерировать ответы в строгом JSON формате, соответствующем заданной схеме.
public class TaskResponse {
private String title;
private Priority priority;
private LocalDate deadline;
// конструкторы, геттеры
}
// JSON Schema для валидации
String schema = """
{
"type": "object",
"properties": {
"title": {"type": "string"},
"priority": {"enum": ["LOW", "MEDIUM", "HIGH"]},
"deadline": {"type": "string", "format": "date"}
},
"required": ["title", "priority"]
}
""";
Валидация ответов
Важно валидировать JSON ответы от LLM, так как модели могут генерировать некорректный формат.
@Component
public class JsonValidator {
private final ObjectMapper objectMapper;
private final JsonSchemaFactory factory;
public <T> T validateAndParse(String json, Class<T> clazz, String schema) {
// Валидация по JSON Schema
JsonSchema jsonSchema = factory.getSchema(schema);
JsonNode jsonNode = objectMapper.readTree(json);
Set<ValidationMessage> errors = jsonSchema.validate(jsonNode);
if (!errors.isEmpty()) {
throw new ValidationException("Invalid JSON: " + errors);
}
return objectMapper.treeToValue(jsonNode, clazz);
}
}
Reactive Programming с Kotlin Coroutines
Асинхронная обработка
LLM запросы могут занимать много времени. Корутины Kotlin позволяют обрабатывать множество запросов одновременно без блокировки потоков.
@Service
class LlmService(private val chatClient: ChatClient) {
suspend fun generateResponseAsync(prompt: String): String = withContext(Dispatchers.IO) {
chatClient.call(prompt)
}
suspend fun batchProcess(prompts: List<String>): List<String> =
prompts.map { prompt ->
async { generateResponseAsync(prompt) }
}.awaitAll()
}
Flow для streaming
fun streamResponse(prompt: String): Flow<String> = flow {
chatClient.stream(prompt).collect { chunk ->
emit(chunk)
}
}.flowOn(Dispatchers.IO)
Батчинг запросов
Батчинг — объединение нескольких запросов в один для оптимизации производительности и снижения стоимости.
@Component
public class BatchProcessor {
private final List<String> batch = new ArrayList<>();
private final int batchSize = 10;
@Scheduled(fixedDelay = 5000)
public void processBatch() {
if (!batch.isEmpty()) {
List<String> currentBatch = new ArrayList<>(batch);
batch.clear();
String combinedPrompt = String.join("\n---\n", currentBatch);
String response = llmClient.process(combinedPrompt);
// Разбор и распределение ответов
distributeResponses(response, currentBatch.size());
}
}
}
Кэширование и оптимизация
Redis для кэширования
Кэширование ответов LLM критично для производительности и экономии средств.
@Service
public class CachedLlmService {
@Cacheable(value = "llm-responses", key = "#prompt.hashCode()")
public String generateResponse(String prompt) {
return llmClient.call(prompt);
}
@CacheEvict(value = "llm-responses", allEntries = true)
public void clearCache() {
// Очистка кэша
}
}
Оптимизация промптов
- Few-shot learning — примеры в промпте для лучшего понимания задачи
- Chain-of-thought — пошаговое рассуждение для сложных задач
- Template engines — переиспользование промптов с параметрами
Мониторинг и метрики
Ключевые метрики
- Latency — время ответа API
- Token usage — потребление токенов для контроля стоимости
- Error rate — частота ошибок
- Throughput — количество запросов в секунду
@Component
public class LlmMetrics {
private final MeterRegistry meterRegistry;
private final Counter requestCounter;
private final Timer responseTimer;
public String timedCall(String prompt) {
return responseTimer.recordCallable(() -> {
requestCounter.increment();
return llmClient.call(prompt);
});
}
}
Безопасность и best practices
Prompt Injection защита
Prompt Injection — атака, при которой пользователь пытается изменить поведение модели через специально crafted input.
@Component
public class PromptSanitizer {
private final List<String> suspiciousPatterns = List.of(
"ignore", "forget", "system:", "assistant:"
);
public String sanitize(String userInput) {
String cleaned = userInput.replaceAll("[<>\"']", "");
for (String pattern : suspiciousPatterns) {
if (cleaned.toLowerCase().contains(pattern)) {
throw new SecurityException("Suspicious input detected");
}
}
return cleaned;
}
}
Rate Limiting
Ограничение частоты запросов для предотвращения злоупотреблений и контроля расходов.
@Component
public class RateLimitedLlmService {
private final RedisTemplate<String, String> redisTemplate;
private final int maxRequestsPerMinute = 60;
public String generateResponse(String userId, String prompt) {
String key = "rate_limit:" + userId;
String count = redisTemplate.opsForValue().get(key);
if (count != null && Integer.parseInt(count) >= maxRequestsPerMinute) {
throw new RateLimitException("Rate limit exceeded");
}
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, Duration.ofMinutes(1));
return llmClient.call(prompt);
}
}
Troubleshooting
Распространенные проблемы
- Token limit exceeded — превышение лимита контекста
- Rate limiting — слишком частые запросы
- Model overloaded — модель перегружена
- Invalid JSON — некорректный формат ответа
Логирование и отладка
@Slf4j
@Component
public class LlmService {
public String callWithLogging(String prompt) {
log.info("LLM request: prompt length={}", prompt.length());
try {
long startTime = System.currentTimeMillis();
String response = llmClient.call(prompt);
long duration = System.currentTimeMillis() - startTime;
log.info("LLM response: duration={}ms, response_length={}",
duration, response.length());
return response;
} catch (Exception e) {
log.error("LLM call failed: {}", e.getMessage(), e);
throw e;
}
}
}
Промпт-инжиниринг и управление генерацией
Основы архитектуры промптов
Базовая структура промпта
System → Context → Examples → Instruction → Input → Output format
System — определяет роль и поведение модели
- Устанавливает контекст работы ИИ
- Задает тон общения и ограничения
- Пример: "Ты — эксперт по Java с 10-летним опытом"
Context — предоставляет релевантную информацию
- Описание проблемы или задачи
- Техническая документация
- Бизнес-требования
Examples — демонстрируют желаемое поведение
- Показывают формат входных и выходных данных
- Иллюстрируют стиль рассуждений
- Помогают избежать неоднозначности
Instruction — четкие команды для выполнения
- Конкретные шаги действий
- Требования к качеству результата
- Ограничения и исключения
Input — данные для обработки
- Исходный код для анализа
- Техническое задание
- Параметры задачи
Output format — структура ответа
- JSON, XML, таблицы, списки
- Шаблоны документации
- Формат кода с комментариями
Техники промпт-инжиниринга
Chain-of-Thought (CoT) — цепочка рассуждений
Метод пошагового решения сложных задач с объяснением каждого шага.
Принцип работы:
- Модель показывает промежуточные шаги
- Повышает точность на логических задачах
- Помогает отлаживать результат
Варианты реализации:
- Zero-shot CoT: "Подумай пошагово"
- Few-shot CoT: Примеры с разбором решения
- Auto-CoT: Автоматическая генерация примеров
Важное замечание: В продакшене CoT-рассуждения часто скрывают от пользователя, показывая только финальный результат.
ReAct — рассуждение и действие
Комбинирует логические рассуждения с выполнением действий.
Цикл ReAct:
- Thought — анализ ситуации
- Action — выбор инструмента
- Observation — получение результата
- Повтор до решения задачи
Особенно эффективен для задач, требующих поиска информации или использования внешних API.
Few-shot Learning — обучение на примерах
Предоставление нескольких примеров входных данных и ожидаемых результатов.
Рекомендации:
- 2-5 примеров обычно достаточно
- Примеры должны покрывать разные сценарии
- Качество примеров важнее количества
Рубрики (Rubrics) — критерии оценки
Четкие критерии для оценки качества результата.
Структура рубрики:
- Критерии качества (функциональность, производительность, читаемость)
- Шкала оценок (1-5, плохо/хорошо/отлично)
- Описание каждого уровня
Управление стилем и ролями
Определение роли
Конкретные роли:
- Senior Java Developer
- Solutions Architect
- Technical Reviewer
- Code Mentor
Контекст экспертизы:
- Опыт работы и технологический стек
- Специализация (микросервисы, высокие нагрузки)
- Принципы и методологии
Стилевые инструкции
Формат кода:
- Соглашения именования
- Структура комментариев
- Использование аннотаций
Тон общения:
- Техническая точность vs доступность
- Формальный vs неформальный стиль
- Детализация объяснений
Сжатые промпты
Техники компрессии
Аббревиатуры и сокращения:
- Использование технических терминов
- Замена длинных фраз символами
- Ссылки на общепринятые паттерны
Структурные оптимизации:
- Удаление избыточных слов
- Объединение связанных инструкций
- Использование списков вместо текста
Контекстные ссылки:
- Ссылки на документацию
- Упоминание стандартов (Java Coding Standards)
- Предположение о базовых знаниях
Инструкции безопасности
Валидация входных данных
Ограничения на код:
- Запрет опасных операций (System.exit, Runtime.exec)
- Проверка на SQL-инъекции
- Ограничения на рефлексию
Контроль выходных данных:
- Санитизация вывода
- Проверка на утечку конфиденциальной информации
- Валидация формата ответа
Этические ограничения
Принципы:
- Отказ от создания вредоносного кода
- Соблюдение лицензионных ограничений
- Защита персональных данных
Параметры контролируемой генерации
Temperature (Температура)
Диапазон: 0.0 - 2.0
Влияние на результат:
- 0.0-0.3: Детерминированный вывод, точные ответы
- 0.4-0.7: Сбалансированность творчества и точности
- 0.8-1.0: Творческие решения, вариативность
- 1.0+: Высокая случайность, может снижать качество
Применение в разработке:
- Низкая температура для code review
- Средняя для архитектурных решений
- Высокая для brainstorming
Top-p (Nucleus Sampling)
Диапазон: 0.0 - 1.0
Принцип работы:
- Выбор из топа токенов с суммарной вероятностью p
- Более гибкий контроль, чем Top-k
- Адаптируется к контексту
Рекомендуемые значения:
- 0.9: Универсальное значение
- 0.95: Больше вариативности
- 0.8: Более консервативный выбор
Top-k
Принцип: Ограничение выбора k наиболее вероятными токенами
Особенности:
- Фиксированное число вариантов
- Менее адаптивен, чем Top-p
- Полезен для ограничения словаря
Max Tokens
Назначение: Ограничение длины ответа
Стратегии:
- Короткие ответы (50-100 токенов) для простых вопросов
- Средние (200-500) для объяснений
- Длинные (1000+) для детального анализа
Stop Sequences
Применение: Автоматическая остановка генерации
Примеры использования:
Stop sequences: ["```", "---", "END"]
Сценарии:
- Завершение блоков кода
- Остановка после определенного формата
- Предотвращение генерации лишнего текста
Практические рекомендации для Java-разработчика
Анализ кода
Промпт-структура:
- Роль: Senior Java Developer
- Контекст: Технические требования проекта
- Инструкция: Критерии анализа
- Формат: Структурированный отчет
Архитектурные решения
Подход:
- Использование CoT для обоснования решений
- ReAct для поиска паттернов и best practices
- Рубрики для оценки альтернатив
Code Review
Параметры:
- Низкая температура (0.1-0.3)
- Top-p: 0.8-0.9
- Четкие stop sequences
Генерация документации
Настройки:
- Средняя температура (0.5-0.7)
- Формат: Markdown/AsciiDoc
- Включение примеров кода
Отладка и оптимизация промптов
Итеративная разработка
Процесс:
- Базовая версия промпта
- Тестирование на разных примерах
- Анализ неудачных случаев
- Рефакторинг инструкций
- Повторное тестирование
Метрики качества
Для кода:
- Корректность синтаксиса
- Соответствие требованиям
- Читаемость и стиль
- Производительность
Для объяснений:
- Техническая точность
- Полнота информации
- Ясность изложения
- Релевантность примеров
Spring AI
Это не просто обзор, а детальное руководство, которое покажет, как фреймворк превращает сложные AI-задачи в понятный и поддерживаемый код.
Конспект лекции. Тема 6: Углубленный разбор Spring AI
1. Введение: Философия Spring AI
Цель Spring AI — не создать новую революционную AI-технологию, а сделать существующие технологии максимально доступными и удобными для Spring-разработчиков.
Его философия строится на трех китах, знакомых каждому, кто работал со Spring:
- Автоконфигурация (Autoconfiguration): Максимум магии "из коробки". Вы добавляете стартер, прописываете пару свойств, и все работает.
- Абстракции (Abstractions): Вы работаете не с конкретной реализацией OpenAI или Google, а с универсальными интерфейсами (
ChatClient
,VectorStore
). Это делает ваш код портативным: сегодня вы используете GPT-4, завтра — Llama 3, а код приложения менять не нужно. - Внедрение зависимостей (Dependency Injection): AI-клиенты становятся обычными бинами, которые можно легко инжектить в ваши сервисы и контроллеры.
Проще говоря, Spring AI делает работу с нейросетями такой же простой, как работу с базой данных через Spring Data.
2. Настройка проекта: Начало работы за 5 минут
Для начала работы вам понадобятся три вещи в вашем pom.xml
(или build.gradle
).
- Spring Boot Parent: Убедитесь, что ваш проект является Spring Boot приложением.
- Spring AI BOM (Bill of Materials): Это специальная зависимость, которая управляет версиями всех модулей Spring AI, чтобы они были совместимы друг с другом.
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M1</version> <!-- Используйте актуальную версию --> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
- Стартер для конкретной модели: Выберите, с какой моделью хотите работать, и добавьте соответствующий стартер.
<!-- Для работы с OpenAI --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-starter</artifactId> </dependency> <!-- ИЛИ для работы с локальной Ollama --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-starter</artifactId> </dependency>
3. Конфигурация: application.properties
После добавления зависимостей вся конфигурация сводится к нескольким строчкам в application.properties
.
- Для OpenAI:
spring.ai.openai.api-key=<ВАШ_API_КЛЮЧ> spring.ai.openai.chat.options.model=gpt-4o
- Для локальной Ollama (убедитесь, что Ollama запущена):
spring.ai.ollama.base-url=http://localhost:11434 spring.ai.ollama.chat.options.model=llama3
И это всё! Теперь вы готовы использовать AI в своем коде.
4. Ключевые абстракции и практические примеры
4.1. ChatClient
— Ваш основной инструмент для общения
ChatClient
— это главный интерфейс для отправки запросов в языковую модель.
Пример: Сервис для генерации шуток
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class JokeService {
private final ChatClient chatClient;
// ChatClient создается и настраивается автоматически!
// Мы просто просим его через конструктор.
public JokeService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String getJoke(String topic) {
// Используем Fluent API для создания промпта
return chatClient.prompt()
.user("Расскажи мне смешную шутку на тему: " + topic)
.call()
.content(); // Получаем ответ в виде строки
}
}
Что здесь произошло?
- Мы "заинжектили"
ChatClient.Builder
(современный способ получить клиент). - Создали промпт с помощью удобного builder-синтаксиса.
call()
отправил запрос, аcontent()
извлек текстовый ответ.- Ваш код не знает, какая модель под капотом — OpenAI или Ollama. Он портативен!
4.2. RAG (Retrieval-Augmented Generation) — "Мозг" для ваших данных
Это самый мощный паттерн, и Spring AI делает его реализацию невероятно простой.
Задача: Создать Q&A сервис, который отвечает на вопросы по нашему собственному документу (например, по "Политике отпусков компании"), а не выдумывает ответы.
Необходимые компоненты:
VectorStore
: Наша векторная база данных. Spring AI поддерживает множество: ChromaDB, Neo4j, PostgreSQL (сpgvector
) и даже простую in-memory реализацию для тестов.EmbeddingClient
: Сервис для превращения текста в векторы (эмбеддинги).
Пошаговая реализация:
Шаг 1: Зависимости. Добавим стартер для векторной базы (например, Chroma).
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chroma-starter</artifactId>
</dependency>
Шаг 2: Загрузка и векторизация данных. Это нужно сделать один раз, при старте приложения.
@Service
public class DataSetupService {
@Value("classpath:/docs/vacation-policy.txt")
private Resource policyDocument;
private final VectorStore vectorStore;
// Инжектим VectorStore, Spring сам его найдет и настроит
public DataSetupService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@PostConstruct
public void setup() {
// Простой способ загрузить, разбить на части и сохранить документ
// В реальном проекте здесь будет более сложная логика
TextReader reader = new TextReader(policyDocument);
reader.getCustomMetadata().put("document_name", "vacation-policy");
// Документ автоматически разбивается на чанки и векторизуется
vectorStore.add(reader.get());
}
}
Шаг 3: Сервис ответов на вопросы.
@Service
public class QaService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
// ... конструктор ...
private final String template = """
Ответь на вопрос, основываясь ИСКЛЮЧИТЕЛЬНО на предоставленной ниже информации.
Если в информации нет ответа, скажи "У меня нет информации по этому вопросу".
ИНФОРМАЦИЯ:
{context}
ВОПРОС:
{question}
""";
public String answerQuestion(String question) {
// 1. Ищем релевантные части документа в векторной базе
List<Document> similarDocuments = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(2) // Найти 2 самых похожих чанка
);
String context = similarDocuments.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n"));
// 2. Создаем промпт с найденным контекстом
PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of(
"context", context,
"question", question
));
// 3. Отправляем промпт в модель
return chatClient.prompt(prompt).call().content();
}
}
Результат: Теперь модель будет отвечать на вопросы вроде "Сколько дней отпуска мне положено?" строго по вашему документу, без "галлюцинаций".
4.3. Мультимодальные запросы
С Spring AI легко отправлять не только текст, но и изображения.
@Service
public class ImageDescriberService {
private final ChatClient chatClient;
// ... конструктор ...
public String describeImage(Resource imageResource) throws IOException {
var userMessage = new UserMessage(
"Опиши, что изображено на этой картинке?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageResource))
);
return chatClient.prompt(new Prompt(userMessage)).call().content();
}
}
5. Заключение
Spring AI — это не просто еще одна библиотека. Это фундаментальный сдвиг, который делает AI-разработку доступной для огромного сообщества Spring-разработчиков. Он снижает порог входа, стандартизирует подходы и позволяет сосредоточиться на бизнес-логике, а не на деталях интеграции с конкретным AI-провайдером. Для любого, кто работает со Spring, это главный инструмент для входа в мир искусственного интеллекта.
Эмбеддинги и векторные БД
Основы эмбеддингов
Эмбеддинг — математическое представление текста, изображения или других данных в виде вектора вещественных чисел. Позволяет преобразовать семантическое содержимое в числовую форму для машинного обучения.
Ключевые характеристики
- Размерность: обычно 384, 512, 768, 1024, 1536 измерений
- Плотность: dense векторы (все элементы ненулевые) vs sparse векторы
- Семантическая близость: похожие по смыслу объекты имеют близкие векторы
Получение эмбеддингов
- Предобученные модели: OpenAI text-embedding-3, Sentence-BERT, E5
- Локальные модели: all-MiniLM-L6-v2, multilingual-e5-large
- Специализированные: Code embeddings для кода, Image embeddings для изображений
// Пример структуры эмбеддинга
public class Embedding {
private String id;
private float[] vector; // Массив вещественных чисел
private Map<String, Object> metadata;
private long timestamp;
}
Метрики сходства
Cosine Similarity (Косинусное сходство)
Измеряет угол между векторами. Значения от -1 до 1, где 1 = идентичность. Применение: текстовый поиск, когда важна семантическая близость независимо от "силы" сигнала.
Dot Product (Скалярное произведение)
Учитывает как угол, так и величину векторов. Чем больше значение, тем ближе векторы. Применение: когда важна и семантика, и "интенсивность" признаков.
Euclidean Distance (Евклидово расстояние)
Геометрическое расстояние между точками. Меньше = ближе. Применение: когда векторы представляют координаты в пространстве признаков.
// Косинусное сходство
public static double cosineSimilarity(float[] a, float[] b) {
double dotProduct = 0.0, normA = 0.0, normB = 0.0;
for (int i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += Math.pow(a[i], 2);
normB += Math.pow(b[i], 2);
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
Индексы для векторного поиска
HNSW (Hierarchical Navigable Small World)
Принцип: Граф со связями между векторами, поиск по слоям от грубого к точному. Плюсы: Быстрый поиск O(log N), хорошее качество Минусы: Много памяти, сложность обновлений Применение: Когда нужна скорость поиска и есть достаточно RAM
IVF (Inverted File)
Принцип: Разделение пространства на кластеры (ячейки), поиск только в ближайших ячейках. Плюсы: Экономия памяти, масштабируемость Минусы: Может пропускать результаты на границах кластеров Применение: Большие датасеты, когда допустима небольшая потеря точности
ScaNN (Scalable Nearest Neighbors)
Принцип: Квантизация векторов + анизотропная оптимизация от Google. Плюсы: Сжатие данных, высокая скорость Минусы: Сложность настройки, потеря точности при сжатии Применение: Промышленные системы с миллиардами векторов
Векторные базы данных
Qdrant
Особенности: Rust-based, RESTful API, поддержка фильтрации по метаданным Сильные стороны: Производительность, простота развертывания, гибридный поиск Применение: Микросервисы, когда нужна простая интеграция и высокая производительность
// Конфигурация Qdrant клиента
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334).build());
Weaviate
Особенности: GraphQL API, встроенные ML модели, автоматическая векторизация Сильные стороны: Богатая экосистема, поддержка мультимодальности Применение: RAG системы, когда нужна автоматическая обработка разных типов данных
Pinecone
Особенности: Полностью управляемый cloud-сервис, serverless архитектура Сильные стороны: Нет необходимости в DevOps, автоскейлинг Применение: Стартапы, MVP, когда хочется сосредоточиться на бизнес-логике
pgvector (PostgreSQL)
Особенности: Расширение для PostgreSQL, SQL-совместимость Сильные стороны: Использование существующей инфраструктуры PostgreSQL Применение: Когда уже есть PostgreSQL и не хочется добавлять новые компоненты
-- Создание таблицы с векторным полем
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536) -- OpenAI embeddings размерность
);
-- Поиск ближайших векторов
SELECT content FROM documents
ORDER BY embedding <-> '[0.1,0.2,...]'::vector
LIMIT 10;
Milvus
Особенности: CNCF проект, поддержка GPU, горизонтальное масштабирование Сильные стороны: Производительность на больших данных, enterprise-готовность Применение: Крупные корпорации, большие объемы данных (>100M векторов)
Практические аспекты
Нормализация векторов
Приведение векторов к единичной длине для корректного сравнения косинусным сходством.
public static float[] normalize(float[] vector) {
double norm = Arrays.stream(vector).mapToDouble(v -> v * v).sum();
norm = Math.sqrt(norm);
return Arrays.stream(vector).map(v -> (float)(v / norm)).toArray();
}
Upsert операции
Upsert = Update + Insert. Обновление существующего вектора или вставка нового. Применение: Когда документы могут изменяться, и нужно поддерживать актуальность индекса.
TTL (Time To Live)
Автоматическое удаление старых векторов через заданное время. Применение: Временные данные, кэши, когда релевантность снижается со временем.
Версионирование эмбеддингов
Отслеживание версий моделей для обеспечения консистентности поиска. Проблема: Эмбеддинги от разных версий моделей нельзя сравнивать напрямую. Решение: Пересчет всех эмбеддингов при смене модели или параллельное хранение версий.
Мультиязычность
Подходы:
- Универсальные модели (multilingual-e5)
- Отдельные модели для каждого языка
- Трансляция + монолингвальная модель
Метрики оценки качества
MTEB (Massive Text Embedding Benchmark)
Стандартный бенчмарк для оценки качества текстовых эмбеддингов на множестве задач. Задачи: Классификация, кластеризация, поиск пар, повторное ранжирование, STS, суммаризация.
Recall@k
Доля релевантных документов среди топ-k результатов. Формула: (количество релевантных в топ-k) / (общее количество релевантных) Применение: Оценка полноты поиска.
nDCG (Normalized Discounted Cumulative Gain)
Учитывает как релевантность, так и позицию результата в ранжировании. Принцип: Более высокие позиции имеют больший вес, нормализация к идеальному ранжированию. Применение: Когда важен порядок результатов, а не только их наличие.
Latency и Throughput
Latency: Время ответа на один запрос (мс) Throughput: Количество запросов в секунду (QPS) Баланс: Обычно между ними есть trade-off - увеличение throughput может повысить latency.
Архитектурные паттерны
Гибридный поиск
Комбинирование векторного и традиционного текстового поиска (BM25, Elasticsearch). Преимущества: Лучшее качество поиска, покрытие разных типов запросов. Реализация: Отдельное ранжирование + объединение результатов или single-stage подход.
Chunking стратегии
Фиксированный размер: 512 токенов с перекрытием 50 токенов Семантический: Разбиение по параграфам, предложениям Рекурсивный: Попытка разбить по структуре, fallback к фиксированному размеру
Кэширование
Уровни:
- Кэш эмбеддингов (избежание повторных вычислений)
- Кэш результатов поиска (для популярных запросов)
- Кэш метаданных (ускорение фильтрации)
Мониторинг и наблюдаемость
Ключевые метрики:
- Время генерации эмбеддингов
- Latency поиска по векторам
- Качество результатов (CTR, пользовательская обратная связь)
- Нагрузка на векторную БД
- Размер индекса и потребление памяти
System design AI
Отлично, мы подошли к одному из самых интересных разделов. Здесь мы объединим все предыдущие знания — от промптинга до Spring AI и RAG — чтобы спроектировать полноценное, полезное приложение. Мы переходим от отдельных вызовов AI к построению целостной системы.
Конспект. Тема 9: Проектирование и реализация AI-ассистента (чат-бота)
1. Введение: Что такое AI-ассистент на самом деле?
Простой Q&A сервис — это здорово, но настоящий AI-ассистент (или чат-бот) умнее. Он не просто отвечает на один вопрос, он:
- Контекстуален: Помнит предыдущие сообщения в диалоге.
- Основан на знаниях: Отвечает не на основе "общих знаний" из интернета, а на основе вашей корпоративной базы знаний.
- Интегрирован: Является частью большой системы (например, внутреннего портала компании или Slack).
Наша задача: Спроектировать архитектуру AI-ассистента для поддержки сотрудников. Он должен отвечать на вопросы по внутренней документации (HR-политики, IT-инструкции), хранящейся в Confluence.
2. Архитектура системы: Взгляд с высоты птичьего полета
Успешный AI-ассистент — это не одна программа, а система из нескольких взаимодействующих компонентов.
-
Frontend: Пользовательский интерфейс. Это может быть виджет чата на внутреннем портале, бот в Slack или Microsoft Teams. Его задача — отображать диалог и отправлять сообщения на наш бэкенд.
-
Backend (наше Spring Boot приложение): Мозг и сердце всей системы. Он выполняет всю работу по оркестрации.
-
LLM Provider: "Генератор речи". Это может быть внешний API (OpenAI GPT-4, Google Gemini) или локально развернутая модель (Llama 3 через Ollama).
- Выбор: API проще в использовании, но дороже и требует отправки данных вовне. Self-hosted обеспечивает приватность, но требует мощной инфраструктуры.
-
Vector Database: "Долгосрочная память" или библиотека ассистента. Здесь хранятся числовые представления (эмбеддинги) всей нашей базы знаний.
- Выбор:
- PostgreSQL с расширением
pgvector
: Отличный выбор для enterprise, если у вас уже есть PostgreSQL. - Elasticsearch: Если он уже используется для поиска, его можно приспособить и для векторного поиска.
- Специализированные БД (Chroma, Pinecone): Очень эффективны, но требуют поддержки отдельной технологии.
- PostgreSQL с расширением
- Выбор:
-
Data Sources (Источники знаний): Откуда ассистент черпает информацию. В нашем случае — это Confluence.
-
ETL-процесс: "Библиотекарь", который читает книги (статьи из Confluence) и раскладывает их по полочкам (в Vector Database). Это критически важный офлайн-процесс.
3. Глубокое погружение в Backend: Компоненты Spring Boot приложения
API Layer (@RestController
)
- Определяет основной эндпоинт для общения с фронтендом:
POST /api/v1/chat
. - Тело запроса: Должно содержать не только сообщение, но и идентификаторы для отслеживания диалога.
{ "userId": "user-123", "conversationId": "conv-abc-789", "message": "А сколько мне положено дней отпуска?" }
- Ответ: Возвращает ответ модели и, опционально, ссылки на источники. Для улучшения UX здесь идеально подходит стриминг (потоковая передача ответа по мере генерации).
Service Layer (@Service
) - Оркестратор
Это ChatService
, который реализует основную логику.
Управление историей чата (Контекст диалога)
Ассистент должен помнить, о чем шла речь.
- Хранение: Историю диалога ("вопрос-ответ") нужно где-то хранить. Это может быть простая таблица в реляционной БД или кэш в Redis.
CREATE TABLE chat_messages (id SERIAL, conversation_id VARCHAR, role VARCHAR, content TEXT, timestamp TIMESTAMPTZ);
- Использование: Перед тем, как задать вопрос LLM, сервис должен извлечь последние 4-6 сообщений из этого диалога и добавить их в промпт. Это дает модели контекст.
4. ETL-процесс: Загрузка знаний в систему
Знания не появляются в векторной базе сами по себе. Нам нужен процесс, который будет наполнять и обновлять "память" ассистента. Это можно реализовать как отдельный сервис или Spring Batch
job, работающий по расписанию (например, каждую ночь).
- Extract (Извлечение): Скрипт подключается к Confluence API и выкачивает все новые или измененные страницы.
- Transform (Преобразование):
- Очистка: Из страниц удаляется лишняя разметка (HTML/Markdown).
- Разбиение (Chunking): Текст каждой страницы разбивается на небольшие, семантически связанные фрагменты (чанки) по 300-500 слов. Это самый важный шаг! Один чанк = одна запись в векторной базе.
- Load (Загрузка):
- Каждый чанк текста с помощью
EmbeddingClient
превращается в вектор. - Этот вектор вместе с самим текстом и метаданными (ссылка на оригинальную страницу Confluence, заголовок) сохраняется в
VectorStore
.
- Каждый чанк текста с помощью
5. Полный цикл обработки запроса пользователя (RAG + Chat History)
Давайте проследим путь одного сообщения:
- Пользователь пишет в чат: "А для удаленных сотрудников?"
- Frontend отправляет на Backend:
POST /api/chat
с сообщением иconversationId
. - ChatController вызывает
ChatService
. ChatService
лезет в базу данных истории и поconversationId
достает предыдущие сообщения. Например:User: "Какая у нас политика по отпускам?"
,AI: "Наша политика предусматривает 28 дней отпуска..."
.ChatService
берет новый вопрос "А для удаленных сотрудников?", превращает его в эмбеддинг и делает поиск поVectorStore
. Он находит релевантные чанки из документа "Политика удаленной работы".ChatService
строит финальный промпт, который выглядит примерно так:Ты — полезный AI-ассистент. Отвечай на вопрос пользователя, основываясь на истории диалога и предоставленном контексте. ИСТОРИЯ ДИАЛОГА: Пользователь: Какая у нас политика по отпускам? Ассистент: Наша политика предусматривает 28 дней отпуска... КОНТЕКСТ ИЗ БАЗЫ ЗНАНИЙ: [...текст из документа "Политика удаленной работы", где говорится, что условия отпуска одинаковы для всех...] АКТУАЛЬНЫЙ ВОПРОС ПОЛЬЗОВАТЕЛЯ: А для удаленных сотрудников?
- Этот большой промпт отправляется в LLM через
ChatClient
. - LLM, видя всю картину, генерирует осмысленный ответ: "Для удаленных сотрудников действуют те же правила — 28 календарных дней отпуска."
ChatService
сохраняет новый вопрос и ответ в базу истории и возвращает ответ на Frontend.- Пользователь видит ответ в чате.
6. Заключение
Мы видим, что AI-ассистент — это не просто "обертка" над LLM. Это полноценная система, требующая продуманной архитектуры, где ключевую роль играют:
- Оркестрация данных: Управление историей чата и контекстом из базы знаний.
- Надежный ETL-процесс: Качество ответов ассистента напрямую зависит от качества данных, загруженных в векторную базу.
Такая архитектура является шаблоном для создания большинства современных корпоративных AI-решений.
Google cloud AI instruments
Отлично! Мы переходим к разделу, посвященному инфраструктуре и инструментам. После проектирования нашего приложения на Spring Boot, нам нужно понять, где и как его развертывать, особенно если мы хотим использовать управляемые облачные сервисы вместо самостоятельной настройки. Этот раздел особенно важен для DevOps-инженеров, но также полезен и разработчикам, чтобы понимать доступные опции.
Конспект лекции. Тема 10: Обзор AI-инструментов в Google Cloud Platform (GCP)
1. Введение: Зачем нужно облако для AI?
Создание, обучение и особенно развертывание AI-моделей — это ресурсоемкая задача. Она требует мощных серверов (часто с дорогими GPU), сложной настройки и постоянного обслуживания. Google Cloud Platform (GCP) предлагает набор управляемых сервисов, которые берут на себя всю эту "тяжелую работу", позволяя командам сосредоточиться на создании продукта.
Сегодня мы рассмотрим четыре ключевых сервиса, которые закрывают разные потребности — от использования готовых моделей до создания собственных сложных MLOps-пайплайнов.
2. Vertex AI Platform: Унифицированная MLOps-платформа
Для кого: ML-инженеры, DevOps/MLOps-специалисты, Data Scientists.
Назначение: Vertex AI — это "швейцарский нож" для всего жизненного цикла машинного обучения. Это центральная консоль, которая объединяет все инструменты, необходимые для создания, развертывания и управления моделями в одном месте.
Ключевые фичи и компоненты:
-
Model Garden ("Сад моделей"):
- Что это: Это витрина, или маркетплейс, готовых к использованию моделей.
- Что там есть:
- Модели Google: Самые передовые модели, такие как Gemini (мультимодальная) и Imagen (генерация изображений).
- Популярные Open-Source модели: Llama, Mistral, Stable Diffusion и десятки других, оптимизированных для работы на инфраструктуре Google.
- Модели от партнеров.
- Зачем это нужно DevOps: Вам не нужно искать модель, думать, как ее скачать и на какой сервер поставить. Вы можете выбрать модель в каталоге и одной кнопкой развернуть ее для тестирования или продакшена.
-
Endpoints ("Эндпоинты"):
- Что это: Это основной способ "опубликовать" вашу модель в продакшене, чтобы приложения могли обращаться к ней по HTTP API.
- Ключевые возможности для DevOps:
- Автоматическое масштабирование: Vertex AI сама добавит или уберет серверы в зависимости от нагрузки на модель.
- Canary-релизы и A/B тестирование: Вы можете развернуть две версии модели на один эндпоинт и направить 10% трафика на новую, чтобы безопасно протестировать ее в бою.
- Полностью управляемая инфраструктура: Вам не нужно думать о серверах, сетях, патчах безопасности. Google делает это за вас.
- Мониторинг: Встроенные дашборды с метриками задержки (latency), количества запросов (RPS) и ошибок.
Вывод: Vertex AI — это выбор для команд, которые серьезно занимаются ML и хотят построить воспроизводимый, масштабируемый и автоматизированный процесс работы с моделями (MLOps).
3. Gemini API: Прямой доступ к флагманской модели
Для кого: Бэкенд-разработчики, которым нужна самая мощная модель "здесь и сейчас".
Назначение: Gemini API — это самый простой и прямой способ встроить флагманскую модель Google в ваше приложение. Если Vertex AI — это целый завод по производству и управлению машинами, то Gemini API — это как взять в лизинг готовую, самую мощную гоночную машину, не думая о ее обслуживании.
Ключевые фичи:
- Мультимодальность: Модель "из коробки" понимает не только текст, но и изображения, аудио и видео.
- Огромное контекстное окно: Способность анализировать миллионы токенов (целые книги или кодовые базы) в одном запросе.
- Простота интеграции: Это обычный REST API. Его легко вызвать из любого языка, включая Java/Kotlin, и для него есть удобные SDK.
Сравнение с Vertex AI: В Vertex AI вы тоже можете использовать Gemini. Главное отличие в назначении:
- Используйте Gemini API, когда вам просто нужен мощный генеративный "мозг" для вашего приложения (например, для чат-бота), и вы не хотите погружаться в MLOps.
- Используйте Gemini через Vertex AI, когда вы хотите кастомизировать модель (дообучить ее), встроить в сложный пайплайн или сравнить с другими моделями в рамках единой платформы.
4. BigQuery ML: Машинное обучение для аналитиков данных
Для кого: Аналитики данных, SQL-разработчики.
Назначение: BigQuery ML "демократизирует" машинное обучение, позволяя создавать модели, не покидая привычной среды SQL. Идея в том, чтобы принести ML к данным, а не перемещать данные к ML-инструментам.
Ключевые фичи:
- ML через SQL: Вместо Python и TensorFlow вы пишете простые SQL-запросы.
CREATE MODEL my_dataset.churn_model OPTIONS(model_type='logistic_reg') AS SELECT ... FROM ...;
SELECT * FROM ML.PREDICT(MODEL my_dataset.churn_model, ...);
- Нулевая инфраструктура для DevOps: Модели обучаются и выполняются на мощностях самого BigQuery. Нет нужды поднимать отдельные серверы, настраивать окружение или думать о масштабировании. Это колоссальное упрощение стека.
- Широкий спектр моделей: Поддерживаются не только классические модели (регрессия, кластеризация), но и возможность импортировать модели из Vertex AI или даже вызывать LLM для анализа текста прямо в SQL.
Вывод: BigQuery ML — идеальный инструмент для задач, где данные уже лежат в DWH, и нужно быстро построить предиктивную модель (например, предсказать отток клиентов или спрос на товар).
5. Document AI: "Парсер документов на стероидах"
Для кого: Бизнес-аналитики, бэкенд-разработчики, автоматизаторы бизнес-процессов.
Назначение: Это специализированный сервис для решения одной, но очень важной задачи — извлечения структурированной информации из неструктурированных документов (PDF, сканы, изображения).
Ключевые фичи:
- Готовые парсеры: Google уже обучил модели для распознавания стандартных документов: счетов-фактур (invoices), квитанций, паспортов, водительских прав и т.д. Вы просто отправляете файл и получаете в ответ JSON с извлеченными полями.
- Custom Extractor (Workbench): Если у вас уникальный тип документа (например, специфический акт сверки), вы можете загрузить 10-20 примеров, вручную разметить в UI, где находятся нужные поля, и Google обучит кастомную модель специально для вас.
- Качество OCR: В основе лежит лучшая в своем классе технология Google для распознавания текста, которая справляется даже с некачественными сканами и рукописным текстом.
Вывод: Document AI — это готовое решение, которое может сэкономить тысячи часов ручной работы. Прежде чем писать свой парсер с помощью LLM (как мы рассматривали в теме 7), стоит проверить, возможно, Document AI решит вашу задачу быстрее и надежнее.
MLOps
Отлично! Мы переходим от конкретных инструментов к методологии. Этот раздел — один из самых важных для понимания того, как компании переходят от единичных AI-экспериментов к надежному и масштабируемому производству. Он соединяет мир разработки (Dev) и эксплуатации (Ops) в контексте машинного обучения.
Конспект лекции. Тема 11: MLOps — DevOps для Машинного Обучения
1. Введение: Почему DevOps недостаточно?
Многие команды начинают свой путь в AI, думая: "У нас уже есть отличный DevOps. Мы просто упакуем ML-модель в Docker-контейнер, выкатим через наш CI/CD пайплайн, и все будет работать".
Этот подход обречен на провал, потому что ML-система — это не просто код. Традиционный DevOps отлично справляется с жизненным циклом кода, но ML-системы состоят из трех постоянно меняющихся компонентов: Код + Модели + Данные.
MLOps (Machine Learning Operations) — это не замена DevOps, а его расширение. Это культура и набор практик, которые применяют принципы DevOps (автоматизация, версионирование, мониторинг) к уникальным вызовам жизненного цикла систем машинного обучения.
2. Ключевые отличия MLOps от классического DevOps
Чтобы понять суть MLOps, нужно рассмотреть четыре фундаментальных отличия.
Отличие 1: Артефакты и Версионирование ("Святая троица")
-
В DevOps: Главный артефакт — это код. Мы версионируем код в Git. Из определенной версии кода мы всегда получим один и тот же билд (например, JAR-файл или Docker-образ).
-
В MLOps: У нас три артефакта, которые неразрывно связаны:
- Код: Код для обучения модели, для обработки данных, для API-сервиса.
- Данные: Набор данных (датасет), на котором была обучена модель.
- Модель: Бинарный файл, который является результатом обучения конкретной версии кода на конкретной версии данных.
Почему это важно? Если вы измените хотя бы данные (например, добавите новую разметку), вы получите совершенно другую модель, даже если код не менялся. MLOps требует инструментов для версионирования всех трех компонентов, чтобы обеспечить воспроизводимость. В любой момент времени вы должны быть в состоянии точно воссоздать любую модель, которая когда-либо была в продакшене.
- Инструменты: Git (для кода), DVC (Data Version Control), MLflow (для отслеживания экспериментов и моделей).
Отличие 2: Триггеры для автоматизации (CI / CD / CT)
-
В DevOps: Главный триггер для CI/CD пайплайна — это коммит кода в репозиторий. Разработчик запушил изменения -> запустились сборка, тесты, деплой.
-
В MLOps: Жизненный цикл сложнее, и триггеров больше. Появляется новая аббревиатура — CT (Continuous Training), непрерывное обучение.
- CI (Непрерывная интеграция): Запускается на коммит кода. Тестирует и собирает компоненты системы.
- CD (Непрерывная поставка): Разворачивает сервис с моделью в окружение.
- CT (Непрерывное обучение): Запускает пайплайн переобучения модели. Триггером может быть:
- Появление новых данных: Например, каждую ночь мы получаем свежие данные о продажах, и модель нужно дообучить.
- Деградация модели в продакшене: Мониторинг показал, что точность модели упала ниже порога.
- Запланированное расписание: Переобучать модель прогноза спроса каждый понедельник.
Отличие 3: Тестирование (Тестируем не только код, но и "разумность")
-
В DevOps: Мы тестируем код. Юнит-тесты, интеграционные тесты, E2E-тесты проверяют, что функции работают корректно, API отвечают правильно, система стабильна.
-
В MLOps: Мы делаем все то же самое, плюс добавляем совершенно новые слои тестирования, которые проверяют качество данных и модели:
- Валидация данных: Проверка, что новые данные для обучения или инференса соответствуют ожиданиям (правильный формат, нет пропусков, статистические свойства не "поехали").
- Валидация модели: После каждого переобучения новая модель (кандидат) должна пройти "экзамен", прежде чем попасть в продакшн. Ее сравнивают со старой моделью на отложенном наборе данных (hold-out set). Новая модель развертывается, только если она статистически значимо лучше старой.
- Тестирование на справедливость и предвзятость (Fairness & Bias): Проверка, что модель не дискриминирует определенные группы пользователей.
Отличие 4: Мониторинг ("Тихий убийца" — деградация модели)
-
В DevOps: Мы мониторим здоровье приложения: задержку (latency), количество ошибок (error rate), использование CPU/памяти. Главный вопрос: "Сервис работает?".
-
В MLOps: Мы мониторим все то же самое, плюс здоровье модели. Главный вопрос: "Сервис все еще работает правильно и умно?". Модель, в отличие от кода, может "портиться" со временем, даже если ее код не менялся. Этот процесс называется дрейфом (drift).
- Дрейф данных (Data Drift): Статистические свойства реальных данных, поступающих на модель, начинают отличаться от данных, на которых она обучалась. Пример: модель прогноза спроса на такси, обученная до пандемии, начала получать совершенно другие данные во время локдауна и стала бесполезной.
- Дрейф концепции (Concept Drift): Сама взаимосвязь между входными данными и тем, что мы хотим предсказать, меняется. Пример: модель для определения спама перестает работать, потому что спамеры изобрели новые трюки, и то, что раньше не было спамом, теперь им является.
3. Заключение
MLOps — это фундаментальный сдвиг в мышлении. Он заставляет нас относиться к данным и моделям как к первоклассным гражданам производственного цикла, наравне с кодом. Внедрение MLOps-практик превращает AI из рискованных научных экспериментов в надежный и предсказуемый инженерный процесс, позволяющий доставлять реальную ценность бизнесу.
Fine-Tuning
Отлично, мы погружаемся глубже в MLOps. Этот раздел — практическое руководство по одной из самых сложных, но мощных техник в арсенале ML-инженера: дообучению (fine-tuning) open-source моделей. Мы рассмотрим этот процесс с точки зрения инфраструктуры, инструментов и автоматизации.
Конспект лекции. Тема 12: Инфраструктура и процесс дообучения (Fine-Tuning) моделей
1. Введение: Зачем нужно дообучение, если есть RAG?
Мы уже знаем, что паттерн RAG (Retrieval-Augmented Generation) отлично подходит для того, чтобы "научить" модель отвечать на основе наших знаний. Так зачем же нужен сложный и дорогой процесс fine-tuning?
Дообучение решает другие задачи. Его цель — не столько дать модели новые знания, сколько изменить ее поведение, стиль или специализировать в узкой области.
- RAG (Дать знания): Как дать юристу прочитать материалы по новому делу.
- Fine-tuning (Изменить поведение): Как превратить юриста широкого профиля в высококлассного специалиста по патентному праву. Он начинает использовать специфическую терминологию, мыслить в рамках определенной логики и следовать особому стилю.
Ключевые причины для дообучения:
- Адаптация стиля: Научить модель говорить "голосом" вашего бренда.
- Следование сложным форматам: Например, всегда генерировать код по строгому корпоративному стандарту.
- Усвоение узкоспециализированной логики: Медицинская диагностика, юридический анализ, научные области.
- Оптимизация производительности: Иногда небольшая дообученная модель может работать лучше и быстрее, чем гигантская модель общего назначения.
2. Процесс дообучения: Четыре этапа
Процесс дообучения — это классический MLOps-пайплайн, который состоит из четырех ключевых этапов.
Этап 1: Подготовка данных (90% успеха)
Это самый важный и трудоемкий этап. Правило "Garbage In, Garbage Out" здесь работает на 1000%. Качество вашего датасета напрямую определяет качество дообученной модели.
- Формат датасета: Данные должны быть представлены в виде структурированных примеров "инструкция-ответ". Самый популярный формат — JSONL, где каждая строка это JSON-объект.
// Пример для чат-бота поддержки {"instruction": "Пользователь жалуется, что не может войти в систему. Что ему ответить?", "output": "Попросите пользователя сбросить пароль через страницу восстановления доступа и уточните, какая ошибка появляется на экране."} // Пример для SQL-генератора {"instruction": "Напиши SQL-запрос, чтобы найти всех пользователей из Германии.", "output": "SELECT * FROM users WHERE country = 'Germany';"}
- Качество > Количество: Вам не нужны миллионы примеров. Часто несколько сотен или тысяч очень качественных, выверенных вручную примеров дают лучший результат, чем сотни тысяч "грязных" данных из интернета.
- Что значит "качественные"? Данные должны быть разнообразными, непротиворечивыми и точно отражать то поведение, которому вы хотите научить модель.
Этап 2: Инфраструктура (Самая дорогая часть)
Дообучение моделей — это вычислительно очень затратная операция. Забудьте про CPU. Здесь правят GPU.
- Требуемое "железо":
- GPU: Индустриальным стандартом являются серверные GPU от NVIDIA: A100 (80GB VRAM) и H100. Это дорогие и мощные ускорители, специально созданные для таких задач. Для небольших моделей и экспериментов могут подойти и потребительские карты вроде RTX 4090.
- Память (VRAM): Ключевой параметр. Чем больше модель (7B, 13B, 70B параметров), тем больше видеопамяти нужно, чтобы она поместилась целиком.
- Как получить доступ (для DevOps):
- Облака (самый реалистичный путь):
- GCP: Заказ виртуальных машин (GCE) с подключенными GPU A100/H100 или использование управляемых сервисов вроде Vertex AI Custom Training Jobs.
- AWS: EC2-инстансы типа
p4d
илиp5
, или использование Amazon SageMaker.
- On-Premise: Покупка и поддержка собственных серверов с GPU. Очень дорого и сложно, подходит только для крупнейших компаний с особыми требованиями к безопасности.
- Облака (самый реалистичный путь):
- Программное окружение:
- Docker: Обязателен. Создается кастомный образ на основе базового образа от NVIDIA (например,
nvidia/cuda:12.1.1-devel-ubuntu22.04
), куда устанавливаются Python, фреймворки (PyTorch) и все необходимые библиотеки. Это обеспечивает воспроизводимость.
- Docker: Обязателен. Создается кастомный образ на основе базового образа от NVIDIA (например,
Этап 3: Запуск процесса обучения (Автоматизируемая часть)
Когда данные и инфраструктура готовы, запускается сам процесс дообучения.
- Ключевая техника — LoRA/QLoRA: Мы не изменяем всю гигантскую модель (70 миллиардов параметров). Вместо этого мы "замораживаем" ее и обучаем лишь небольшую "насадку" — адаптер. Этот адаптер содержит всего несколько миллионов параметров. В результате мы получаем не новую 140-гигабайтную модель, а крошечный файл адаптера на 10-100 МБ.
- Инструменты и фреймворки:
- Hugging Face Transformers: Библиотека, которая является стандартом де-факто для работы с моделями. Предоставляет API (
Trainer
) для написания циклов обучения. - Axolotl / LLaMA Factory: Это фреймворки-оркестраторы более высокого уровня. Они позволяют описать весь процесс дообучения (путь к данным, модель, параметры LoRA, настройки обучения) в одном YAML-файле. Это идеальный инструмент для автоматизации и MLOps, так как всю логику можно хранить в Git.
- Hugging Face Transformers: Библиотека, которая является стандартом де-факто для работы с моделями. Предоставляет API (
Этап 4: Оценка и версионирование артефактов
После завершения обучения мы получаем главный артефакт.
- Артефакт: Файл (или несколько файлов) с весами LoRA-адаптера.
- Версионирование: Этот файл должен быть сохранен и версионирован, как и любой другой артефакт. Для этого используют хранилища вроде Hugging Face Hub, MLflow Model Registry или просто S3/GCS с четкой системой именования.
- Оценка (Evaluation): Как понять, что модель стала лучше?
- Количественная: Модель (базовая + новый адаптер) прогоняется на тестовом наборе данных, и считаются метрики.
- Качественная: Самое важное. Эксперты или тестировщики "вручную" общаются с моделью, чтобы оценить, действительно ли ее поведение изменилось в нужную сторону.
- Развертывание: Если новая модель (т.е. новый адаптер) признана успешной, она развертывается в продакшн. При инференсе базовая модель загружается в память, и к ней "пристегивается" нужный LoRA-адаптер.
3. Заключение
Дообучение — это сложный, итеративный и дорогой процесс, который является вершиной MLOps-пирамиды. Он требует глубокого понимания данных, мощной инфраструктуры и выстроенных процессов автоматизации. Однако именно fine-tuning позволяет создавать уникальные, высокоспециализированные AI-решения, которые дают компании настоящее конкурентное преимущество.