Привет! Меня зовут Алексей, я разработчик в Битрикс24.
В первой части рассказывал про retrieval-часть нашего RAG для AI-помощника Марты: как мы перестраивали поиск по базе знаний, подбирали retrieval pipeline под продуктовую документацию и постепенно улучшали качество поиска через серию небольших экспериментов. Ещё разобрал, почему production RAG — это постоянная работа с retrieval, шумом и latency.
Сегодня — как мы измеряли качество всей RAG-системы целиком. Расскажу про экспертный и синтетический датасеты, почему retrieval-метрики могут расходиться с реальным качеством ответов и как мы замкнули агентную петлю.
В конце покажу финальную картину метрик с замечаниями и поделюсь, какие полезные мысли появились во время работы.
Глоссарий
embedding — векторное представление текста, по которому ищут похожие фрагменты.
retrieval — этап поиска релевантных кусков базы знаний под вопрос пользователя.
чанк — небольшой фрагмент документа, по которому работает retrieval.
реранкер — отдельная модель, которая пересортировывает retrieval-кандидатов по релевантности.
RAGAS — библиотека для оценки качества RAG-систем.
LLM as a judge — приём, когда одна LLM оценивает ответы другой.
Recall@K, MRR, Hit Rate@K — стандартные retrieval-метрики. Hit Rate@K = «есть ли хотя бы один правильный ответ в первых K результатах».
F1/Precision/Recall классификации — бинарная оценка: агент корректно берётся за вопрос или уходит в эскалацию.
Precision = доля корректных среди вопросов, за которые агент взялся, Recall = доля корректно обработанных среди вопросов, на которые в принципе можно было ответить, F1 — их гармоническое среднее. Подробнее в разделе про датасеты.
Датасеты и метрики
Чтобы вообще понимать, какой эксперимент лучше другого, нужны датасеты и метрики. Я собрал два набора:
Экспертный (Golden). 155 реальных вопросов пользователей с разметкой от отдела документации: правильно ли агент классифицировал ситуацию («ответить» / «эскалировать»), правильно ли ответил, какие статьи и фрагменты должны были быть использованы. Это эталон, который ближе всего к реальности. Маленький, потому что разметка дорогая, и собран как раз из сложных кейсов, которые мы накопили за время эксплуатации предыдущей версии RAG. То есть это не «средние пользовательские вопросы», а как раз места, где старая система проседала — почти каждый вопрос там потенциально болезненный.
Синтетический (Silver). ~900 вопросов, сгенерированных по методике Dragon. Кратко: алгоритм по документам базы знаний строит граф знаний, а потом из этого графа генерирует пять типов вопросов разной сложности — простые, наборы, мультихоп (когда для ответа нужно связать несколько фактов), условные, сравнительные.
Дальше идёт серия фильтраций. Это около десяти этапов, среди которых самый интересный — прогон через две слабые open-source LLM на 7B параметров. Логика такая: если 7B-модель без доступа к статьям отвечает правильно, значит вопрос уже много где встречался в её претрейне и не годится для оценки RAG (мы будем мерить не нашу систему, а память pretrain). Такие вопросы выкидываются. На выходе — корпус, который хорошо нагружает retrieval и плохо ломается о pretrain.
Я поделил silver-набор 70/30 на dev и test для подбора гиперпараметров и финальной валидации. Один прогон retrieval-эксперимента на ~600 вопросах dev-сета — десятки минут с concurrency=5. End-to-end eval на тех же 600 вопросах — уже 2–3 часа. Когда эксперимент стоит часы, очень не хочется тратить его на проверку «а вдруг» — отсюда привычка планировать серии заранее.
Отдельно стоит сказать, где какой датасет работает.
Для тюнинга retrieval мы активно использовали silver — там нужно много вопросов для статистической значимости, и синтетика отлично для этого подходит.
Для тюнинга финального агента synthetic нам плохо подошел. Здесь включается LLM с её reasoning и формулировкой ответа. Я несколько раз замечал, что улучшение метрик на silver приводило к просадке на экспертном датасете. Видимо, синтетика хороша для качественной оценки retrieval-«железа», но плохо ловит реальные паттерны пользовательских запросов и стиля ответов. Для финального этапа тюнинга агента приходилось ориентироваться и на экспертный датасет тоже, несмотря на его маленький размер.
Честно про утечку
Это сразу поднимает вопрос leakage: ниже в финальной таблице я показываю результат на экспертном датасете, и часть тюнинга тоже сверялась с ним. Полностью «чистым» holdout его называть нельзя.
Как было устроено на практике:
Главная метрика, по которой autoresearch-петля промотировала или откатывала итерации, — это composite_score на silver_dev (train). Прямой оптимизации под экспертный датасет не было.
Параллельно я смотрел метрики на golden как «сигнал тревоги»: если правки сильно улучшают silver_dev и одновременно ломают golden, что-то идёт не туда, и такие итерации откатывались.
Часть итераций ближе к концу действительно использовала golden-метрики в gate, когда мы хотели проверить именно поведение на сложных кейсах. Это уже косвенный leak.
Отдельного «совсем не виданного» экспертного holdout у меня нет — экспертная разметка дорогая.
Что я предлагаю с этим делать читателю: смотреть на сдвиг метрик (Δ F1, Δ Recall, Δ latency между старым и новым агентом) как на честный знак направления, а к абсолютным значениям на golden относиться чуть осторожнее, чем если бы это был чистый holdout. По динамике пользовательских жалоб из внутреннего канала поддержки видимые изменения на golden, бьются с реальностью. Но это уже не статистика, а наблюдение.
По метрикам — стандартный набор для retrieval (Recall@K, Hit Rate@K, MRR), классификационный F1 и RAGAS для оценки ответов (faithfulness, answer relevancy, context precision/recall), плюс LLM-as-a-judge поверх «золотого» ответа для качественной оценки. На каждое A/B-сравнение — bootstrap-доверительный интервал на 1000 ресэмплов по вопросам и paired permutation test (5000 перестановок) на разницу метрик одной и той же пары прогонов. Без этого легко принять шум за улучшение.
Дисциплина важна: я несколько раз ловил себя на том, что радуюсь приросту в +1 п. п. Recall@10, который потом оказывался в 95% CI шума. Без статистики легко полгода крутить параметры и в итоге быть на том же месте.
Автоматическая оптимизация агента
Когда retrieval затюнен, остаётся самая капризная часть — сам агент. Это не только промпт, но и архитектура: какие у агента инструменты, в каком порядке он их вызывает, как он обрабатывает «не нашёл», как формулирует финальный ответ, какие у него ограничения. Ручной промпт-инжиниринг здесь масштабируется плохо: правишь текст, гоняешь на десятке вопросов, на десяти точках данных шум перекрывает сигнал, правишь ещё, через неделю в системе живёт промпт, который сложно объяснить и ещё сложнее воспроизвести.
Я попробовал два подхода к автоматизации.
GEPA: black-box оптимизация
Первое — взял библиотеку GEPA. Идея: оптимизатор перебирает варианты промпта, для каждого варианта прогоняет полный eval, и через несколько десятков итераций находит конфигурацию с лучшими метриками. Это классическая чёрно-ящичная оптимизация — GEPA не знает, что за модель и что за задача, для неё это просто функция «промпт → скор».
GEPA в итоге нашёл промпт лучше baseline на ~11 п. п. F1 за 34 итерации и упёрся в плато. Главная проблема — это очень дорого по времени: один eval-прогон занимает 1.5-2 часа, и 34 итерации заняли у меня в общей сложности три-четыре дня реального времени с учётом периодических сетевых проблем в корпоративной инфраструктуре. GEPA эффективна, но как чёрный ящик: она просто видит финальную метрику и пытается двигать промпт градиентным способом.
Замкнуть агентную петлю: white-box оптимизация
Дальше я попробовал другое. Опытный инженер, когда копается с агентом, обычно не ограничивается A/B по агрегированным метрикам. Он лезет в данные: смотрит трейсы, читает размышления модели, смотрит, какие поисковые запросы агент сформулировал, какие чанки он получил, как он на них среагировал. И на основании этой богатой картины обобщает несколько примеров до гипотезы — «агент часто переспрашивает на multi-hop вопросах, потому что не умеет декомпозировать запрос». Дальше эту гипотезу можно проверить целевым изменением промпта или кода и снова померить.
Эта работа по своей природе как «белый ящик». И это ровно то, что хорошо умеет делать кодовый агент: читать файлы, гонять команды, формулировать гипотезы по логам, делать целевые правки в коде.
По-правильному этот подход называется «замкнуть агентную петлю»: ты даёшь оптимизатору не только финальный скор, а доступ ко всей машине, включая трейсы и код. Широкой общественности он больше известен под названием autoresearch — благодаря репозиторию Андрея Карпатого.
Кодинг-агенту даём инструкцию «оптимизируй промпт и код helpdesk-агента», доступ к eval-скрипту и доступ к Langfuse trace API, после чего запускаем в цикле без ограничения по числу итераций (с возможностью остановиться в любой момент).
На каждую итерацию агент:
Прогоняет eval, получает метрики.
Достаёт через субагентов 20–50 случайных «провальных» трейсов из Langfuse.
Анализирует их и формулирует гипотезу: «агент часто переспрашивает на multi-hop вопросах, потому что в промпте нет инструкции декомпозировать запрос».
Меняет промпт или код агента под гипотезу.
Перепрогоняет eval. Если стало лучше — коммит, новый baseline. Если хуже — git revert, новая попытка.

