
РазборЕвгений13 мин чтения
RAG-система: как отвечать по своим документам и проверять цитаты
Как устроена RAG-система: подготовка документов, поиск, проверка цитат кодом и честный отказ. Плюс пример, где модель ошибается с документами и без них.
Содержание · 9 разделов
Спросите локальную модель, сколько дней отпуска положено в выдуманной компании «Северный ветер», и она может спокойно ответить «20 рабочих дней» или «24». В регламенте написано 28, но модель этого регламента не видела, поэтому собирает правдоподобное число из того, что запомнила при обучении. Так же уверенно она назовёт суточные и сроки реакции, которых в её памяти нет.
Для таких вопросов и нужна RAG-система. Она находит в ваших документах нужный фрагмент и передаёт его модели вместе с вопросом как контекст для ответа. В статье разберём, из чего она состоит, как нарезать документы и искать по ним, и главное, как проверить, что цитата в ответе действительно взята из источника.
Что такое RAG-система
RAG расшифровывается как retrieval-augmented generation, генерация с дополнением из поиска. Термин ввели Льюис и соавторы в 2020 году: их модель сочетала обычную языковую модель с внешней памятью, векторным индексом Википедии, к которому обращался нейросетевой поисковик. В оригинальной работе поисковик и генератор дообучались вместе. Сегодня словом RAG чаще называют более простую схему, где готовая LLM получает найденные фрагменты прямо в промпте.
Anthropic в статье про Contextual Retrieval описывает её так: система находит релевантную информацию в базе знаний и добавляет её к запросу пользователя. Там же есть полезная оговорка. Если база меньше 200 тысяч токенов, это около 500 страниц, её можно целиком положить в промпт и обойтись без поиска. Цифра привязана к контекстному окну Claude на момент публикации, но сама мысль универсальна: сначала прикиньте объём, потом стройте инфраструктуру.
Архитектура в два контура
Удобно думать о RAG как о двух независимых процессах.
Контур индексации работает заранее, при загрузке документов:
- Документы режутся на фрагменты (чанки), по описанию Anthropic обычно не длиннее нескольких сотен токенов.
- Для каждого фрагмента считается эмбеддинг, числовой вектор, и сохраняется в векторной базе. Параллельно можно построить индекс BM25 для поиска по словам.
Контур запроса срабатывает на каждый вопрос:
- Вопрос превращается в вектор, совместимый с векторами фрагментов. Обычно оба считает одна специализированная модель эмбеддингов. В документации Ollama советуют использовать одну и ту же модель для индексации и для запросов. Если модель требует разных префиксов или режимов для вопроса и документа, их нужно соблюдать.
- Поиск возвращает несколько самых близких фрагментов.
- Фрагменты и вопрос уходят в промпт, модель пишет ответ.
В эту схему стоит явно добавить четвёртый шаг: код проверяет ответ до того, как его увидит пользователь. Есть ли в ответе ссылка на источник, существует ли такой фрагмент, совпадает ли цитата с текстом дословно. Ответ, который проверку не прошёл, пользователь не видит: система отвечает отказом или просит модель ответить заново.
Проверка кодом подтверждает происхождение цитаты, то есть что она взята из нужного фрагмента. Подкрепляет ли цитата сам ответ, код не скажет. Для этого нужна смысловая проверка, третий уровень из раздела про цитаты ниже. Только вместе они превращают «модель сказала» в «модель сказала и это подтверждается документом».
Как нарезать документы на чанки
Универсального размера чанка нет, и любой совет вида «берите 512 токенов» стоит воспринимать как стартовую точку. Anthropic прямо пишет, что размер, границы и перекрытие фрагментов влияют на качество поиска, и советует проверять варианты на своих данных.
Важнее размера граница. Фрагмент, разрезанный посреди раздела, теряет контекст. В той же статье Anthropic приводит пример: строка «Выручка компании выросла на 3% по сравнению с прошлым кварталом» сама по себе не говорит, о какой компании и каком квартале речь, и найти её по вопросу трудно.
Поэтому нарезку лучше привязывать к структуре документа. В библиотеке Unstructured для этого есть стратегия by_title. Она режет документ по границам разделов, но по умолчанию склеивает небольшие соседние разделы в один чанк. Чтобы соседние разделы не объединялись в один чанк, склейку отключают параметром combine_text_under_n_chars=0. Текст режется посимвольно только тогда, когда отдельный элемент не влезает в лимит. По умолчанию жёсткий лимит там 500 символов, а перекрытие равно нулю. Это значения библиотеки, а не рекомендация для ваших документов.
В условном примере ниже корпус маленький, и нарезка там проще: два предложения плюс заголовок документа. Заголовок в каждом фрагменте решает ту же проблему, что и пример про выручку. Фрагмент про суточные сам сообщает, что он из регламента о командировках.
BM25, векторы или гибрид
У двух основных способов поиска разные сильные стороны.
| Способ | Что находит хорошо | Где ошибается |
|---|---|---|
| BM25 (поиск по словам) | Точные совпадения, коды ошибок, артикулы, термины | Перефразировку, синонимы |
| Векторный поиск | Смысл вопроса, сказанный другими словами | Редкие идентификаторы и точные строки |
| Гибрид | Оба типа запросов | Требует настройки слияния результатов |
Anthropic иллюстрирует это кодом ошибки TS-999: эмбеддинги могут пропустить точное совпадение, а BM25 его поймает. Гибридный поиск запускает оба метода параллельно и объединяет оценки. В документации Weaviate описаны два способа слияния, и с версии 1.24 по умолчанию используется relativeScoreFusion. Это поведение конкретной базы, у других значения по умолчанию свои.
Что дал Contextual Retrieval
Anthropic предложила перед индексацией дописывать к каждому чанку короткий контекст в 50–100 токенов. Его генерирует модель, которая видит весь документ. Контекст добавляется и перед расчётом эмбеддинга, и перед построением индекса BM25.
В их тестах доля провалов поиска в топ-20 (сколько раз нужный фрагмент не попал в двадцать найденных) изменилась так:
- обычные эмбеддинги: 5,7%;
- контекстные эмбеддинги: 3,7%;
- контекстные эмбеддинги вместе с контекстным BM25: 2,9%;
- то же плюс reranking: 1,9%.
Эти цифры измеряют промахи поиска, а не долю неверных ответов. Получены они на наборах данных Anthropic (код, художественные тексты, научные статьи) с конкретной моделью эмбеддингов и конкретным reranker. На ваших документах выигрыш может быть другим, сама Anthropic советует проверять на своём случае. Из этих цифр не следует, что на любом корпусе улучшения выстроятся в том же порядке. Если выбираете между базовым поиском, Contextual Retrieval и reranking, сравните их на своём фиксированном наборе вопросов. Reranking в любом случае добавляет задержку и стоимость.
Как проверять цитаты
Поиск нашёл правильный фрагмент. Модель написала ответ. Откуда известно, что ответ взят из фрагмента, а не досочинён поверх него?
В руководстве Anthropic по снижению галлюцинаций собраны базовые приёмы: разрешить модели говорить «не знаю», ограничить ответ предоставленными документами, просить дословные цитаты и отзывать утверждение, если подтверждающей цитаты нет. Там же честно сказано, что эти приёмы заметно снижают галлюцинации, но полностью их не убирают. RAG работает так же: он сокращает выдумку, а не исключает её.
Поэтому проверку цитаты мы делим на три уровня.
- Ссылка. Указанный id источника существует и указывает на фрагмент, который был в промпте.
- Совпадение. Цитата есть в этом фрагменте, дословно или после оговорённой нормализации. Это проверяется кодом за пару строк, но подтверждает только происхождение цитаты, а не правильность ответа.
- Подтверждение. Цитата действительно подкрепляет ответ. Тут нужна смысловая проверка каждого существенного утверждения, обычно с помощью другой модели. Модель-оценщик тоже ошибается, поэтому её саму сначала проверяют на наборе с известной разметкой.
Первые два уровня дешёвые и детерминированные. Ниже проверка нормализованного совпадения, как в примере дальше: регистр, кавычки и лишние пробелы не учитываются. Это уже не буквальная проверка, и удаление кавычек может скрыть значимое различие, например между названием и обычным словосочетанием. Если нужна строгая дословность, сравнивайте исходную подстроку или позиции в тексте, а нормализацию ограничьте и покройте тестами.
def quote_ok(quote: str, fragment: str) -> bool:
def norm(s: str) -> str:
for ch in '«»"\'„“”':
s = s.replace(ch, "")
return " ".join(s.lower().split())
q = norm(quote)
return bool(q) and q in norm(fragment)
Если вы работаете с Claude, первые два уровня частично закрывает встроенная функция Citations. Модель возвращает ссылки на конкретные места в документах: страницы для PDF, диапазоны символов для обычного текста. Дословный фрагмент приходит в поле cited_text. Anthropic гарантирует, что указатели корректны, потому что API сам разбирает цитаты. Для RAG там же советуют класть каждый чанк в отдельный текстовый документ, тогда модель сможет цитировать отдельные предложения. Одно ограничение: Citations нельзя включить вместе со structured outputs, API вернёт ошибку 400.
Третий уровень эта функция не закрывает. Корректный указатель на дословный текст ещё не значит, что текст подтверждает вывод модели.
Пример: три режима работы с документами
Возьмём условную схему и посмотрим, что в ней может пойти не так. Это сценарии, а не результаты замера.
Корпус. Восемь выдуманных регламентов компании «Северный ветер»: отпуск, командировки, сроки реакции и другие внутренние правила. Фрагменты по два предложения, к каждому добавлен заголовок документа.
Поиск. BM25, в промпт идут три лучших фрагмента.
Вопросы. Часть с ответом в документах, часть без ответа, например про спортзал или бонус за рекомендацию.
Режимы такие:
- A. Модель без документов.
- B. Три найденных фрагмента и вопрос, без дополнительных правил.
- C. Строгий промпт: отвечать только по документам, писать NOT_FOUND, если ответа нет, указывать источник в виде [id] и добавлять строку «ЦИТАТА:» с дословным фрагментом. Затем код проверяет, что цитата есть в указанном фрагменте, без учёта регистра и кавычек.
Что может пойти не так
В режиме A модели неоткуда взять правила компании. Она может уверенно назвать 20 или 24 дня отпуска вместо 28, а на вопросе без ответа придумать процент бонуса или правило про больничный.
В режиме B у модели появляется основание для ответа, и ошибок может стать меньше. Выдуманные числа и неверные выводы это не исключает: модель может проигнорировать фрагмент, смешать его со своими знаниями или неверно его прочитать. На вопросе без ответа она может отказаться своими словами, ответить по нерелевантному фрагменту или выдумать ответ. Свободную формулировку отказа сложно надёжно обработать программно.
В режиме C отказ получает маркер NOT_FOUND, но модель может перестраховаться и отказать там, где ответ в документах есть. Строку с цитатой она может пропустить или пересказать текст: написать «командировки по России» вместо «командировке по России». Такое расхождение автоматически обнаружит проверка подстроки.
Если пускать к пользователю только ответы с прошедшей цитатой, остальные придётся превратить в отказ или перезапросить. Такая проверка может отсеять и правильные ответы, например когда модель пересказала цитату. А неправильный ответ с подлинной, но не подтверждающей его цитатой может её пройти: код проверяет только, что цитата взята из фрагмента.
Что из этого следует
- Без поиска по документам модели неоткуда знать внутренние правила, и отвечать она будет догадкой.
- Найденные фрагменты дают модели основание для ответа, но не гарантируют, что она им воспользуется.
- Фиксированный маркер отказа упрощает обработку, пока модель соблюдает формат. Код проверяет, что в выводе ровно одно состояние, ответ или отказ. Некорректный вывод уходит на повтор с лимитом попыток и общим тайм-аутом, после которых пользователь получает контролируемый отказ или вопрос передаётся человеку.
- Требование цитаты модель может нарушить, поэтому цитату проверяет код, а ответ без проверенной цитаты пользователь не видит.
Отказы и метрики качества
Отказ тоже результат, и его стоит проектировать. В режиме B из примера модель может отказать своими словами. Такой ответ трудно отличить от обычного программно. Фиксированный маркер вроде NOT_FOUND помогает, пока модель соблюдает формат: код видит отказ и может показать пользователю заготовленное сообщение или передать вопрос человеку. Модель может выдать NOT_FOUND вместе с ответом или отказать без маркера, поэтому вывод, где нет ровно одного из двух состояний, считается некорректным и уходит на повтор. Повторов должно быть ограниченное число, после них система отвечает контролируемым отказом или передаёт вопрос человеку.
Второй сигнал к отказу может дать сам поиск. Если у вопроса лучший балл BM25 заметно ниже обычного, это дополнительный признак, что в документах ответа нет. Порог подбирают на своём наборе вопросов: абсолютные значения BM25 зависят от корпуса.
Когда вопросов становится больше, ручной подсчёт заменяют метрики. В библиотеке Ragas для RAG есть две базовые.
Faithfulness показывает, насколько ответ согласуется с найденным контекстом. Из ответа извлекаются утверждения, и считается доля тех, что подтверждаются контекстом, по шкале от 0 до 1. Утверждения извлекает и проверяет LLM. Метрика оценивает опору на найденные фрагменты, а не истинность в мире: если в контекст попал устаревший регламент, ответ по нему может получить высокий балл.
Context precision оценивает поисковик: насколько релевантные фрагменты стоят выше нерелевантных. Эталонный ответ нужен варианту ContextPrecision, который сверяет фрагменты с референсом, а для работы без эталона в Ragas есть LLMContextPrecisionWithoutReference.
Faithfulness близка к третьему уровню проверки цитаты, context precision к качеству поиска. Вместе они помогают найти часть ошибок, но не все. Высокая точность контекста не значит, что поиск нашёл всё нужное, поэтому добавьте recall@k или context recall. Высокая faithfulness не гарантирует полноту ответа и правильность отказа: их проверяйте отдельно, на вопросах с ответом и без. И faithfulness смотрит на опору на контекст в целом, а связь каждого утверждения с приложенной к нему цитатой нужно проверять отдельно.
Права, версии и чужие инструкции
Корректная цитата не делает ответ безопасным или актуальным. Если система отвечает по внутренним документам, ей нужны ещё три правила.
- Права проверяются до того, как фрагмент попадёт в промпт. Иначе модель процитирует документ, который пользователю видеть нельзя, и проверка цитаты это пропустит.
- У документов есть версии. Устаревший регламент нужно убирать из индекса или помечать, иначе модель уверенно и с цитатой ответит по старым правилам.
- Текст документов считается данными, а не инструкциями. Фраза внутри документа вроде «игнорируй прежние правила» не должна менять поведение модели, а проверка цитаты от такой подмены не защищает.
Чек-лист RAG-системы
- Оцените объём базы. Если она помещается в контекст модели, возможно, поиск не нужен.
- Режьте документы по структуре и добавляйте в каждый фрагмент заголовок или короткий контекст.
- Размер чанка подбирайте на своих данных, значения по умолчанию в библиотеках служат только стартом.
- Для документов с кодами, артикулами и терминами добавьте BM25 к векторному поиску.
- Индексируйте и ищите одной моделью эмбеддингов и соблюдайте её префиксы для вопросов и документов.
- Задайте фиксированный маркер отказа и проверяйте кодом, что в выводе ровно одно состояние: ответ или отказ.
- Требуйте в ответе id источника и дословную цитату.
- Проверяйте цитату кодом: id существует, текст совпадает дословно или после оговорённой нормализации. Это проверка происхождения цитаты, а не правильности ответа.
- Ответ без прошедшей проверки не показывайте: отказ или повторный запрос с лимитом попыток и тайм-аутом.
- Опору ответа на найденный контекст оценивайте метрикой вроде faithfulness.
- Соответствие утверждения его цитате проверяйте отдельно, на парах «утверждение и цитируемый фрагмент». Модель-оценщика сначала проверьте на размеченном наборе.
- Следите за баллом поиска: низкий балл может означать, что ответа в документах нет.
- Фильтруйте фрагменты по правам пользователя до промпта, следите за версиями документов, не исполняйте инструкции из их текста.
- Соберите набор вопросов с известными ответами и без ответа и прогоняйте его после каждого изменения. Считайте полноту поиска, ложные отказы и ответы на вопросы без данных.
Читайте также
- Локальная нейросеть на ПК: как запустить локальный ИИ и какую модель выбрать
- Галлюцинации нейросети: что это и как я ловлю их у своих ИИ-агентов
- Где хранить контекст ИИ-агента: пять слоёв вместо огромного промпта
- Контекстное окно нейросети: что это, какой размер у Claude и GPT и почему длинный чат дорогой
- Как пользоваться ИИ: пять правил из 800 сессий с ИИ за месяц
Источники
- Lewis et al. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- Anthropic. Introducing Contextual Retrieval
- Anthropic. Citations
- Anthropic. Reduce hallucinations
- Ragas. Faithfulness
- Ragas. Context Precision
- Yandex AI Studio. Поисковые индексы Vector Store
- Weaviate. Hybrid search
- Unstructured. Chunking
- Ollama. Embeddings