Natalia Vegman
← Все статьи

RAG простыми словами — и когда он вам не нужен

2026-02-18Case study

Полночь, ресепшен, заезжает гость с иностранным паспортом. Администратор на третьей смене в своей жизни открывает внутреннего ассистента и спрашивает: «как оформить регистрацию иностранного гостя». Ассистент отвечает подробно, уверенно и неправильно — он нашёл в базе фрагмент регламента про граждан РФ и аккуратно его пересказал.

Это не выдуманная сцена. Я разбирала такой сбой в ассистенте для отельной автоматизации: он отвечает сотрудникам по базе внутренних инструкций и регламентов. Под капотом у него RAG — схема, про которую часто пишут, что она «избавляет от галлюцинаций». Она от них не избавляет. Она их перемещает: из головы модели в ваш поиск.

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

Что такое RAG на пальцах: поиск + генерация

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

Вариантов, по сути, два.

  • Дообучить модель на своих документах: дорого, долго, и при каждом изменении регламента процедуру надо повторять.
  • Модель не трогать вообще, а перед вопросом найти в своих документах подходящий кусок текста и положить его прямо в запрос: «вот выдержка из инструкции, ответь строго по ней».

Это и есть RAG (retrieval‑augmented generation — «генерация с опорой на найденное»).

Важно с самого начала держать в голове, что RAG — это две независимые ступени:

  • Поиск (retrieval). По вопросу пользователя достать из базы несколько наиболее подходящих фрагментов текста.
  • Генерация. Отдать модели вопрос вместе с этими фрагментами и попросить сформулировать ответ.

Отсюда главный вывод для новичка, который экономит недели: модель в RAG ничего не «знает» о вашей базе. Она читает только то, что ей положили в запрос прямо сейчас. Если поиск принёс мусор — модель уверенно и красиво перескажет мусор.

Поэтому, когда ассистент отвечает неверно, первый вопрос всегда не «какая у нас модель», а «что нашёл поиск».

flowchart LR
  Q["Вопрос"] --> S["Поиск по базе"]
  S --> CH["Найденные чанки"]
  CH --> PR["Промпт: вопрос + чанки"]
  PR --> M["Модель"]
  M --> A["Ответ"]
  KB[("База знаний")] --- S
  KB -.->|"модель её<br/>не видит"| M
  S -.->|"нашёл мусор"| A2["Модель уверенно<br/>перескажет мусор"]
Модель не знает вашей базы — она читает только то, что поиск положил в запрос прямо сейчас

Из чего он собран на самом деле

Теперь по деталям — на примере той самой отельной базы знаний.

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

Эмбеддинги. Чтобы искать по смыслу, а не по буквам, каждый чанк превращают в набор чисел — вектор. Это делает отдельная модель‑векторизатор; у нас это GigaEmbeddings. Смысл превращения в том, что тексты, похожие по содержанию, получают близкие наборы чисел, даже если написаны разными словами. Вопрос пользователя прогоняют через ту же модель и ищут чанки, чьи числа оказались ближе всего.

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

Обратите внимание на важную деталь, которую новички почти всегда склеивают: векторизация и генерация — это разные модели и разные этапы. GigaEmbeddings превращает тексты в числа на этапе поиска. GigaChat пишет ответ на этапе генерации. Они не взаимозаменяемы, и менять их можно независимо: смена модели‑генератора не улучшит поиск, а смена векторизатора требует пересчитать всю базу заново.

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

У нас фрагменты с оценкой ниже 0.11 отбрасываются совсем, а до модели доходят только те, что выше 0.3. Эти числа подобраны эмпирически, под конкретную базу и конкретный векторизатор. Не переносите их к себе: у вас будут свои.

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

Первая честная проблема: похожие тексты не значит правильные

Векторный поиск отвечает на вопрос «что здесь похоже?». А пользователю нужен ответ на вопрос «что здесь правильно?». Это не одно и то же, и вся практическая работа над RAG живёт в этом зазоре.

Два сбоя из реальной эксплуатации.

Сбой первый: ноль фрагментов по целому разделу. По теме регистрации иностранных гостей поиск не возвращал вообще ничего. Не «плохо», а ноль: фильтр по домену документов отсекал нужные фрагменты до того, как до них доходило дело.

Ассистент при этом не молчал — он отвечал общими словами, и со стороны это выглядело как «модель плоховата». Лечилось не моделью, а подстраховкой: если по теме не нашлось ничего, включается запасной поиск без ограничивающего фильтра. И отдельно — регулярной проверкой, что ни один раздел базы не даёт пустоту.

Сбой второй: чанк без контекста врёт. Все фрагменты одного раздела трактовались как относящиеся к иностранцам, хотя часть из них была про граждан РФ. Причина простая: при нарезке фрагмент теряет то, что было понятно из заголовка и соседнего абзаца. Внутри самого куска текста может не быть ни слова о том, к какой категории гостей он относится.

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

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

