Агенты для работы с юридическими документами: извлечение контрактов, сравнение и выявление рисков


title: "Агенты для работы с юридическими документами: извлечение контрактов, сравнение и выявление рисков" slug: legal-document-agents-contracts-2026-ru date: 2026-02-24 lang: ru

Агенты для работы с юридическими документами: извлечение контрактов, сравнение и выявление рисков

Ключевые факты

  • Версии пакетов (PyPI, март 2026): pypdf==6.7.4, pdfplumber==0.11.9, python-docx==1.2.0, langchain==1.2.10, langgraph==1.0.10
  • Типы пунктов контракта для извлечения: NDA/конфиденциальность, ограничение ответственности, возмещение убытков, передача прав ИС/собственности, расторжение, условия оплаты, регулирующее право, разрешение споров/арбитраж, гарантии, непреодолимая сила
  • Риск галлюцинаций: НИКОГДА не выдумывайте текст пункта, даты, денежные суммы или имена сторон — агенты должны цитировать текст по умолчанию из исходного документа со ссылками на страницу/раздел
  • Ворота для проверки человеком: ОБЯЗАТЕЛЬНЫ перед завершением любого анализа контракта — агенты помогают юристам, никогда их не заменяют
  • Формат вывода: Всегда структурированный JSON с {"clause_type": "...", "text": "...", "section": "...", "page": N, "confidence": 0.0-1.0}
  • Размер чанка для длинных контрактов: 1000-1500 токенов на чанк с перекрытием в 200 токенов, чтобы предотвратить разделение пунктов между границами
  • Стратегия классификации пунктов: Двухэтапный подход — быстрый фильтр ключевых слов (дешевый) для очевидных случаев, затем классификация LLM (дорогая, точная) только для неоднозначных разделов
  • Категории выявления рисков: Критический (неограниченная ответственность, широкая передача прав ИС), Высокий (односторонние права на изменение, отсутствие ограничения ответственности), Средний (короткое уведомление об автоматическом продлении), Низкий (неблагоприятная юрисдикция)
  • Стандартные пункты, которые должны быть присутствуют: Ограничение ответственности, возмещение убытков, конфиденциальность, регулирующее право, расторжение — выявление при отсутствии
  • Определение юрисдикции: Извлечение регулирующего права и места арбитража из текста контракта — маршрутизация документа к подходящему квалифицированному юристу для проверки
  • Генерация редлайна: Сравнение на уровне слов с использованием difflib.SequenceMatcher — классификация изменений как "существенные" (влияют на юридическое содержание), "незначительные" (уточнения) или "форматирование"
  • Уверенность для автоматического выявления: Используйте сопоставление ключевых слов для уверенности > 0,85; передача LLM для уверенности < 0,85
  • Оптимизация стоимости: Пакет 10 разделов на один вызов LLM вместо по одному за раз; используйте Claude Haiku для классификации, Opus для оценки рисков
  • Синтаксический анализ документов: pypdf для native PDF, pdfplumber для таблиц/форм, python-docx для DOCX — OCR (Tesseract/AWS Textract) только для сканированных изображений
  • Определение раздела: Юридические контракты используют нумерованные разделы ("1.", "1.1", "Статья I") — определите по шаблону regex + размер шрифта/выделение в PDF
  • Извлечение ключевых терминов: Извлечение денежных лимитов из пунктов об ограничении ответственности ($\d+ или Nx fees paid), сроков из пунктов о расторжении (\d+ (days|months|years))
  • Обработка конфиденциальности: Шифрование содержимого документа с помощью Fernet (симметричное шифрование); логирование всех попыток доступа; ограничение доступа только авторизованным пользователям
  • Уровни рабочего потока проверки: Автоматическое одобрение (оценка риска < 20, стоимость < $10K), проверка параллегалом (< 40, < $50K), доцент (< 65, < $500K), партнер (критические риски или высокая стоимость)
  • Допуск ложных срабатываний: Лучше выявить 10 неважных проблем, чем пропустить 1 критический риск — отдавайте приоритет полноте над точностью при выявлении рисков
  • Выравнивание сравнения пунктов: Сопоставьте разделы по номеру раздела в первую очередь, затем по сходству названия, если номера не совпадают между версиями контракта
  • Ограничения агента: Агенты определяют ШАБЛОНЫ, а не юридические выводы — "этот пункт может создать неограниченную ответственность", а не "этот пункт незаконен"

