Вы когда-нибудь пытались скормить LLM 1000-страничный регламент, надеясь, что она найдёт ответ про контроль AC-1? Плоский чанкинг в RAG превращает документ в мешанину обрывков. LLM видит только ближайшие по эмбеддингам куски, но теряет структурную иерархию. Результат — галлюцинации, потерянные параграфы и проклятые «может быть, это где-то рядом».
На дворе июль 2026. Эмбеддинги всё ещё не умеют в «тут была ссылка на раздел 3.1.2», а LLM упорно игнорируют контекст, когда он не помещается в окно. Но есть обход: вместо того чтобы толкать весь документ в контекст, мы будем гулять по оглавлению, как по карте, и на каждом шагу решать, какой узел открыть дальше. Это Loop Engineering.
Дисклеймер: Вся логика, код и промпты в этой статье проверены на моделях GPT-4o (OpenAI) и Claude 3.5 Sonnet (Anthropic). На 12 июля 2026 года это самые производительные коммерческие модели для задач иерархического анализа.
Проблема: плоские чанки не видят структуры
Классический RAG режет документ на куски по 512–1024 токена, индексирует эмбеддинги и ищет по сходству. Для новостей — ок. Для нормативных документов — катастрофа.
Возьмите NIST SP 800-53 (редакция 5.1.1, ~800 страниц). Один вопрос: «Какие требования к аудиту для систем с уровнем воздействия High?» Ответ лежит в разделе AU, но чанк может быть разрезан между AU-1 и AU-2. Эмбеддинг «запомнит» соседний параграф про SI (системную целостность), и LLM начнёт выдумывать.
Мы уже обсуждали почему поиск в RAG должен быть фильтрацией, а не поиском, и почему RAG ломается на нормативных документах. Но там мы фокусировались на предварительной фильтрации. Сейчас пойдём глубже: динамический даунстрим-поиск по иерархии.
Loop Engineering: как это работает
Идея проста: вместо того чтобы искать чанки, мы строим дерево оглавления и даём LLM возможность исследовать дерево итеративно — похоже на DFS с бэктрекингом. На каждом шаге модель видит только текущий узел (заголовок + краткое описание) и список его детей. Модель решает: «Дальше в этот подраздел» или «Назад, это не то».
Цикл продолжается, пока модель не накопит достаточно контекста для ответа, либо не упрётся в листья (конкретные параграфы). После этого подаём собранный путь + фрагменты листьев в финальный промпт.
Важно: Loop Engineering не заменяет чанкинг. Он надстраивает иерархический слой поверх чанков. Если у вас уже есть хорошая стратегия чанкинга, Loop Engineering сделает её умнее.
Как НЕ надо: типичные ошибки новичков
- Загружать всё оглавление сразу. Если TOC занимает 4000 токенов, LLM начнёт терять фокус. Делите на уровни.
- Игнорировать лимиты глубины. Без ограничения рекурсии модель может уйти в подраздел «3.2.2.3.1.1» и забыть, зачем пришла.
- Подавать листья без контекста родителя. LLM не поймёт «AC-1.a» без заголовка «Access Control Policy». Всегда передавайте полный путь.
- Смешивать естественные вопросы и структуры таблиц. Для табличных данных нужен Proxy-Pointer RAG — отдельная техника.
Пошаговый план реализации
1 Парсинг документа и извлечение TOC
Используем PyMuPDF для PDF с TOC, для веб-документов — BeautifulSoup + регулярки. Главное — сохранить иерархию: title, level, page_number.
import fitz
doc = fitz.open("nist_sp_800_53.pdf")
toc = doc.get_toc() # list of (level, title, page)
doc.close()
# Пример структуры
# [ (1, '1. Introduction', 1),
# (2, '1.1 Purpose', 2),
# (1, '2. Controls', 20), ... ]
2 Построение дерева и вырезание текста по страницам
Каждый узел дерева хранит: заголовок, диапазон страниц, children, краткое описание (первые 200 символов текста раздела). Описание нужно, чтобы LLM понимала, о чём раздел, не открывая его целиком.
class TOCNode:
def __init__(self, title, level, start_page, end_page, text_preview=""):
self.title = title
self.level = level
self.start_page = start_page
self.end_page = end_page
self.text_preview = text_preview
self.children = []
def build_tree(toc):
root = TOCNode("Document", 0, 1, doc.page_count)
stack = [root]
for level, title, page in toc:
node = TOCNode(title, level, page, page)
# pop until parent found
while stack and stack[-1].level >= level:
stack.pop()
stack[-1].children.append(node)
stack.append(node)
return root
3 Цикл поиска (Loop)
Теперь самое интересное. Пишем цикл, который на каждом шаге показывает модели список детей текущего узла и просит выбрать один (или вернуться, если путь ошибочный).
import openai # версия >= 1.30.0
client = openai.OpenAI(api_key="...")
def loop_search(question, node, max_depth=5):
context_path = []
current = node
depth = 0
while depth < max_depth:
children = current.children
if not children:
# лист — берём текст
context_path.append(current.text_preview)
break
prompt = f"""You are navigating a document. The user asks: "{question}".
Current section: "{current.title}".
Its subsections:\n"""
for i, child in enumerate(children):
prompt += f"{i+1}. {child.title}: {child.text_preview[:100]}...\n"
prompt += "\nWhich subsection should I open next? Answer with number only, or 0 if the answer is already found in the current section."
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.1
)
choice = int(response.choices[0].message.content.strip())
if choice == 0:
context_path.append(current.text_preview)
break
if 1 <= choice <= len(children):
current = children[choice-1]
context_path.append(current.text_preview)
depth += 1
else:
break
return "\n\n---\n\n".join(context_path)
4 Финальная генерация ответа
Собранный контекст подаём в LLM вместе с вопросом. Теперь модель видит полный путь от корня до нужного параграфа, а не случайные чанки.
context = loop_search("What are the audit logging requirements for High-impact systems?", root_node)
final_prompt = f"Context:\n{context}\n\nQuestion: {{question}}\nAnswer concisely."
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": final_prompt}]
)
print(response.choices[0].message.content)
Нюансы, которые превратят код в продакшен
- Ограничение глубины. Ставьте max_depth=4..5. Больше — модель теряет нить, растёт стоимость.
- Кэширование превью. Для больших документов генерируйте превью один раз при старте и храните в MongoDB/Postgres.
- Гибрид с чанкингом. Если внутри раздела 50 страниц, лучше разбить его на подчанки и внутри узла хранить список чанков. LLM тогда выберет не просто раздел, а конкретный чанк.
- Обработка «No answer». Добавьте fallback: если после max_depth ответ не найден, модель должна вернуть «Информация не найдена» вместо галлюцинации.
- Семантический подсказчик. Перед циклом можно сделать быстрый эмбеддинг-поиск по превью всех узлов первого уровня, чтобы не ходить по всему дереву. Это ускорит поиск в 2–3 раза.
Когда Loop Engineering не нужен (и когда нужен обязательно)
| Сценарий | Loop Engineering | Плоский RAG |
|---|---|---|
| Поиск точного параграфа в 800‑страничном стандарте | ✅ | ❌ |
| Сводка по всему документу | ⚡️ лучший | ⚡️ нормально |
| Поиск по множеству коротких писем | ❌ | ✅ |
| Код-ревью (структура файлов) | ✅ | ❌ |
Полный код и ноутбуки
Все примеры из статьи, включая парсинг NIST, построение дерева и Loop Engineer, доступны в репозитории на GitHub. Ссылка в конце статьи (актуальный репозиторий с обновлениями на июль 2026).
Для тех, кто хочет глубже: Loop Engineering — частный случай итеративного agentic RAG. Если вам интересна тема агентов, почитайте нашу статью про Agentic RAG. А если вы работаете с регулируемыми документами — рекомендую серию про граф знаний для Compliance.
Будущее: от Loop к многоходовым трассировкам
Сейчас мы ходим по дереву последовательно. Но уже сейчас можно запускать несколько агентов параллельно — каждый исследует свою ветку, а потом результаты объединяются. Это напоминает Monte Carlo Tree Search для документов. Комбинация Loop Engineering + семантический поиск превью + параллельные агенты даст точность близкую к 100% для юридических и регуляторных текстов.
Единственное, что тормозит индустрию — это стоимость. Один запрос с 5 итерациями стоит ~$0.15 на GPT-4o. Для enterprise это копейки, для стартапа — уже заметно. Но с выходом GPT-4o-mini (в 10 раз дешевле) и ростом локальных моделей (Llama 4, Mistral Large 3) — порог входа снижается каждый квартал.
Не ждите, пока RAG «починят» из коробки. Берите оглавление, стройте дерево, запускайте цикл. LLM вас отблагодарит точным ответом.