AI / LLM
Архитектура RAG: пайплайн, который ломается на ретривале, а не на генерации
Выкатили саппорт-бота на корпусе документов. Пользователь спрашивает: «какой у нас отток за Q3?» Ретривер достаёт чанк с оттоком за Q2 и другой с выручкой за Q3 — близко, но не ответ. Модель не говорит «не могу найти отток за Q3». Она смешивает два чанка и выдаёт уверенное, конкретное, неверное число. Месяц никто не ловит ошибку, потому что ответ выглядит правильным. Шаг генерации был безупречен. Шаг ретривала промахнулся, а промах ретривала не падает громко — он бегло галлюцинирует.
Пайплайн от начала до конца
RAG (retrieval-augmented generation) — это фиксированная последовательность этапов, и большая часть продакшен-боли живёт в ранних. На этапе индексации ты чанкуешь документы на пассажи, эмбеддишь каждый чанк в вектор (массив float-ов фиксированной длины) и кладёшь векторы в векторное хранилище с индексом приближённого ближайшего соседа (ANN). На этапе запроса ты эмбеддишь вопрос, делаешь top-k ретривал — достаёшь k ближайших чанков, опционально реранкируешь их более точной моделью, вписываешь выживших в бюджет контекстного окна, собираешь промпт и даёшь LLM генерировать.
Два факта переосмысливают всё ниже. Первый: генератор настолько хорош, насколько хорошо то, что ему отдал ретривал — мусорные чанки на входе, уверенный мусор на выходе. Второй: в продакшен-RAG доминирующий провал — это ретривал, а не генерация. Индустриальные разборы относят большинство плохих ответов на сторону ретривала: достали не те чанки, или нужный чанк вообще не был проиндексирован. Так что инженерные усилия, которые окупаются, в основном выше по течению, до модели.
Чанкинг: лезвие «размер vs recall»
Чанкинг — решение, которое тихо задаёт твой потолок. Эмбеддишь чанк — и сжимаешь весь его смысл в один вектор; чанк — это атомарная единица, которую ретривал вообще способен вернуть. Ошибёшься — и никакой реранкер не спасёт.
Трейдофф острый. Маленькие чанки (скажем, 128–256 токенов) эмбеддятся точно — один вектор, одна плотная мысль — поэтому поиск по сходству целится хорошо. Но они дробят любое правило, пересекающее границу: условие попадает в чанк 7, исключение — в чанк 8, и top-k, взявший только один, вернёт полуправду. Большие чанки (800–1000+ токенов) держат контекст целым, но размывают эмбеддинг — один вектор теперь усредняет несколько идей, поэтому сигнал для конкретного подфакта, который тебе нужен, вымывается, и recall для узких запросов падает. Частая продакшен-точка старта — 300–800 токенов для прозы с оверлапом 10–15%, дальше тюнят на eval-сете.
Оверлап — дешёвая страховка от потери на границе: повтори последние ~10–15% каждого чанка в начале следующего, чтобы факт, оседлавший шов, выжил хотя бы в одном чанке. Слишком много оверлапа — и индекс заполняется почти-дубликатами: один и тот же пассаж достаётся трижды, съедая слоты top-k и бюджет контекста.
| Ручка | Крутишь вверх → | Крутишь вниз → | Что взвешивает сеньор |
|---|---|---|---|
| Размер чанка | Контекст цел, но размытый эмбеддинг бьёт по recall узких запросов | Точный эмбеддинг, но правила рвутся по границам | Подгони чанк под единицу-носитель ответа; тюнь на eval |
| Оверлап | Граничные факты выживают; индекс пухнет от дубликатов | Худой индекс; факты на шве теряются | ~10–15% для прозы; меньше для FAQ |
| Top-k | Выше recall; раздувание контекста, стоимость и lost-in-the-middle | Дёшево, сфокусированно; один промах = нет ответа | Достань широко (k=20–50), реранкни до 3–8 |
| Размерность эмбеддинга | Лучше точность; больше индекс, медленнее ANN, больше хранилища | Быстрее, дешевле поиск; часть точности теряется | 1024 dims часто ~= 3072 при 1/3 хранилища |
Эмбеддинг и векторное хранилище
Модель эмбеддингов отображает текст в вектор, чья геометрия кодирует смысл — близкие векторы означают близкий смысл. Размерность — реальный рычаг стоимости, а не деталь. OpenAI text-embedding-3-small по умолчанию 1536 dims; text-embedding-3-large — 3072. Большие векторы обычно ретривят точнее, но раздувают индекс и замедляют ANN-поиск: грубо, падение с ~1536 до ~768 dims может срезать латентность поиска примерно с 50ms к 20ms. С усечением в стиле Matryoshka можно гонять text-embedding-3-large на 1024 dims и приземлиться близко к качеству 3072 dims за треть хранилища (≈4KB против ≈12KB на вектор) — дефолтный размен сеньора, когда важны хранилище и p99-латентность.
Векторное хранилище не сканирует каждый вектор — на масштабе в миллионы это слишком медленно. Оно использует ANN-индекс (HNSW — частый выбор), который меняет щепотку recall на огромное ускорение, отвечая на запросы ближайшего соседа за единицы–десятки миллисекунд. «Приближённый» — слово, которое кусает: индекс может тихо промахнуться мимо настоящего ближайшего соседа, и чанк с ответом существует, но никогда не достаётся. Этот промах невидим — он выглядит идентично «ответа нет в корпусе».
Top-k, реранкинг и бюджет контекста
Top-k ретривал — ручка «recall vs шум». Маленькое k (3) дёшево и сфокусированно, но беспощадно: один промах ретривала — и ответа в промпте просто нет. Большое k (50) почти гарантирует, что нужный чанк где-то в наборе — но теперь ты набил контекст 45 нерелевантными пассажами, которые стоят токенов, денег, латентности и, хуже того, отвлекают.
Паттерн сеньора двухстадийный: доставай широко, реранкируй узко. Стадия один (ANN-поиск по эмбеддингам) оптимизирует recall — широкая сеть, k=20–50 кандидатов, дёшево. Стадия два гоняет кросс-энкодер реранкер, который читает запрос и каждого кандидата вместе (а не как независимые векторы) и оценивает релевантность куда точнее, затем оставляет top 3–8. Реранкеры медленнее на единицу — поэтому их и запускают только на шорт-листе, а не на всём корпусе.
Что выжило, должно влезть в бюджет контекстного окна: окно модели конечно и делится с системным промптом, вопросом и генерацией. Даже с окном на 128k токенов больше доставленного текста не бесплатно и даже не нейтрально — и здесь кусает порядок.
Почему это работает
«Lost in the middle» (потеря в середине): у LLM наблюдается U-образный перекос внимания — они лучше всего внимают началу и концу контекста и хуже всего середине, независимо от релевантности. Когда чанк-носитель ответа оседает в середине длинного набитого промпта, измеренная точность может упасть на 30%+ против того же чанка по краям. Практический ход: не сваливай 50 чанков в порядке ретривала. Реранкни, оставь немного и помести самое сильное доказательство в самое начало или самый конец собранного контекста.
Провал, который определяет RAG в продакшене
Опасный провал — не падение, а уверенный неверный ответ. Когда ретривал промахивается (промах ANN, плохой чанкинг, факт не проиндексирован или проиндексирован, но устарел), модель всё равно получает какой-то контекст и обучена быть полезной, поэтому экстраполирует из того, что рядом, и выдаёт беглый, конкретный, неверный ответ вместо «не знаю». Баг с оттоком за Q3 из Hook — ровно это: близкие-но-неверные чанки, смешанные с уверенностью.
Два родственника делают хуже. Устаревший индекс: источник изменился, эмбеддинги — нет, и запрос, работавший в прошлый вторник, сегодня возвращает прошлоквартальную политику, без единого изменения кода и без ошибки. Отравленный индекс: атакующий (или небрежный ингест) подсаживает зловредный или противоречивый чанк; исследования показывают, что один внедрённый пассаж может перевернуть ответ, и модель не отметит противоречие — она выберет одно и подаст как решённый факт. Митигации все про признание незнания: ставь гейт на скор сходства ретривала, инструктируй модель отвечать только из предоставленного контекста и говорить «не знаю», когда ничто не проходит порог, и держи индекс свежим и под контролем доступа.
RAG-бот по юридическим документам не должен выдумывать ссылки, а ответы могут отставать от корпуса на час. Выбери схему ретривала.
В продакшен-RAG пользователь получил уверенный, но неверный ответ. Где чаще всего кроется вина?
Ты достаёшь 40 чанков-кандидатов и кладёшь самый релевантный ровно в середину промпта. Что предсказывает «lost in the middle»?
Расставь пайплайн RAG на этапе запроса от вопроса к ответу:
- 1 Заэмбеддить вопрос пользователя в вектор запроса
- 2 ANN top-k ретривал: достать k ближайших чанков (широко, recall-first)
- 3 Реранкнуть кандидатов кросс-энкодером; оставить top немного
- 4 Вписать выживших в бюджет контекста; сильнейшее доказательство — по краям
- 5 Собрать промпт и дать LLM генерировать (по контексту или абстейн)
- 01Разбери, почему ретривал, а не генерация, — доминирующий провал в продакшен-RAG, и что промах ретривала на самом деле делает.
- 02Объясни двухстадийный паттерн «достань широко, потом реранкни узко» и почему одного эмбеддинг-top-k мало.
RAG — это задача ретривала в костюме генерации, и почти вся продакшен-боль выше модели. Чанкинг задаёт потолок: маленькие чанки эмбеддятся точно, но рвут правила по границам, большие держат контекст, но размывают эмбеддинг и бьют по recall — поэтому подгоняй чанк под единицу-носитель ответа, бери оверлап ~10–15% и тюнь на eval. Размерность эмбеддинга — реальный рычаг стоимости (1536 против 3072; 1024 часто ≈ 3072 за треть хранилища), а ANN-индекс меняет щепотку recall на миллисекундный поиск — но «приближённый» означает, что он может тихо промахнуться мимо настоящего соседа. Достань широко (k=20–50), потом реранкни узко кросс-энкодером до 3–8, впиши в бюджет контекста и помести сильнейшее доказательство по краям, чтобы обойти lost-in-the-middle, где точность в середине может упасть на 30%+. Определяющий провал — это промах, который модель замазывает уверенным, беглым, неверным ответом; устаревший и отравленный индексы делают хуже. Фикс — признавать незнание: ставь гейт на скор ретривала, инструктируй модель отвечать только из контекста и говорить «не знаю» и держи индекс свежим и под контролем доступа.