Что такое агент для работы с юридическими документами

Агент для работы с юридическими документами — это специализированная AI система, которая помогает юристам и юридическим командам с механическими задачами анализа контрактов: извлечение текста пункта из PDF, классификация пунктов по типам (возмещение убытков, конфиденциальность и т.д.), сравнение версий контрактов для создания редлайнов и выявление пунктов, которые отклоняются от рыночных стандартов.

Что могут делать агенты:

  • Парсить файлы PDF/DOCX и извлекать структурированный текст по разделам
  • Определять типы пунктов на основе содержания и заголовков (например, "это пункт об ограничении ответственности")
  • Извлекать ключевые термины (денежные лимиты, периоды времени, имена сторон, даты)
  • Сравнивать две версии контракта и выделять изменения
  • Выявлять пункты с необычными условиями (например, неограниченное возмещение убытков, бессрочная передача прав ИС)
  • Определять регулирующее право и юрисдикцию из текста контракта
  • Маршрутизировать контракты к подходящим рецензентам на основе оценки рисков и юрисдикции

Что агенты НИКОГДА не могут и не должны делать:

  • Предоставлять юридическое консультирование или определять, является ли пункт "законным" или "исполнимым"
  • Выносить обязательные юридические решения или заменять проверку юристом
  • Подписывать или выполнять контракты от имени сторон
  • Гарантировать точность извлечения (ошибки OCR, неоднозначное форматирование)
  • Понимать контекст-специфичные деловые последствия условий
  • Практиковать право без лицензии

Критическое ограничение: Все выходные данные агента ДОЛЖНЫ быть рассмотрены лицензированным юристом перед использованием. Агенты сокращают время проверки путем предварительной обработки и выявления проблем, но окончательное юридическое решение остается за человеком-юристом.

Почему это важно: Проверка контракта имеет высокие ставки. Пропущенный пункт может стоить миллионов. Агенты берут на себя утомительную работу (парсинг 100-страничных PDF, сравнение пункта за пунктом между версиями, проверка стандартных условий), а люди решают вопросы суждения (является ли эта крышка возмещения ответственности разумной для этой сделки? соответствует ли эта гарантия нашему аппетиту к риску?).

Рамки принятия решений

Задача Пригодность агента Подход Требуется проверка человеком
Извлечение пунктов Высокая Двухэтапный: предварительный фильтр ключевых слов + LLM для неоднозначных Да — проверить точность классификации, особенно для существенных пунктов
Сравнение контрактов Высокая Автоматизированное сравнение на уровне слов, выявление существенных vs незначительных изменений Да — проверить все существенные изменения; незначительные можно быстро просмотреть
Оценка рисков Средняя Сопоставление шаблонов + оценка LLM, вывод оценки риска 0-100 Да — критические/высокие риски требуют юридического суждения; средние/низкие могут быть проверены параллегалом
Полный юридический анализ Низкая Агенты предварительно обрабатывают и выявляют проблемы, но не могут заменить полный анализ Всегда — агенты входят в процесс анализа, не замена
Определение юрисдикции Высокая Regex + сопоставление ключевых слов для пунктов регулирующего права Нет — решение маршрутизации, не юридический вывод; но проверить перед назначением рецензента
Определение отсутствующих пунктов Высокая Проверка на стандартные пункты (ограничение, возмещение и т.д.) Да — определить, является ли отсутствие намеренным или ошибкой
Генерация редлайна Высокая Автоматизированное текстовое сравнение с классификацией значимости изменений Да — юрист проверяет редлайн для оценки деловых последствий
Извлечение условий Высокая Regex + LLM для структурированных значений (даты, суммы, периоды) Да — проверить соответствие извлеченных значений исходному документу
Сравнение NDA Высокая Специализированный под-агент для распространенных шаблонов пунктов NDA Зависит — стандартные NDA могут допускать проверку параллегалом; индивидуальные NDA требуют юриста
Анализ документов M&A Низкая Слишком высокие ставки для текущих возможностей агента Всегда — M&A требует глубокого юридического опыта