flowchart TB
  T["Задача"] --> C1{"Ответ содержится<br/>в связном тексте?"}
  C1 -->|"нет: цифры, таблицы"| N1["Запрос к данным,<br/>а не RAG"]
  C1 -->|"да"| C2{"Текста много?"}
  C2 -->|"нет"| N2["Положить текст<br/>прямо в промпт"]
  C2 -->|"да"| C3{"Неизвестно заранее,<br/>где лежит нужное?"}
  C3 -->|"нет"| N3["Обычный поиск<br/>по разделу"]
  C3 -->|"да"| YES["Это случай для RAG"]
RAG нужен при трёх условиях сразу; если хоть одно не выполняется, задача решается проще

Когда RAG не нужен

Теперь то, ради чего всё затевалось. RAG — инструмент для одной конкретной ситуации: ответ содержится в связном тексте, текста много, и заранее неизвестно, в каком именно месте лежит нужное.

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

  • Таблицы. Самый частый и самый дорогой промах. Когда на входе таблица, а не текст, RAG работает плохо. Вопросы к таблицам — это «сколько», «у кого больше», «покажи все за период». Такие вопросы решаются фильтрами и группировками, то есть маленькими операциями над данными, а не поиском по смыслу. Векторный поиск найдёт «похожие строки» — но похожие строки не складываются в правильную сумму.
  • Точные справочники. Если у сущности есть код, артикул или идентификатор, ищите по нему напрямую. Точное совпадение и быстрее, и надёжнее, и его не надо настраивать порогами.
  • Короткие регламенты. Если весь свод правил — несколько страниц, его можно целиком положить модели в запрос. Никакой векторной базы, никакой нарезки, никаких порогов. Меньше деталей — меньше мест, где сломается.
  • Один документ на один вопрос. Когда пользователь и так знает, в какой инструкции ответ, вы решаете задачу навигации, а не поиска.

Правило, к которому я пришла: RAG стоит заводить тогда, когда вы уже пробовали обойтись без него и упёрлись. Не наоборот.

Применить у себя

Что проверить на практике

Если у вас уже есть RAG или вы только собираетесь его делать — вот что реально стоит сделать руками.

  • Соберите двадцать настоящих вопросов от тех, кто будет этим пользоваться. Не придуманных вами — настоящих, с их формулировками и опечатками.
  • Прогоните их и смотрите не на ответы, а на найденные фрагменты. Выведите их себе в лог вместе с оценками похожести. Пока вы этого не видите, вы чините вслепую.
  • Отдельно проверьте каждый раздел базы на пустоту: задайте по вопросу из каждой темы и убедитесь, что ни по одной не возвращается ноль фрагментов. Это ловит сбои фильтрации, которые иначе всплывут через месяц у пользователя.
  • Возьмите несколько узких отраслевых терминов и проверьте, находятся ли они. Если нет — вам нужен не другой векторизатор, а добавленный поиск по вхождению слова.
  • Покрутите порог в обе стороны и посмотрите, что меняется: при слишком низком к модели поедет шум, при слишком высоком начнутся ответы «не знаю» там, где ответ есть.
  • Откройте пять случайных чанков и прочитайте их так, будто вы видите текст впервые и без заголовка. Если непонятно, к чему они относятся, — модели тоже будет непонятно. Это сигнал добавить категорию и преамбулу.
  • Найдите в своём списке вопрос, который на самом деле про таблицу («сколько», «сравни», «за период»). Решите его отдельно — фильтром, а не поиском.

Как понять, что получилось

Критерий не «ответы стали красивее». Красиво они звучали и раньше — в этом и опасность.

Проверяйте так:

  • Для каждого контрольного вопроса нужный фрагмент попадает в найденное. Это измеряется отдельно от качества ответа и чинится отдельными средствами. Если фрагмент не найден, разговор про формулировки бессмысленен.
  • Ни один раздел базы не даёт ноль. Пустой результат должен быть событием, о котором вы узнаёте из логов, а не из жалобы сотрудника.
  • Ответ проверяем по источнику. Возьмите ответ и найдите в процитированных фрагментах ту самую фразу. Если её там нет — модель дописала от себя, и это лечится инструкцией отвечать строго по контексту.
  • Вы можете назвать виновника любой ошибки. Вот это главный признак зрелости системы: глядя на неверный ответ, вы за минуту говорите — «поиск не нашёл» или «нашёл верно, но модель переврала». Пока эти два случая для вас неразличимы, вы не отлаживаете систему, а гадаете.

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

← All posts

RAG in Practice: What It Actually Is, and When You Don't Need It

2026-02-18Case study

