У большинства разработчиков, не работающих с ИИ, ментальная модель RAG либо ошибочна, либо опасно неполна. И дело не в том, что они плохие инженеры, — просто почти всё, что написано про RAG, это либо десятиминутный туториал по фреймворку, за которым не видно ни одного реального решения, либо научная статья для тех, кто и так занимается этим профессионально. Между этими крайностями почти ничего нет.

Эта статья — как раз то, что между ними. Недавно я собрал продакшн-систему RAG поверх 62 книг по античной истории (~46 000 чанков) и проверил каждое проектное решение на фиксированном тестовом наборе — включая те решения, которые не сработали. Я пропущу маркетинг фреймворков и расскажу о тех немногих вещах, которые действительно определяют, можно ли доверять вашей RAG-системе, — и подкреплю всё цифрами.

Всем привет, меня зовут Лев Рябов, я фронтенд-разработчик в М2 Tech. После прочтения этой статьи вы поймете, что чтобы во всём разобраться, знания ML не нужны. Если вы умеете писать REST API и делать запросы к PostgreSQL, вы сможете построить всё, что здесь описано.

Ментальная модель за 60 секунд

Что такое RAG (Retrieval-Augmented Generation)? Несмотря на пугающее название, базовая схема проста:

Как работает RAG
Как работает RAG

Вот и вся идея. Поисковик, прикрученный к LLM, с инструкцией отвечать по результатам поиска, а не по данным, на которых модель обучалась.

Единственный по-настоящему новый компонент для типичного backend-разработчика — это эмбеддинги. Модель эмбеддингов превращает текст в вектор — массив из сотен или тысяч чисел. Тексты с близким смыслом получают близкие векторы. 

Поэтому поиск релевантного текста превращается в поиск ближайших векторов. На этапе загрузки система рассчитывает эмбеддинги для всех чанков документов. Когда приходит запрос, она рассчитывает эмбеддинг вопроса и достаёт заданное количество ближайших чанков.

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

Всё остальное — разбиение документов на чанки, извлечение фрагментов и сборка промпта — привычная инженерная работа. Поэтому для создания базовой RAG-системы не обязательно быть ML-инженером. Главная сложность здесь не в пороге входа, а в ловушках, которые редко показывают в туториалах.

Ловушка: наивный RAG работает в демо и врёт в проде

А теперь то, что многих шокирует.

Типичный базовый RAG из туториалов выглядит примерно так: нужно разбить документы на фиксированные чанки по 500 токенов, заэмбеддить тем, что использовал автор туториала, достать топ-5 и скормить модели — вроде бы работает сразу же. Вы задаёте вопрос и получаете гладкий, уверенный, хорошо структурированный ответ со ссылкой на источник. Демо готово, катим в прод, да?

Я замерил ровно этот бейзлайн на своём корпусе с помощью тестового набора из 135 вопросов, для каждого из которых знал, в каких фрагментах лежит ответ. Результат: recall@5 = 35,2% (или топ 5 лучших поисковых результатов, в которых содержится правильный ответ). 

Прочитайте это ещё раз. Примерно для двух вопросов из трёх нужный фрагмент вообще не доходил до модели. И вот что опасно: модель всё равно отвечала. Гладко. Тем же уверенным тоном, что и когда извлечение срабатывало.

Именно здесь  разработчики без опыта в ИИ понимают про RAG неправильно. Сбой системы выглядит не как «ничего не найдено» у поисковика и не как исключение при вызове API. Режим отказа — это уверенная, правдоподобная выдумка, которую невозможно отличить от правильного ответа, если вы заранее не знаете правильный ответ. Система, которая права в 65% случаев и молча выдумывает в остальных 35%, хуже, чем бесполезна — потому что вы не можете понять, какой ответ перед вами.

Ничто в демо не выдаст, что так происходит. Единственный способ узнать — измерять. И это подводит нас к самому важному совету во всей статье.

Прежде чем что-либо улучшать: соберите тестовый набор

Этот шаг пропускают все туториалы для начинающих, а ценнее он любого приёма, который вы потом добавите.

