Все уроки · 44
Способы реализации RAG
SDK напрямую, LangChain, LlamaIndex, Haystack или полностью вручную: компромиссы между контролем, удобством и привязкой к инструменту — и минимальный RAG в ~30 строк на Python без API-ключей.
Зачем это вам, даже если вы никогда не напишете такой код
Каждые несколько недель кто-нибудь спрашивает меня: «LangChain или LlamaIndex?» — как будто один из них и есть правильный ответ. Выбирать между ними стоит по тому, что каждый вариант решает за вас: любое решение, которое библиотека принимает от вашего имени, вам потом придётся либо терпеть, либо отменять.
Через две минуты вы сможете прочитать RAG-пайплайн целиком, сверху вниз, — даже если почти не программируете — и будете знать, какой из пяти способов его построить подходит вашей ситуации. Это ценнее любого названия фреймворка.
Несколько терминов, которые встретятся дальше: векторная БД — база данных, которая хранит эмбеддинги (списки чисел, передающие смысл) и находит ближайшие к вашему вопросу (как это работает). Фреймворк — это набор готовых «кубиков»: загрузчики, разделители, поисковые компоненты (retrievers). Это удобно, но фреймворк навязывает свои решения.
Пять способов построить один и тот же пайплайн
RAG-пайплайн из прошлого урока — разбиение, эмбеддинг, индексация, поиск, сборка, генерация — можно построить на разных уровнях абстракции. От самого низкого к самому высокому:
- Полностью вручную — API эмбеддингов или локальная модель, numpy, ваши собственные строки промптов. Полный контроль и полная ответственность.
- SDK напрямую — SDK провайдера модели плюс клиент векторной БД. Стандартный выбор для реальной эксплуатации: минимум магии, легко понять, что происходит.
- LangChain — универсальный фреймворк для LLM. Огромная экосистема, множество интеграций, слои абстракции на несколько уровней вглубь.
- LlamaIndex — фреймворк, нацеленный именно на загрузку данных и поиск по ним. Часто самый быстрый путь к хорошему прототипу RAG.
- Haystack — фреймворк компании deepset, построенный вокруг пайплайнов, со строгой типизацией. Популярен в рабочих системах поиска и ответов на вопросы.
Каждый вариант реализует одни и те же шесть шагов. Различия — в том, сколько всего он скрывает, сколько решает за вас и насколько трудно от него уйти:
| Путь | Что вы пишете | Что он скрывает | Сложность ухода | Выбирайте, когда |
|---|---|---|---|---|
| Полностью вручную | вызовы эмбеддингов, сортировка, строки промптов | ничего | нет — всё ваше | вы учитесь или пайплайн небольшой и нестандартный |
| SDK напрямую + векторная БД | несколько сотен строк вашего собственного связующего кода | ничего | нет | вы выпускаете узкоспециализированный продукт |
| LangChain | цепочки из готовых компонентов | разбиение, поиск, сборку промпта | средняя — её типы расползаются по вашему коду | вам нужна широта интеграций |
| LlamaIndex | индексы и поисковые движки (query engines) | загрузку данных, разбиение, настройки поиска по умолчанию | средняя | нужен «чат с этими 500 PDF» — на этой неделе |
| Haystack | типизированные компоненты пайплайна | соединение компонентов, часть поиска | средняя | платформа поиска и ответов на вопросы, над которой работает команда |
Эти варианты различаются объёмом решений, которые за вас уже кто-то принял. Поэтому спросить стоит так: какие из этих готовых решений я согласен унаследовать?
Ручной вариант от начала до конца
Вся идея, сжатая в два небольших файла: «игрушечная» модель эмбеддингов и «игрушечная» LLM (ключи API не нужны — важна механика), а затем сам цикл RAG:
1# Крошечные заглушки вместо настоящей модели эмбеддингов и LLM.2# Замените эти две функции вызовами настоящих API — остальное не изменится.3import math4 5def embed(text):6 # Настоящая модель эмбеддингов превращает текст в длинный список чисел7 # (сотни или тысячи), в котором закодирован его смысл.8 # Мы подделываем её четырьмя числами: как часто встречаются слова "refund",9 # "password" и "plan", плюс длина текста.10 # Для понимания механики хватает; для настоящего поиска — нет.11 words = text.lower().split()12 return [13 words.count(#98c379">"refund") / (len(words) + 1),14 words.count(#98c379">"password") / (len(words) + 1),15 words.count(#98c379">"plan") / (len(words) + 1),16 len(words) / 100,17 ]18 19def llm(prompt):20 # Настоящая LLM читает промпт и пишет ответ.21 # Наша только печатает то, что ей передали, — чтобы вы точно увидели,22 # какие фрагменты дошли до модели. В этом вся суть RAG:23 # управляете тем, что попадает сюда, — управляете ответом.24 for line in prompt.splitlines():25 if line.startswith(#98c379">"[") or line.startswith("Q:"):26 print(line)27 return #98c379">"Answer grounded in the chunks above."28 29def cosine(a, b):30 # «Косинусное сходство»: насколько близки два списка чисел?31 # 1.0 — почти одинаковые, 0.0 — никак не связаны.32 # Именно так поиск решает, какие фрагменты останутся.33 dot = sum(x * y for x, y in zip(a, b))34 na = math.sqrt(sum(x * x for x in a))35 nb = math.sqrt(sum(x * x for x in b))36 return dot / (na * nb) if na and nb else 0.0Читайте две вкладки по порядку: mocks.py — это актёры (поддельная модель
эмбеддингов и поддельная LLM), rag.py — сама пьеса.
Если код — не ваш язык, вот та же история простыми словами:
- Три предложения изображают справочный центр вашей компании.
- Индексация (один раз, офлайн): каждое предложение превращается в список чисел — эмбеддинг, — который приблизительно передаёт его смысл. Вы храните и предложение, и его числа. Это ваш индекс, в котором можно искать.
- Вопрос проходит точно такое же преобразование. Одна и та же линейка с обеих сторон — поэтому это работает.
- Поиск: сравните числа вопроса с числами каждого фрагмента и оставьте три ближайших. Никакого «понимания» здесь нет — только арифметика над списками чисел.
- Сборка: вставьте эти фрагменты над вопросом — как если бы вы протянули человеку страницу заметок и сказали: «отвечай, используя только это и ничего больше».
- Генерация: модель читает заметки и пишет ответ.
Вот и весь RAG: шесть ходов, ни один из которых сам по себе не хитёр.
В реальной эксплуатации embed и llm становятся настоящими вызовами API, а
отсортированный список — запросом к векторной базе, но форма кода не меняется. В
этом и суть: RAG — обычный пайплайн, ничего сложного в нём нет. И качество пайплайна определяет
шаг 4: если поиск передаст модели не те три фрагмента, ответ не спасёт ни
фреймворк, ни модель.
Фреймворки: сильные и слабые стороны
LangChain позволяет запуститься за минуты: загрузчики документов, разделители, обёртки над векторными хранилищами, цепочки. Цена — глубина абстракции: когда качество поиска хромает, вы можете обнаружить, что отлаживаете фреймворк вместо своего пайплайна. Его сильная сторона — широта: один API поверх десятков провайдеров.
LlamaIndex начинает со стороны данных. Индексы, поисковые движки (query engines) и разумные настройки RAG по умолчанию составляют основу продукта. Для задачи «чат с этими 500 PDF» он часто оказывается самым быстрым путём. Цена того же рода, что и у LangChain: вы принимаете его абстракции.
Haystack тяготеет к системам поиска для реальной эксплуатации: явные пайплайны, типизированные компоненты, проще тестировать. Сообщество меньше, чем у LangChain, зато интерфейс предсказуемее.
У всех трёх есть общая проблема: они быстро меняются и при этом ломают совместимость. Зафиксируйте версию, перед обновлением читайте список изменений (changelog) и держите собственную тонкую обёртку вокруг тех немногих вызовов, которые вы используете, — тогда несовместимое изменение затронет один файл, а не тридцать.
Привязка реальна, но неглубока
Привязка к фреймворку в RAG — это в основном привязка к понятиям (загрузчики, поисковые компоненты, цепочки). Чужой инфраструктуры за ними нет. Всё подключается к одним и тем же «кирпичикам»: API эмбеддингов, векторное хранилище и API генерации. При уходе с фреймворка переписывают связующий код, а данные остаются на месте.
Как выбирать
Правило выбора, которое не устарело:
- Изучаете идею? Соберите всё вручную один раз, как в примере выше. Вам больше никогда не придётся гадать, что скрывает абстракция поискового компонента (retriever).
- Выпускаете узкоспециализированный продукт? SDK напрямую плюс клиент векторной БД. Большинство RAG-систем в реальной эксплуатации устроены именно так — несколько сотен строк, которые вы полностью понимаете.
- Быстро делаете прототип на разношёрстных документах? LlamaIndex или LangChain. Примите их абстракции, чтобы выиграть несколько дней.
- Большая платформа поиска и ответов на вопросы, над которой работает команда? Структура Haystack окупает себя.
Фреймворк загружает данные, а за качество отвечаете вы
Трудные 20% RAG — стратегия разбиения на фрагменты, оценка качества поиска, реранкинг, дисциплина в промптах — не встроены ни в один фреймворк. Какой бы путь вы ни выбрали, работа над качеством всё равно остаётся за вами.
Вы переходите на другую модель эмбеддингов. Что нужно сделать?
Какую часть RAG не делает за вас ни один фреймворк?
Разработчик-одиночка выпускает узкий продукт. Совет этого урока:
Итог
Теперь вы можете построить RAG пятью разными способами и знаете компромисс каждого: контроль, удобство и привязка к концепциям. Всё это связывает одна нить: то, что видит модель, — системный промпт, найденные фрагменты, вопрос, — это контекст, который вы сконструировали. Управлять им осознанно — отдельное умение.
Если какой-то из шести шагов всё ещё кажется туманным, пройдите урок RAG-пайплайн шаг за шагом ещё раз — это те же шесть шагов, по одному.