ChatGPT, Claude, Gemini и другие языковые модели обучены на огромном количестве текстов из интернета и открытых источников. Из-за этого они хорошо справляются с общими задачами — например, могут объяснить какое-то понятие, помочь набросать структуру текста или пересказать общеизвестные факты. Однако у них нет доступа к вашим внутренним данным: корпоративной базе знаний, актуальным версиям документов, регламентам и прочей информации.
Поэтому если спросить модель о том, чего не было в ее обучающих данных, по существу она ответить не сможет — ей просто не на что опереться. В лучшем случае нейросеть честно признается, что не знает. Но часто происходит другое: модель выдает правдоподобный, но выдуманный ответ — то есть галлюцинирует.
Чтобы нейросеть могла правильно отвечать даже на специфические вопросы, ей можно дать своего рода шпаргалку. В нее модель и будет заглядывать каждый раз, прежде чем ответить на то, чего она не знает. Этот прием называется RAG, и в статье мы разберем его подробнее. Вы узнаете, что это такое, где применяется и как устроено, а в конце соберем простой RAG на Python в Google Colab. Статья рассчитана на новичков, поэтому все будем упрощать и объяснять на примерах.
Что такое RAG и где его используют
Аббревиатура RAG расшифровывается как retrieval-augmented generation — «генерация, дополненная поиском». Проще говоря, модель сначала находит подходящие фрагменты в источниках, а уже потом собирает ответ на их основе. Это похоже на студента на экзамене с открытым конспектом: он не пытается вспомнить все наизусть, а быстро находит нужную страницу и отвечает по ней.
Главное преимущество такого подхода в том, что нейросеть получает доступ к свежим и закрытым данным, которых не было в обучении, и отвечает строго по ним, ничего не добавляя от себя. При этом вам не нужно дорого и долго переобучать модель — достаточно открыть ей доступ к нужным документам.
Заодно RAG решает проблему ограниченной памяти модели. У любой нейросети есть контекстное окно — максимальный объем текста, который она может удерживать в рамках запроса. Вся база знаний туда обычно не помещается, поэтому RAG обходит это ограничение: он не передает модели все документы, а находит несколько релевантных фрагментов и отправляет их вместе с вопросом.
Представьте, что у вас есть сотня PDF-файлов, в каждом не меньше тысячи страниц, — и все они описывают регламенты компании. Где-то в одном из них есть пара абзацев про то, за сколько дней нужно подавать заявление на отпуск. Загрузить все эти файлы в чат ChatGPT или любой другой нейросети не выйдет — а RAG справится: сам найдет нужный фрагмент и передаст модели только его.
Именно из‑за своей специфики RAG используют везде, где важно отвечать строго по проверенным данным. Вот несколько типичных сценариев:
- чат-боты по базе знаний компании;
- служба поддержки — отвечает по инструкциям и регламентам;
- юридические и комплаенс-помощники — ищут ответы в договорах и законах;
- ассистенты для разработчиков — отвечают по документации и API;
- аналитика по отчетам и исследованиям — быстро находит нужные цифры и выводы в объемных документах.
Возможно, вы слышали про поисковик Perplexity. Если задать ему вопрос, он сначала ищет материалы в интернете, отбирает самые релевантные и только затем, опираясь на них, формулирует ответ. Под ответом Perplexity показывает ссылки на источники, из которых взята информация. Так же работает и RAG: модель не выдумывает ответ, а сначала находит нужные фрагменты в источниках и отвечает уже по ним. У Perplexity такими источниками выступают сайты из интернета — но точно так же RAG можно подключить и к вашим документам.
Как работает RAG — два этапа от документа до ответа
Работу RAG удобно разделить на два этапа. Первый — подготовительный, его проходят один раз заранее. Второй запускается каждый раз, когда пользователь задает вопрос. На схеме ниже вы можете посмотреть, как устроен весь процесс:

Этап 1. Подготовка документов
Сначала нужно собрать все материалы, по которым нейросеть будет отвечать: инструкции, регламенты, статьи из базы знаний, PDF-файлы и другие документы. Затем их предобрабатывают — извлекают чистый текст, убирают лишнее форматирование, повторяющиеся элементы, артефакты копирования, навигацию и прочий мусор. Без этой подготовки качество поиска и ответов заметно падает.
После очистки текст делят на небольшие фрагменты — чанки. Например, вместо того чтобы хранить и искать по целой статье на 20 страниц, ее режут на куски по 1–3 абзаца или по небольшим разделам. Так поиск быстрее находит нужное место, а модель получает ровно тот фрагмент, что отвечает на вопрос, и не размазывает ответ по всему документу. Режут обычно по смыслу на абзацы, подразделы или пункты, — чтобы каждый чанк был понятен сам по себе.
Далее каждый чанк превращают в эмбеддинг — вектор (набор чисел), который отражает смысл фрагмента. Подробнее об этом механизме у нас есть отдельная большая статья, но если коротко — представьте, что модель переводит текст в координаты вроде [0.12, −0.03, 0.77, …]. В этом случае чанки «заявление на отпуск» и «как оформить отпуск» получат близкие векторы, а «рецепт борща» или «как заменить шину» — далекие. Поэтому, когда пользователь спрашивает «за сколько дней подавать заявление на отпуск», система превращает вопрос в эмбеддинг и находит в базе те чанки, чьи векторы ближе всего по смыслу.
И в завершение подготовительного этапа все эти векторы складывают в специальную векторную базу данных. От обычной базы она отличается тем, что умеет быстро находить ближайшие по смыслу векторы среди миллионов — а это как раз то, что нужно для поиска. Вот несколько примеров таких баз: Pinecone, Weaviate, Qdrant, Chroma и pgvector — расширение для PostgreSQL.
Этап 2. Ответ на вопрос
Когда подготовительный этап завершен, документы уже нарезаны на чанки, преобразованы в эмбеддинги и сохранены в векторной базе — теперь RAG может отвечать на вопросы по вашим материалам. Дальше все работает автоматически, и каждый раз, когда пользователь задает вопрос, запускается следующий процесс:
- вопрос преобразуется в эмбеддинг;
- по этому вектору в базе выполняется поиск: находятся самые близкие по смыслу фрагменты — те, которые с наибольшей вероятностью содержат ответ;
- найденные фрагменты система добавляет в контекст запроса и отправляет нейросети вместе с вопросом;
- модель читает эти фрагменты и формулирует ответ строго по ним, не добавляя ничего от себя.
Давайте еще раз уточним: нейросеть видит только те фрагменты, которые нашлись на этапе поиска. Если документы подготовлены плохо или поиск выдал нерелевантные куски, ответ получится слабым — даже у самой умной модели.
Собираем простой RAG на Python своими руками
В этом разделе мы соберем базовый RAG: возьмем несколько коротких документов, нарежем их на чанки, переведем в эмбеддинги, найдем ближайший по смыслу к вопросу фрагмент и передадим его модели. Есть и более продвинутые подходы — гибридный поиск, переранжирование, агентный RAG, GraphRAG. Они точнее работают на больших и сложных базах, но устроены сложнее. Однако принцип у всех один: сначала найти нужное, потом ответить.
Почти все действия мы выполним в бесплатном онлайн‑сервисе Google Colab — вам нужно создать новый блокнот и повторять шаги. В конце понадобится API‑ключ, чтобы отправить найденный фрагмент языковой модели и получить ответ. Здесь мы будем использовать Claude от Anthropic, но подойдет и любая другая модель. Можно и без ключа: тогда вы дойдете до готовой шпаргалки с вопросом и нужным фрагментом, но финальный ответ модель уже не сгенерирует.
Шаг 1. Устанавливаем библиотеку и загружаем документы
Чтобы не обучать нейросеть с нуля, возьмем готовую предобученную модель. Подключить ее можно с помощью библиотеки sentence-transformers — она сама загрузит модель и затем будет превращать все наши тексты в эмбеддинги.
Скопируйте команду ниже в первую ячейку и запустите ее:
!pip install -q sentence-transformers
Чтобы сэкономить время, можете сразу вставить и запустить в следующей ячейке готовый список. Это и есть наша база знаний — документы, уже разбитые на чанки. В реальном проекте таких кусков тысячи, и, как вы помните, их сначала нужно собрать и очистить. Но здесь, чтобы увидеть принцип, хватит и нескольких:
docs = ["Сотрудникам предоставляется 28 календарных дней оплачиваемого отпуска в год.", "Заявление на отпуск нужно подавать не позднее чем за две недели до его начала.", "Рабочий день длится с 10:00 до 19:00, обеденный перерыв — один час.", "Удаленно можно работать до трех дней в неделю по согласованию с руководителем.", "Раз в год компания компенсирует обучение на сумму до 50 000 рублей.", "Больничный оплачивается на основании электронного листа нетрудоспособности.",]
Вот как это должно выглядеть в Google Colab:

Шаг 2. Превращаем чанки в эмбеддинги
На прошлом шаге мы установили библиотеку — а сейчас загрузим саму модель, которая умеет переводить текст в числа. Возьмем модель paraphrase-multilingual-MiniLM-L12-v2, которая понимает русский язык. Команда model.encode(…) прогонит через нее документы и вернет для каждого эмбеддинг. А параметр normalize_embeddings=True приводит векторы к единому масштабу, чтобы их было удобно сравнивать. Вот код, который выполнит все эти действия:
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
doc_embeddings = model.encode(docs, normalize_embeddings=True)
print("Готово: каждый кусок превращен в вектор длиной", doc_embeddings.shape[1])
После выполнения команды каждый фрагмент становится вектором из 384 чисел:

Шаг 3. Ищем нужный фрагмент по вопросу
Теперь напишем функцию search. Она превращает вопрос в эмбеддинг, сравнивает его со всеми векторами документов и возвращает близкий по смыслу фрагмент. Чтобы проверить, как это работает, прогоним через нее три вопроса:
def search(question, top_k=1):
q = model.encode([question], normalize_embeddings=True)[0]
scores = doc_embeddings @ q # косинусная близость
best = np.argsort(scores)[::-1][:top_k]
return [(docs[i], float(scores[i])) for i in best]
questions = [ "Сколько дней отпуска у сотрудника?", "Можно ли работать из дома?", "Как оплачивается больничный?",]
for q in questions:
chunk, score = search(q)[0]
print(f"Вопрос: {q}")
print(f"Найдено: {chunk}")
print(f"Близость: {score:.2f}\n")
После запуска для каждого вопроса модель покажет найденный фрагмент и число «Близость» — показатель того, насколько фрагмент подходит по смыслу.
Чем ближе к 1, тем сильнее совпадение; чем ближе к 0 — тем дальше по смыслу:
Вопрос: Сколько дней отпуска у сотрудника? Найдено: Сотрудникам предоставляется 28 календарных дней оплачиваемого отпуска в год. Близость: 0.68 Вопрос: Можно ли работать из дома? Найдено: Удаленно можно работать до трех дней в неделю по согласованию с руководителем. Близость: 0.35 Вопрос: Как оплачивается больничный? Найдено: Больничный оплачивается на основании электронного листа нетрудоспособности. Близость: 0.50
Шаг 4. Собираем подсказку для модели
Найденный с помощью функции search фрагмент — это и есть наша шпаргалка. Но просто так передать ее модели нельзя: нужно правильно оформить запрос. Для этого напишем функцию build_prompt: она формирует короткую инструкцию для модели — «ответь только по этому тексту, а если ответа в нем нет, так и скажи» — и подставляет в нее найденный фрагмент и сам вопрос. Пока мы только собираем этот запрос и выводим его на экран, чтобы посмотреть, как он выглядит:
def build_prompt(question, context):
return f"""Ответь на вопрос, опираясь только на контекст ниже.Если ответа в контексте нет, так и скажи.
Контекст:
{context}
Вопрос:
{question}
Ответ:"""
question = "Сколько дней отпуска положено сотруднику?"
context = search(question)[0][0]
print(build_prompt(question, context))
Получится подсказка с вопросом и найденным фрагментом:
Ответь на вопрос, опираясь только на контекст ниже. Если ответа в контексте нет, так и скажи. Контекст: Сотрудникам предоставляется 28 календарных дней оплачиваемого отпуска в год. Вопрос: Сколько дней отпуска положено сотруднику? Ответ:
На этом основные шаги закончены. Осталось отправить нашу подсказку настоящей языковой модели и получить от нее готовый ответ. Для этого нужно получить API-ключ и оплатить доступ к модели. Подробно останавливаться на получении ключа и оплате мы не будем — если вы никогда этого не делали, посмотрите наш материал про Claude Code, где мы все подробно разобрали.
Шаг 5. Получаем ответ от нейросети
Установим библиотеку Anthropic:
!pip install -q anthropic
Чтобы модель ответила, ей необходим доступ к API. Перейдем на платформу Anthropic Console, создадим там API-ключ и вернемся в Google Colab. Сам ключ в коде писать нельзя — это небезопасно, поэтому Colab хранит такие данные отдельно. Слева в Colab откройте раздел «Секреты» (значок ключа), нажмите «Добавить секрет», в поле названия впишите ANTHROPIC_API_KEY, в поле значения вставьте свой ключ и включите тумблер «Доступ из блокнотов»:

У нас все готово для последнего шага. Код ниже подключается к Claude и отправляет ему подсказку. Обратите внимание на строку userdata.get(«ANTHROPIC_API_KEY») — именно так код безопасно достает ключ из секретов Colab, не показывая его в самом коде. Дальше функция ask_llm берет вопрос, находит к нему фрагмент через search, собирает подсказку через build_prompt и передает все модели, а та возвращает осмысленный ответ:
import anthropic
from google.colab import userdata
client = anthropic.Anthropic(api_key=userdata.get("ANTHROPIC_API_KEY"))
def ask_llm(question, context):
prompt = build_prompt(question, context)
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=300,
messages=[{"role": "user", "content": prompt}],
)
return message.content[0].text
question = "Сколько дней отпуска положено сотруднику?"
context = search(question)[0][0]
print("Контекст:", context)
print("Ответ:", ask_llm(question, context))
Мы использовали модель claude-sonnet-4-6. Если к моменту чтения она устареет или окажется недоступной, не страшно — просто укажите в параметре model актуальный идентификатор. Список доступных моделей и их точные названия можно посмотреть в Anthropic Console. После запуска модель прочитает подсказку и вернет аккуратный ответ, опираясь только на найденный фрагмент:
Контекст: Сотрудникам предоставляется 28 календарных дней оплачиваемого отпуска в год. Ответ: Сотруднику положено 28 календарных дней оплачиваемого отпуска в год.
А теперь проверим, не начнет ли модель фантазировать, если ответа в документах нет. Зададим ей вопрос на тему, которой в нашей базе точно не было:
question = "Можно ли приводить на работу собаку?" context = search(question)[0][0] print(ask_llm(question, context))
Поскольку в нашей базе нет информации про животных, модель не будет добавлять от себя: она честно ответит, что в контексте нет нужных данных. Именно так и должен вести себя хороший RAG. Это ключевое отличие от обычной нейросети, которая на такой вопрос легко придумает несуществующее правило:

Если вы повторили за нами все шаги, то увидели весь цикл RAG в миниатюре: документы разбиваются на куски, куски превращаются в эмбеддинги, по вопросу находится самый близкий по смыслу фрагмент, и уже он становится основой для ответа. Ровно так же — только на тысячах документов и с настоящей языковой моделью — RAG работает в реальных продуктах. Разница лишь в масштабе: в нашем примере ушли доли цента, а на большой базе с потоком запросов в контекст уходит куда больше токенов — и расходы на модель растут пропорционально.
Полезные статьи и ссылки по теме
- LangChain — один из самых популярных фреймворков для сборки RAG.
- LlamaIndex — фреймворк для подключения документов и поиска по ним.
- Haystack — фреймворк для поисковых и QA‑систем на базе LLM.
- Векторизация текста в NLP: от слов к числам
- Гайд по работе языковых моделей
- С чего начать учить Python