На графике метрика — composite_score: среднее значение judge-оценки 0/1 (правильно ли агент ответил, по сравнению с эталонным ответом) на silver_dev. Если на конкретном вопросе агент вместо ответа ушёл в эскалацию, ему ставится 0 — то есть composite одновременно ловит и долю ответов, и их правильность. Это, по сути, функция ошибки, которую петля минимизирует — точнее, максимизирует. Шкала такая, как у LLM-as-a-judge: 0 — никогда не угадывает, 1 — всегда даёт корректный ответ; на старте мы стояли в районе ноля, потому что после первой переборки модели сильно сбился промпт.
На графике слева — вся история; видны несколько разрывов, когда мы перестраивали датасет или меняли оценщика. Это не «улучшения» агента, а перекалибровка метрики: после такой смены сравнивать с предыдущими итерациями нельзя, поэтому baseline сбрасывается. На правой панели — окно реального прогресса после последней перекалибровки: с baseline 0.130 (post-revamp) дошли до 0.321 за ~70 итераций. Около половины итераций — отвергнутые гипотезы, которые петля откатила сама.
Важное наблюдение: agentic loop не работает «полностью автономно». По моим наблюдениям, через 10–15 итераций агент устойчиво упирается в потолок. Это происходило несколько раз, и каждый раз приходилось вмешиваться вручную: задавать наводящие вопросы, подсказывать новые гипотезы, обращать внимание на классы ошибок, которые агент не замечал сам. После такого вмешательства петля снова начинает двигаться вперёд ещё на 10–15 итераций. Это не «запустил и забыл», это инструмент усиления эксперта, а не его замены.
Когда что использовать. Если у вас есть доступ к трассировке агента и вы умеете её предоставить инструменту — agentic loop почти всегда лучше: меньше итераций на единицу прогресса, более содержательные изменения, побочный продукт — записанные гипотезы и объяснения в коммитах. Если нет (например, вы тюните чужой агент по API и видите только вход-выход) — GEPA и подобные black-box подходы остаются единственным реалистичным вариантом.
Ещё один важный момент: длинные прогоны eval делают цикл болезненным. Когда одна итерация стоит час, а сеть периодически отваливается, вокруг скрипта приходится строить инфраструктуру:
Сохранять промежуточное состояние и уметь продолжить с того места, где упало.
Останавливать прогон, если подряд приходит серия ошибок — не имеет смысла бить по сломанному.
Надежно перезапрашивать отдельные запросы при таймаутах.
Без этого автомат просто останавливается посередине дороги и теряет состояние. У нас несколько недель ушло именно на эту обвязку, прежде чем оптимизация стала возможной в принципе.
Мультиязычность
Одна из исходных задач проекта — снять привязку к русскоязычному хелпдеску. Логика, которую мы заложили:
Выбор языковой зоны хелпдеска — по региону пользователя и языку интерфейса. Есть таблица соответствий с правилами замены: если основного варианта нет, берём ближайший доступный хелпдеск.
Поиск идёт по чанкам выбранной языковой зоны. Индекс не один большой, а отдельные коллекции в векторной БД — по одной на каждый язык хелпдеска (благо embedding-модель мультиязычная).
Ответ агент генерирует на языке пользователя, при этом для терминов интерфейса в скобках указывает оригинал. Это снимает головную боль с переводов UI: пользователь видит и перевод, и точное слово, которое нужно найти у себя на экране.
Что получилось в итоге
Полная финальная картина на экспертном датасете. Напомню: это 155 вопросов, не средних, а специально отобранных сложных — мы их насобирали за год эксплуатации первой версии RAG, как раз из тех мест, где она проседала. Каждый из них для системы потенциально болезненный. То есть метрики ниже — это не «как мы работаем на спокойных пользовательских запросах», а «как мы работаем на худшем, что у нас есть».
Метрика | Старый агент (внешний вендор) | Новый агент (свой стек) |
|---|---|---|
F1 (классификация) | 0.636 | 0.908 |
Precision | 0.919 | 0.891 |
Recall | 0.486 | 0.926 |
Среднее время ответа | 18.0 с | 11.1 с |
p50 latency | 21.8 с | 10.1 с |
p95 latency | 35.2 с | 18.9 с |
Несколько комментариев:
Recall с 0.49 до 0.93. Старый агент примерно в половине случаев из этого набора уходил в уточнение вместо ответа. На новом агенте на этих же 155 вопросах отказов почти вдвое меньше. На «среднем» пользовательском вопросе картина наверняка мягче, но на нашем «сложном» наборе это самый заметный сдвиг.
Precision с 0.92 до 0.89. Естественная плата за «отвечает чаще» — иногда отвечает там, где надо бы эскалировать. На итоговом F1 этот размен сильно положительный, но это размен, а не «бесплатный апгрейд».
Latency почти в два раза быстрее по медиане. Хвост (p95) тоже заметно сократился: с 35.2 до 18.9 секунды. На типичном запросе разница в два раза заметна и пользователю.
Из побочных результатов: мы отвязались от стороннего вендора в этом конкретном продукте — расходов на внешние API за каждый запрос больше нет. И поддержали мультиязычность.
Что я понял по дороге
Не существует «универсального RAG». Для каждой задачи свои нюансы: что у вас на входе, какие вопросы задают пользователи, что они ждут в ответе. Базовое решение из туториала собирается за вечер и обычно работает на условные 60% качества; оставшиеся 40% — это и есть содержательная работа.
Без eval и метрик ничего не получится. Я несколько раз ловил себя на «о, кажется стало лучше» — а потом по статистике видно, что это был шум. Когда у тебя 600+ вопросов и paired-тест с правильным CI, ты перестаёшь верить интуиции, и это очень освобождает.
Retrieval-метрики и end-to-end метрики — это разные вещи. Один и тот же параметр (порог реранкера в нашем случае) по одной метрике оптимален в одной точке, по другой — в совсем другой. Если оптимизируешь только retrieval — можно прийти к решениям, которые на конечного пользователя действуют хуже. Финальная истина живёт на end-to-end eval.
Маленькие неочевидные трюки складываются в большой результат. Хлебные крошки в заголовках, описание скриншотов, deep-подчанки, префиксы у embedding, правильный порог реранкера — каждое от +0.5 до +6 п. п. на своей метрике, разный масштаб у разных трюков. По отдельности — почти шум, вместе и в правильной последовательности — заметный качественный скачок.
Агентная оптимизация — это реально полезный инструмент, если у вас белый ящик и хороший eval. В нашем случае agentic loop с кодинг-агентом побил GEPA по эффективности в разы. Но это не «запустил и забыл» — петля упирается в потолок каждые 10–15 итераций и требует ручного направления.
Длинные прогоны eval — это инфраструктурная задача. Когда один прогон стоит часы, нужны промежуточные чекпоинты с возможностью продолжить, аварийная остановка по серии ошибок, надёжный повтор отдельных запросов, надёжное хранение трейсов. Без этого автомат просто умирает на полпути.
PS Если у вас уже есть свой RAG и вы пытаетесь его улучшать — не пытайтесь делать всё сразу. Сначала статистически нормальный eval, потом по одному параметру за раз, потом небольшой шаг назад и проверка, что вы не сломали то, что работало. Это медленно, но это работает.



















