К содержимому журнала

AI и модели

Как мы переписали ретривал и подняли качество ответов на 34%

Когда мы запускали ассистента по внутренней базе знаний, ответы выглядели прилично, но команда не приняла бы их к работе: на четырёх вопросах из десяти модель не видела нужные документы. Проблема была не в модели — она ни разу не получила их на вход. За шесть недель мы переписали ретривал и подняли долю качественных ответов с 58 до 78 процентов.

Воронка из сходящихся линий, сужающихся к одной точке
Содержание

Это не история про новую модель. Это история про то, что происходит до модели: как вопрос превращается в фрагменты документов, которые она увидит. Наш ассистент по внутренней базе знаний использовал ту же модель, что и до переделки, — изменилось только то, что попадало ей в контекст. Качество ответов выросло на треть. Рассказываем по шагам, с цифрами и с ошибками, которые мы совершили по пути.

Точка старта: простой векторный поиск #

Первая версия была скучной до неприличия: документы резались на фрагменты по пятьсот токенов, эмбеддинги складывались в векторный индекс, на вопрос пользователя отдавались пять ближайших фрагментов. Работало ровно до первого реального вопроса. Аббревиатуры находились впустую: «ОСБ» из внутреннего словаря не имел ничего общего с «операционной системой баз». Версионированные регламенты соревновались в выдаче: на вопрос про актуальный процесс отвечал фрагмент из документа двухлетней давности. Табличные данные резались пополам — у фрагмента была шапка таблицы без строк.

Диагноз был неприятным и ясным: модель отвечала уверенно на неполных данных. Пользователь не мог отличить ответ из найденного фрагмента от ответа из выдумки, и это был самый дорогой вид ошибки — доверие к продукту расходовалось быстрее, чем он дорабатывался.

Первым делом мы перестали оптимизировать вслепую. Собрали eval-набор: триста реальных вопросов от поддержки и продуктовой команды, к каждому — эталонный фрагмент, который обязан быть в выдаче. Набор собирался неделю и окупился в первый же день: без него каждое изменение пришлось бы оценивать «на глаз», а глаз в ретривале ошибается так же регулярно, как и в бенчмарках производительности.

Что мы сделали: шесть шагов #

  1. Разбили документы по смыслу, а не по счётчику токенов: границы фрагментов совпали с разделами, таблицы ушли в отдельные фрагменты целиком, к каждому добавили заголовок документа и раздела;
  2. Добавили лексический поиск BM25 параллельно векторному: аббревиатуры, артикулы и точные термины векторная модель пропускала, а лексическая находила;
  3. Объединили выдачи и поставили реранкер: кросс-энкодер переупорядочивал двадцать кандидатов и отбирал лучшие десять;
  4. Навесили метаданные — версия документа, дата, отдел, гриф доступа — и встроили фильтры в сам поиск, а не в постобработку;
  5. Добавили переформулировку запроса: разговорный вопрос переписывался в поисковый — «а что там со старыми закупками» превращался в «регламент архивации закупок прошлых лет»;
  6. Прогнали весь eval-набор после каждого изменения и отклоняли всё, что давало прирост меньше двух процентов.
Лабораторная оценка качества: таблицы метрик и кривые на экране
Оценка качества в лаборатории: триста эталонных вопросов, четыре метрики и один железный ритуал — мерить до и после каждого изменения.

Что получилось в цифрах #

Главный показатель для нас — не метрика ретривала, а доля ответов, принятых экспертами: триста вопросов прогоняются через ассистента, ответы оценивают люди и модель-судья, спорные случаи разбираются вручную. Именно эта метрика легла в заголовок: плюс 34% относительного роста. Но смотреть стоит на всю таблицу — она честно показывает, откуда взялся рост.

Метрики до и после
МетрикаДоПослеИзменение
Ответы, принятые экспертами58%78%+34% относительного роста
Эталонный фрагмент в топ-562%81%+31%
MRR по эталонным фрагментам0,550,73+33%
Ответы с галлюцинацией14%6%−57%

