Logo Craft Homelab Docs Контакты Telegram
RAG на своём сервере: как языковая модель начинает понимать вашу документацию Трендовые github проекты в нашем телеграм канале. Подпишись →
4 сентября 2026 г.

Локальный ассистент по документации проекта на базе retrieval-пайплайна

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

Ниже — рабочая схема такого пайплайна, которую можно поднять на домашнем сервере поверх собственной базы знаний, вики или каталога markdown-файлов из репозитория.

Из чего состоит retrieval-пайплайн

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

  1. Пользователь присылает вопрос.
  2. Вопрос превращается в вектор — числовое представление смысла текста.
  3. По этому вектору в векторной базе ищутся документы с ближайшими векторами.
  4. Найденные фрагменты подставляются в промпт и уходят в LLM вместе с вопросом.

Модель отвечает, опираясь на переданный контекст, а не на то, что запомнила при обучении. За счёт этого ответ становится актуальным и привязанным именно к вашему проекту.

Эмбеддинги: как компьютер сопоставляет смысл, а не слова

В основе поиска лежат эмбеддинги — векторы, представляющие смысл текста. Два предложения с одинаковым смыслом получают близкие векторы, даже если в них нет общих слов. Классический пример: «руководство по диагностике двигателя» и «поиск причины перебоев в работе мотора» — разные слова, но об одном и том же. Обычный поиск по ключевым словам их не свяжет, а эмбеддинги — свяжут, потому что векторы окажутся рядом в многомерном пространстве.

На практике для генерации эмбеддингов достаточно небольшой открытой модели, которая свободно работает на CPU домашнего сервера:

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 окажутся близки по косинусному расстоянию.

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

Шаг 1. Разбиение документов на чанки

Документация редко помещается в контекст модели целиком, поэтому текст режут на смысловые куски по несколько сотен слов с небольшим перекрытием — это не даёт терять смысл на границах фрагментов:

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

Для локальной разработки и небольших баз знаний хорошо подходит 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],
    )

Для больших объёмов документации на homelab-сервере логичная замена — FAISS с сохранением индекса на диск, чтобы не пересчитывать эмбеддинги при каждом перезапуске сервиса.

Шаг 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]

Количество возвращаемых фрагментов (top_k) напрямую влияет на качество ответа: слишком мало — модель не увидит нужный контекст, слишком много — в промпте появится шум, который снижает точность ответа.

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

Найденные фрагменты подставляются в промпт, и модель отвечает строго на их основе:

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

Явное указание «отвечай только на основе документов» в промпте — не формальность, а рабочий способ снизить количество выдуманных моделью фактов.

Собираем ассистента по документации репозитория

Дальше эти шаги удобно объединить в класс, который умеет добавлять файлы документации и запоминать, из какого файла взят каждый фрагмент — это даёт возможность прикладывать источники к ответу:

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,
        }

Такой класс достаточно один раз проиндексировать всеми markdown-файлами из репозитория, чтобы получить чат, который отвечает на вопросы по конкретному проекту, а не по общим представлениям о том, как «обычно» устроены такие системы.

Гибридный поиск как следующий шаг

Чистый семантический поиск не всегда выигрывает у поиска по ключевым словам: если пользователь ищет конкретное имя переменной окружения или название функции, точное совпадение может оказаться важнее смыслового сходства. Поэтому в продуктивных системах используют гибридный поиск — комбинацию классического ключевого поиска (BM25) и семантического поиска с последующим переранжированием результатов более мощной моделью (кросс-энкодером). Такая связка не пропускает ни точные совпадения, ни смысловые аналоги и заметно повышает точность выдачи на больших базах документации.

Что в итоге определяет качество RAG

RAG не делает модель умнее — он даёт ей доступ к нужным данным в нужный момент. Качество итоговой системы держится на нескольких настройках:

  • размер чанка и величина перекрытия при разбиении текста;
  • выбор модели эмбеддингов под конкретный язык и предметную область;
  • количество фрагментов, передаваемых в контекст на один запрос;
  • формулировка промпта, которая ограничивает модель только переданным контекстом.

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