304 lines
18 KiB
HTML
304 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Архитектура — Сверка договоров</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0d1117; --fg: #c9d1d9; --muted: #8b949e;
|
||
--accent: #58a6ff; --green: #3fb950; --orange: #d2991d;
|
||
--yellow: #e3b341; --red: #f85149; --border: #30363d;
|
||
--code-bg: #161b22; --card-bg: #161b22;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font: 15px/1.65 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 860px; margin: 0 auto; padding: 32px 20px 80px; }
|
||
h1 { font-size: 26px; margin-bottom: 8px; }
|
||
h2 { font-size: 19px; margin-top: 36px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); color: var(--accent); }
|
||
h3 { font-size: 16px; margin-top: 24px; margin-bottom: 6px; color: var(--green); }
|
||
p { margin: 8px 0; }
|
||
blockquote { border-left: 3px solid var(--orange); margin: 12px 0; padding: 8px 16px; background: var(--card-bg); border-radius: 0 6px 6px 0; font-style: italic; color: var(--yellow); }
|
||
blockquote strong { color: var(--orange); }
|
||
ul, ol { padding-left: 24px; margin: 8px 0; }
|
||
li { margin: 4px 0; }
|
||
code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; font-size: 13px; border: 1px solid var(--border); }
|
||
pre { background: var(--code-bg); padding: 12px 16px; border-radius: 6px; overflow-x: auto; font-size: 13px; border: 1px solid var(--border); margin: 12px 0; }
|
||
.tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: 600; margin-right: 6px; }
|
||
.tag-ok { background: #1a3a2a; color: var(--green); }
|
||
.tag-llm { background: #1a2a3a; color: var(--accent); }
|
||
.tag-warn { background: #3a2a1a; color: var(--orange); }
|
||
.tag-future { background: #2a1a3a; color: #bc8cff; }
|
||
.nav { margin-bottom: 24px; }
|
||
.nav a { color: var(--accent); text-decoration: none; }
|
||
.nav a:hover { text-decoration: underline; }
|
||
hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="nav"><a href="/">← Вернуться к загрузке</a></div>
|
||
|
||
<h1>🏗️ Архитектура — сверка договоров</h1>
|
||
<p style="color:var(--muted)">Пайплайн: загрузка → парсинг → классификация → группировка → сравнение → аудит</p>
|
||
|
||
<!--
|
||
Все комментарии ниже — прямые цитаты или пересказ пожеланий Заказчика (Сергей Мищук) из переписки 2025–2026.
|
||
-->
|
||
|
||
<hr>
|
||
|
||
<h2>1. Загрузка файлов</h2>
|
||
<p>Пользователь выбирает <code>.docx</code> / <code>.pdf</code> / <code>.doc</code> / <code>.zip</code>.</p>
|
||
|
||
<ul>
|
||
<li><strong>PDF:</strong> <code>pdfplumber</code> извлекает текст и таблицы с сохранением структуры колонок</li>
|
||
<li><strong>DOCX:</strong> <code>python-docx</code> читает XML — извлекает параграфы и таблицы построчно</li>
|
||
<li><strong>DOC:</strong> обрабатывается как DOCX (python-docx читает большинство .doc)</li>
|
||
<li><strong>ZIP:</strong> архив распаковывается с лимитом 500 файлов и проверкой на сжатие (защита от ZIP-бомбы). Каждый файл внутри проходит тот же путь загрузки</li>
|
||
<li>Файлы не поддерживаемых форматов — отклоняются с пометкой <code>unknown_format</code></li>
|
||
</ul>
|
||
|
||
<p><strong>Результат:</strong> <code>elements_json</code> — массив элементов двух типов:</p>
|
||
<ul>
|
||
<li><code>{type: "paragraph", text: "...", style: "..."}</code></li>
|
||
<li><code>{type: "table", rows: [["колонка1", "колонка2"], ...]}</code></li>
|
||
</ul>
|
||
|
||
<p>Документ сохраняется в БД, считается <code>content_hash</code> (SHA-256) — если в этом же батче уже есть файл с таким содержимым, он помечается как <code>duplicate</code> и не дублируется.</p>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «100+ файлов за раз. Файлы: .docx/.pdf, возможно ZIP.<br>
|
||
Загрузка из локали (файловая шара / облачный диск), НЕ S3 (конфиденциальность).»
|
||
</blockquote>
|
||
|
||
<hr>
|
||
|
||
<h2>2. Классификация документов</h2>
|
||
<p>Каждый загруженный файл проходит <strong>трёхэтапный фильтр</strong>.</p>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Оставлять только: договоры, доп. соглашения, спецификации.<br>
|
||
Игнорировать: акты сверки, счета/счета-фактуры/УПД, акты услуг, платёжки.»
|
||
</blockquote>
|
||
|
||
<h3>Этап 1 — имя файла <span class="tag tag-ok">0 токенов</span></h3>
|
||
<p>Проверка имени файла на мусорные ключевые слова: «счёт», «акт», «платёж», «УПД», «сверка», «invoice» и др. Совпадение → <code>garbage</code>, дальше не идёт.</p>
|
||
|
||
<h3>Этап 2 — заголовки в тексте <span class="tag tag-ok">0 токенов</span></h3>
|
||
<p>Проверка первых 2000 символов текста на заголовки: «СЧЕТ-ФАКТУРА», «АКТ СВЕРКИ», «АКТ ОКАЗАННЫХ УСЛУГ» и т.п. Совпадение → <code>garbage</code>.</p>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Названия документов неинформативные. Есть лишние документы.<br>
|
||
По всем контрагентам.»
|
||
</blockquote>
|
||
|
||
<h3>Этап 3 — LLM <span class="tag tag-llm">LLM</span></h3>
|
||
<p>Оставшиеся документы отправляются в LLM. Модель получает выжимку текста (первые 1500 символов + строки с маркерами «договор», «соглашение», «спецификация») и возвращает JSON:</p>
|
||
|
||
<ul>
|
||
<li><code>doc_type</code> — <code>contract</code> / <code>supplement</code> / <code>specification</code> / <code>other</code></li>
|
||
<li><code>own_number</code> — номер этого документа</li>
|
||
<li><code>parent_number</code> — номер родительского договора (для допников и спецификаций)</li>
|
||
<li><code>doc_date</code> — дата документа YYYY-MM-DD</li>
|
||
<li><code>counterparty</code> — название контрагента</li>
|
||
</ul>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «НУБЕС = Исполнитель во всех целевых договорах.»<br>
|
||
→ counterparty = <strong>другая сторона</strong> (Заказчик).
|
||
</blockquote>
|
||
|
||
<hr>
|
||
|
||
<h2>3. Группировка по договорам</h2>
|
||
<p><strong>Детерминированная (без LLM).</strong> Документы группируются по номерам договоров.</p>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Система сама должна разбираться, кто к кому.<br>
|
||
зачем нам базовый договор? Вся информация есть в допниках и спеках.»<br>
|
||
«Я не должен распознавать ничего. Система должна сделать всё.»
|
||
</blockquote>
|
||
|
||
<ul>
|
||
<li>Номера нормализуются: uppercase, удаляются все символы кроме букв и цифр.<br>
|
||
«МЭС-123/2024» → «МЭС1232024»</li>
|
||
<li>Допники и спецификации с одинаковым <code>parent_number</code> попадают в <strong>одну виртуальную группу</strong> — даже если сам договор не загружен</li>
|
||
<li>Неопознанные файлы → отдельная группа «Не распознано» с указанием причины</li>
|
||
</ul>
|
||
|
||
<hr>
|
||
|
||
<h2>4. Сравнение — извлечение + дифф</h2>
|
||
<p>Для каждой группы запускается последовательная обработка.</p>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Важно. В произвольной, не повторяющейся.» (порядок допников)<br>
|
||
«Главное — точность разбора, не UI. Сначала базовая функция → потом юзабельность.»
|
||
</blockquote>
|
||
|
||
<h3>4a. Сортировка</h3>
|
||
<p>Документы внутри группы сортируются по <code>doc_date</code> (из классификации), затем по времени загрузки.</p>
|
||
|
||
<h3>4b. Базовый договор (первый в группе) <span class="tag tag-llm">LLM</span></h3>
|
||
<ul>
|
||
<li>Текст спецификации услуг → LLM с промптом <code>extract</code></li>
|
||
<li>LLM получает текст и инструкцию «найди таблицу услуг, извлеки каждую строку как ADD»</li>
|
||
<li>Ответ: массив операций ADD — каждая услуга с полями: название, цена, количество, сумма, дата начала</li>
|
||
</ul>
|
||
|
||
<h3>4c. Дополнительные соглашения <span class="tag tag-llm">LLM</span></h3>
|
||
<ul>
|
||
<li>Текущая спецификация + текст допника → LLM с промптом <code>diff</code></li>
|
||
<li>LLM определяет изменения:
|
||
<ul>
|
||
<li><code>ADD</code> — новая услуга</li>
|
||
<li><code>UPDATE</code> — изменение цены/количества/суммы</li>
|
||
<li><code>DELETE</code> — услуга исключена</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
|
||
<h3>4d. Режим «изложить в новой редакции»</h3>
|
||
<p>Если допник говорит «изложить в новой редакции» — LLM возвращает полный новый список (<code>full_replace</code>). Все старые строки заменяются новыми.</p>
|
||
|
||
<hr>
|
||
|
||
<h2>5. Защита и валидация <span class="tag tag-warn">валидация</span></h2>
|
||
|
||
<ul>
|
||
<li><code>ADD</code> без имени услуги → <code>UNRESOLVED</code> (не применяется, помечается для ручной проверки)</li>
|
||
<li><code>UPDATE</code>/<code>DELETE</code> с пустым идентификатором → <code>UNRESOLVED</code></li>
|
||
<li><strong>Арифметика:</strong> <code>sum = price × qty</code> — расхождение → флаг</li>
|
||
<li><strong>Дубликаты:</strong> <code>content_hash</code> — один и тот же файл дважды не обрабатывается</li>
|
||
<li><strong>Даты:</strong> <code>01.03.2026</code> → <code>2026-03-01</code> (авто-нормализация)</li>
|
||
<li><strong>Числа:</strong> <code>1 000,50</code> → <code>1000.50</code> (авто-нормализация)</li>
|
||
</ul>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «кривых документов можно ожидать.<br>
|
||
Если не может разобраться — пусть зовет на помощь юзера.»
|
||
</blockquote>
|
||
|
||
<hr>
|
||
|
||
<h2>6. Event Sourcing — аудит <span class="tag tag-ok">аудит</span></h2>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Наверно я захочу иметь возможность посмотреть любые промежуточные результаты.<br>
|
||
Было бы хорошо пояснить или показать процесс, что в каком порядке происходит.»
|
||
</blockquote>
|
||
|
||
<ul>
|
||
<li><strong>spec_events:</strong> полный лог всех операций. Каждое событие содержит:
|
||
<ul>
|
||
<li>тип (<code>ADD/UPDATE/DELETE/UNRESOLVED</code>)</li>
|
||
<li>источник — какой документ</li>
|
||
<li>версия промпта</li>
|
||
<li>полный сырой ответ LLM</li>
|
||
<li>временную метку</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong>spec_current:</strong> материализованное представление = сумма всех событий</li>
|
||
<li><strong>Промежуточные результаты:</strong> для каждого файла доступны:
|
||
<ul>
|
||
<li>текст, отправленный в LLM</li>
|
||
<li>сырой ответ LLM</li>
|
||
<li>распарсенный JSON операций</li>
|
||
<li>какие операции были применены</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
|
||
<hr>
|
||
|
||
<h2>7. Промпты <span class="tag tag-llm">LLM</span></h2>
|
||
<p>Три роли промптов, хранятся в БД с версионированием:</p>
|
||
|
||
<table style="width:100%;border-collapse:collapse;margin:12px 0;border:1px solid var(--border)">
|
||
<tr style="background:var(--code-bg)">
|
||
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Роль</th>
|
||
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Назначение</th>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)"><code>extract</code></td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Извлечение строк спецификации из договора</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)"><code>diff</code></td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Сравнение допников с текущей спецификацией</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)"><code>classify</code></td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Определение типа/номера/даты/контрагента документа</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>Встроенный редактор с историей версий: сохранил новую версию → она сразу используется LLM. Старые версии остаются в истории — можно откатиться.</p>
|
||
|
||
<hr>
|
||
|
||
<h2>8. Стек технологий</h2>
|
||
|
||
<table style="width:100%;border-collapse:collapse;margin:12px 0;border:1px solid var(--border)">
|
||
<tr style="background:var(--code-bg)">
|
||
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Компонент</th>
|
||
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Где</th>
|
||
<th style="padding:8px;text-align:left;border:1px solid var(--border)">Технология</th>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)">Веб-интерфейс</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Managed Python (Nubes)</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Python 3.12, Jinja2, ванильный JS</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)">Механизм обработки</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Выделенная VM</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Python 3.12, httpx, pdfplumber, python-docx</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)">База данных</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">VM PostgreSQL 15</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">JSONB, UUID, advisory locks</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)">LLM</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">api.aillm.ru</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">gpt-oss-120b (бесплатно, OpenAI API)</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px;border:1px solid var(--border)">Деплой</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Managed Python + VM</td>
|
||
<td style="padding:8px;border:1px solid var(--border)">Dockerfile, Nginx + Let's Encrypt</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<hr>
|
||
|
||
<h2>9. Конечная цель <span class="tag tag-future">будущее</span></h2>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Конечная цель: построчное сравнение CRM ↔ фискальная система.<br>
|
||
Особый фокус: даты начала услуг.<br>
|
||
PAYG (суффикс -m) — особый случай. Сверку с CRM тоже можно делегировать AI.»
|
||
</blockquote>
|
||
|
||
<p>Текущий пайплайн завершается на этапе <strong>извлечения спецификации из договоров и отслеживания изменений по допникам</strong>. Модуль сравнения с CRM — спроектирован как <code>CanonicalRow</code> адаптер, ожидает формата данных от заказчика.</p>
|
||
|
||
<hr>
|
||
|
||
<h2>10. Что дальше <span class="tag tag-future">требует участия Заказчика</span></h2>
|
||
|
||
<blockquote>
|
||
📋 <strong>Заказчик:</strong> «Это всё пока опыты и набивание шишек.<br>
|
||
Не надеюсь с первого захода на идеальный результат.»
|
||
</blockquote>
|
||
|
||
<ol>
|
||
<li><strong>Золотой набор:</strong> 30–50 реальных документов с ручной разметкой — измерить точность LLM</li>
|
||
<li><strong>Формат CRM:</strong> в каком виде данные из системы учёта клиентов — для модуля сверки</li>
|
||
<li><strong>Приоритет:</strong> точность vs скорость обработки — влияет на выбор модели LLM (локальная vs облачная)</li>
|
||
</ol>
|
||
|
||
</body>
|
||
</html>
|