Прежде чем трогать размеры чанков, эмбеддеры, реранкеры или агентные фреймворки, сделайте вот что:

  1. Напишите 30–50 реальных вопросов по вашему корпусу — таких, какие будут задавать настоящие пользователи, а не таких, ответы на которые удобно лежат в ваших документах.

  2. Для каждого вопроса зафиксируйте, где находится ответ — в каком документе, примерно в каком фрагменте.

  3. И самое важное — добавьте 5-10 вопросов, на которые корпус не может ответить. Это ваши ловушки на галлюцинации. Правильное поведение здесь — отказ; всё остальное — выдумка, которую вы иначе никогда бы не заметили.

  4. Измеряйте две вещи по отдельности:

  1. Извлечение (retrieval): попадает ли нужный фрагмент в топ-k результатов? Чистый код, без оценки со стороны LLM, — это та метрика, которую нужно оптимизировать в первую очередь.

  2. Генерация (generation): если нужные фрагменты переданы, обоснован ли ответ ими? Отказывается ли система отвечать там, где должна?

Это стоит одного-двух дней и меняет всё, потому что советы про RAG в интернете дико зависят от корпуса. То, что преобразило одну систему, на другой не даёт ничего. Без собственного тестового набора любой пост в блоге — включая этот — это лотерея. С ним вы можете проверить любой приём на своих данных за час. Поэтому любой совет из статьи или документации, и даже этого текста, стоит воспринимать как гипотезу, которую нужно проверить на собственных данных.

Конкретный пример, почему это важно: стандартный совет образца 2024 года гласит, что «гибридный поиск (ключевые слова BM25 + векторы) на больших объёмах всегда бьёт чисто векторный поиск». Я в это верил. Я ожидал, что на моём корпусе он победит, и заранее зафиксировал этот прогноз. Когда я реально это прогнал, гибридное извлечение оказалось побайтово идентичным обычному плотному (dense) извлечению — тот же recall, категория за категорией, ни одного изменившегося результата. Я знаю это только потому, что измерил. Полный разбор этого отрицательного результата — отдельная история.

Ваш тестовый набор — это не тестовая инфраструктура, которую пишут один раз и забывают. Это ваш руль.

Что улучшило качество поиска на моём корпусе

Когда тестовый набор был готов, вот что оказалось важным на моём корпусе — в порядке убывания влияния.

1. Модель эмбеддингов — самый мощный рычаг

Замена дефолтного эмбеддера BGE-M3 на сильный qwen3-embedding-8b через хостинговый API — подняла recall@5 с 35% до 53%. Это скачок на 18 процентных пунктов от изменения одной строки конфига. Сильнее всего выросла самая проблемная категория синонимы(17 вопросов) — на 41,7 пункта: вопросы на современном английском к тексту в чопорном викторианском переводе. Слабый эмбеддер просто не мог преодолеть разрыв в лексике; сильный — мог.

Вывод: не берите эмбеддер из туториала по умолчанию без проверки на собственных данных. Это компонент, который и выполняет собственно «понимание» при извлечении, модели различаются колоссально, а заменить его — проще простого в начале разработки.

2. Что должен содержать чанк

Разговоры про чанкинг обычно зациклены на количестве токенов. Для меня же больший выигрыш дало то, что чанк содержит. Сырой кусок в 500 токенов из середины главы 12 теряет часть контекста — эмбеддер видит «затем он двинулся на север», не имея понятия, кто такой «он» и о какой войне речь.

Решение: на этапе загрузки добавляйте контекст в начало каждого чанка перед эмбеддингом — название книги, путь заголовков глав, однострочную заметку о том, чему посвящён этот раздел. На самой слабой категории вопросов — вопросах, требующих синтеза по всему разделу, — это преобразило результаты: +18,2 пункта recall@5. В чанкинге важен смысл внутри каждого чанка, а не арифметика.

3. То, что не помогло: измеряйте, прежде чем добавлять

Два приёма, которые рекомендуют во многих подборках про «продвинутый RAG»:

  • Гибридный поиск BM25 + векторы: как описано выше — ноль изменений на моём корпусе. Сильный эмбеддер и так находил всё, что мог найти поиск по ключевым словам.

  • Реранкинг на кросс-энкодере: я замерил пять реранкеров. Только самый дорогой хостинговый вообще обошёл бейзлайн без реранкера — примерно на 1,7 пункта, ценой постоянной платной зависимости в каждом запросе. Выкинул.

Смысл не в том, что «никогда их не используйте». На вашем корпусе они могут и выиграть. Он в том, что оба приёма увеличивают стоимость, задержку и сложность, и единственный способ узнать, окупаются ли они, — это ваш тестовый набор. «Модно» — это не доказательство.

Как заставить систему отказываться, а не галлюцинировать

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

Три важные составляющие:

1.Ограничьте ответ найденными источниками

Составьте промпт для обоснованных ответов. Дайте модели инструкцию отвечать только по предоставленным источникам, указывать, какой источник подтверждает каждое утверждение, и явно отказываться, когда в источниках нет ответа. Это необходимое условие, но только промпта не достаточно.