Используйте агентов для:

  • Большого объема низкорисковых контрактов (NDA, стандартные соглашения с поставщиками)
  • Начального распределения и оценки рисков перед проверкой человеком
  • Сравнения версий для определения измененных пунктов
  • Извлечения структурированных данных из контрактов для отчетности

НЕ используйте агентов для:

  • Окончательного одобрения контракта без проверки человеком
  • Высокостоимостных, сложных транзакций (M&A, лицензирование корпоративного программного обеспечения)
  • Контрактов в незнакомых юрисдикциях без проверки местного юриста
  • Юридически привилегированных коммуникаций (работа адвоката-клиента)

Справочная таблица параметров

Параметр Значение Примечания
chunk_size 1000-1500 токенов Баланс между контекстным окном и стоимостью; контракты имеют длинные пункты
chunk_overlap 200 токенов Предотвращает разделение пункта между чанками
confidence_threshold 0.85 Используйте сопоставление ключевых слов выше этого; классификация LLM ниже
max_clauses_to_extract Неограниченное (все пункты) Извлеките все; отфильтруйте по типу позже
output_format JSON со схемой {"clause_type": str, "text": str, "section": str, "page": int, "confidence": float}
llm_model_classification claude-haiku-4-5 Дешевая модель для классификации типа пункта
llm_model_risk_assessment claude-opus-4-6 Дорогая модель для анализа риска (ошибки дорогостоящие)
batch_size 10 разделов Пакет нескольких разделов за один вызов LLM для экономии затрат
risk_score_scale 0-100 Критический=25pts, Высокий=10pts, Средний=5pts, Низкий=2pts, Отсутствующий=8pts
redline_diff_level Уровень слова Используйте difflib.SequenceMatcher для сравнения слово в слово
change_significance_threshold Изменение текста на 5% Изменения > 5% или содержащие существенные термины = "существенное"

Архитектура: извлечение и классификация пунктов

Шаг 1: Парсинг документа

Юридические контракты обычно структурированы с четко обозначенными разделами. Агент должен:

  1. Определить формат: PDF native, отсканированный PDF, DOCX, изображение

  2. Выбрать парсер:

    • pypdf для извлечения текста из native PDF с обнаружением структуры
    • pdfplumber для таблиц и форм
    • python-docx для документов Word
    • AWS Textract или Tesseract для отсканированных/образных контрактов
  3. Определить разделы по regex и маркерам: \d+\., Article, SECTION, прямое выделение (шрифт, размер)

import pypdf
import pdfplumber
import re

def extract_sections(pdf_path):
    """Extract sections from PDF by number and title."""
    sections = []

    with pdfplumber.open(pdf_path) as pdf:
        for page_num, page in enumerate(pdf.pages, start=1):
            text = page.extract_text()
            # Regex for numbered sections: 1. Title, 1.1 Subtitle, Article I
            matches = re.finditer(r'^(\d+\.?\d*|Article [IVX]+)\s+(.+)$', text, re.MULTILINE)
            for match in matches:
                section_num = match.group(1)
                section_title = match.group(2)
                sections.append({
                    'number': section_num,
                    'title': section_title,
                    'page': page_num,
                    'content': None  # populated in step 2
                })

    return sections

Проблема: распределение контента по разделам

После определения заголовков разделов вам нужно парсить контент, который может охватывать несколько страниц. Используйте "следующий раздел" как граница:

def chunk_by_sections(pdf_path, sections):
    """Map content to each section."""
    with pdfplumber.open(pdf_path) as pdf:
        full_text = ""
        for page in pdf.pages:
            full_text += page.extract_text() + "\n"

    for i, section in enumerate(sections):
        # Find content between this section and the next
        start_pattern = re.escape(section['number'])
        if i < len(sections) - 1:
            next_pattern = re.escape(sections[i+1]['number'])
            match = re.search(
                f"{start_pattern}.*?(?={next_pattern})",
                full_text,
                re.DOTALL
            )
        else:
            # Last section: go to end of document
            match = re.search(f"{start_pattern}.*", full_text, re.DOTALL)

        if match:
            section['content'] = match.group(0)

    return sections

Шаг 2: Разбиение на чанки с перекрытием

Длинные пункты часто охватывают сотни токенов. Если вы разобьете на границах разделов и пункты разделят чанки, LLM может пропустить контекст. Решение: наложенные чанки.

