Материал подготовлен в рамках курса «ИИ для Python‑разработчиков».

Привет, Хабр! Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.

Вы когда‑нибудь пытались спросить у ChatGPT о том, как работает ваш собственный проект? Модель вежливо отвечает, но её ответы — это общие шаблоны, основанные на обучении на миллионах чужих репозиториев.

Она не знает, что в вашей системе используется кастомная авторизация через Redis, а соединение с базой данных настраивается через переменные окружения с префиксом MY_APP_.

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

Retrieval‑Augmented Generation (RAG) решает эту проблему. Вместо того чтобы заставлять модель «помнить» всё, мы даём ей доступ к нужным документам в момент ответа. Модель конечно не становится умнее, но она получает шпаргалку.

Что такое RAG и почему это работает

RAG — это архитектурный паттерн, который добавляет к LLM этап поиска. Вместо того чтобы отвечать на основе заученных данных, модель сначала находит релевантные документы в вашей базе знаний, а затем использует их как контекст для генерации ответа.

Это как экзамен, на который вы приносите учебник. Вы не обязаны помнить каждую формулу — вы знаете, где её искать.

В типичном RAG‑пайплайне четыре шага:

  1. Вы получаете вопрос от пользователя.

  2. Превращаете вопрос в вектор — числовое представление смысла.

  3. Ищете в векторной базе документы, чьи векторы ближе всего к вектору вопроса.

  4. Подставляете найденные документы в промпт и отправляете в LLM.

Модель отвечает на основе ваших документов, а не на основе общих знаний. Ответ становится точным, актуальным и относящимся к вашему контексту.

Эмбеддинги — как компьютер понимает смысл

Ключевая технология, которая положена в основу RAG, — это эмбеддинги, векторы чисел, которые представляют смысл текста. Например, два предложения со схожим смыслом будут иметь похожие векторы, даже если в них нет общих слов.

Возьмём классический пример:

  • “A beginner's guide to engine troubleshooting”

  • “Motor diagnostics for intermittent power loss”

Эти предложения говорят об одном и том же, но не имеют общих слов. Ключевой поиск не свяжет их — он ищет точное совпадение.

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

На практике вы загружаете модель эмбеддингов, например all-MiniLM-L6-v2 от Hugging Face:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")

documents = [
    "A beginner's guide to engine troubleshooting",
    "Motor diagnostics for intermittent power loss"
]

# Получаем векторы
embeddings = model.encode(documents)
# embeddings[0] и embeddings[1] будут близки по косинусному расстоянию

В результате, семантический поиск ищет смысл, а не слова. Когда пользователь спрашивает «как настроить подключение к БД», система найдёт документы про «конфигурацию пула соединений», даже если точных совпадений нет.

Полный RAG‑пайплайн на Python

Теперь давайте перейдем к практике и соберём рабочий пример. Мы будем использовать Sentence Transformers для генерации эмбеддингов и FAISS или ChromaDB для векторного поиска и OpenAI/Groq API для генерации ответа.

Шаг 1. Подготовка документов

Документы редко помещаются в контекст целиком — они слишком большие. Их нужно разбить на чанки — смысловые куски по 500–1000 слов с небольшим перекрытием, чтобы не терять контекст на границах.

def chunk_text(text, chunk_size=500, overlap=50):

    words = text.split()
    chunks = []

    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        chunks.append(chunk)
    return chunks

Шаг 2. Создание векторного индекса

Далее для каждого чанка генерируем эмбеддинг и сохраняем его в базу. Для этого мы воспользуемся ChromaDB — простой векторной базой для локальной разработки:

import chromadb

from sentence_transformers import SentenceTransformer
 
client = chromadb.Client()
collection = client.create_collection("docs")
 
model = SentenceTransformer("all-MiniLM-L6-v2")
 
for i, chunk in enumerate(chunks):
    embedding = model.encode(chunk).tolist()
    collection.add(
        ids=[str(i)],
        embeddings=[embedding],
        documents=[chunk]
    )

Шаг 3. Поиск по вопросу

Когда приходит запрос, мы генерируем эмбеддинг вопроса и ищем в базе самые близкие чанки:

def retrieve(query, top_k=3):
    query_embedding = model.encode(query).tolist()
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k
    )
    return results["documents"][0]