2.Тестируйте отказ как полноценную фичу. 

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

3.Проверьте модель

Помните: выбор модели важен и здесь. По моим замерам, способность честно отказываться сильно различалась между LLM — замена модели-генератора подняла долю отказов на вопросах за пределами корпуса с 73% до 100% при том же промпте. У одного лишь промптинга был потолок; я прогнал промпт через пять версий и наблюдал, как правки скользят вдоль фиксированной кривой компромиссов, не сдвигая её.

Итоговое состояние моей системы, измеренное на полном тестовом наборе, вышло таким: 0% ложных отказов — она отвечает на каждый вопрос, на который вообще можно ответить, — и 96% честных отказов на вопросах-ловушках. Именно эти две цифры вместе и означают «надёжный» применительно к RAG. И обе — обычные числа из интеграционных тестов, которые можно прогонять в CI, а не субъективные ощущения.

С чего конкретно начать TypeScript-разработчику

Чтобы начать, вам не нужны ни AI-фреймворк, ни облачная векторная БД, ни Python. Вот стек, который я бы сегодня вручил TypeScript-разработчику:

  • PostgreSQL и pgvector — для хранения эмбеддингов и векторного поиска в знакомой базе данных.

  • Хостинговый API эмбеддингов — для преобразования документов и пользовательских запросов в векторы.

  • LLM API — для формирования ответа на основе найденных фрагментов.

  • TypeScript-код, связывающий этапы пайплайна. В моём случае первая реализация заняла около 200 строк. Полезно сначала собрать её без фреймворка: так проще понять, какие задачи он впоследствии будет абстрагировать.

Пять шагов:

1. Загрузка (ingest). Разбейте документы на чанки по естественным границам — разделам и абзацам, — добавляя в начало текста каждого чанка контекст из названия и заголовков.

2. Эмбеддинг и хранение. Один вызов API на чанк, вставка в таблицу со столбцом типа vector, добавление индекса.

3. Извлечение. Преобразуйте в вектор входящий вопрос и выполните запрос SELECT ... ORDER BY embedding <=> $1 LIMIT 10

Оператор <=> позволяет сортировать векторы по косинусному расстоянию. Вот и вся «векторная база данных».

4. Генерация. соберите промпт из извлечённых чанков с пометками об источниках, инструкций про обоснованность и отказ, а также самого вопроса.

5. Соберите тестовый набор и итерируйте. Сначала измеряйте извлечение. Чините извлечение, прежде чем винить модель. Прогоняйте заново при каждом изменении.

Шаги 1–4 делаются за выходные. Шаг 5 — это то, что отличает демо от рабочей системы.

Вывод

Главная сложность RAG — не в генерации ответа, а в том, чтобы понять, получила ли модель нужную информацию и как она поведёт себя, если этой информации нет. Поэтому работу системы нужно оценивать по частям: отдельно проверять retrieval, отдельно — обоснованность ответа и способность честно отказываться. 

Надёжный RAG начинается с вопросов, на которые система должна уметь отвечать, заранее размеченных источников и измеримых критериев качества. Сначала измеряйте — и только потом усложняйте.

Комментарии (2)


  1. Innesiya
    17.08.2026 09:05

    Добавление заголовков в начало чанка — это по сути contextual retrieval, и у него есть цена на ingest: если контекст клеится статически из оглавления, это копейки, а если генерируется моделью, стоимость загрузки растет линейно к числу чанков. Отдельно зацепилась за нулевой эффект BM25: на 62 книгах одной доменной области лексика довольно однородная, а гибрид обычно выстреливает там, где в запросах есть коды, артикулы, редкие имена собственные — такого класса вопросов в наборе могло просто не быть. И вопрос по реранкеру: recall вы меряли на k=5, а как ведет себя кривая при k=10 и k=20? Если между 5 и 20 прирост заметный, реранкер мог быть выброшен зря — его смысл как раз в том, чтобы достать топ-20 и ужать до 5.


    1. Ttyicc Автор
      17.08.2026 09:05

      да, contextual retrieval. В данном случае я использовал локальную модель для генерации. Вышло примерно 12 часов на 46000 чанков. Про гибрид, все верно, лексика +/- однородная поэтому и результаты не изменились. В этом и смысл, что перед тем как что-то добавить, провеить а нужно ли. По реранкеру использовал k=50, но в модель попадает только k=5. В результате большинсиво реранкеров не дало осязаемого эффекта, так как топ 5 оставались преженими для модели. То что некоторые переехали с 34 позиции на 10 не имеет значения.