Игорь Градов
Игорь Градов
6 мин
ai

RAG нейросети на Python с нуля: собираем поиск по документам без фреймворков

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

RAG нейросети на 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 плюс открытые модели остаётся самым простым вариантом.

Мнение редакции dzen.guru

RAG-система без фреймворка, это как собрать мебель без инструкции IKEA: дольше, зато понимаешь каждый винт. По моему опыту, именно это понимание отделяет того, кто «использует ИИ», от того, кто строит с ним продукт. Честная оговорка: чистый RAG не гарантирует точных ответов. Если нужного фрагмента в базе нет, модель всё равно что-то сгенерирует, и это «что-то» может выглядеть убедительно. Добавляйте в системный промпт явную инструкцию: «Если в контексте нет ответа, скажи: информация не найдена».

Хотите быстрее разобраться в нейросетях для контента?

На dzen.guru собраны практические инструменты и гайды для авторов, которые работают с ИИ каждый день.

Попробовать инструменты

Собрать рабочий RAG-пайплайн за вечер реально, и после этого любой фреймворк перестаёт быть чёрным ящиком: вы точно знаете, что внутри нарезка, векторы, поиск, промпт, ответ.

Поделиться:TelegramVK
Игорь Градов
Игорь Градов

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

Комментарии

Читайте также

Suno, нейросеть для создания музыки, получила MIDI и синтезатор: генератор треков стал DAW
ai

Suno, нейросеть для создания музыки, получила MIDI и синтезатор: генератор треков стал DAW

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

5 мин
Дата-центры ИИ утроят цены на газ в ряде регионов США, предупреждает Noreva
ai

Дата-центры ИИ утроят цены на газ в ряде регионов США, предупреждает Noreva

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

5 мин
Масштабирование ИИ на 500+ человек: какие метрики работают, а курсы нет
ai

Масштабирование ИИ на 500+ человек: какие метрики работают, а курсы нет

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

6 мин