AI-інтеграція у системи управління документами
Електронні системи документообігу — EDM, ECM, EDMS — зберігають документи, але не розуміють їх зміст. Співробітник отримує скан контракту → вручну вносить реквізити → вибирає тип документа → призначає маршрут узгодження. AI-інтеграція автоматизує весь цей процес: документ надходить у систему, AI читає його, витягує реквізити, класифікує, створює карточку та запускає потрібний маршрут.
Архітектура AI-прошарку для систем EDM
[Вхідний документ]
PDF/scan/DOCX/email
↓
[Document Preprocessor]
OCR (Tesseract/Google Cloud Vision) → нормалізований текст
↓
[AI Processing Pipeline]
├── Classification: тип документа
├── NER: контрагент, дати, суми, реквізити
├── Summary: стисле резюме змісту
└── Routing: визначення маршруту узгодження
↓
[EDM API]
Створення карточки + запуск workflow
Класифікація документів
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
import torch
class DocumentClassifier:
DOCUMENT_TYPES = [
"contract", "invoice", "delivery_note", "act",
"order", "memo", "commercial_proposal",
"power_of_attorney", "charter", "protocol", "incoming_letter"
]
def __init__(self, model_path: str = "cointegrated/rubert-tiny2"):
# Для production — fine-tuned BERT на корпусі документів компанії
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
self.model = AutoModelForSequenceClassification.from_pretrained(
model_path,
num_labels=len(self.DOCUMENT_TYPES)
)
self.model.eval()
def classify(self, text: str) -> dict:
# Беремо перші 512 токенів (заголовок документа містить основну семантику)
inputs = self.tokenizer(
text[:2000],
return_tensors="pt",
truncation=True,
max_length=512,
padding=True
)
with torch.no_grad():
logits = self.model(**inputs).logits
probs = torch.softmax(logits, dim=-1)[0]
top_idx = probs.argmax().item()
return {
"type": self.DOCUMENT_TYPES[top_idx],
"confidence": float(probs[top_idx]),
"alternatives": [
{"type": self.DOCUMENT_TYPES[i], "score": float(probs[i])}
for i in probs.topk(3).indices.tolist()
if i != top_idx
]
}
Витяг реквізитів через NER + LLM
Для структурованих документів (рахунки, контракти) краще працює комбінація: NER для швидкого витягу стандартних полів + LLM для складних випадків:
from langchain_openai import ChatOpenAI
import re
from datetime import datetime
class DocumentExtractor:
EXTRACTION_PROMPT = """Витягни реквізити з документа.
Текст документа:
{text}
Тип документа: {doc_type}
Витягни (поверни null якщо не знайдено):
- contractor_name: назва контрагента
- contractor_inn: ІНН контрагента
- contract_number: номер контракту/рахунку
- contract_date: дата документа (ISO 8601)
- total_amount: сума (число)
- currency: валюта (RUB/USD/EUR)
- payment_deadline: термін оплати (якщо є)
- subject: предмет контракту (1-2 речення)
- signatory: підписант зі сторони контрагента
Поверни JSON."""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def extract_requisites(self, text: str, doc_type: str) -> dict:
# Спочатку швидкий regex-витяг
fast_extract = self._regex_extract(text)
# LLM для пропущених полів та валідації
llm_result = self.llm.invoke(
self.EXTRACTION_PROMPT.format(
text=text[:3000],
doc_type=doc_type
)
)
import json
llm_data = json.loads(llm_result.content)
# Об'єднуємо: regex має пріоритет для числових полів (точніший)
return {**llm_data, **fast_extract}
def _regex_extract(self, text: str) -> dict:
result = {}
# INN: 10 або 12 цифр
inn_match = re.search(r'\bINN[:\s]*(\d{10,12})\b', text)
if inn_match:
result["contractor_inn"] = inn_match.group(1)
# Суми з валютою
amount_match = re.search(
r'(\d[\d\s,]*\.?\d*)\s*(rub|rubles|RUB|USD|EUR)',
text, re.IGNORECASE
)
if amount_match:
amount_str = amount_match.group(1).replace(' ', '').replace(',', '.')
result["total_amount"] = float(amount_str)
return result
Інтеграція з популярними системами EDM
class SEDIntegration:
"""Інтеграція з 1C:Document Management, Directum, DocsVision"""
def push_to_directum(self, extracted: dict, original_file: bytes) -> dict:
"""Створює карточку документа в Directum"""
import requests
# Завантажуємо файл
upload_response = requests.post(
f"{self.directum_url}/api/v1/documents",
headers={"Authorization": f"Bearer {self.token}"},
files={"file": original_file}
)
doc_id = upload_response.json()["id"]
# Заповнюємо карточку
card_response = requests.patch(
f"{self.directum_url}/api/v1/documents/{doc_id}/properties",
headers={"Authorization": f"Bearer {self.token}"},
json={
"DocumentType": extracted["type"],
"Counterparty": extracted.get("contractor_name"),
"INN": extracted.get("contractor_inn"),
"Amount": extracted.get("total_amount"),
"DocumentDate": extracted.get("contract_date"),
"Subject": extracted.get("subject")
}
)
# Запускаємо маршрут узгодження
route = self._determine_route(extracted)
requests.post(
f"{self.directum_url}/api/v1/documents/{doc_id}/workflow/{route}",
headers={"Authorization": f"Bearer {self.token}"}
)
return {"doc_id": doc_id, "route": route}
def _determine_route(self, extracted: dict) -> str:
"""Визначає маршрут узгодження за параметрами документа"""
amount = extracted.get("total_amount", 0)
doc_type = extracted.get("type", "")
if doc_type == "contract":
if amount > 1_000_000:
return "contract_large" # директор + юрист + фінанси
elif amount > 100_000:
return "contract_medium" # менеджер + юрист
else:
return "contract_standard" # тільки менеджер
elif doc_type == "invoice":
return "invoice_approval"
return "standard"
Кейс: виробнича компанія, 500 вхідних документів на місяць. До впровадження: 2 операторів витрачали 40% робочого часу на ручний внесення реквізитів. Після: точність автоматичного витягу реквізитів 94% (перевірено на 1000 документів), 89% документів обробляються без участі оператора, оператори займаються тільки винятками (confidence < 0.8) та перевіркою спірних маршрутів. Час обробки вхідного документа: 8 хвилин → 45 секунд.
Терміни
- Класифікатор + екстрактор реквізитів: 3–4 тижні
- Інтеграція з конкретною системою EDM: 2–3 тижні
- Fine-tuning моделей на документах клієнта: 1–2 додаткові тижні