Midnight, the front desk, a guest walks in with a foreign passport. The receptionist — third shift of their life — opens the internal assistant and asks: "how do I register a foreign guest." The assistant answers in detail, confidently, and incorrectly — it found a fragment of the regulations about Russian citizens in the knowledge base and neatly paraphrased it.

This isn't a made-up scene. I debugged exactly this failure in an assistant for hotel automation: it answers staff questions from a base of internal instructions and regulations. Under the hood it uses RAG — the setup people often describe as the thing that "gets rid of hallucinations." It does not get rid of them. It relocates them: out of the model's head and into your search.

By the end of this article you'll understand what RAG is, what parts it's assembled from, and — more importantly — how to tell a task where you genuinely need it from a task where it will only give you three new ways to get things wrong.

RAG in plain terms: search + generation

A language model knows what was in its training data. Your internal regulations were not in there. Ask it about your check-in procedure and it will either honestly say it doesn't know, or invent a plausible answer. The second happens more often.

There are essentially two options. The first is to fine-tune the model on your documents: expensive, slow, and every time a regulation changes you have to repeat the whole procedure. The second is to leave the model alone entirely and, before asking it a question, find a suitable piece of text in your own documents and put it directly into the prompt: "here's an excerpt from the instructions, answer strictly based on it." That is RAG (retrieval-augmented generation — "generation grounded in what was found").

So RAG is two separate stages, and it's useful to keep them apart in your head from the very beginning:

  • Retrieval. Given the user's question, pull several of the most relevant text fragments out of the base.
  • Generation. Hand the model the question along with those fragments and ask it to formulate an answer.

From this follows the key insight for a beginner, the one that saves weeks: the model in a RAG system doesn't "know" anything about your knowledge base. It reads only what was placed into the prompt right now. If retrieval brought back garbage, the model will confidently and elegantly paraphrase garbage. So when the assistant answers incorrectly, the first question is never "which model are we on," it's "what did retrieval find."

flowchart LR
  Q["Вопрос"] --> S["Поиск по базе"]
  S --> CH["Найденные чанки"]
  CH --> PR["Промпт: вопрос + чанки"]
  PR --> M["Модель"]
  M --> A["Ответ"]
  KB[("База знаний")] --- S
  KB -.->|"модель её<br/>не видит"| M
  S -.->|"нашёл мусор"| A2["Модель уверенно<br/>перескажет мусор"]
Модель не знает вашей базы — она читает только то, что поиск положил в запрос прямо сейчас

What it's actually made of

Now the details — using that same hotel knowledge base as the example.

Chunks. You can't search over whole documents: a twenty-page instruction won't fit into a prompt, and most of it isn't relevant to the question anyway. So texts get cut into fragments — these are called chunks. Our base has about fifteen hundred of them. A chunk is both the unit of search and the unit the model will ultimately read.

Embeddings. To search by meaning rather than by letters, each chunk is turned into a set of numbers — a vector. A separate vectorizer model does this; ours is GigaEmbeddings. The point of the conversion is that texts similar in content get similar sets of numbers, even if they're written in different words. The user's question is run through the same model, and you look for the chunks whose numbers came out closest.

Vector database. The place where these vectors live and where you search them quickly. In our case, ChromaDB. Essentially it's a specialized store that can answer the request "give me the k vectors closest to this one."

Note an important detail that beginners almost always conflate: vectorization and generation are different models and different stages. GigaEmbeddings turns texts into numbers at the retrieval stage. GigaChat writes the answer at the generation stage. They are not interchangeable, and they can be swapped independently: changing the generator model won't improve retrieval, and changing the vectorizer requires recomputing the entire base from scratch.

Similarity threshold. Vector search always returns a result — even if there's nothing on the topic in the base, you'll get back the "least dissimilar" thing. So you need a threshold: how close does something have to be before a fragment counts as found at all. In our setup, fragments scoring below 0.11 are discarded entirely, and only those above 0.3 reach the model. These numbers were tuned empirically, for a specific base and a specific vectorizer. Don't carry them over to your own system: yours will be different.

Hybrid search. Initially retrieval was purely semantic — vectors only. It missed on specialized terminology: a narrow term from an industry regulation gets smeared out in the vector and lost among texts that are similar in general meaning. We had to add plain keyword matching and merge the results. It sounds boring and old-fashioned, but that's exactly what fixes an entire class of errors.

The first honest problem: similar texts aren't the same as correct ones

Vector search answers the question "what here is similar?" But the user needs an answer to "what here is correct?" These are not the same thing, and all the practical work on RAG lives in that gap. Two failures from real operation.

Failure one: zero fragments for an entire section. On the topic of registering foreign guests, retrieval returned nothing at all. Not "poor results" — zero: a document-domain filter was cutting off the needed fragments before they were ever considered. The assistant didn't stay silent, though — it answered in generalities, and from the outside it looked like "the model is a bit weak." The fix wasn't the model, it was a fallback: if nothing is found on a topic, a backup search kicks in without the restricting filter. And separately — a regular check that no section of the base returns emptiness.

