Технологии

Опыт использования S3 Vector с локальной LLM для RAG

В нашей компании AnyMaint, которая занимается разработкой софта для управления техническим обслуживанием и ремонтом (CMMS) промышленного оборудования, одной из главных задач является нормализация имён тулов (инструментов). Под «тулом» мы подразумеваем любой промышленный актив: машины, станки, приборы, оборудование и т.д. Зачем это нужно? Чтобы переиспользовать данные одной машины — историю поломок, процедуры ремонта, графики обслуживания — для другого тула, нам необходимо знать, что эти два тула по сути являются одной и той же моделью. Без нормализации мы не можем переносить знания между похожими машинами. Проблема нормализации тулов Нормализация — непростая задача. Одно и то же оборудование могут называть по-разному в разных компаниях (и даже в рамках одной): Разные языки. Разные коды производителя. Разные системы категорий. Аббревиатуры против полных названий. Имя тула | Перевод | Normalized name | |---|---|---| מזגן חדר חומר גלם | Air conditioner raw material room | air conditioner | מזגן מזכירה | Secretary’s Air conditioner | air conditioner | מזגן אוויר צח (1) | Clear air conditioner | air conditioner | מזגנים כללי | General air conditioners | air conditioner | Eppendorf Centrifuge D456 XXX-123 | centrifuge | | Sorvall Super T21 centrifuge XYZ-333 | centrifuge | | צנטריפוגה Z 33UK GH-098 | centrifuge | centrifuge | Больше года для этой задачи мы использовали систему RAG (Retrieval-Augmented Generation), построенную на AWS Knowledge Base. Наш воркфлоу был таким: Когда в системе появляется новый тул, мы ищем похожие в RAG. Если совпадения найдены (с достаточно высоким скором), берем их нормализованное имя. Если совпадений нет, мы передаем всю доступную инфу о туле в LLM вместе со списком наиболее вероятных нормализованных названий. LLM принимает финальное решение о нормализации. Почему мы решили уйти с AWS Knowledge Base Система работала неплохо, но под капотом AWS Knowledge Base использует AWS OpenSearch — он быстрый, но дорогой. Наш процесс нормализации асинхронный, и нам не нужен сверхбыстрый отклик. Платить за эту инфраструктуру нам не хотелось В нашей базе около 20 000 тулов с верифицированной нормализацией. Задачи нормализации запускаются в Lambda-функциях по событиям EventBridge. Когда AWS анонсировала S3 Vector Search (на момент написания статьи — еще в бете), мы решили его попробовать. Потенциальная экономия была слишком привлекательной. Новая архитектура с S3 Vector Search Схема работы Вот как работает наша новая схема: Генерация эмбеддингов: Когда появляется новый тул, мы создаем его эмбеддинг, используя локальную LLM-модель. Векторный поиск: Ищем в S3 Vector store, где хранятся пре-индексированные эмбеддинги существующих тулов. Сопоставление: Если distance ниже порога, используем нормализованное имя из найденного тула. Fallback: Если хорошее совпадение не найдено, окончательное решение принимает Claude Sonnet 3.5. Выбор модели эмбеддингов Мы попробовали несколько моделей и остановились на Xenova/bge-m3: Многоязыковая поддержка (критично для нас). Доступна как на Python, так и на TypeScript. Хороший баланс между качеством и размером. Эмбеддинги на 1024 измерения. Docker-образ для Лямбды Одной из самых больших проблем была производительность холодного старта. Загрузка тяжелой ML-модели в лямбде занимает время. Мы решили это так: Создали Docker-образ, который уже содержит загруженную модель Xenova/bge-m3 .Модель скачивается во время Docker build, а не в рантайме. Лямбда грузит пре-кэшированную модель из файловой системы образа. Пример Dockerfile: # Используем AWS Lambda Node.js 20 в качестве базового образа FROM public.ecr.aws/lambda/nodejs:20 # Устанавливаем рабочую директорию WORKDIR ${LAMBDA_TASK_ROOT} # Копируем package.json и package-lock.json для лучшего кэширования COPY package*.json ./ # Устанавливаем зависимости RUN npm install # Копируем остальной исходный код COPY . . RUN npm run build # Прогреваем модель, загружая ее во время сборки # Это снижает cold start time Lambda, предварительно загружая модель transformers RUN echo "Starting model warmup..." && \ node dist/src/scripts/warmup-model.js # Устанавливаем переменные окружения для Lambda ENV NODE_ENV=production ENV AWS_NODEJS_CONNECTION_REUSE_ENABLED=1 # Обработчик функции Lambda CMD ["src/lambda_functions/get-tools-chunk/index.handler"] Скрипт для загрузки модели (warmup-model.js ): /** * Скрипт для прогрева модели, который предварительно скачивает модель transformers во время Docker build. */ import { Embeddings } from '@/helpers/Embeddings'; async function warmupModel() { console.log('Warming up S3VectorStorage model in Docker build...'); try { // Создаем инстанс const vectorStorage = new Embeddings({ modelConfigKey: 'bge-m3', }); // Тестируем создание эмбеддинга, чтобы триггернуть скачивание модели console.log('Creating test embedding to download model...'); const embedding = await vectorStorage.createEmbedding( 'Warmup text for model download' ); console.log('Model downloaded and embedding created successfully'); console.log('Embedding length:', embedding.length); console.log('First few values:', embedding.slice(0, 3)); console.log('Model warmup completed successfully!'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('Model warmup failed:', errorMessage); // Не выходим с ошибкой, просто логируем проблему. } } // Запускаем прогрев warmupModel() .then(() => { console.log('Model warmup process finished.'); process.exit(0); }) .catch(error => { const errorMessage = error instanceof Error ? error.message : String(error); console.error('Model warmup failed:', errorMessage); // Не фейлим Docker build, просто ворнинг. process.exit(0); }); Текущий размер нашего Docker-образа — 778 МБ. Лямбда настроена с 4 ГБ памяти. Как результат: Загрузка модели занимает 5–6 секунд. Первый холодный старт иногда вылетает по таймауту. Последующие запуски быстрые (модель остается в памяти). Для нашей асинхронной задачи старт за 5–6 секунд вполне приемлем. S3 Vector Search Integration В отличие от AWS Knowledge Base, где требовался один файл на тул, S3 Vector работает иначе — вы просто используете API для сохранения документов и их эмбеддингов. Сервис сам занимается индексацией. Фрагмент класса S3VectorStorage (Python): class S3VectorStorage: def __init__(self, bucket_name: str, index_name: str, region_name: str = 'us-east-1', ): """ Инициализирует S3VectorStorage с поддержкой конфигурируемой модели. """ self.region_name = region_name self.s3_vectors = boto3.client('s3vectors', region_name=region_name) self.bucket_name = bucket_name self.index_name = index_name logger.info(f"S3VectorStorage initialized with index: {self.index_name}") def store_document(self, document_id: str, embedding: List[float], content: str, additional_attributes: Dict[str, Any] = None) -> str: """ Сохранить документ с его вектором и атрибутами. """ try: # Подготовка атрибутов метаданных attributes = { 'content': content } if additional_attributes: attributes.update(additional_attributes) # Сохраняем векторный документ logger.info(f"Storing document: {document_id}") self.s3_vectors.put_vectors( vectorBucketName=self.bucket_name, indexName=self.index_name, vectors=[ { "key": document_id, "data": {"float32": embedding}, "metadata": attributes } ] ) return document_id except Exception as e: logger.error(f"Failed to store document {document_id}: {e}") raise Для поиска сходства мы используем косинусное расстояние. Важный нюанс при миграции: AWS Knowledge Base возвращает Similarity Score (чем выше, тем лучше). S3 Vector возвращает Distance (чем ниже, тем лучше). В Knowledge Base мы использовали порог score >= 0.64 . С метрикой расстояния S3 Vector мы до сих пор экспериментируем, чтобы найти оптимальный порог. Метод поиска search_documents : def search_documents(self, query_embedding: List[float], query_filter: Dict[str, Any] = None, max_results: int = 10) -> List[Dict]: """ Ищет похожие документы на основе эмбеддинга запроса. """ try: # Подготовка параметров поиска search_params = { 'vectorBucketName': self.bucket_name, 'indexName': self.index_name, 'queryVector': {'float32': query_embedding}, 'topK': max_results, 'returnMetadata': True, 'returnDistance': True } if query_filter: # Создаем фильтр с оператором $eq для каждой пары ключ-значение filter_conditions = [] for key, value in query_filter.items(): filter_conditions.append({key: {'$eq': value}}) # Используем оператор $and, если условий несколько if len(filter_conditions) == 1: search_params['filter'] = filter_conditions[0] else: search_params['filter'] = {'$and': filter_conditions} # Выполняем поиск response = self.s3_vectors.query_vectors(**search_params) results = [] for match in response.get('vectors', []): result = { 'key': match['key'], 'distance': match['distance'], 'metadata': match.get('metadata', {}) } results.append(result) logger.info(f"Found {len(results)} matching documents") return results except Exception as e: logger.error(f"Search failed: {e}") raise Сравнение производительности Latency AWS Knowledge Base: около 1 секунды для векторного поиска. S3 Vector: около 7–8 секунд (включая генерацию эмбеддингов + поиск). С LLM fallback: плюс время на вызов Claude API. Более высокий latency в S3 Vector в основном из-за локальной генерации эмбеддингов (5–6 секунд на загрузку модели). Если использовать эмбеддинги Bedrock, задержка будет ближе к Knowledge Base. Cost Мы пока не перешли в продакшн, поэтому не могу дать точных цифр. Но ожидаемая экономия значительна: Нет OpenSearch кластера, который нужно саппортить. Хранилище S3 сильно дешевле, чем OpenSearch. Платим только за фактические вызовы API, а не за простаивающую инфраструктуру. Выводы Cold starts (холодные старты) решают: Даже 5–6 секунд — проблема, если у вас строгие SLA. Можно использовать Provisioned Concurrency, переключиться на Bedrock или пре-прогревать лямбды. Distance vs Score: Будьте внимательны при миграции между системами. Distance и Similarity Score — инверсные метрики. Тщательно тестируйте свои пороги. Трейд-оффы размера модели: Крупные модели точнее, но медленнее грузятся. Xenova/bge-m3 стал для нас хорошим компромиссом.

Фильтры и сортировка