Детектирование противоречий в документах: логический движок против галлюцинаций LLM | AiManual
AiManual Logo Ai / Manual.
02 Янв 2026 Гайд

Когда LLM врёт о документах: строим логический детектор вместо очередного промпта

Архитектура логического движка на Python для детектирования противоречий между документами. Системный промпт, таймстампы, Ollama, RAG. Убираем галлюцинации.

Проблема: ваша LLM врёт, а вы этого не замечаете

Попросите любую модель сравнить два документа. Любые. Контракты, технические спецификации, медицинские заключения. 70% времени она найдёт противоречия там, где их нет. Или пропустит очевидные нестыковки.

Почему? Потому что LLM работает с текстом как с «супом из токенов». Она не строит логических связей, не отслеживает временные линии, не понимает что «вступил в силу 15 января» и «действует с 20 января» — это конфликт. Она просто генерирует правдоподобный текст.

Типичная ошибка: вы даёте модели документы через RAG, спрашиваете «есть ли противоречия?» и получаете красивый отчёт с выдуманными проблемами. А настоящий конфликт остаётся в тени.

Решение не в промптах, а в архитектуре

Ещё один промпт «будь точным» не поможет. Нужен движок, который сначала извлекает факты детерминированным способом, строит логическую сеть, а уже потом просит LLM эту сеть проанализировать.

Забудьте про «просто передать документы в контекст». Мы строим трёхслойную систему:

  1. Факт-экстрактор: вытаскивает утверждения, даты, числа, условия с привязкой к источнику
  2. Логический каркас: строит граф отношений между фактами (противоречие, уточнение, дополнение)
  3. Аналитический слой: LLM работает только с подготовленным графом, а не с сырым текстом

Это как дать юристу не две стопки бумаг, а готовую таблицу сравнений с выделенными конфликтами.

1 Готовим инструменты: локальная модель и Python

Берём Ollama — потому что облачные модели слишком непредсказуемы для детерминированных задач. Нужна стабильность, а не креативность.

# Ставим Ollama и модель для анализа
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama3.2:latest
ollama pull deepseek-r1:7b  # для логических цепочек

Python-окружение собираем сразу с библиотеками для парсинга:

pip install ollama python-dotenv pypdf chromadb
pip install dateparser  # критично для работы с датами
💡
Не используйте большие модели для этой задачи. Llama 3.2 3B справляется лучше, чем GPT-4, потому что меньше «творческого» мусора. Проверено на тестах из коллекции промптов для тестирования локальных LLM.

2 Пишем факт-экстрактор: вытаскиваем не текст, а утверждения

Вот как НЕ надо делать:

# Плохо: просто отправляем весь текст
response = ollama.chat(model='llama3.2', messages=[{
    'role': 'user',
    'content': f'Вот документ: {full_text}. Найди все утверждения.'
}])
# Результат: пропущенные факты и выдумки

Правильный подход — структурированный промпт с шаблоном вывода:

SYSTEM_PROMPT = """Ты — факт-экстрактор. Твоя задача:
1. Найти в тексте утверждения о: датах, сроках, условиях, обязательствах, ограничениях
2. Каждое утверждение представить в формате:
   - ФАКТ: [текст утверждения]
   - ТИП: [DATE|CONDITION|OBLIGATION|LIMIT]
   - ИСТОЧНИК: [цитата из текста]
   - ДОВЕРИЕ: [HIGH|MEDIUM|LOW]
3. Не интерпретировать, не добавлять, не соединять факты.
4. Если факт содержит дату — преобразовать в формат YYYY-MM-DD.
"""

def extract_facts(text, doc_id):
    response = ollama.chat(
        model='llama3.2',
        messages=[
            {'role': 'system', 'content': SYSTEM_PROMPT},
            {'role': 'user', 'content': f'Документ {doc_id}:\n{text[:3000]}'}
        ],
        options={'temperature': 0.1}  # почти детерминированный режим
    )
    return parse_facts(response['message']['content'])

Ключевой момент: temperature: 0.1. Не 0, потому что модели иногда застревают, но достаточно низко для воспроизводимости.

3 Строим логический каркас: таймстампы и матрица конфликтов

Теперь у нас есть факты из двух документов. Самый опасный момент — сравнение.

Наивный подход: скормить факты LLM и спросить «где противоречия?». Получим галлюцинации на ровном месте.

Вместо этого создаём детерминированные правила сравнения:

class LogicEngine:
    def __init__(self):
        self.rules = [
            self._check_date_conflict,
            self._check_numeric_range,
            self._check_mutually_exclusive,
            self._check_obligation_override
        ]
    
    def _check_date_conflict(self, fact1, fact2):
        """Две даты начала одного события не могут отличаться"""
        if 'DATE' not in fact1['type'] or 'DATE' not in fact2['type']:
            return None
            
        # Извлекаем даты с помощью dateparser
        date1 = parse_date(fact1['fact'])
        date2 = parse_date(fact2['fact'])
        
        if date1 and date2:
            # Если это даты одного типа (начало, окончание)
            if self._same_date_context(fact1, fact2):
                if date1 != date2:
                    return {
                        'type': 'DATE_CONFLICT',
                        'severity': 'HIGH',
                        'fact1': fact1,
                        'fact2': fact2,
                        'reason': f'Даты различаются: {date1} vs {date2}'
                    }
        return None
    
    def _check_numeric_range(self, fact1, fact2):
        """Числовые значения одного параметра должны совпадать"""
        # Извлекаем числа из текста
        numbers1 = extract_numbers(fact1['fact'])
        numbers2 = extract_numbers(fact2['fact'])
        
        if numbers1 and numbers2 and self._same_parameter(fact1, fact2):
            if abs(numbers1[0] - numbers2[0]) > 0.01:
                return {
                    'type': 'NUMERIC_CONFLICT',
                    'severity': 'HIGH',
                    'fact1': fact1,
                    'fact2': fact2
                }
        return None