def chunk_with_overlap(sections, chunk_size=1500, overlap=200):
    """Create overlapping chunks from section content."""
    chunks = []

    for section in sections:
        content = section['content']
        tokens = content.split()  # Simple tokenization

        i = 0
        while i < len(tokens):
            # Extract chunk of size chunk_size, end at sentence boundary
            chunk_tokens = tokens[i:i+chunk_size]
            chunk_text = ' '.join(chunk_tokens)

            # Find last period to avoid mid-sentence splits
            last_period = chunk_text.rfind('.')
            if last_period > 0 and last_period > len(chunk_text) * 0.8:
                chunk_text = chunk_text[:last_period+1]
                chunk_tokens = chunk_text.split()

            chunks.append({
                'section_number': section['number'],
                'section_title': section['title'],
                'page': section['page'],
                'content': chunk_text,
                'token_count': len(chunk_tokens)
            })

            # Move forward by (chunk_size - overlap)
            i += len(chunk_tokens) - overlap

    return chunks

Шаг 3: Двухэтапная классификация пунктов

Этап 1: Быстрое сопоставление ключевых слов (уверенность > 0.85 → отсутствие вызова LLM)

# Keyword patterns for common clause types
CLAUSE_KEYWORDS = {
    'limitation_of_liability': [
        'limitation of liability', 'limited liability', 'liability cap',
        'shall not be liable', 'notwithstanding', 'aggregate liability'
    ],
    'indemnification': [
        'indemnify', 'indemnification', 'hold harmless',
        'defend against', 'third-party claims'
    ],
    'confidentiality': [
        'confidential', 'confidentiality', 'proprietary',
        'trade secret', 'non-disclosure'
    ],
    'ip_assignment': [
        'assignment', 'owns', 'intellectual property', 'copyright',
        'patent', 'rights of', 'ownership of', 'transfer'
    ],
    'termination': [
        'terminate', 'termination', 'term', 'duration',
        'expiration', 'notice of termination'
    ],
    'nda': [
        'non-disclosure', 'confidential information', 'nda',
        'not disclose', 'disclosure', 'restricted to'
    ]
}

def quick_classify(chunk_text, confidence_threshold=0.85):
    """Fast keyword-based classification."""
    text_lower = chunk_text.lower()

    matches = {}
    for clause_type, keywords in CLAUSE_KEYWORDS.items():
        match_count = sum(1 for kw in keywords if kw in text_lower)
        if match_count > 0:
            # Simple confidence: proportion of keywords matched
            confidence = min(match_count / len(keywords), 1.0)
            matches[clause_type] = confidence

    # Return highest confidence match if above threshold
    if matches:
        best_match = max(matches.items(), key=lambda x: x[1])
        if best_match[1] >= confidence_threshold:
            return {'clause_type': best_match[0], 'confidence': best_match[1], 'method': 'keyword'}

    return None

Этап 2: Классификация LLM для неоднозначных случаев (< 0.85 уверенности)

from anthropic import Anthropic

client = Anthropic()

def llm_classify(chunk_text, conversation_history=None):
    """Classify clause type using Claude."""

    if conversation_history is None:
        conversation_history = []

    system_prompt = """You are a legal clause classifier. Analyze the provided text and determine
    its clause type. Respond in JSON format:
    {
        "clause_type": "limitation_of_liability|indemnification|confidentiality|ip_assignment|termination|nda|governing_law|dispute_resolution|warranty|force_majeure|payment_terms|other",
        "confidence": 0.0-1.0,
        "reasoning": "Why you classified it this way"
    }

    Only identify ONE primary clause type. If multiple types are present, pick the most prominent.
    If the text is not a recognizable clause type, respond with "other"."""

    conversation_history.append({
        "role": "user",
        "content": f"Classify this contract clause:\n\n{chunk_text}"
    })

    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=200,
        system=system_prompt,
        messages=conversation_history
    )

    assistant_message = response.content[0].text
    conversation_history.append({
        "role": "assistant",
        "content": assistant_message
    })

    import json
    try:
        result = json.loads(assistant_message)
        return result, conversation_history
    except json.JSONDecodeError:
        return {"clause_type": "other", "confidence": 0.0, "reasoning": "Failed to parse"}, conversation_history

