Давайте честно: RAG-системы в 2026 году — это уже не экзотика. Они прекрасно находят документы, выцепляют релевантные куски, но стоит копнуть глубже — и LLM умудряется ляпнуть то, чего в контексте не было и близко. Вы просите: "Сколько сотрудников в компании X по данным отчёта за 2025?" — модель выдаёт "3200", хотя в документе чёрным по белому написано "2300". Или вставляет цитату с несуществующей страницы.
Стандартный рефлекс — тащить внешний API проверки фактов (например, FactCheck.org или Semantic Scholar) — убивает latency. Да и не всегда они доступны in-house. Нам нужно решение, которое работает внутри пайплайна, на том же Python, без доп HTTP-запросов, и укладывается в 50 миллисекунд. Самовосстанавливающийся RAG — это когда система не просто генерирует ответ, а сразу же проверяет его на галлюцинации и, если находит баг, чинит его, не отдавая пользователю ерунду.
Подход, который мы разберём, не требует ни Fine-tuning'а, ни доступа к внешним сервисам. Только Python, регулярные выражения и умная логика повторного запроса. Всё работает локально (или на вашем сервере) за десятки миллисекунд.
Три кита галлюцинаций: откуда они берутся
Прежде чем чинить, нужно понять, что ломается. В статье Как устроены галлюцинации в LLM мы копали residual stream и выяснили: модель склонна "додумывать" информацию, если контекст не перекрывает вопрос полностью. На практике это выливается в три конкретных паттерна:
- Fake citations — LLM приписывает утверждению несуществующую ссылку: "Как сказано в [1], ..." хотя в первом документе этого нет.
- Numeric contradictions — числа, даты, проценты, которые расходятся с источником (самый частый случай, как в примере выше).
- Entity fabrication — модель выдумывает имя, название компании, технологию, которых нет ни в одном из извлечённых документов.
Важно: это не 'плохая' LLM. Это фундаментальная особенность autoregressive генерации. Как только вероятность следующего токена падает ниже порога — сеть начинает 'импровизировать'. Конфликт контекста в RAG — одна из причин: когда документы содержат противоречивые данные, модель выбирает 'среднее'. Наша задача — отловить и подтолкнуть её обратно к фактам.
Архитектура самовосстановления: детектор + хилер
Классическая RAG-схема: query -> retriever -> augment -> generate. Мы вставляем между генерацией и выводом два блока:
- Hallucination Detector — сканирует ответ на предмет потенциальных галлюцинаций, сверяя каждое 'твёрдое' утверждение с исходными документами (которые лежат в памяти, никуда не ходим).
- Healer — если детектор нашёл проблему, формирует короткий 'исправляющий' промпт, который отправляется той же LLM (без нового поиска). Ответ фиксится локально, без перезапуска всего пайплайна.
Критическое требование: heal-запрос не должен снова галлюцинировать. Поэтому мы передаём LLM не весь контекст, а только те фрагменты, которые относятся к ошибке — уменьшаем шум.
1 Пишем детектор: regex + fuzzy matching
Начнём с 'fake citations'. Почти все LLM маркируют источники квадратными скобками: [1], [2], [3,4] и т.д. Мы парсим все такие ссылки, а затем проверяем, существует ли в переданных документах утверждение, которое модель приписывает этому источнику. Если не находим — галлюцинация.
import re
from difflib import SequenceMatcher
class HallucinationDetector:
def __init__(self, documents: list[str]):
self.documents = documents
def _extract_citations(self, text: str) -> list[str]:
# Находит все [N] или [N,M] ссылки
pattern = r'\[(\d+(?:,\s*\d+)*)\]'
matches = re.findall(pattern, text)
return [m.replace(' ', '') for m in matches]
def _check_citation_validity(self, statement: str, doc_idx: int) -> bool:
if doc_idx >= len(self.documents):
return False
doc = self.documents[doc_idx]
# Простейшая проверка: ищем подстроку из statement в doc (первые 50 символов)
# В реальности — используем эмбеддинги, но для speed используем SequenceMatcher
for sent in doc.split('.'):
sim = SequenceMatcher(None, statement[:50], sent[:50]).ratio()
if sim > 0.7:
return True
return False
def detect(self, answer: str) -> list[dict]:
issues = []
citations = self._extract_citations(answer)
# Разбиваем ответ на предложения с цитатами
sentences = re.split(r'(?<=\.)\s*', answer)
for sent in sentences:
if '[' in sent:
cited_idx = self._extract_citations(sent)
for idx_str in cited_idx:
for num in idx_str.split(','):
num = int(num) - 1 # нумерация с 1
if not self._check_citation_validity(sent, num):
issues.append({
'type': 'fake_citation',
'sentence': sent,
'cited_doc': num
})
# Числовые противоречия
numbers = re.findall(r'\b\d{2,}\b', answer)
for num in numbers:
for doc in self.documents:
if num in doc:
continue # число присутствует — ок
# Если числа нет ни в одном документе — potential issue
# (серьёзнее смотреть контекст, но для демо сойдёт)
else:
issues.append({
'type': 'numeric_contradiction',
'value': num
})
break
return issues2 Хилер: точечный repair-запрос
После того как мы нашли проблемное предложение, не нужно перегенерять весь ответ. Формируем минимальный промпт:
class Healer:
def __init__(self, llm):
self.llm = llm
def heal(self, original_answer: str, issue: dict, relevant_context: str) -> str:
prompt = f"""
Ты — ассистент, который исправляет фактические ошибки.
Вот предложение из ответа, которое может содержать ошибку:
{issue['sentence']}
Тип проблемы: {issue['type']}
Вот релевантный контекст из документа:
{relevant_context}
Перепиши ТОЛЬКО это предложение так, чтобы оно строго соответствовало контексту.
Если в контексте нет информации — удали предложение или замени на 'Информация не найдена.'
Новое предложение:"""
corrected_sentence = self.llm.generate(prompt)
# Заменяем в оригинальном ответе
healed = original_answer.replace(issue['sentence'], corrected_sentence)
return healedГлавный трюк: мы передаём не весь контекст размером 10K токенов, а только те абзацы, где есть совпадающие ключевые слова из проблемного предложения. Это уменьшает latency heal-шага до 10-30 мс (для маленьких моделей типа Llama 4 или Mistral Large).
3 Собираем пайплайн
class SelfHealingRAG:
def __init__(self, retriever, llm, detector, healer):
self.retriever = retriever
self.llm = llm
self.detector = detector
self.healer = healer
def answer(self, query: str) -> str:
docs = self.retriever.retrieve(query)
context = '\n'.join(docs)
initial_answer = self.llm.generate(f"Ответь на вопрос, используя контекст.\nКонтекст: {context}\nВопрос: {query}")
self.detector = HallucinationDetector(docs) # пересоздаём с актуальными документами
issues = self.detector.detect(initial_answer)
if not issues:
return initial_answer
# Для каждого issue находим локальный контекст
healed_answer = initial_answer
for issue in issues:
# поиск релевантного контекста (упрощённо — весь документ с индексом)
if issue['type'] == 'fake_citation' and 'cited_doc' in issue:
idx = issue['cited_doc']
relevant = docs[idx] if idx < len(docs) else ''
else:
relevant = docs[0] # fallback
healed_answer = self.healer.heal(healed_answer, issue, relevant)
return healed_answerПример работы: пользователь спрашивает "Какой revenue был у Apple в 2024?" RAG возвращает "Apple отчиталась о revenue $385 млрд в 2024 г. [1]" — детектор находит, что в документе [1] написано $391 млрд — обнаруживает числовое противоречие. Хилер исправляет на "$391 млрд [1]". Всё это занимает ~45 мс на одной Tesla T4.
Как избежать ложных срабатываний и не сломать хороший ответ
Главная опасность — гиперкоррекция. Если детектор слишком агрессивен, он будет править то, что на самом деле верно. Например, модель сказала "В 2023 году компания выпустила iPhone 15" — а в документах только "2023 год, презентация iPhone 15". Формально фраза есть, но наши fuzzy-проверки могут не найти точного совпадения и решить, что это галлюцинация.
Решение: добавляем порог уверенности. Если similarity между утверждением и контекстом меньше 0.6 — считаем галлюцинацией, если между 0.6 и 0.8 — только предупреждаем (логируем), правим только при строгом <0.6. В production можно использовать эксперимент с воспроизведением конфликтов, чтобы подобрать пороги.
Ещё момент: не пытайтесь исправлять каждое найденное несоответствие. Если модель уверенно генерирует числа, которые есть в контексте, но детектор ошибочно считает их галлюцинацией — вы только ухудшите качество. Лучше пропустить heal, чем вставить чушь.
Производительность: как уложиться в 50ms
Узкое место — не heal, а детектор, если он будет перебирать все документы для каждого числа. Оптимизации:
- Храните документы в инвертированном индексе (TF-IDF, BM25). Для каждого ключевого слова — список документов, где оно встречается. Тогда проверка числа требует O(1) lookup.
- Используйте кэширование результатов детекции для одинаковых вопросов (вопрос -> issues). Подойдёт LRU cache с TTL 5 секунд.
- Запускайте детектор и heal в асинхронном режиме (asyncio) — можно параллельно проверять разные типы галлюцинаций.
- Если у вас LLM работает на GPU, heal-запросы — это дополнительные токены. Оптимально использовать tiny-модель (например, Qwen 2.5 0.5B) для heal, чтобы не блокировать основную LLM. В 2026 году это уже стандарт: RAG 2026 roadmap рекомендует разделять генерацию и верификацию по разным моделям.
Типичные ошибки при внедрении (и как их не совершить)
| Ошибка | Последствие | Как исправить |
|---|---|---|
| Передавать весь context в heal-промпт | LLM снова может запутаться | Вырезайте только предложения с совпадениями ключевых слов |
| Не проверять числа из таблиц | Пропуск галлюцинаций, если число написано цифрами, а в контексте — прописью | Добавьте канонизацию: 3200 -> 3,200 и т.д. |
| Детектор исправляет, но ответ становится бессвязным | Текст теряет смысл | Делайте второй проход с проверкой связности (perplexity) |
Неочевидный совет: используйте семантическую близость вместо точного совпадения
Регулярные выражения и exact match — быстро, но часто ломаются. Модель может перефразировать: 'годовой доход' вместо 'revenue' — детектор пропустит. В 2026 году даже на CPU можно за 5ms получать эмбеддинги через all-MiniLM-L6-v2. Сравнивайте предложение ответа с каждым предложением документа по косинусной близости. Если расстояние больше 0.3 — подозрение на галлюцинацию. Это ловит большинство numeric contradictions и entity fabrication, даже если модель хитро перефразировала.
Да, это добавляет 10-15ms к детекции. Но в обмен вы получаете детектор, который реже пропускает реальные галлюцинации. В сценариях, где цена ошибки высока (медицина, финансы), эти миллисекунды оправданы. Как мы обсуждали в статье про доверие RAG, лучше задержать ответ на 20ms, чем выдать ложь.
Что дальше? Эволюция self-healing RAG
К 2026 году мы видим, что самовосстановление становится стандартной частью production RAG. Крупные компании (OpenAI, Anthropic) уже встраивают встроенные детекторы, но их API — чёрный ящик. In-house решение даёт полный контроль. Наш подход — база. Если вы интегрируете его с многошаговым reasoning (например, ReAct) — получите систему, которая не только чинит ошибки, но и сама решает, когда нужно запросить дополнительный документ. Мультимодальные RAG тоже выигрывают: проверка изображений (caption vs detected objects) — отдельный вызов для детектора.
Главное — не забывайте: самовосстановление не панацея. Лучше не допускать галлюцинаций вообще (через хороший ретривер и качественную аугментацию), чем исправлять их постфактум. Но когда время поджимает, а баги всё равно проскальзывают — пусть у вас будет этот молоток.