Проблема: ваша LLM врёт, а вы этого не замечаете
Попросите любую модель сравнить два документа. Любые. Контракты, технические спецификации, медицинские заключения. 70% времени она найдёт противоречия там, где их нет. Или пропустит очевидные нестыковки.
Почему? Потому что LLM работает с текстом как с «супом из токенов». Она не строит логических связей, не отслеживает временные линии, не понимает что «вступил в силу 15 января» и «действует с 20 января» — это конфликт. Она просто генерирует правдоподобный текст.
Типичная ошибка: вы даёте модели документы через RAG, спрашиваете «есть ли противоречия?» и получаете красивый отчёт с выдуманными проблемами. А настоящий конфликт остаётся в тени.
Решение не в промптах, а в архитектуре
Ещё один промпт «будь точным» не поможет. Нужен движок, который сначала извлекает факты детерминированным способом, строит логическую сеть, а уже потом просит LLM эту сеть проанализировать.
Забудьте про «просто передать документы в контекст». Мы строим трёхслойную систему:
- Факт-экстрактор: вытаскивает утверждения, даты, числа, условия с привязкой к источнику
- Логический каркас: строит граф отношений между фактами (противоречие, уточнение, дополнение)
- Аналитический слой: 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 # критично для работы с датами
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-системе, этот подход интегрируется прямо в пайплайн обработки документов. Факт-экстракция становится этапом индексирования.
Что делать, когда документов не два, а двести?
Масштабирование простое:
- Извлекаем факты из всех документов один раз
- Строим факт-базу в векторном хранилище (ChromaDB)
- Для нового документа: извлекаем его факты → ищем похожие в базе → проверяем конфликты
- Кэшируем результаты сравнения
Архитектура превращается в систему мониторинга согласованности документов. Новый контракт приходит — система сразу показывает: «это противоречит пункту 4.3 из договора от 2023-05-12».
Следующий шаг: тестирование на реальных данных
Возьмите три документа, которые должны согласовываться:
- Техническое задание
- Договор
- Акт выполненных работ
Запустите через движок. Увидите пропущенные даты, конфликтующие суммы, условия, которые в одном документе есть, а в другом — нет.
Потом попробуйте передать те же документы напрямую в LLM с промптом «найди противоречия». Разница будет болезненной.
И главное — этот движок не стареет. Новые правила добавляются кодом, а не промптами. Сегодня проверяете даты, завтра добавляете проверку юрисдикций, послезавтра — финансовых лимитов.
LLM остаётся там, где она сильна: анализ контекста, оценка критичности, формулировка рекомендаций. Но поиск противоречий — это работа алгоритмов, а не статистики на токенах.