ГлавнаяБлогКак чинить RAG: стратегии чанкования и поиска
AI / Нейросети

Как чинить RAG: стратегии чанкования и поиска

Узнайте, как исправить две типичные ошибки RAG: мелкие чанки и неоднозначные совпадения. Практические стратегии чанкования и фильтрации метаданных с примерами кода на Python.

Al
Редакция Algolitalgolit.ru
12 мин чтения18 июля 2026 г.

Зачем это читать?

RAG-системы часто дают неверные ответы, но виновата не модель, а то, что вы ей скормили. В этой статье мы разберём две типичные поломки RAG на реальных вопросах, починим их и рассмотрим полный инструментарий стратегий чанкования и поиска. Вы узнаете, как правильно нарезать документы и фильтровать результаты, чтобы модель получала полные и релевантные контексты.

Два способа, которыми RAG ломается

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

Я задал два новых вопроса той же системе. Оба провалились, но по разным причинам на разных уровнях.

Провал 1: Слишком мелкие чанки

Вопрос: «Могу ли я компенсировать расходы на домашний офис во время онбординга?»

Система извлекла пять чанков. Перекрёстная ссылка из политики расходов: «См. раздел 5.3 Руководства по льготам для получения информации о надбавке на домашний офис». Заголовок раздела с вводной строкой: «5.3 Обустройство домашнего офиса для гибридных и удалённых сотрудников». Упоминание эргономической оценки. Оценки были приличные, около 0.47. Система нашла правильные документы.

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

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

Это не галлюцинация, это неполный ответ. Потому что я (или RAG) дал ей неполные доказательства.

Диагноз: Разбиение по абзацам обрезало раздел слишком коротко. Правильное место, неполный ответ.

Провал 2: Неоднозначное совпадение

Вопрос: «Что такое правило 90 дней?»

Пять чанков из четырёх разных документов, все с низкими похожими оценками от 0.27 до 0.39. Ничего не выделялось, и это именно проблема. «90 дней» встречается в совершенно разных контекстах в этих политиках:

  • Испытательный срок: 90-дневный обзор с менеджером.
  • Соответствие RRSP: начинается после 90 дней работы.
  • Расходы: подать отчёт в течение 90 дней.
  • Безопасность: повышенные права доступа истекают через 90 дней.

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

Модель справилась разумно: из полученных чанков она выдала два — правило безопасности и испытательный срок — и спросила «какой контекст вы имели в виду?» Но подумайте, что произошло. Система искала всё, вытянула из четырёх разных документов, сожгла токены, и лучшее, что смогла сделать — попросить уточнить.

С 6 документами — нормально. С 6000 — каждый размытый вопрос заваливает модель нерелевантным контекстом. Растут затраты, задержка, падает качество.

Диагноз: Одни и те же слова, разные значения. Эмбеддинги не могут их различить.

Ключевая идея

Ни один из провалов не является проблемой модели. Модель сделала именно то, что ей сказали: ответить на основе предоставленного контекста. Оба провала — в поиске. Хорошая новость: поиск настраивается.

Два провала также находятся на разных уровнях. Один — о том, как вы разбиваете документы (чанкование). Другой — о том, как вы их ищете (поиск). Понимание этой разницы важно, потому что исправление для каждого своё.

Исправление 1: Чанкование на основе заголовков

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

Чанкование на основе заголовков сохраняет раздел целиком: каждый заголовок и всё под ним до следующего заголовка становится одним чанком. «5.3 Обустройство домашнего офиса» плюс вводная строка И список сумм — всё одним куском.

import re

HEADING_RE = re.compile(r"^#{2,6}\s")  # разделяем по ## и глубже, заголовок прикрепляем