Самый недооценённый вклад дала нарезка по смыслу: одна только она подняла попадание эталона в топ-5 с 62 до 69 процентов. Гибрид с BM25 добавил аббревиатуры и артикулы, реранкер убрал из выдачи похожие, но нерелевантные фрагменты. Переформулировка запроса дала скромные два процента по метрикам, но заметно улучшила поведение на разговорных вопросах — их в реальном трафике больше трети.

Сколько это стоило #

Шесть недель работы одного инженера с половиной моего времени, четыреста долларов на облако для экспериментов и ни одной строки в проде, пока метрики не выросли. Реранкер мы держим на собственной машине — стоимость инференса на десятках тысяч запросов в день оказалась ничтожной на фоне экономии на облачных токенах. Если интересно, как устроен такой локальный стек и что для него нужно по железу, у нас есть подробный материал о локальных языковых моделях .

Ядро гибридного поиска умещается в пятьдесят строк — вот упрощённая версия того, что работает в проде.

hybrid_search.pyPython
from dataclasses import dataclass


@dataclass
class Chunk:
    id: str
    text: str
    bm25_score: float = 0.0
    vector_score: float = 0.0


def normalize(scores: dict) -> dict:
    """Приводит баллы разных систем к общей шкале 0..1."""
    values = list(scores.values())
    low, high = min(values), max(values)
    span = (high - low) or 1.0
    return {key: (value - low) / span for key, value in scores.items()}


def hybrid_search(query: str, top_k: int = 10) -> list:
    lexical = bm25_index.top(query, limit=top_k * 3)      # лексический поиск
    semantic = vector_index.search(query, limit=top_k * 3)  # векторный поиск

    pool = {chunk.id: chunk for chunk in lexical + semantic}
    lex_norm = normalize({c.id: c.bm25_score for c in lexical})
    vec_norm = normalize({c.id: c.vector_score for c in semantic})

    for chunk_id, chunk in pool.items():
        chunk.fused = 0.6 * vec_norm.get(chunk_id, 0.0) + 0.4 * lex_norm.get(chunk_id, 0.0)

    candidates = sorted(pool.values(), key=lambda c: c.fused, reverse=True)[: top_k * 2]
    return reranker.score(query, candidates)[:top_k]

Что не сработало, тоже зафиксируем. Полноценный HyDE — генерация гипотетического ответа перед поиском — давал плюс один процент по метрикам и минус восемьсот миллисекунд к задержке: не окупился. Сложные веса седьмого знака в формуле слияния давали прирост меньше погрешности измерения: остановились на честных 0,6 и 0,4, подобранных по eval-набору. А самая большая техническая ошибка была организационной: первую неделю мы мерили качество на выборке, собранной автором системы, — и улучшения были стабильны ровно до передачи набора сторонним экспертам.

Ретривал — не отдельный островок, а один из шагов большого конвейера: как он встроен в полный цикл агентного запроса и что происходит дальше, мы разбирали в материале о пути запроса к AI-агенту . А если после переделки поиск всё равно отвечает медленно, проблема уже не в ретривале, а в хранилище — маршрут диагностики описан в разборе медленных запросов в PostgreSQL .

Выводы #

Тридцать четыре процента качества лежали не в модели, а в данных, которые ей подавали. Нарезка по смыслу, гибридный поиск и реранкер — инженерные решения, каждое из которых измеряется за день и не требует дообучения. При этом фундаментом выступает не архитектура, а дисциплина измерений: eval-набор, собранный до первого изменения, и правило «нет прогона — нет релиза». Начинайте с него, а не с выбора реранкера — иначе будет нечего измерять.

Был ли материал полезен?

0 полезных · 0 неполезных · 0 оценок

Голосование временно недоступно: подключаем безопасную идентификацию читателя.