Как мы переписали ретривал и подняли качество ответов на 34%
Когда мы запускали ассистента по внутренней базе знаний, ответы выглядели прилично, но команда не приняла бы их к работе: на четырёх вопросах из десяти модель не видела нужные документы. Проблема была не в модели — она ни разу не получила их на вход. За шесть недель мы переписали ретривал и подняли долю качественных ответов с 58 до 78 процентов.
Это не история про новую модель. Это история про то, что происходит до модели: как вопрос превращается в фрагменты документов, которые она увидит. Наш ассистент по внутренней базе знаний использовал ту же модель, что и до переделки, — изменилось только то, что попадало ей в контекст. Качество ответов выросло на треть. Рассказываем по шагам, с цифрами и с ошибками, которые мы совершили по пути.
Первая версия была скучной до неприличия: документы резались на фрагменты по пятьсот токенов, эмбеддинги складывались в векторный индекс, на вопрос пользователя отдавались пять ближайших фрагментов. Работало ровно до первого реального вопроса. Аббревиатуры находились впустую: «ОСБ» из внутреннего словаря не имел ничего общего с «операционной системой баз». Версионированные регламенты соревновались в выдаче: на вопрос про актуальный процесс отвечал фрагмент из документа двухлетней давности. Табличные данные резались пополам — у фрагмента была шапка таблицы без строк.
Диагноз был неприятным и ясным: модель отвечала уверенно на неполных данных. Пользователь не мог отличить ответ из найденного фрагмента от ответа из выдумки, и это был самый дорогой вид ошибки — доверие к продукту расходовалось быстрее, чем он дорабатывался.
Первым делом мы перестали оптимизировать вслепую. Собрали eval-набор: триста реальных вопросов от поддержки и продуктовой команды, к каждому — эталонный фрагмент, который обязан быть в выдаче. Набор собирался неделю и окупился в первый же день: без него каждое изменение пришлось бы оценивать «на глаз», а глаз в ретривале ошибается так же регулярно, как и в бенчмарках производительности.
01 Разбили документы по смыслу, а не по счётчику токенов: границы фрагментов совпали с разделами, таблицы ушли в отдельные фрагменты целиком, к каждому добавили заголовок документа и раздела;
02 Добавили лексический поиск BM25 параллельно векторному: аббревиатуры, артикулы и точные термины векторная модель пропускала, а лексическая находила;
03 Объединили выдачи и поставили реранкер: кросс-энкодер переупорядочивал двадцать кандидатов и отбирал лучшие десять;
04 Навесили метаданные — версия документа, дата, отдел, гриф доступа — и встроили фильтры в сам поиск, а не в постобработку;
05 Добавили переформулировку запроса: разговорный вопрос переписывался в поисковый — «а что там со старыми закупками» превращался в «регламент архивации закупок прошлых лет»;
06 Прогнали весь eval-набор после каждого изменения и отклоняли всё, что давало прирост меньше двух процентов.
Оценка качества в лаборатории: триста эталонных вопросов, четыре метрики и один железный ритуал — мерить до и после каждого изменения.
Главный показатель для нас — не метрика ретривала, а доля ответов, принятых экспертами: триста вопросов прогоняются через ассистента, ответы оценивают люди и модель-судья, спорные случаи разбираются вручную. Именно эта метрика легла в заголовок: плюс 34% относительного роста. Но смотреть стоит на всю таблицу — она честно показывает, откуда взялся рост.
Метрики до и после
Метрика
До
После
Изменение
Ответы, принятые экспертами
58%
78%
+34% относительного роста
Эталонный фрагмент в топ-5
62%
81%
+31%
MRR по эталонным фрагментам
0,55
0,73
+33%
Ответы с галлюцинацией
14%
6%
−57%
Самый недооценённый вклад дала нарезка по смыслу: одна только она подняла попадание эталона в топ-5 с 62 до 69 процентов. Гибрид с BM25 добавил аббревиатуры и артикулы, реранкер убрал из выдачи похожие, но нерелевантные фрагменты. Переформулировка запроса дала скромные два процента по метрикам, но заметно улучшила поведение на разговорных вопросах — их в реальном трафике больше трети.
Шесть недель работы одного инженера с половиной моего времени, четыреста долларов на облако для экспериментов и ни одной строки в проде, пока метрики не выросли. Реранкер мы держим на собственной машине — стоимость инференса на десятках тысяч запросов в день оказалась ничтожной на фоне экономии на облачных токенах. Если интересно, как устроен такой локальный стек и что для него нужно по железу, у нас есть подробный материал о локальных языковых моделях .
Ядро гибридного поиска умещается в пятьдесят строк — вот упрощённая версия того, что работает в проде.
hybrid_search.pyPython
from dataclasses import dataclass@dataclassclassChunk:id:str text:str bm25_score:float=0.0 vector_score:float=0.0defnormalize(scores:dict)->dict:"""Приводит баллы разных систем к общей шкале 0..1.""" values =list(scores.values()) low, high =min(values),max(values) span =(high - low)or1.0return{key:(value - low)/ span for key, value in scores.items()}defhybrid_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-набор, собранный до первого изменения, и правило «нет прогона — нет релиза». Начинайте с него, а не с выбора реранкера — иначе будет нечего измерять.