Полный конвейер двухэтапной классификации:

def classify_clauses(chunks, batch_size=10):
    """Two-stage classification: keyword first, then LLM."""

    results = []
    conversation_history = None

    for i, chunk in enumerate(chunks):
        # Stage 1: Keyword matching
        keyword_result = quick_classify(chunk['content'])

        if keyword_result and keyword_result['confidence'] >= 0.85:
            # High confidence → use keyword result
            results.append({
                **chunk,
                **keyword_result
            })
        else:
            # Low confidence → use LLM
            if conversation_history is None:
                conversation_history = []

            llm_result, conversation_history = llm_classify(chunk['content'], conversation_history)

            results.append({
                **chunk,
                **llm_result,
                'method': 'llm'
            })

        # Batch processing for cost optimization
        if (i + 1) % batch_size == 0:
            conversation_history = None  # Reset conversation every batch

    return results

Извлечение ключевых условий

После классификации пункты часто требуют структурированного извлечения: сумма денег (лимит ответственности), дата (срок действия), период (уведомление за 30 дней). Используйте regex + LLM для надежного извлечения.

import re

def extract_key_terms(clause_text, clause_type):
    """Extract structured values from a classified clause."""

    terms = {}

    if clause_type == 'limitation_of_liability':
        # Extract monetary cap: $1M, £500K, etc.
        amount_pattern = r'(\$|£|€)?(\d{1,3}(?:,\d{3})*|\d+)(?:\s*(million|billion|thousand|M|B|K))?'
        match = re.search(amount_pattern, clause_text, re.IGNORECASE)
        if match:
            terms['liability_cap'] = match.group(0)

    elif clause_type == 'termination':
        # Extract notice period: "30 days notice", "60 days"
        notice_pattern = r'(\d+)\s+(days|months|years)\s+(?:notice|notice of)?'
        match = re.search(notice_pattern, clause_text, re.IGNORECASE)
        if match:
            terms['notice_period'] = f"{match.group(1)} {match.group(2)}"

        # Extract term length: "for a term of 3 years"
        term_pattern = r'(?:term|duration)\s+(?:of)?\s+(\d+)\s+(days|months|years)'
        match = re.search(term_pattern, clause_text, re.IGNORECASE)
        if match:
            terms['contract_duration'] = f"{match.group(1)} {match.group(2)}"

    elif clause_type == 'payment_terms':
        # Extract payment amounts and due dates
        amount_pattern = r'(\$|£|€)?(\d{1,3}(?:,\d{3})*|\d+)'
        amounts = re.findall(amount_pattern, clause_text)
        if amounts:
            terms['payment_amounts'] = [a[0] + a[1] for a in amounts]

        # Extract due date: "within 30 days", "net 60"
        due_pattern = r'(?:within|net|due)\s+(\d+)\s+(days|months)?'
        match = re.search(due_pattern, clause_text, re.IGNORECASE)
        if match:
            terms['payment_due'] = f"{match.group(1)} {match.group(2) or 'days'}"

    return terms

Выявление и оценка рисков

Агент должен выявлять пункты, которые отклоняются от рыночных норм и представляют риск.

