~/wiki / dannye-i-khranenie / rag-baza-znanii-dlya-vibesoding

RAG: как подключить свои данные к языковой модели и получить умного ассистента

Основной чат

Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.

$ cd раздел/ $ join vibe dev
RAG: как подключить свои данные к языковой модели и получить умного ассистента - обложка

Вы спрашиваете ChatGPT про свой проект — а он фантазирует, потому что не видел вашей документации. Просите Claude объяснить логику кода — а он не знает архитектурных решений, принятых три месяца назад. Делаете бота для поддержки клиентов — а модель придумывает ответы вместо того, чтобы искать их в базе знаний.

Это классическая проблема: LLM хорошо рассуждают, но знают только то, на чём обучены. Ваши данные туда не попали.

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


Как работает RAG: три шага

Схема проще, чем кажется по названию.

Шаг 1: Индексация (делается один раз)

Берёте документы — Markdown-файлы, PDF, страницы из Notion, записи из БД — нарезаете на чанки (фрагменты по 300–1000 токенов), конвертируете каждый чанк в вектор через модель эмбеддингов и сохраняете в векторную базу данных.

Вектор — это массив чисел, который описывает смысл текста. Похожие по смыслу тексты дают близкие векторы. Именно это позволяет искать «не по словам, а по смыслу».

Шаг 2: Retrieval (при каждом запросе)

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

Шаг 3: Generation (при каждом запросе)

Найденные чанки вставляются в промпт вместе с вопросом пользователя. LLM видит: «вот контекст из документов, вот вопрос — ответь на основе контекста». Модель отвечает, опираясь на реальные данные, а не на память.

plaintext
Пользователь: «Как настроить деплой на VPS?»
         ↓
Эмбеддинг запроса → вектор вопроса
         ↓
Векторный поиск → топ-5 чанков из документации
         ↓
Промпт: [чанк 1] [чанк 2] [чанк 3] + «Как настроить деплой на VPS?»
         ↓
LLM → ответ, основанный на вашей документации

RAG vs файн-тюнинг: что выбрать

Частый вопрос: зачем RAG, если можно дообучить модель на своих данных?

Критерий RAG Файн-тюнинг
Стоимость Дёшево (векторная база + API) Дорого (GPU, время, датасет)
Обновление данных Мгновенно — добавил документ, переиндексировал Надо переобучать
Прозрачность Видно, откуда взят ответ «Чёрный ящик»
Точность на узкой теме Хорошая при правильном чанкинге Очень высокая
Галлюцинации Меньше — модель опирается на контекст Больше без контекста
Порог входа Низкий — запускается за вечер Высокий

Для большинства вайбкодинг-задач — бот поддержки, ассистент по документации, поиск по базе знаний, персональный агент — RAG закрывает 90% потребностей без дорогого файн-тюнинга.


Ключевые компоненты: что нужно выбрать

Перед кодом — быстрый обзор инструментов, из которых собирается RAG.

Модель эмбеддингов

Конвертирует текст в вектор. От выбора зависит качество поиска.

Модель Размер вектора Где работает Стоимость
text-embedding-3-small (OpenAI) 1536 Облако ~$0.02 / 1M токенов
text-embedding-3-large (OpenAI) 3072 Облако ~$0.13 / 1M токенов
nomic-embed-text 768 Локально (Ollama) Бесплатно
mxbai-embed-large 1024 Локально (Ollama) Бесплатно
multilingual-e5-large 1024 Локально / HF Бесплатно, хорош для русского

Для русскоязычного контента — multilingual-e5-large или text-embedding-3-small от OpenAI.

Векторная база данных

Хранит векторы и делает быстрый поиск ближайших соседей (ANN).

База Тип Когда использовать
ChromaDB Встраиваемая / сервер Локальная разработка, прототип
pgvector Расширение PostgreSQL Уже используете Postgres
Qdrant Отдельный сервис Продакшен, большой объём
Weaviate Отдельный сервис Нужны гибридный поиск и схемы
FAISS Библиотека (in-memory) Исследования, нет persistence

Для старта — ChromaDB: ноль инфраструктуры, всё в одном Python-пакете. Для продакшена с существующим Postgres — pgvector.

Оркестратор

LangChain и LlamaIndex — фреймворки, которые склеивают компоненты. LangChain универсальнее, LlamaIndex заточен именно под RAG.

Для небольших проектов оркестратор вообще не нужен — достаточно 100 строк чистого кода.


Собираем RAG с нуля: код

Пример: RAG-ассистент по локальной папке с Markdown-документами. ChromaDB + OpenAI embeddings + GPT-4o (или любая другая LLM).

Установка

bash
pip install chromadb openai tiktoken langchain langchain-openai langchain-community

