Как вы делаете active learning loop для улучшения retrieval?
Краткий тезис
Active learning loop для улучшения retrieval — это циклический процесс, в котором система в production собирает обратную связь от пользователей (лайки/дизлайки), выделяет hard negatives (нерелевантные чанки, которые модель посчитала релевантными), дообучает embedding model на этих данных с помощью triplet loss, пересчитывает эмбеддинги всех документов (backfill) и через A/B тест проверяет, улучшилась ли метрика recall@k. Такой подход позволяет непрерывно адаптировать поиск под реальные запросы без ручной разметки.
1. Термин: Active Learning Loop
Active learning — это стратегия машинного обучения, при которой модель сама выбирает наиболее информативные примеры для дообучения, запрашивая метку у эксперта (или, в нашем случае, у пользователя). В контексте RAG active learning loop означает:
- Система работает в production, логируя запросы и результаты поиска.
- Пользователь неявно (кликом, временем чтения) или явно (лайк/дизлайк) оценивает релевантность найденных чанков.
- Из этих данных отбираются сложные негативные примеры (hard negatives), на которых модель ошибается.
- Модель эмбеддингов дообучается, чтобы лучше различать релевантные и нерелевантные чанки.
- Обновлённая модель разворачивается, и цикл повторяется.
Зачем это нужно Статическая embedding model (например, all-MiniLM-L6-v2) может плохо работать на специфической доменной терминологии. Active learning позволяет адаптировать retrieval под реальные запросы пользователей без дорогой ручной разметки тысяч примеров.
2. Сбор данных в production
Первый шаг — логирование всех взаимодействий пользователя с retrieval-компонентом. Для каждого запроса сохраняем:
- query — текст запроса.
- retrieved_chunks — список чанков (с текстом и метаданными), которые вернул поиск (обычно top‑k).
- user_feedback — бинарная метка: лайк (релевантно) или дизлайк (нерелевантно). Можно также использовать косвенные сигналы: время чтения чанка, клик по ссылке, копирование текста.
Пример структуры лога
log_entry = {
"query_id": "q_001",
"query": "как настроить SSL в nginx?",
"retrieved_chunks": [
{"chunk_id": "c_12", "text": "Для настройки SSL...", "score": 0.92},
{"chunk_id": "c_45", "text": "Nginx — это веб-сервер...", "score": 0.87},
...
],
"feedback": {"c_12": 1, "c_45": 0} # 1 — лайк, 0 — дизлайк
}
Инструменты Apache Kafka, AWS Kinesis или просто PostgreSQL для хранения логов. Важно обеспечить низкую задержку записи, чтобы не влиять на latency ответа.
3. Идентификация hard negatives
Hard negative — это чанк, который retrieval вернул с высоким score (модель считает его релевантным), но пользователь поставил дизлайк. Именно такие примеры наиболее полезны для дообучения, потому что они показывают границу, где модель ошибается.
Процесс отбора
- Для каждого запроса с дизлайком берём все чанки, которые получили дизлайк.
- Если у запроса есть хотя бы один лайкнутый чанк (positive), добавляем пару (query, positive, hard_negative) в тренировочный датасет.
- Если лайков нет, но есть дизлайки, можно использовать чанки с самым высоким score среди дизлайков как hard negatives (но без positive такой пример не пригоден для triplet loss).
Фильтрация шума Не все дизлайки одинаково полезны. Пользователь мог случайно нажать. Рекомендуется:
- Учитывать только дизлайки, если пользователь перед этим хотя бы 5 секунд просматривал чанк (косвенный сигнал).
- Использовать агрегацию: если один и тот же чанк получил дизлайки от >3 разных пользователей по разным запросам — он точно hard negative.
4. Переобучение embedding модели
Для дообучения используем triplet loss. Triplet состоит из:
- anchor — эмбеддинг запроса.
- positive — эмбеддинг релевантного чанка (лайк).
- negative — эмбеддинг нерелевантного чанка (дизлайк).
Формула triplet loss:
L = max(0, d(anchor, positive) - d(anchor, negative) + margin)
где d — косинусное расстояние (или евклидово), margin — гиперпараметр (обычно 0.2–0.5).
Пример кода с sentence-transformers
from sentence_transformers import SentenceTransformer, losses, InputExample
from torch.utils.data import DataLoader
model = SentenceTransformer('all-MiniLM-L6-v2')
train_examples = [
InputExample(texts=[query, positive_chunk, hard_negative_chunk])
for query, positive, hard_negative in training_triplets
]
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=32)
train_loss = losses.TripletLoss(model=model, distance_metric=losses.TripletDistanceMetric.COSINE, triplet_margin=0.3)
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=3, warmup_steps=100)
Важные детали
- Размер батча: 32–64, чтобы внутри батча были разные триплеты.
- Количество эпох: 2–5, чтобы не переобучиться на небольшом датасете.
- Можно добавить online hard negative mining — внутри батча выбирать самые сложные негативы для каждого anchor.
5. Backfill эмбеддингов
После дообучения модели необходимо пересчитать эмбеддинги для всех документов в векторной базе данных. Это называется backfill. Без этого этапа новая модель будет использоваться только для новых запросов, а старые чанки останутся со старыми эмбеддингами, что приведёт к несоответствию.
Процесс
- Загружаем новую модель.
- Для каждого чанка из хранилища документов вычисляем новый эмбеддинг.
- Обновляем записи в векторной БД (например, в FAISS, Pinecone, Weaviate).
- Если база большая (миллионы чанков), используем пакетную обработку с параллелизмом (например, Ray или Spark).
Пример на FAISS
import faiss
import numpy as np
# Предположим, у нас есть список текстов chunks и старая индексная структура
new_embeddings = model.encode(chunks, batch_size=256, show_progress_bar=True)
dimension = new_embeddings.shape[1]
# Создаём новый индекс
new_index = faiss.IndexFlatIP(dimension) # inner product для косинусного сходства
new_index.add(new_embeddings)
# Сохраняем
faiss.write_index(new_index, "embeddings_v2.index")
Время выполнения Для 1 млн чанков на GPU может занять несколько часов. Планируйте backfill в окно низкой нагрузки.
6. A/B тестирование новой retrieval модели
Перед полным развёртыванием необходимо проверить, что новая модель действительно улучшает качество. Для этого проводим A/B тест:
- Контрольная группа (A): старая модель (baseline).
- Экспериментальная группа (B): новая модель (с дообучением).
- Split 90% трафика на A, 10% на B (или 50/50, если трафика много).
- Метрики: recall@k, MRR, hit rate, а также бизнес-метрики (конверсия, время на странице).
Длительность минимум 1–2 недели, чтобы собрать статистически значимые данные. Используйте t-тест или бутстреп для проверки значимости.
Пример настройки в Python (псевдокод):
import random
def get_retrieval_model(user_id):
if random.random() < 0.1:
return "new_model"
else:
return "baseline"
# В production вызываем соответствующую модель
model_name = get_retrieval_model(user_id)
chunks = retrieve(query, model_name)
Важно Убедитесь, что пользователь всегда попадает в одну и ту же группу (по user_id), чтобы избежать смещения.
7. Метрики для оценки улучшения
Основная метрика — recall@k (доля релевантных документов среди top‑k). Также используем:
| Метрика | Формула | Интерпретация |
|---|---|---|
| Recall@k | relevant_in_top_k / total_relevant | Насколько полно retrieval находит релевантные чанки |
| MRR | mean(1/rank_first_relevant) | Насколько высоко первый релевантный чанк |
| Hit Rate@k | queries_with_at_least_one_relevant / total_queries | Доля запросов, для которых хоть что-то найдено |
Для A/B теста сравниваем относительное улучшение:
improvement = (recall_B - recall_A) / recall_A * 100%
Если improvement > 5% (и p‑value < 0.05) — можно roll out.
8. Roll out и мониторинг
После успешного A/B теста:
- Постепенно увеличиваем долю трафика на новую модель (10% → 30% → 50% → 100%) в течение нескольких дней.
- Мониторим latency (новая модель может быть медленнее), error rate, user feedback.
- Если метрики падают — откатываемся на предыдущую версию.
Инструменты мониторинга Prometheus + Grafana, ELK stack, MLflow для отслеживания версий моделей.
9. Инструменты и библиотеки
| Компонент | Инструменты |
|---|---|
| Embedding model | sentence-transformers, transformers, openai (если используете API) |
| Векторная БД | FAISS, Pinecone, Weaviate, Qdrant, Milvus |
| Логирование | Kafka, PostgreSQL, MongoDB |
| A/B тестирование | Собственный код, LaunchDarkly, Statsig |
| Мониторинг | Prometheus, Grafana, MLflow |
10. Вызовы и подводные камни
- Шум в фидбеке Пользователи могут ставить дизлайки по причинам, не связанным с retrieval (плохой ответ LLM). Решение: использовать только явные сигналы (лайк/дизлайк) после просмотра чанка.
- Дисбаланс классов Лайков обычно меньше, чем дизлайков. Можно использовать взвешивание или аугментацию (синтезировать hard negatives из случайных чанков).
- Дрейф данных Со временем запросы пользователей меняются. Active learning loop должен быть непрерывным, с периодическим переобучением (например, раз в неделю).
- Стоимость backfill Пересчёт эмбеддингов для миллионов документов требует ресурсов. Можно делать инкрементальный backfill только для новых/изменённых документов.
- Overfitting на небольшом датасете Если hard negatives мало, модель может переобучиться. Используйте регуляризацию (early stopping, dropout в head).
11. Альтернативные подходы
- Fine-tuning с учителем Если есть размеченный датасет (query, relevant, irrelevant) от экспертов, можно использовать его вместо (или вместе с) пользовательским фидбеком.
- LLM-as-judge Использовать LLM (например, GPT-4) для оценки релевантности чанков и генерации синтетических hard negatives.
- Cross-encoder reranker Вместо дообучения embedding model можно обучить cross-encoder, который ранжирует чанки поверх retrieval. Active learning loop тогда применяется к reranker’у.
Пет-проект для закрепления
Задача Реализовать active learning loop для улучшения retrieval в RAG-системе, которая отвечает на вопросы по документации Python.
Инструменты Python, sentence-transformers, FAISS, Flask (для API), SQLite (для логов).
Шаги:
- Подготовка данных Возьмите 500 страниц документации Python (например, из официального сайта). Разбейте на чанки по 512 токенов.
- Базовая RAG Реализуйте простой RAG с
all-MiniLM-L6-v2и FAISS. Напишите API, которое принимает query, возвращает top‑5 чанков и позволяет поставить лайк/дизлайк. - Сбор логов Каждый запрос сохраняйте в SQLite вместе с фидбеком.
- Генерация hard negatives Раз в день запускайте скрипт, который из логов собирает триплеты (query, positive, hard_negative).
- Дообучение Используйте
TripletLossизsentence-transformers. Обучайте 3 эпохи. - Backfill Пересчитайте эмбеддинги всех чанков новой моделью.
- A/B тест Сделайте две версии API (старая и новая). Сравните recall@k на тестовом наборе из 100 вопросов (можно взять из Stack Overflow).
Ожидаемый результат После 2–3 циклов active learning recall@5 должен вырасти на 10–15% по сравнению с baseline.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 532 | Как вы оцениваете качество retrieval'а в RAG-системе? |
| 533 | Как вы fine-tune'ите embedding модель для RAG? |
| 534 | Как вы проводите A/B тестирование в RAG? |
| 535 | Как вы мониторите RAG-систему в production? |
| 536 | Как вы собираете обратную связь от пользователей в RAG? |
| 537 | Что такое hard negative mining и как его применять? |
Навигация
- Предыдущий: 530
- Следующий: 532
- Индекс: 00. Индекс разборов