Шаг 4. Генерация ответа с контекстом

После этого, найденные документы подставляются в промпт, и LLM генерирует ответ на их основе:

from openai import OpenAI

client = OpenAI()

def ask(query):
    docs = retrieve(query)
    context = "\n\n".join(docs)
    
    prompt = f"""
    Ответь на вопрос, используя только информацию из предоставленных документов.
    Если в документах нет ответа, скажи об этом прямо.
    
    Документы:
    {context}
    
    Вопрос: {query}
    """
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

Чат‑бот по документации проекта

Теперь представьте, что у вас есть репозиторий с 50 файлами документации. Вы хотите, чтобы разработчики могли задавать вопросы и получать ответы, основанные именно на вашей документации, а не на общих знаниях модели.

Давайте соберём это в класс, как в проектах SimpleRAG или RAG‑STARTER:

class ProjectRAG:
    def __init__(self, api_key):
        self.embedder = SentenceTransformer("all-MiniLM-L6-v2")
        self.client = chromadb.Client()
        self.collection = self.client.create_collection("project_docs")
        self.llm = OpenAI(api_key=api_key)
    
    def add_document(self, file_path):
        with open(file_path, 'r') as f:
            text = f.read()
        
        chunks = self._chunk_text(text)
        embeddings = self.embedder.encode(chunks)
        
        self.collection.add(
            ids=[f"{file_path}_{i}" for i in range(len(chunks))],
            embeddings=embeddings.tolist(),
            documents=chunks,
            metadatas=[{"source": file_path} for _ in chunks]
        )
    
    def ask(self, query):
        q_emb = self.embedder.encode([query]).tolist()
        results = self.collection.query(
            query_embeddings=q_emb,
            n_results=3
        )
        
        context = "\n\n".join(results["documents"][0])
        sources = set(r["source"] for r in results["metadatas"][0])
        
        prompt = f"""
        Ты — ассистент по проекту. Отвечай, используя только документы ниже.
        Если ответа нет в документах, скажи "В документации нет информации".
        В конце ответа укажи источники.
        
        Документы:
        {context}
        
        Вопрос: {query}
        """
        
        response = self.llm.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}]
        )
        return {
            "answer": response.choices[0].message.content,
            "sources": sources
        }

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

А ответ модели будет иметь следующий вид:

Что дальше: гибридный поиск и улучшения

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

Поэтому в продуктиве используют гибридный поиск — комбинацию ключевого (BM25) и семантического поиска с последующим переранжированием.

На практике это выглядит так:

  1. Вы находите документы двумя способами — по ключевым словам и по смыслу.

  2. Объединяете результаты.

  3. Переранжируете их с помощью более мощной модели (например, кросс‑энкодер).

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

Заключение. RAG — это не про магию, это про архитектуру

RAG не делает LLM умнее, он просто даёт ей доступ к вашей информации. Модель — это интерпретатор, а RAG — это система доступа к данным, которая решает, что подать на вход интерпретатору.

Для того, чтобы RAG работал хорошо, нужно правильно разбивать документы на чанки с перекрытием, выбирать модель эмбеддингов под задачу, настраивать нужное количество чанков и проектировать промпт так, чтобы модель использовала только контекст.

RAG превращает LLM из генератора «общих ответов» в эксперта по вашей предметной области. И для этого не нужно дообучать модель — достаточно правильно организовать доступ к документам.

ЧТО ПОЧИТАТЬ ПО ТЕМЕ:

Работа с LLM в разработке быстро выходит за рамки генерации кода. Возникают практические вопросы: можно ли доверять ответам модели, как найти причину ошибки и проверить предложенное исправление → научиться использовать искусственный интеллект как помощника в разработке без потери контроля над качеством результата.

На бесплатных демо-уроках разберём реальные сценарии применения искусственного интеллекта:

  • 8 сентября в 20:00. «ИИ против бага: как разобрать инцидент в Python‑проекте от логов до исправления». Записаться

  • 22 сентября в 20:00. «Можно ли доверять ИИ‑коду: как находить галлюцинации, уязвимости и устаревшие решения». Записаться

Больше бесплатных уроков сентября смотрите в дайджесте.