def assess_risk(classified_clauses):
    """Assess risk of each clause and contract overall."""

    risk_scoring = {
        'critical': 25,
        'high': 10,
        'medium': 5,
        'low': 2
    }

    clause_risks = []

    for clause in classified_clauses:
        clause_type = clause['clause_type']
        text = clause['content']

        risk_level = 'low'
        risk_factors = []

        if clause_type == 'limitation_of_liability':
            # Critical: no liability cap (unlimited liability)
            if 'no limitation' in text.lower() or 'unlimited' in text.lower():
                risk_level = 'critical'
                risk_factors.append('Unlimited liability exposure')
            # High: cap is very high (e.g., > 2x annual fees)
            elif re.search(r'liability.*equal.*fees', text, re.IGNORECASE):
                risk_level = 'high'
                risk_factors.append('Liability cap is multiple of annual fees')

        elif clause_type == 'indemnification':
            # Critical: indemnity for vendor's own negligence
            if 'sole negligence' in text.lower() or 'own negligence' in text.lower():
                risk_level = 'critical'
                risk_factors.append('Indemnity covers vendor sole negligence')
            # High: unilateral indemnification
            if 'indemnify' in text.lower() and 'shall not indemnify' not in text.lower():
                risk_level = 'high'
                risk_factors.append('Unilateral indemnification obligation')

        elif clause_type == 'ip_assignment':
            # Critical: broad assignment of pre-existing IP
            if 'pre-existing' in text.lower() and 'assign' in text.lower():
                risk_level = 'critical'
                risk_factors.append('Assignment includes pre-existing IP')
            # High: perpetual assignment
            if 'perpetual' in text.lower():
                risk_level = 'high'
                risk_factors.append('Perpetual IP assignment')

        elif clause_type == 'termination':
            # Medium: auto-renewal with short notice
            if 'auto-renew' in text.lower() and '30 days' in text.lower():
                risk_level = 'medium'
                risk_factors.append('Auto-renewal with 30-day notice')

        clause_risks.append({
            'section': clause['section_number'],
            'clause_type': clause_type,
            'risk_level': risk_level,
            'risk_factors': risk_factors,
            'risk_score': risk_scoring.get(risk_level, 0)
        })

    # Calculate overall contract risk score (out of 100)
    total_risk_score = sum(cr['risk_score'] for cr in clause_risks)

    # Normalize to 0-100 scale
    contract_risk_score = min(total_risk_score * 2, 100)

    return {
        'contract_risk_score': contract_risk_score,
        'clause_risks': clause_risks,
        'review_tier': classify_review_tier(contract_risk_score)
    }

def classify_review_tier(risk_score):
    """Route contract to appropriate reviewer based on risk."""
    if risk_score < 20:
        return 'auto_approve'
    elif risk_score < 40:
        return 'paralegal_review'
    elif risk_score < 65:
        return 'associate_review'
    else:
        return 'partner_review'

Использование Claude для оценки рисков:

Для более совершенной оценки рисков используйте Claude Opus для анализа:

def llm_risk_assessment(clause, contract_context):
    """Use Claude Opus for detailed risk assessment."""

    client = Anthropic()

    prompt = f"""Analyze this contract clause for legal and business risks:

Clause Type: {clause['clause_type']}
Section: {clause['section_number']}
Content: {clause['content']}

Contract Context:
- Industry: {contract_context.get('industry', 'unknown')}
- Contract Value: {contract_context.get('value', 'unknown')}
- Party Type: {contract_context.get('party_type', 'unknown')} (vendor, customer, partner)

Provide a JSON response:
{{
    "risk_level": "critical|high|medium|low",
    "risk_factors": ["factor1", "factor2"],
    "recommendations": ["recommendation1"],
    "market_standard": "description of what's typical in this industry"
}}

Be conservative: if unsure, escalate to higher risk level."""

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": prompt
        }]
    )

    import json
    result = json.loads(response.content[0].text)

    return result

Сравнение контрактов и создание редлайнов

Когда у вас есть две версии контракта (редакция 1 vs редакция 2), агент должен генерировать редлайн, подчеркивая изменения.

import difflib

def generate_redline(original_text, revised_text):
    """Generate word-level diff between contract versions."""

    # Tokenize into words
    original_words = original_text.split()
    revised_words = revised_text.split()

    # Use difflib.SequenceMatcher for word-by-word comparison
    matcher = difflib.SequenceMatcher(None, original_words, revised_words)

    redline = []

    for tag, i1, i2, j1, j2 in matcher.get_opcodes():
        if tag == 'replace':
            redline.append({
                'type': 'changed',
                'original': ' '.join(original_words[i1:i2]),
                'revised': ' '.join(revised_words[j1:j2])
            })
        elif tag == 'delete':
            redline.append({
                'type': 'deleted',
                'original': ' '.join(original_words[i1:i2])
            })
        elif tag == 'insert':
            redline.append({
                'type': 'added',
                'revised': ' '.join(revised_words[j1:j2])
            })
        elif tag == 'equal':
            redline.append({
                'type': 'unchanged',
                'text': ' '.join(original_words[i1:i2])
            })

    return redline

def classify_change_significance(original, revised, material_keywords):
    """Classify if a change is material or minor."""

    # If > 5% of text changed, likely material
    total_words = max(len(original.split()), len(revised.split()))
    diff_words = abs(len(original.split()) - len(revised.split()))

    if (diff_words / total_words) > 0.05:
        return 'material'

    # If contains material keywords (liability, payment, IP, etc.), material
    for keyword in material_keywords:
        if keyword in original.lower() or keyword in revised.lower():
            return 'material'

    return 'minor'