Failure two: a chunk without context lies. All fragments in one section were being treated as relating to foreigners, even though some of them were about Russian citizens. The reason is simple: during chunking, a fragment loses what was obvious from the heading and the neighboring paragraph. Inside the piece of text itself there may not be a single word about which category of guests it applies to. Retrieval honestly finds something similar, the model honestly paraphrases what was found, and the answer is wrong. We had to tag fragments with categories during chunking and add an explanatory preamble to them — a short text that gives the chunk back its lost context.

These two stories add up to the conclusion I consider the main practical takeaway: answer quality more often comes down not to the model, but to how the documents are chunked and labeled. The temptation to solve the problem by swapping models is enormous — it's one line in a config. But the fix almost always lies at the data preparation stage.

flowchart TB
  T["Задача"] --> C1{"Ответ содержится<br/>в связном тексте?"}
  C1 -->|"нет: цифры, таблицы"| N1["Запрос к данным,<br/>а не RAG"]
  C1 -->|"да"| C2{"Текста много?"}
  C2 -->|"нет"| N2["Положить текст<br/>прямо в промпт"]
  C2 -->|"да"| C3{"Неизвестно заранее,<br/>где лежит нужное?"}
  C3 -->|"нет"| N3["Обычный поиск<br/>по разделу"]
  C3 -->|"да"| YES["Это случай для RAG"]
RAG нужен при трёх условиях сразу; если хоть одно не выполняется, задача решается проще

When you don't need RAG

Now for the part all of this was leading up to. RAG is a tool for one specific situation: the answer is contained in connected prose, there's a lot of that prose, and you don't know in advance where exactly the needed part sits. If even one of those conditions doesn't hold, you're building an elaborate construction where something simple would have done.

  • Tables. This is the most common and most expensive mistake. When the input is a table rather than text, RAG works badly. Questions about tables are "how many," "who has more," "show me everything for this period." Such questions are solved with filters and groupings — small operations over data, not search by meaning. Vector search will find you "similar rows" — but similar rows don't add up to a correct total.
  • Exact reference data. If an entity has a code, a SKU, or an identifier, look it up directly. An exact match is faster, more reliable, and doesn't need thresholds tuned.
  • Short regulations. If the entire set of rules is a few pages, you can put all of it into the model's prompt. No vector database, no chunking, no thresholds. Fewer parts means fewer places to break.
  • One document per question. When the user already knows which instruction holds the answer, you're solving a navigation problem, not a search problem.

The rule I've arrived at: it's worth setting up RAG once you've already tried to get by without it and hit a wall. Not the other way around.

Put it to work

What to check in practice

If you already have a RAG system, or you're just about to build one — here's what's actually worth doing by hand.

  • Collect twenty real questions from the people who will be using it. Not ones you made up — real ones, with their phrasing and their typos.
  • Run them and look not at the answers, but at the retrieved fragments. Dump them to a log along with similarity scores. Until you can see this, you're fixing things blind.
  • Separately, check every section of the base for emptiness: ask one question from each topic and make sure none of them returns zero fragments. This catches filtering failures that would otherwise surface a month later, on a user.
  • Take a few narrow industry terms and check whether they're found. If not — what you need isn't a different vectorizer, it's added keyword matching.
  • Turn the threshold both ways and see what changes: too low and noise flows to the model, too high and you start getting "I don't know" where the answer exists.
  • Open five random chunks and read them as if you were seeing the text for the first time and without a heading. If it's unclear what they relate to, it will be unclear to the model too. That's your signal to add a category and a preamble.
  • Find the question in your list that is really about a table ("how many," "compare," "for this period"). Solve it separately — with a filter, not with search.

How to tell whether it worked

The criterion is not "the answers got prettier." They sounded pretty before, too — that's exactly the danger. Check it like this:

  • For every control question, the needed fragment shows up in the retrieved set. This is measured separately from answer quality and fixed with separate means. If the fragment wasn't found, discussing phrasing is pointless.
  • No section of the base returns zero. An empty result should be an event you learn about from logs, not from a staff complaint.
  • The answer is verifiable against its source. Take the answer and find that exact phrase in the quoted fragments. If it isn't there, the model filled in the gap on its own, and that's cured by an instruction to answer strictly from the context.
  • You can name the culprit behind any error. This, I'd say, is the main sign of a mature system: looking at a wrong answer, within a minute you can say — "retrieval didn't find it" or "it found the right thing, but the model garbled it." As long as those two cases are indistinguishable to you, you aren't debugging a system, you're guessing.

And one last thing. If after all these checks it turns out that your task is about tables, exact codes, or three pages of rules, the most valuable outcome of this article is that you won't build a RAG system. That's not a defeat, it's months saved.