def chunk_by_heading(text: str, source: str) -> list[dict]:
    """
    Каждый заголовок + всё под ним до следующего заголовка = один чанк.
    """
    chunks, current = [], []
    for line in text.split("\n"):
        if HEADING_RE.match(line) and current:
            block = "\n".join(current).strip()
            if block:
                chunks.append({"text": block, "source": source})
            current = [line]
        else:
            current.append(line)
    block = "\n".join(current).strip()
    if block:
        chunks.append({"text": block, "source": source})
    return chunks

Тот же вопрос: «Могу ли я компенсировать расходы на домашний офис во время онбординга?» На этот раз извлечённый чанк содержит полный раздел: 750 долларов при найме, 250 долларов в год на обновление, до 1200 долларов на эргономическую оценку — всё это.

Модель получает полные доказательства и даёт полный ответ. Исправление заключалось не в лучшей модели, а в лучшем разбиении.

Инструментарий чанкования: выбирайте разбиение осознанно

Чанкование на основе заголовков исправило наш первый провал. Но это не универсальный ответ. Если в документах нет заголовков, оно не поможет. Если раздел огромный, вы возвращаетесь к проблеме «слишком большого контекста».

Давайте пройдёмся по полному меню. Та же идея: как разбивает и когда использовать.

Фиксированное разбиение

Режем каждые N токенов. Это базовый уровень, с которого все начинают. Работает для однородного текста: транскриптов, логов. Но режет посреди предложения и абзаца, так что ответы могут оказаться на границе и быть разрезанными пополам. Это именно тот провал, который мы только что исправили.

Рекурсивное разбиение

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

Семантическое чанкование

Вместо разбиения по структуре — разбиение по смыслу. Фреймворки вроде LangChain (SemanticChunker) и LlamaIndex (SemanticSplitterNodeParser) дают это одним вызовом. Под капотом эмбеддится каждое предложение, сравнивается с соседними, и разрез делается там, где похожесть падает. Текст сам говорит, где тема меняется. Это дороже на старте, потому что вы эмбеддите предложение за предложением, но для плотного материала без чётких заголовков часто стоит того.

Родительско-дочернее чанкование

Храните чанки на двух уровнях. Маленькие чанки делают поиск точным. Большие родительские чанки дают модели полный контекст. Когда маленький чанк совпадает, вы возвращаете его родителя модели. Ищите по маленьким, подавайте большие. Получаете точный поиск и полный контекст, но ценой дополнительной работы.

Нет универсального правильного ответа. Исследование Chroma по стратегиям чанкования для поиска показало, что то, как вы чанкуете, может быть так же важно, как и то, какую модель эмбеддингов вы выбираете. Две ручки, сравнимый вес.

Разумная отправная точка для большинства систем: рекурсивное разбиение, чанки по 256–512 токенов, перекрытие 10–20%. Это настройка по умолчанию, а не закон. Чанкование — это решение, которое вы принимаете на основе формы ваших данных, а не настройка, скопированная из туториала.

И единственный способ узнать, работает ли ваш выбор — измерить. Соберите набор из 20–30 тестовых вопросов с ожидаемыми ответами. Меняйте одну вещь, запускайте заново, сравнивайте. «Кажется лучше» — не метрика.

Исправление 2: Фильтрация метаданных для поиска

Проблема «правила 90 дней» не в размере чанка. Она в устранении неоднозначности. Эмбеддинги содержат одну и ту же фразу, поэтому семантический поиск сам по себе не может их разделить. Но мы знаем кое-что полезное, чего поиск не знает: каждый чанк пришёл из конкретного документа.

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

# При загрузке ставим на каждый чанк метку источника
{"text": "...", "source_label": "employee-handbook"}
{"text": "...", "source_label": "expense-policy"}
{"text": "...", "source_label": "benefits-guide"}

# Во время поиска сужаем множество кандидатов ДО оценки
import numpy as np

