RAG нейросети на Python с нуля: собираем поиск по документам без фреймворков
RAG (Retrieval-Augmented Generation, генерация с опорой на найденные документы) позволяет нейросети отвечать не по памяти, а по вашим файлам, и этот гайд покажет, как собрать такую систему на Python с нуля, без готовых фреймворков.

Любая языковая модель ограничена датой обучения и не видит ваших документов. RAG в нейросетях решает обе проблемы без дорогого дообучения (fine-tuning): модель получает нужный контекст прямо в промпте и отвечает по фактам, а не по догадкам.
Автор оригинального материала, разработчик с опытом в классическом машинном обучении и компьютерном зрении, собрал RAG-пайплайн без LangChain, LlamaIndex и других фреймворков-обёрток. Цель: понять, как RAG в нейросетях устроен «под капотом», от нарезки текста до генерации ответа. Ниже адаптированная пошаговая инструкция по его подходу.
Что понадобится?
- Python 3.10+ и базовое владение языком
- Ollama (локальный сервер для запуска моделей) с тремя моделями:
nomic-embed-textдля превращения текста в векторы (эмбеддинги, числовые представления смысла текста)cross-encoder/ms-marco-MiniLM-L-6-v2для переранжирования результатов поискаllama3.1:8bдля генерации итогового ответа- Библиотека requests (для HTTP-запросов к Ollama)
- Любой текстовый документ, на котором хотите проверить систему
- Примерно 2 от 3 часов на первую рабочую версию
Три компонента RAG: нарезка, поиск, генерация
RAG-система состоит из трёх последовательных этапов. Сначала документ разрезают на небольшие фрагменты (чанки). Затем каждый фрагмент превращают в вектор и ищут среди них ближайшие к вопросу пользователя. Найденные фрагменты подставляют в промпт, и модель генерирует ответ, опираясь на них, а не на свою «память».
Веса модели при этом не меняются вообще. Работает механизм обучения в контексте (in-context learning): блок внимания (self-attention) на этапе инференса (вычисления ответа) связывает вопрос пользователя с переданным контекстом.
Шаг 1. Нарезка текста на чанки (chunking)
Зачем резать? Эмбеддинг-модель сворачивает весь входной текст в один вектор фиксированной длины. Чем длиннее текст, тем больше разнородных тем «смешиваются» в этом векторе и тем хуже поиск по смыслу. Кроме того, у модели есть жёсткий лимит на длину входа (сложность self-attention растёт квадратично).
В оригинальном проекте реализованы две стратегии из четырёх возможных:
- Фиксированная нарезка (Fixed chunking): текст режется по N символов с перекрытием (overlap), чтобы не терять контекст на границах
- Рекурсивная нарезка (Recursive chunking): сначала разделение по абзацам, потом по предложениям, потом по словам
Ещё существуют семантическая и структурная нарезки, но они требуют дополнительных проходов через эмбеддинг-модель или парсер.
Код фиксированной нарезки:
def split_into_chunks(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
chunks = []
if chunk_size <= 0:
raise ValueError("Неверная длина")
if overlap >= chunk_size:
overlap = chunk_size - 1
step = chunk_size - overlap
for i in range(len(text) // step + 1):
start = i * step
end = start + chunk_size
chunk = text[start:end]
if len(chunk) != 0:
chunks.append(chunk)
return chunks
Рекурсивная нарезка работает иначе: пробует разделить текст по самому крупному разделителю (двойной перенос строки), если фрагмент всё ещё велик, переходит к следующему (одинарный перенос, точка с пробелом, пробел). Базовый случай рекурсии: разделители кончились, остаток режется фиксированной нарезкой без перекрытия.
def recursive_split(text, chunk_size=500, separators=None):
if separators is None:
separators = ["\n\n", "\n", ". ", " ", ""]
if len(separators) == 0:
return split_into_chunks(text, chunk_size=chunk_size, overlap=0)
sep = separators[0]
rest_separators = separators[1:]
pieces = text.split(sep)
chunks = []
for piece in pieces:
if len(piece) <= chunk_size:
chunks.append(piece)
else:
sub_chunks = recursive_split(piece, chunk_size=chunk_size,
separators=rest_separators)
chunks.extend(sub_chunks)
return chunks
Когда рекурсия доходит до отдельных слов, их информативность для поиска почти нулевая. Поэтому нужен постпроцессинг, который склеивает обрывки обратно до целевого размера:
def merge_small_chunks(pieces: list[str], chunk_size: int = 500,
separator: str = " ") -> list[str]:
buffer = []
result = []
for piece in pieces:
buffer.append(piece)
if len(separator.join(buffer)) > chunk_size:
buffer.pop()
if buffer:
result.append(separator.join(buffer))
buffer = [piece]
if buffer:
result.append(separator.join(buffer))
return result
Шаг 2. Эмбеддинги и поиск (Retrieval)
Каждый чанк превращается в вектор (эмбеддинг) через bi-encoder (кодирует запрос и документ независимо). Похожесть двух текстов определяется косинусным сходством: чем меньше угол между векторами, тем ближе их смысл. Значение 1 означает максимальную близость, 0 означает отсутствие связи.
Для повышения точности после быстрого поиска по всей базе bi-encoder отобранные кандидаты проходят через cross-encoder (подаёт запрос и документ как единую последовательность, обрабатывает все токены (минимальные единицы текста для модели) одновременно). Cross-encoder точнее, но медленнее: для каждой пары запрос-документ нужен отдельный проход.
Получение эмбеддинга через Ollama:
def get_embedding(text: str, model: str = "nomic-embed-text") -> list[float]:
response = requests.post(
"http://localhost:11434/api/embeddings",
json={"model": model, "prompt": text}
)
return response.json()["embedding"]
Далее реализуется класс VectorStore, который хранит чанки и их векторы и выполняет поиск ближайших по косинусному сходству.
Шаг 3. Генерация ответа
Найденные фрагменты подставляются в системный промпт (инструкция, задающая поведение модели) вместе с вопросом пользователя. Модель llama3.1:8b через Ollama генерирует ответ, опираясь на переданный контекст. Веса модели не затрагиваются.
Допустим, у вас есть внутренний регламент компании на 50 страниц. Вы нарезаете его рекурсивной стратегией с chunk_size=500. Получается около 200 чанков. Каждый превращается в вектор через nomic-embed-text. Пользователь спрашивает: «Какой порядок согласования договоров?». Bi-encoder находит 10 ближайших чанков, cross-encoder переранжирует их и оставляет 3 самых релевантных. Эти 3 фрагмента попадают в промпт, и llama3.1:8b формирует ответ со ссылкой на конкретные пункты регламента, а не выдумывает процедуру.
- Слишком большой chunk_size. Если чанк на 2000 символов, в вектор «замешивается» несколько тем сразу, и поиск теряет точность. Начинайте с 500 символов и подбирайте экспериментально.
- Нулевое перекрытие (overlap). Без перекрытия важное предложение на границе двух чанков разрезается пополам и теряет смысл. Overlap в 50 от 100 символов страхует от этого.
- Пропуск постпроцессинга после рекурсивной нарезки. Без склейки мелких кусков вы получаете отдельные слова с нулевой информативностью для поиска.
- Только bi-encoder без переранжирования. Быстрый поиск находит кандидатов, но без cross-encoder в топ-3 могут попасть «примерно похожие» чанки, а не точные. Добавление реранкера заметно повышает качество ответов.
- Галлюцинации (когда модель уверенно выдумывает то, чего не было) не исчезают полностью. RAG снижает их частоту, но если релевантного чанка в базе нет, модель может сгенерировать правдоподобный, но ложный ответ.
Что делать с этим прямо сейчас?
Разработчику: соберите минимальный пайплайн по шагам выше, потратьте вечер. Понимание того, как RAG в нейросетях работает без фреймворков, потом сэкономит дни отладки, когда LangChain или LlamaIndex ведут себя непрозрачно.
Автору Дзена или копирайтеру: RAG-подход можно использовать для создания бота-помощника по вашим же материалам. Загружаете архив статей, задаёте вопрос, получаете ответ с опорой на собственные тексты, а не фантазии модели.
Предпринимателю: все три модели из этого гайда запускаются локально через Ollama, без облака и без передачи данных за рубеж. Для внутренних регламентов, баз знаний и документации это принципиально: данные не покидают вашу машину.
В России доступны и альтернативные эмбеддинг-модели, например от Сбера (GigaChat) или Яндекса (YandexGPT), но для локального запуска без API связка Ollama плюс открытые модели остаётся самым простым вариантом.
RAG-система без фреймворка, это как собрать мебель без инструкции IKEA: дольше, зато понимаешь каждый винт. По моему опыту, именно это понимание отделяет того, кто «использует ИИ», от того, кто строит с ним продукт. Честная оговорка: чистый RAG не гарантирует точных ответов. Если нужного фрагмента в базе нет, модель всё равно что-то сгенерирует, и это «что-то» может выглядеть убедительно. Добавляйте в системный промпт явную инструкцию: «Если в контексте нет ответа, скажи: информация не найдена».
Хотите быстрее разобраться в нейросетях для контента?
На dzen.guru собраны практические инструменты и гайды для авторов, которые работают с ИИ каждый день.
Попробовать инструментыСобрать рабочий RAG-пайплайн за вечер реально, и после этого любой фреймворк перестаёт быть чёрным ящиком: вы точно знаете, что внутри нарезка, векторы, поиск, промпт, ответ.

Основатель dzen.guru. Эксперт по монетизации и продвижению на Дзен. Автор курса «Старт на Дзен 2026».
Читайте также

Suno, нейросеть для создания музыки, получила MIDI и синтезатор: генератор треков стал DAW
Suno выпустила Studio 2.0, обновление своего онлайн-редактора музыки, которое добавляет поддержку MIDI, встроенные эффекты, синтезатор и чат-помощника,…

Дата-центры ИИ утроят цены на газ в ряде регионов США, предупреждает Noreva
Крупнейшие технологические компании, включая Amazon, Google, Meta и Microsoft, массово строят собственные газовые электростанции для дата-центров ИИ, но…

Масштабирование ИИ на 500+ человек: какие метрики работают, а курсы нет
Управление внедрением ИИ в крупных командах: практика масштабирования на 500+ человек Команда банковского Data Office за год внедрения ИИ-агентов (программ,…
Комментарии