Полный агент сравнения контрактов:

def compare_contracts(original_path, revised_path):
    """Full pipeline: parse, extract, compare, flag changes."""

    # Parse both documents
    original_sections = chunk_by_sections(extract_sections(original_path))
    revised_sections = chunk_by_sections(extract_sections(revised_path))

    # Align sections by number
    changes = []
    material_keywords = ['liability', 'indemnif', 'payment', 'intellectual property', 'termination', 'confiden']

    for orig_sec, rev_sec in zip(original_sections, revised_sections):
        if orig_sec['number'] == rev_sec['number']:
            # Same section: compare content
            redline = generate_redline(orig_sec['content'], rev_sec['content'])

            # Classify significance
            significance = classify_change_significance(
                orig_sec['content'],
                rev_sec['content'],
                material_keywords
            )

            changes.append({
                'section': orig_sec['number'],
                'title': orig_sec['title'],
                'significance': significance,
                'redline': redline
            })

    return {
        'changes': changes,
        'material_changes': [c for c in changes if c['significance'] == 'material'],
        'minor_changes': [c for c in changes if c['significance'] == 'minor']
    }

Выявление отсутствующих пунктов

Хорошие контракты включают определенные стандартные пункты. Агент должен выявлять их отсутствие.

STANDARD_CLAUSES = {
    'limitation_of_liability': {
        'importance': 'critical',
        'keywords': ['limitation of liability', 'liability cap', 'aggregate liability']
    },
    'indemnification': {
        'importance': 'high',
        'keywords': ['indemnify', 'indemnification', 'hold harmless']
    },
    'confidentiality': {
        'importance': 'high',
        'keywords': ['confidential', 'confidentiality', 'proprietary', 'trade secret']
    },
    'governing_law': {
        'importance': 'high',
        'keywords': ['governing law', 'applicable law', 'jurisdiction']
    },
    'termination': {
        'importance': 'high',
        'keywords': ['terminate', 'termination', 'term']
    },
    'warranty': {
        'importance': 'medium',
        'keywords': ['warrant', 'warranty', 'represent', 'representation']
    },
    'dispute_resolution': {
        'importance': 'medium',
        'keywords': ['arbitration', 'litigation', 'mediation', 'dispute resolution']
    }
}

def check_missing_clauses(classified_clauses):
    """Flag missing standard clauses."""

    found_clauses = set(c['clause_type'] for c in classified_clauses if c['confidence'] > 0.5)

    missing = []
    for clause_type, details in STANDARD_CLAUSES.items():
        if clause_type not in found_clauses:
            missing.append({
                'clause_type': clause_type,
                'importance': details['importance'],
                'message': f"Missing {clause_type.replace('_', ' ')} clause"
            })

    return missing

Конфиденциальность и управление доступом

Контракты содержат конфиденциальную информацию. Используйте Fernet для шифрования.

from cryptography.fernet import Fernet
import logging

def encrypt_document(document_text, encryption_key):
    """Encrypt sensitive document content."""
    f = Fernet(encryption_key)
    encrypted = f.encrypt(document_text.encode())
    return encrypted

def decrypt_document(encrypted_text, encryption_key):
    """Decrypt document for authorized access."""
    f = Fernet(encryption_key)
    decrypted = f.decrypt(encrypted_text).decode()
    return decrypted

# Logging for audit trail
logging.basicConfig(
    filename='contract_access_log.txt',
    level=logging.INFO,
    format='%(asctime)s - %(user)s - %(action)s - %(contract_id)s'
)

def log_access(user_id, contract_id, action):
    """Log all access to encrypted documents."""
    logging.info(f"{user_id} - {action} - {contract_id}")

Полный пример: анализ входящего контракта поставщика