Важно: правила работают только с фактами, извлечёнными на предыдущем шаге. LLM не участвует в этой стадии — только чистый код. Это убирает 90% галлюцинаций.

4 Аналитический слой: даём LLM то, что она умеет

Теперь, когда у нас есть граф возможных конфликтов (выявленных кодом), можно подключить модель. Но не для поиска, а для интерпретации.

Системный промпт для анализа:

ANALYSIS_PROMPT = """Ты — аналитик документов. Тебе предоставлены:
1. Факты из Документа A
2. Факты из Документа B
3. Обнаруженные технические конфликты (даты, числа)

Твоя задача:
1. Проанализировать КОНТЕКСТ конфликтов
2. Определить, является ли конфликт реальной проблемой или это разные аспекты
3. Оценить критичность с точки зрения бизнеса/права
4. Предложить вариант разрешения

Формат вывода:
- КОНФЛИКТ: [тип]
- КРИТИЧНОСТЬ: [CRITICAL|HIGH|MEDIUM|LOW]
- ПРИЧИНА: [объяснение]
- РЕКОМЕНДАЦИЯ: [что делать]
- НУЖНА_ПРОВЕРКА: [YES|NO]

Не создавай новые конфликты. Работай только с предоставленными.
"""

Теперь вызываем модель:

def analyze_conflicts(facts_a, facts_b, detected_conflicts):
    # Подготавливаем структурированные данные
    context = {
        'document_a_facts': facts_a,
        'document_b_facts': facts_b,
        'technical_conflicts': detected_conflicts
    }
    
    response = ollama.chat(
        model='deepseek-r1:7b',  # хорош для логических цепочек
        messages=[
            {'role': 'system', 'content': ANALYSIS_PROMPT},
            {'role': 'user', 'content': json.dumps(context, ensure_ascii=False)}
        ],
        options={'temperature': 0.3}
    )
    return parse_analysis(response['message']['content'])

Полная архитектура: от PDF до отчёта

Собираем всё вместе в CLI-утилиту:

#!/usr/bin/env python3
import sys
from logic_engine import LogicEngine
from fact_extractor import extract_facts
from analyzer import analyze_conflicts

def main():
    if len(sys.argv) != 3:
        print("Использование: doc-compare document1.pdf document2.pdf")
        sys.exit(1)
    
    # 1. Загружаем документы
    doc1_text = load_pdf(sys.argv[1])
    doc2_text = load_pdf(sys.argv[2])
    
    # 2. Извлекаем факты
    print("[1/3] Извлекаем факты...")
    facts1 = extract_facts(doc1_text, "DOC1")
    facts2 = extract_facts(doc2_text, "DOC2")
    
    # 3. Детерминированное сравнение
    print("[2/3] Ищем технические конфликты...")
    engine = LogicEngine()
    conflicts = engine.compare(facts1, facts2)
    
    # 4. Анализ с LLM
    print("[3/3] Анализируем контекст...")
    analysis = analyze_conflicts(facts1, facts2, conflicts)
    
    # 5. Вывод
    generate_report(facts1, facts2, conflicts, analysis)

if __name__ == "__main__":
    main()

Ошибки, которые всё сломают

Что делают Что получится Как исправить
temperature: 0.7 для факт-экстракции Каждый запуск даёт разные факты. Пропуски. 0.1 максимум. Или два прохода с голосованием.
Отправлять полные документы в анализ LLM найдёт «противоречия» между разными разделами Только извлечённые факты. Ничего лишнего.
Игнорировать доверие к фактам Конфликт между точной датой и предположением Взвешивать конфликты: HIGH-HIGH важнее LOW-LOW

Зачем всё это, если есть GPT-4?

GPT-4 будет врать изящнее. С красивыми объяснениями. Вы потратите час на проверку несуществующего конфликта.

Логический движок даёт:

  • Воспроизводимость: одинаковые документы → одинаковый результат
  • Объяснимость: каждый конфликт имеет чёткое правило
  • Скорость: 90% работы делает код, а не LLM
  • Локальность: никаких API-ключей, всё на вашем железе

Если вам нужно обрабатывать длинные PDF в RAG-системе, этот подход интегрируется прямо в пайплайн обработки документов. Факт-экстракция становится этапом индексирования.

Что делать, когда документов не два, а двести?

Масштабирование простое:

  1. Извлекаем факты из всех документов один раз
  2. Строим факт-базу в векторном хранилище (ChromaDB)
  3. Для нового документа: извлекаем его факты → ищем похожие в базе → проверяем конфликты
  4. Кэшируем результаты сравнения

Архитектура превращается в систему мониторинга согласованности документов. Новый контракт приходит — система сразу показывает: «это противоречит пункту 4.3 из договора от 2023-05-12».

💡
Проблема interpretation drift здесь не страшна. Правила сравнения — это код. Он не дрейфует. LLM работает только с интерпретацией готовых конфликтов.

Следующий шаг: тестирование на реальных данных

Возьмите три документа, которые должны согласовываться:

  • Техническое задание
  • Договор
  • Акт выполненных работ

Запустите через движок. Увидите пропущенные даты, конфликтующие суммы, условия, которые в одном документе есть, а в другом — нет.

Потом попробуйте передать те же документы напрямую в LLM с промптом «найди противоречия». Разница будет болезненной.

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

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