Индексация документов

python
import os
import glob
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

DOCS_DIR = "./docs"       # папка с вашими .md файлами
CHROMA_DIR = "./chroma_db"  # куда сохранять базу

def load_markdown_files(directory: str) -> list[dict]:
    """Загружает все .md файлы, возвращает список {text, source}."""
    documents = []
    for filepath in glob.glob(f"{directory}/**/*.md", recursive=True):
        with open(filepath, "r", encoding="utf-8") as f:
            text = f.read()
        documents.append({"text": text, "source": filepath})
    return documents

def build_index(docs_dir: str, chroma_dir: str):
    # Загружаем документы
    raw_docs = load_markdown_files(docs_dir)
    print(f"Загружено документов: {len(raw_docs)}")

    # Нарезаем на чанки
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,       # символов в чанке
        chunk_overlap=100,    # перекрытие — чтобы не потерять контекст на границах
        separators=["\n## ", "\n### ", "\n\n", "\n", " "],
    )

    chunks = []
    metadatas = []
    for doc in raw_docs:
        parts = splitter.split_text(doc["text"])
        chunks.extend(parts)
        metadatas.extend([{"source": doc["source"]}] * len(parts))

    print(f"Чанков после нарезки: {len(chunks)}")

    # Создаём векторную базу
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vectorstore = Chroma.from_texts(
        texts=chunks,
        embedding=embeddings,
        metadatas=metadatas,
        persist_directory=chroma_dir,
    )
    print(f"Индекс сохранён в {chroma_dir}")
    return vectorstore

if __name__ == "__main__":
    build_index(DOCS_DIR, CHROMA_DIR)

Поиск и генерация ответа

python
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.schema import HumanMessage, SystemMessage

CHROMA_DIR = "./chroma_db"

def load_vectorstore(chroma_dir: str):
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    return Chroma(
        persist_directory=chroma_dir,
        embedding_function=embeddings,
    )

def ask(question: str, vectorstore, k: int = 5) -> str:
    # Ищем релевантные чанки
    results = vectorstore.similarity_search(question, k=k)
    context = "\n\n---\n\n".join([doc.page_content for doc in results])
    sources = list({doc.metadata.get("source", "") for doc in results})

    # Строим промпт
    system_prompt = """Ты — ассистент по документации. 
Отвечай только на основе предоставленного контекста.
Если ответа нет в контексте — честно скажи об этом.
Не придумывай информацию."""

    user_prompt = f"""Контекст из документации:
{context}

Вопрос: {question}"""

    # Генерируем ответ
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    response = llm.invoke([
        SystemMessage(content=system_prompt),
        HumanMessage(content=user_prompt),
    ])

    return response.content, sources

if __name__ == "__main__":
    vs = load_vectorstore(CHROMA_DIR)
    answer, sources = ask("Как настроить автодеплой на VPS?", vs)
    print(answer)
    print("\nИсточники:", sources)

Полностью локальный RAG через Ollama

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

bash
# Устанавливаем Ollama
curl -fsSL https://ollama.com/install.sh | sh

# Скачиваем модели
ollama pull nomic-embed-text   # эмбеддинги
ollama pull llama3.2           # или qwen2.5, mistral
python
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.llms import Ollama
from langchain_community.vectorstores import Chroma

def build_local_index(docs_dir: str, chroma_dir: str):
    raw_docs = load_markdown_files(docs_dir)

    from langchain.text_splitter import RecursiveCharacterTextSplitter
    splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
    chunks, metadatas = [], []
    for doc in raw_docs:
        parts = splitter.split_text(doc["text"])
        chunks.extend(parts)
        metadatas.extend([{"source": doc["source"]}] * len(parts))

    # Локальные эмбеддинги через Ollama
    embeddings = OllamaEmbeddings(model="nomic-embed-text")
    vectorstore = Chroma.from_texts(
        texts=chunks,
        embedding=embeddings,
        metadatas=metadatas,
        persist_directory=chroma_dir,
    )
    return vectorstore

def ask_local(question: str, vectorstore) -> str:
    results = vectorstore.similarity_search(question, k=5)
    context = "\n\n".join([doc.page_content for doc in results])

    llm = Ollama(model="llama3.2")
    prompt = f"""Контекст: {context}

Вопрос: {question}

Отвечай только на основе контекста. Если ответа нет — скажи об этом."""

    return llm.invoke(prompt)

Производительность на локальной машине зависит от железа: llama3.2 8B хорошо работает на M1/M2 Mac и на видеокарте от RTX 3080. Для CPU — берите модели 1–3B (qwen2.5:1.5b, llama3.2:1b).


RAG с pgvector: если уже используете PostgreSQL