def analyze_vendor_contract(pdf_path, vendor_name):
    """End-to-end contract analysis pipeline."""

    # Step 1: Parse document
    sections = chunk_by_sections(extract_sections(pdf_path))
    chunks = chunk_with_overlap(sections)

    # Step 2: Classify clauses
    classified = classify_clauses(chunks)

    # Step 3: Extract key terms
    for clause in classified:
        clause['key_terms'] = extract_key_terms(clause['content'], clause['clause_type'])

    # Step 4: Assess risks
    risk_assessment = assess_risk(classified)

    # Step 5: Check for missing clauses
    missing = check_missing_clauses(classified)

    # Step 6: Generate summary for review
    summary = {
        'vendor': vendor_name,
        'document_path': pdf_path,
        'clause_count': len(classified),
        'risk_score': risk_assessment['contract_risk_score'],
        'review_tier': risk_assessment['review_tier'],
        'critical_issues': [r for r in risk_assessment['clause_risks'] if r['risk_level'] == 'critical'],
        'missing_clauses': missing,
        'clauses_by_type': {}
    }

    # Group clauses by type
    for clause in classified:
        ct = clause['clause_type']
        if ct not in summary['clauses_by_type']:
            summary['clauses_by_type'][ct] = []
        summary['clauses_by_type'][ct].append({
            'section': clause['section_number'],
            'confidence': clause['confidence'],
            'key_terms': clause.get('key_terms', {}),
            'risk_level': next(
                (r['risk_level'] for r in risk_assessment['clause_risks'] if r['section'] == clause['section_number']),
                'low'
            )
        })

    return summary

Оптимизация стоимости

Использование Claude API может быть дорогостоящим при обработке больших объемов контрактов. Несколько стратегий:

  1. Используйте Haiku для классификации — Haiku быстрее и дешевле для простых задач классификации
  2. Используйте Opus только для оценки рисков — более сложный анализ требует более мощной модели
  3. Пакетная обработка — группируйте 10 разделов в один вызов LLM
  4. Кэширование промптов — если анализируете похожие контракты, используйте кэширование контекста Anthropic
def batch_classify_sections(sections, batch_size=10):
    """Batch multiple sections in one LLM call for cost savings."""

    client = Anthropic()
    results = []

    for i in range(0, len(sections), batch_size):
        batch = sections[i:i+batch_size]

        prompt = "Classify each of these contract sections. Return JSON array:\n\n"
        for idx, section in enumerate(batch):
            prompt += f"{idx}. {section['content']}\n\n"

        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=1000,
            messages=[{"role": "user", "content": prompt}]
        )

        import json
        classifications = json.loads(response.content[0].text)

        for section, classification in zip(batch, classifications):
            results.append({**section, **classification})

    return results

Тестирование и проверка точности

Перед внедрением на production протестируйте точность агента на наборе контрактов с известными классификациями.

def evaluate_extraction_accuracy(test_contracts, ground_truth):
    """Measure precision and recall of clause extraction."""

    true_positives = 0
    false_positives = 0
    false_negatives = 0

    for contract_path in test_contracts:
        extracted = analyze_vendor_contract(contract_path, "test")
        expected = ground_truth[contract_path]

        extracted_types = set(extracted['clauses_by_type'].keys())
        expected_types = set(expected.keys())

        true_positives += len(extracted_types & expected_types)
        false_positives += len(extracted_types - expected_types)
        false_negatives += len(expected_types - extracted_types)

    precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
    recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0

    return {
        'precision': precision,
        'recall': recall,
        'f1_score': 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
    }

Ограничения и когда НЕ использовать

Агенты НЕ должны использоваться для:

  1. M&A контрактов — слишком высокие ставки, требуется глубокий юридический анализ
  2. Высокостоимостных сделок (> $1M) — всегда требует партнерского юридического обзора
  3. Контрактов в иностранных юрисдикциях без локального юриста — локальное право может кардинально отличаться
  4. Бизнес-критических условий — никогда не должны приниматься решения на основе агента без проверки человеком

Требование человеческого обзора:

  • Все высокие риски требуют обзора юристом, прежде чем принимать решение
  • Критические пункты всегда должны быть прочитаны человеком
  • Окончательное одобрение контракта всегда требует подписания квалифицированного юриста

Проблемы OCR:

  • Отсканированные контракты часто содержат ошибки OCR, которые могут привести к неправильной классификации
  • Используйте Tesseract или AWS Textract для отсканированных документов, затем проверьте результаты вручную
  • Рассмотрите возможность использования высокостоимостной точной OCR для критических документов