def retrieve(question, chunks, embeddings, top_k=5, source_label=None):
    q_vec = np.array(embed_text(question))
    if source_label:
        # вся фишка в этой предварительной фильтрации
        candidates = [i for i, c in enumerate(chunks) if c.get("source_label") == source_label]
    else:
        candidates = range(len(chunks))
    scored = [
        (i, np.dot(q_vec, embeddings[i]) / (np.linalg.norm(q_vec) * np.linalg.norm(embeddings[i])))
        for i in candidates
    ]
    scored.sort(key=lambda x: x[1], reverse=True)
    return [{**chunks[i], "score": s} for i, s in scored[:top_k]]

Тот же вопрос: «Что такое правило 90 дней?», отфильтрованный по руководству сотрудника. Теперь он ищет по 26 из 185 чанков вместо всех. Ответ чистый: это испытательный срок. Двухнедельные встречи с менеджером, никаких внутренних переводов, формальный обзор в конце.

Переключите фильтр на политику расходов? Тот же вопрос, другой чистый ответ: подавать отчёты о расходах в течение 90 дней с момента транзакции.

Тот же размытый вопрос. Каждый раз другой правильный ответ. Потому что мы сузили пространство поиска до того, как запустили поиск по сходству.

Кто выбирает фильтр?

Очевидная загвоздка: я выбрал фильтр вручную. Я решил «искать в руководстве сотрудника». Но если пользователь просто спрашивает «Что такое правило 90 дней?», как система узнает, в каком документе искать?

Для этого существует паттерн под названием самозапрашивающий поисковик. Вы передаёте вопрос модели сначала и спрашиваете: «На основе этого вопроса, какой фильтр метаданных применить?» Модель читает вопрос, смотрит на существующие поля и извлекает фильтр. «Что такое правило 90 дней для расходов?» возвращает source = expense-policy. Если вопрос действительно неоднозначный, она может попросить пользователя уточнить.

Для этого не нужна большая модель. Выбор фильтра — это просто задача классификации, поэтому модель, выбирающая фильтр, — это не модель, пишущая ответ. Маленькая, быстрая, дешёвая модель читает вопрос и выбирает фильтр. Большая, дорогая вступает в дело только для финального ответа. Та же идея «правильная модель для правильной задачи» применена внутри одного пайплайна. И это не только фильтры. Та же маленькая модель может обрабатывать другие быстрые решения: маршрутизация вопроса, классификация того, о чём спрашивают, извлечение имени или даты.

Фреймворки вроде LangChain дают это как строительный блок. Управляемые сервисы вроде Amazon Bedrock Knowledge Bases выполняют шаг фильтрации как часть пайплайна. Они не подменяют модели за вашей спиной. Вы всё ещё выбираете, какая модель что делает. Они занимаются проводкой: извлекают фильтр из вопроса, применяют его до поиска.

Одно замечание по продакшену о метке источника. В демо метка живёт в словаре Python, и мы прикрепляем её к каждому чанку во время чанкования. Это метаданные на уровне чанка. Каждый кусок знает, из какого документа он пришёл, потому что мы поставили метку при загрузке.

Это не единственное место, где могут жить метаданные. Они также могут находиться на самом файле. Если ваши документы в Amazon S3, функция S3 annotations позволяет прикрепить богатый, обновляемый контекст прямо к объекту, и он остаётся доступным для поиска.

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

Практический вывод

Прямо сейчас сделайте три вещи:

  1. Проверьте своё чанкование. Если ваши документы имеют заголовки, используйте чанкование на основе заголовков. Если нет — начните с рекурсивного разбиения на 256–512 токенов с перекрытием 10–20%. Соберите 20–30 тестовых вопросов и измерьте, сколько ответов полны.
  2. Добавьте метаданные. Пометьте каждый чанк источником документа. Реализуйте предварительную фильтрацию: сначала сузьте по источнику, потом ищите по сходству. Для неоднозначных вопросов используйте маленькую модель для выбора фильтра.
  3. Измеряйте. Не доверяйте ощущениям. Сравните точность ответов до и после каждого изменения. Только так вы узнаете, что действительно работает.
#RAG#чанкование#поиск#метаданные#Python
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →