Задокументированы все 4 попытки, хеш-анализ, находка с Node.js fetch, хронология 12 коммитов за 14.06.2026.
10 KiB
Ответ: архитектура сервиса "Сверка договоров"
Дата: 2026-06-13
1. Правильно ли разбиты слои?
Разбивка в целом правильная. Принцип «один файл — одна ответственность» выдержан. Критических проблем нет, но есть два момента, которые стоит учесть.
Что хорошо
parser.py— чисто I/O-слой: байты → JSON. Никакой логики.textify.py— чисто форматирование: JSON → текст. Никакой логики.db.py— чисто транспорт к БД. Не знает о бизнес-сущностях.test_routes.py— отдельный Blueprint, не засоряет app.py.
Что стоит скорректировать
db.py — разделить на транспорт и схему.
Сейчас там ensure_db() — это уже «знание» о схеме. Когда появятся таблицы
(contracts, supplements, spec_rows, spec_history), их создание (DDL)
стоит вынести в отдельный schema.py. db.py остаётся просто connect() и query().
Слой LLM стоит разделить надвое:
llm_client.py— HTTP-клиент к aillm.ru: отправить промпт → получить строку ответа. Не знает ни о договорах, ни о спецификациях.extractor.py— бизнес-логика: взять текст договора, сформировать промпт, вызвать llm_client, распарсить ответ в строки спецификации.
Это важно: если поменяется LLM — меняем только llm_client.py.
Если поменяется формат ответа — только extractor.py.
Итоговый состав слоёв:
parser.py bytes → elements JSON (уже есть, не трогать)
textify.py elements → текст для LLM (уже есть, не трогать)
db.py connect() + query() (уже есть, убрать ensure_db)
schema.py DDL: CREATE TABLE IF NOT EXISTS
upload.py сохранить файл в БД (documents)
llm_client.py HTTP к aillm.ru → строка ответа
extractor.py текст → структурированные строки (промпт + парсинг ответа)
differ.py сравнение строк между допниками → список изменений
api.py Blueprint: /contracts, /supplements, /history
app.py сборка слоёв, Flask-приложение
test_routes.py Blueprint /test (уже есть)
2. В каком порядке создавать
Каждый этап самодостаточен и проверяем до перехода к следующему.
Этап 1 — основа хранения
schema.py → DDL всех таблиц.
Запустить python schema.py — таблицы созданы. Проверить через /test sql.
Этап 2 — загрузка файлов
upload.py → принять файл, вызвать parser.parse(), вызвать textify.to_text(),
сохранить в documents(original_bytes, parsed_text, mime, filename).
Добавить POST /upload в app.py. Проверить curl-ом.
Этап 3 — LLM клиент
llm_client.py → POST к aillm.ru, вернуть строку.
Проверить отдельно: python llm_client.py с тестовым промптом.
Этап 4 — извлечение строк спецификации
extractor.py → взять parsed_text, сформировать промпт, вызвать llm_client,
распарсить ответ в список spec_row.
Проверить на одном docx через тест-скрипт.
Этап 5 — сравнение (diff)
differ.py → взять два списка spec_row, вернуть изменения.
Это чистая функция: diff(rows_old, rows_new) → changes.
Проверить unit-тестом без БД.
Этап 6 — API
api.py → Blueprint с GET/POST для договоров, допников, истории.
Подключить в app.py.
3. Поток данных между слоями
Правило: слои передают данные через простые Python-структуры (dict, list). Никаких прямых вызовов «через слой» — только соседние слои.
Файл (bytes)
│
▼
parser.parse(bytes, mime) → {"elements": [...]}
│
▼
textify.to_text(elements) → str
│
▼
upload.py: сохранить в DB, получить document_id
│
▼
extractor.extract(parsed_text) → [{"pos": 1, "name": "...", "qty": 10, ...}]
│ (внутри вызывает llm_client.ask(prompt) → str)
│
▼
schema: сохранить строки в spec_rows(document_id, pos, ...)
│
▼
differ.diff(rows_v1, rows_v2) → [{"pos": 3, "field": "qty", "old": 5, "new": 10}]
│
▼
schema: сохранить в spec_history
Между слоями нет импортов друг друга, кроме:
upload.pyимпортируетparserиtextify(это нормально — upload оркеструет парсинг)extractor.pyимпортируетllm_client(клиент — зависимость экстрактора)app.pyиapi.pyимпортируют всё — они и есть точки сборки
db.py никто не импортирует напрямую, кроме upload.py, extractor.py и api.py.
schema.py вызывается только один раз при старте из app.py.
4. Как должен выглядеть app.py
app.py — точка входа и сборки. Бизнес-логики ноль.
from flask import Flask
import db, schema
from test_routes import test_bp
from api import api_bp
def create_app():
app = Flask(__name__)
# 1. Инициализация схемы при старте
schema.ensure_schema()
# 2. Регистрация Blueprint-ов
app.register_blueprint(test_bp)
app.register_blueprint(api_bp)
# 3. Системные маршруты
@app.route("/health")
def health():
return "OK", 200
return app
if __name__ == "__main__":
create_app().run(host="0.0.0.0", port=5000)
Правило: если в app.py появляется if, for или бизнес-слово — это уже лишнее.
5. Потенциальные проблемы
LLM не гарантирует структуру ответа
Самая острая проблема. Модель может вернуть текст в произвольном формате, сломать JSON, пропустить поля, придумать данные.
Решение:
- В
extractor.py— строгая схема промпта с примером ответа. - Парсинг ответа через
try/exceptс явным возвратом{"error": "parse_failed", "raw": ответ}. - Никогда не падать — помечать строки как
unresolved.
Идентификация изменённой строки в допнике
Самая неоднозначная задача: иногда в допнике новое полное состояние, иногда — дельта. Определить это автоматически сложно.
Решение для differ.py:
- Сначала попробовать детерминированный diff по позиции/артикулу.
- Если совпадение < порога — пометить как
ambiguous, не фантазировать. - Заказчик потом разбирает вручную
ambiguous-записи.
Сопоставление артикула с каталогом
Задача нетривиальная, код-имён в спецификациях нет, матч только по описанию.
Решение: вынести в отдельный matcher.py, реализовать как отдельный шаг после основного пайплайна. Пометить как optional, не блокировать основной поток.
Размер документов vs контекст LLM
Большая спецификация (100+ строк) может не влезть в контекст.
Решение в extractor.py: разбивать таблицы на чанки, обрабатывать частями,
собирать результат. Это нужно заложить сразу — переделывать потом дороже.
Транзакционность при загрузке
Файл загружен → парсинг ок → LLM вызов → ошибка → документ в БД наполовину.
Решение: хранить в documents поле status (uploaded / parsed / extracted / error).
Обновлять после каждого шага. Зависший uploaded — сигнал для повтора.
Итог
| Вопрос | Ответ |
|---|---|
| Разбивка слоёв | Правильная. Добавить schema.py, разделить LLM на llm_client + extractor |
| Порядок | schema → upload → llm_client → extractor → differ → api |
| Поток данных | Через dict/list, нет перекрёстных импортов |
| app.py | Только сборка: ensure_schema() + register_blueprint() |
| Риски | LLM-нестабильность, diff-амбивалентность, чанкинг, транзакционность |
Всё, что не решается надёжно детерминированно — помечать unresolved, не фантазировать.