Self-Healing RAG: исправляем галлюцинации LLM в реальном времени без API | AiManual
AiManual Logo Ai / Manual.
05 Май 2026 Гайд

Самовосстанавливающийся RAG: как исправить галлюцинации LLM в реальном времени без внешних API

Пошаговый гайд по созданию самовосстанавливающегося RAG-пайплайна на чистом Python. Детектим fake citations, числовые противоречия и сущности — чиним ответ за <

Давайте честно: 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. Мы вставляем между генерацией и выводом два блока:

  1. Hallucination Detector — сканирует ответ на предмет потенциальных галлюцинаций, сверяя каждое 'твёрдое' утверждение с исходными документами (которые лежат в памяти, никуда не ходим).
  2. 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 issues
💡
Этот детектор — минимальная версия. В production стоит добавить Named Entity Recognition (spaCy) для обнаружения выдуманных имён компаний и проверять их через fuzzy lookup в документах.

2 Хилер: точечный 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) — отдельный вызов для детектора.

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

Подписаться на канал