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: Парсинг документа
Юридические контракты обычно структурированы с четко обозначенными разделами. Агент должен:
-
Определить формат: PDF native, отсканированный PDF, DOCX, изображение
-
Выбрать парсер:
pypdfдля извлечения текста из native PDF с обнаружением структурыpdfplumberдля таблиц и формpython-docxдля документов Word- AWS Textract или Tesseract для отсканированных/образных контрактов
-
Определить разделы по 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 может быть дорогостоящим при обработке больших объемов контрактов. Несколько стратегий:
- Используйте Haiku для классификации — Haiku быстрее и дешевле для простых задач классификации
- Используйте Opus только для оценки рисков — более сложный анализ требует более мощной модели
- Пакетная обработка — группируйте 10 разделов в один вызов LLM
- Кэширование промптов — если анализируете похожие контракты, используйте кэширование контекста 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
}
Ограничения и когда НЕ использовать
Агенты НЕ должны использоваться для:
- M&A контрактов — слишком высокие ставки, требуется глубокий юридический анализ
- Высокостоимостных сделок (> $1M) — всегда требует партнерского юридического обзора
- Контрактов в иностранных юрисдикциях без локального юриста — локальное право может кардинально отличаться
- Бизнес-критических условий — никогда не должны приниматься решения на основе агента без проверки человеком
Требование человеческого обзора:
- Все высокие риски требуют обзора юристом, прежде чем принимать решение
- Критические пункты всегда должны быть прочитаны человеком
- Окончательное одобрение контракта всегда требует подписания квалифицированного юриста
Проблемы OCR:
- Отсканированные контракты часто содержат ошибки OCR, которые могут привести к неправильной классификации
- Используйте Tesseract или AWS Textract для отсканированных документов, затем проверьте результаты вручную
- Рассмотрите возможность использования высокостоимостной точной OCR для критических документов