Перейти к содержимому

Как устроен RAG внутри

Внутри любой RAG-системы работают два независимых цикла, и путают их постоянно. Индексация идёт заранее, по расписанию, и о ней никто не думает, пока она не сломается. Ответ на запрос проходится целиком на каждый заданный вопрос, за секунду-две.

Разделять их важно практически: почти все проблемы качества лежат в первом цикле, а замечают их во втором.

flowchart TB
    subgraph IDX ["Индексация: заранее и по расписанию"]
      direction TB
      SRC["Источники «Маркета»: ADR, вики, runbook, задачи, инциденты"]
      CHUNK["Разбивка на фрагменты + метаданные"]
      EMB["Превращение фрагментов в векторы"]
      STORE["Хранилище с поиском"]
      SRC --> CHUNK --> EMB --> STORE
    end
    subgraph ASK ["Ответ: на каждый вопрос"]
      direction TB
      Q["Вопрос пользователя"]
      FIND["Поиск подходящих фрагментов"]
      PROMPT["Сборка промпта: вопрос + найденные фрагменты"]
      ANS["Ответ со ссылками на источники"]
      Q --> FIND --> PROMPT --> ANS
    end
    STORE -.->|"поиск идёт по готовому индексу"| FIND

Левая дорожка проходится заранее и обновляется по расписанию; правая проходится на каждый заданный вопрос.

Система забирает документы из мест, где они живут: репозиториев, вики, трекера задач, базы инцидентов. Забирает не разово, а постоянно: как только документ меняется, обновляется и его фрагмент в индексе.

Здесь принимается решение, которое потом дорого менять: какие метаданные тащить вместе с текстом. Минимум, который понадобится почти наверняка:

  • Домен и команда-владелец. Без этого невозможна маршрутизация запросов и невозможно спросить с кого-то за качество ответов.
  • Дата последнего изменения. Чтобы отличать актуальный регламент от прошлогоднего черновика.
  • Ссылка на оригинал. Она попадёт в ответ и позволит человеку проверить.
  • Права доступа. Кто имеет право видеть этот документ. Добавить это поле задним числом, когда индекс уже собран и работает, заметно дороже, чем заложить сразу.
  • Тип документа. ADR, runbook, задача, страница вики. Пригодится, чтобы при прочих равных предпочитать архитектурное решение обсуждению в тикете.

Документы не кладут в индекс целиком. Причины две: сорокастраничный регламент выплат селлерам не влезет в контекст вместе с остальными результатами, а его усреднённый «смысл» слишком размыт, и находиться он будет на любой вопрос про деньги и ни на один точно.

Поэтому документ режут на фрагменты (chunks). Резать по числу символов плохо: разрез приходится на середину предложения или отрывает таблицу от её заголовка. Резать нужно по смысловым границам: разделам, подразделам, отдельным пунктам регламента, отдельным функциям в коде.

Практические ориентиры:

  • Размер фрагмента: примерно от абзаца до раздела. Слишком мелкие теряют контекст («в этом случае комиссия не удерживается»: в каком случае?), слишком крупные размывают поиск.
  • Небольшое перекрытие между соседними фрагментами спасает мысль, оказавшуюся на границе разреза.
  • Каждый фрагмент должен нести свой заголовочный путь: «Регламент выплат → Возвраты → Возврат после отгрузки». Иначе, вырванный из документа, он непонятен ни модели, ни человеку.

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

Чтобы искать по смыслу, а не по буквам, каждый фрагмент превращают в эмбеддинг (embedding), длинный список чисел, координаты текста в пространстве смыслов. Работает это так, что тексты о близких вещах получают близкие координаты. Дальше поиск сводится к геометрии: находим фрагменты, чьи координаты ближе всего к координатам вопроса.

Что это даёт: пользователь спрашивает «когда селлеру приходят деньги», а находится раздел «сроки перечисления вознаграждения продавцу». Ни одного общего слова, но смысл тот же; обычный поиск по словам здесь промахнулся бы.

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

Поиск возвращает не один фрагмент, а несколько лучших, обычно от трёх до десяти. Число выбирается не наугад:

  • Мало фрагментов. Высокий риск, что нужного среди них нет. Модель ответит по тому, что дали, и ответ будет неполным, хотя выглядеть будет уверенно.
  • Много фрагментов. Контекст наполняется приблизительно подходящим текстом. Работает та же «потеря в середине», о которой рассказано на странице Токены и контекстное окно: нужный фрагмент есть, но тонет среди шести похожих.

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

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

  • Отвечать только по приведённым фрагментам. Без этого модель охотно дополнит ответ общими знаниями о том, как «обычно» устроены маркетплейсы, и отличить это от факта про «Маркет» будет невозможно.
  • Честно говорить, когда данных не хватает. «В доступных документах этого нет» тоже правильный ответ, и система должна его уметь. Если такого ответа она не даёт никогда, значит, вместо него вы получаете выдумку.

Плюс требование ссылаться на источники в ответе. Это не украшение: по ссылке человек проверит ответ за секунды вместо того, чтобы верить на слово.

Пять типовых поломок, отсортированных по частоте:

  • Документ изменили, а индекс не обновили, и система уверенно отвечает по прошлогоднему регламенту.
  • Нарезка разорвала смысл: нужный фрагмент нашёлся, но без условия, к которому относился.
  • Вопрос задан не тем словарём, что документ, и точного идентификатора в нём тоже нет.
  • Нужный фрагмент вообще не попал в выдачу, потому что его вытеснили похожие.
  • Фрагменты пришли правильные, но модель ответила «из головы» и разошлась с ними.

Первые четыре относятся к поиску, последняя к генерации. Различать их важно, а как это измерять, разобрано на странице Качество и оценка.

Автор учебника: Шахматов Алексей