English translation is not available yet. Showing Russian content.

Как вы делаете 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 (модель считает его релевантным), но пользователь поставил дизлайк. Именно такие примеры наиболее полезны для дообучения, потому что они показывают границу, где модель ошибается.

Процесс отбора

  1. Для каждого запроса с дизлайком берём все чанки, которые получили дизлайк.
  2. Если у запроса есть хотя бы один лайкнутый чанк (positive), добавляем пару (query, positive, hard_negative) в тренировочный датасет.
  3. Если лайков нет, но есть дизлайки, можно использовать чанки с самым высоким 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. Без этого этапа новая модель будет использоваться только для новых запросов, а старые чанки останутся со старыми эмбеддингами, что приведёт к несоответствию.

Процесс

  1. Загружаем новую модель.
  2. Для каждого чанка из хранилища документов вычисляем новый эмбеддинг.
  3. Обновляем записи в векторной БД (например, в FAISS, Pinecone, Weaviate).
  4. Если база большая (миллионы чанков), используем пакетную обработку с параллелизмом (например, 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@krelevant_in_top_k / total_relevantНасколько полно retrieval находит релевантные чанки
MRRmean(1/rank_first_relevant)Насколько высоко первый релевантный чанк
Hit Rate@kqueries_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 теста:

  1. Постепенно увеличиваем долю трафика на новую модель (10% → 30% → 50% → 100%) в течение нескольких дней.
  2. Мониторим latency (новая модель может быть медленнее), error rate, user feedback.
  3. Если метрики падают — откатываемся на предыдущую версию.

Инструменты мониторинга Prometheus + Grafana, ELK stack, MLflow для отслеживания версий моделей.


9. Инструменты и библиотеки

КомпонентИнструменты
Embedding modelsentence-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 (для логов).

Шаги:

  1. Подготовка данных Возьмите 500 страниц документации Python (например, из официального сайта). Разбейте на чанки по 512 токенов.
  2. Базовая RAG Реализуйте простой RAG с all-MiniLM-L6-v2 и FAISS. Напишите API, которое принимает query, возвращает top‑5 чанков и позволяет поставить лайк/дизлайк.
  3. Сбор логов Каждый запрос сохраняйте в SQLite вместе с фидбеком.
  4. Генерация hard negatives Раз в день запускайте скрипт, который из логов собирает триплеты (query, positive, hard_negative).
  5. Дообучение Используйте TripletLoss из sentence-transformers. Обучайте 3 эпохи.
  6. Backfill Пересчитайте эмбеддинги всех чанков новой моделью.
  7. 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 и как его применять?

Навигация