RAG: как подключить свои данные к языковой модели и получить умного ассистента
Основной чат
Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.
Вы спрашиваете ChatGPT про свой проект — а он фантазирует, потому что не видел вашей документации. Просите Claude объяснить логику кода — а он не знает архитектурных решений, принятых три месяца назад. Делаете бота для поддержки клиентов — а модель придумывает ответы вместо того, чтобы искать их в базе знаний.
Это классическая проблема: LLM хорошо рассуждают, но знают только то, на чём обучены. Ваши данные туда не попали.
RAG (Retrieval-Augmented Generation) — архитектурный паттерн, который это исправляет. Модель не запоминает ваши данные — она ищет нужные фрагменты в момент ответа и строит ответ на их основе. Это дешевле дообучения, работает с любой LLM и обновляется без переобучения.
Как работает RAG: три шага
Схема проще, чем кажется по названию.
Шаг 1: Индексация (делается один раз)
Берёте документы — Markdown-файлы, PDF, страницы из Notion, записи из БД — нарезаете на чанки (фрагменты по 300–1000 токенов), конвертируете каждый чанк в вектор через модель эмбеддингов и сохраняете в векторную базу данных.
Вектор — это массив чисел, который описывает смысл текста. Похожие по смыслу тексты дают близкие векторы. Именно это позволяет искать «не по словам, а по смыслу».
Шаг 2: Retrieval (при каждом запросе)
Пользователь задаёт вопрос. Вопрос тоже конвертируется в вектор той же моделью эмбеддингов. Векторная база находит K ближайших чанков — тех, чьи векторы наиболее близки к вектору вопроса.
Шаг 3: Generation (при каждом запросе)
Найденные чанки вставляются в промпт вместе с вопросом пользователя. LLM видит: «вот контекст из документов, вот вопрос — ответь на основе контекста». Модель отвечает, опираясь на реальные данные, а не на память.
Пользователь: «Как настроить деплой на 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).
Установка
pip install chromadb openai tiktoken langchain langchain-openai langchain-community
Индексация документов
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)
Поиск и генерация ответа
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.
# Устанавливаем Ollama
curl -fsSL https://ollama.com/install.sh | sh
# Скачиваем модели
ollama pull nomic-embed-text # эмбеддинги
ollama pull llama3.2 # или qwen2.5, mistral
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, чем поднимать отдельный сервис.
-- Включаем расширение
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);
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 делает это автоматически.
Метаданные. Сохраняйте источник, заголовок раздела, дату. Это позволяет показывать пользователю, откуда взят ответ, и фильтровать поиск по разделам.
# Пример нарезки с сохранением заголовка раздела
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 (ключевой поиск) и векторного.
-- Добавляем полнотекстовый поиск в таблицу
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 |
| Переиндексировать всё при каждом изменении | Долго и дорого | Инкрементальная индексация по изменённым файлам |
Инкрементальная индексация
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:
pip install ragas
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 в продакшен
Индексация:
☐ Выбрана модель эмбеддингов (одна и та же для индексации и поиска)
☐ Размер чанков 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.