Если в проекте есть Postgres — проще добавить расширение pgvector, чем поднимать отдельный сервис.

sql
-- Включаем расширение
CREATE EXTENSION IF NOT EXISTS vector;

-- Создаём таблицу для чанков
CREATE TABLE documents (
    id          SERIAL PRIMARY KEY,
    content     TEXT NOT NULL,
    source      TEXT,
    embedding   vector(1536),  -- размер под text-embedding-3-small
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Индекс для быстрого поиска (HNSW — лучшее соотношение скорость/точность)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
python
import psycopg2
import numpy as np
from openai import OpenAI

client = OpenAI()

def get_embedding(text: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

def insert_chunk(conn, content: str, source: str):
    embedding = get_embedding(content)
    with conn.cursor() as cur:
        cur.execute(
            "INSERT INTO documents (content, source, embedding) VALUES (%s, %s, %s)",
            (content, source, embedding)
        )
    conn.commit()

def search_similar(conn, query: str, k: int = 5) -> list[dict]:
    query_embedding = get_embedding(query)
    with conn.cursor() as cur:
        cur.execute("""
            SELECT content, source,
                   1 - (embedding <=> %s::vector) AS similarity
            FROM documents
            ORDER BY embedding <=> %s::vector
            LIMIT %s
        """, (query_embedding, query_embedding, k))
        rows = cur.fetchall()
    return [
        {"content": r[0], "source": r[1], "similarity": r[2]}
        for r in rows
    ]

<=> — это косинусное расстояние в pgvector. Чем меньше расстояние, тем ближе векторы. Индекс HNSW даёт поиск за O(log n) вместо O(n) при полном переборе.


Чанкинг: самая важная и недооценённая часть

Качество RAG сильно зависит от того, как вы нарезаете документы. Неправильный чанкинг — и модель не найдёт нужное, даже если оно есть в базе.

Правила нарезки

Размер чанка. 300–500 токенов — для точечных фактов (FAQ, документация API). 800–1200 токенов — для концептуальных объяснений. Слишком мелкие чанки теряют контекст. Слишком крупные — «разбавляют» релевантность.

Перекрытие (overlap). 10–15% от размера чанка. Нужно, чтобы мысль, начатая в конце одного чанка, не обрывалась для модели.

Границы нарезки. Режьте по смысловым границам — заголовкам (##, ###), абзацам (\n\n), а не посередине предложения. RecursiveCharacterTextSplitter с правильными separators делает это автоматически.

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

python
# Пример нарезки с сохранением заголовка раздела
import re

def chunk_markdown_with_headers(text: str, source: str) -> list[dict]:
    """Нарезает Markdown, сохраняя заголовок раздела в метаданных."""
    chunks = []
    current_header = ""
    current_text = []

    for line in text.split("\n"):
        if line.startswith("## "):
            # Сохраняем накопленный текст
            if current_text:
                chunks.append({
                    "text": "\n".join(current_text).strip(),
                    "source": source,
                    "section": current_header,
                })
                current_text = []
            current_header = line.lstrip("# ").strip()
        current_text.append(line)

    # Последний раздел
    if current_text:
        chunks.append({
            "text": "\n".join(current_text).strip(),
            "source": source,
            "section": current_header,
        })

    return [c for c in chunks if len(c["text"]) > 50]  # убираем пустые

Гибридный поиск: векторный + ключевой

Чистый векторный поиск хорошо находит смысловые совпадения, но может промахнуться по точным именам, версиям, кодовым константам. pgvector 0.6+ и Qdrant поддерживают гибридный поиск — комбинацию BM25 (ключевой поиск) и векторного.

sql
-- Добавляем полнотекстовый поиск в таблицу
ALTER TABLE documents ADD COLUMN ts tsvector
    GENERATED ALWAYS AS (to_tsvector('russian', content)) STORED;
CREATE INDEX ON documents USING gin(ts);

-- Гибридный запрос: взвешенная сумма BM25 и косинусного сходства
SELECT content, source,
       (0.5 * ts_rank(ts, query) + 0.5 * (1 - (embedding <=> $1::vector))) AS score
FROM documents, to_tsquery('russian', $2) query
WHERE ts @@ query OR (embedding <=> $1::vector) < 0.4
ORDER BY score DESC
LIMIT 5;

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


Реальные сценарии применения в вайбкодинге

Бот-ассистент по документации проекта. Индексируете README, CHANGELOG, wiki команды — и получаете бота, который отвечает на вопросы новых разработчиков без «спроси старших». Обновление базы — один скрипт при каждом коммите в /docs.

Персональный поиск по заметкам Obsidian. RAG по папке с Markdown-файлами превращается в ассистента, который знает всё, что вы когда-либо записали. Спрашиваете «что я думал про архитектуру микросервисов» — и получаете выдержки из своих же заметок с объяснением.

Поддержка клиентов без галлюцинаций. Индексируете базу знаний, FAQ, инструкции к продукту. Бот отвечает строго по документам и честно говорит «не знаю», если ответа нет.

Агент-ревьюер кода. Индексируете корпоративный style guide, архитектурные решения (ADR), прошлые code review. Claude Code или Codex получает этот контекст через MCP-сервер и ревьюит PR с учётом реальных стандартов проекта.

RAG поверх Telegram-канала. Парсите канал (см. статью про парсер каналов), индексируете посты — и делаете поиск по архиву смыслов, а не по ключевым словам.


Частые ошибки и как их избегать

Ошибка Последствие Решение
Слишком крупные чанки (>2000 токенов) Модель «теряется» в контексте, точность падает 800–1000 токенов оптимум
Один вектор на весь документ Поиск не находит детали внутри длинного текста Нарезайте на чанки
Разные модели эмбеддингов при индексации и поиске Полная ерунда на выходе Всегда одна и та же модель для обоих шагов
k=1 при поиске Одна ошибка в базе — неверный ответ k=5–10, пусть LLM сама выберет релевантное
Нет метаданных об источнике Нельзя проверить откуда ответ Всегда сохраняйте source и section
Переиндексировать всё при каждом изменении Долго и дорого Инкрементальная индексация по изменённым файлам

Инкрементальная индексация

python
import hashlib
import json
import os

HASH_FILE = ".index_hashes.json"

def file_hash(filepath: str) -> str:
    with open(filepath, "rb") as f:
        return hashlib.md5(f.read()).hexdigest()

def get_changed_files(docs_dir: str) -> list[str]:
    """Возвращает только изменившиеся файлы."""
    hashes = {}
    if os.path.exists(HASH_FILE):
        with open(HASH_FILE) as f:
            hashes = json.load(f)

    changed = []
    current = {}
    for filepath in glob.glob(f"{docs_dir}/**/*.md", recursive=True):
        h = file_hash(filepath)
        current[filepath] = h
        if hashes.get(filepath) != h:
            changed.append(filepath)

    with open(HASH_FILE, "w") as f:
        json.dump(current, f)

    return changed

Оценка качества RAG

Перед деплоем стоит проверить, что система реально работает. Метрики для оценки:

Relevance (релевантность) — насколько найденные чанки относятся к вопросу. Проверяется вручную на тестовой выборке вопросов.

Faithfulness (достоверность) — нет ли в ответе информации, которой не было в контексте. Галлюцинации — главный враг RAG.

Answer correctness — совпадает ли ответ с эталонным. Для этого нужен датасет вопрос → правильный ответ.

Для автоматической оценки — библиотека RAGAS:

bash
pip install ragas
python
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall

# dataset — список {question, answer, contexts, ground_truth}
result = evaluate(dataset, metrics=[faithfulness, answer_relevancy, context_recall])
print(result)

Чеклист: RAG в продакшен

plaintext
Индексация:
☐ Выбрана модель эмбеддингов (одна и та же для индексации и поиска)
☐ Размер чанков 300–1000 токенов, перекрытие 10–15%
☐ Нарезка по смысловым границам (заголовки, абзацы)
☐ Метаданные: source, section, дата
☐ Реализована инкрементальная переиндексация

Поиск:
☐ k=5–10 чанков при поиске
☐ Метрика схожести соответствует модели (косинусная для большинства)
☐ Рассмотрен гибридный поиск если есть точные термины/имена

Генерация:
☐ Системный промпт явно требует отвечать только по контексту
☐ Модель честно говорит «не знаю», если ответа нет в контексте
☐ Источники показываются пользователю

Инфраструктура:
☐ Векторная база с persistence (не in-memory)
☐ Логируются все запросы и найденные чанки
☐ Есть мониторинг качества на тестовых вопросах

Безопасность:
☐ API-ключи в переменных среды
☐ Если данные приватные — используется локальная LLM (Ollama)
☐ Нет утечки системного промпта в ответе

Итог

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

Качество системы на 80% определяется чанкингом и метаданными — не выбором LLM и не сложностью оркестратора. Начните с простого: папка с Markdown, ChromaDB, OpenAI embeddings. Убедитесь, что поиск возвращает правильные фрагменты — и только потом усложняйте.

Следующий уровень после базового RAG — агентный RAG с переформулировкой запроса, multi-hop reasoning и самооценкой ответа. Но это уже отдельная статья.


Актуально для ChromaDB 0.6+, pgvector 0.8+, LangChain 0.3+, Ollama 0.5+. Июнь 2026.

$ cd ../ ← назад к Данные и хранение